mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cd1ab07a2 | ||
|
|
137be678c0 | ||
|
|
bcf0aaab17 | ||
|
|
9c331e1777 | ||
|
|
789516e573 | ||
|
|
02f964c7d9 | ||
|
|
ea4d1de772 | ||
|
|
0f3d71a504 | ||
|
|
5dc00a8bc6 | ||
|
|
5690e3fe9e | ||
|
|
f6fcb280fc | ||
|
|
65a20815bf | ||
|
|
9a55202a9f | ||
|
|
06ec65fb10 | ||
|
|
371ecd9d3a | ||
|
|
ff3fce821c | ||
|
|
cc706a48da | ||
|
|
e4093209cc | ||
|
|
1f7e5632ac | ||
|
|
bda9e7b2b2 | ||
|
|
5427ddb8d6 | ||
|
|
b34d40252f | ||
|
|
f73d74dd73 | ||
|
|
eb48b8e65f | ||
|
|
ce0fe368ab | ||
|
|
34ef71707a | ||
|
|
ffeb9dbaa5 | ||
|
|
62a1d09f48 | ||
|
|
60039de67d | ||
|
|
526df6e41a | ||
|
|
cf8b451e35 | ||
|
|
059260d318 | ||
|
|
45134f903d | ||
|
|
fefe4edda1 | ||
|
|
aabb1bc264 | ||
|
|
02ba2d3b60 | ||
|
|
925c5aa389 | ||
|
|
2b6ce14a32 | ||
|
|
ed45e524b9 | ||
|
|
e645204e37 | ||
|
|
ff1429c6b3 | ||
|
|
abe17a02c4 | ||
|
|
7bde73102b | ||
|
|
3f27093f82 | ||
|
|
37ed53cb3c | ||
|
|
4b20d3ef0a | ||
|
|
12492b5749 | ||
|
|
af2b422730 | ||
|
|
0c63f65aa7 | ||
|
|
4b2d1e6745 | ||
|
|
25e6a03435 | ||
|
|
d681f91de9 | ||
|
|
12e2bcbbd9 | ||
|
|
ec3e438c99 | ||
|
|
2089237d23 | ||
|
|
7bcd0ac3e5 | ||
|
|
8e9ab12219 | ||
|
|
33b539858f | ||
|
|
cdfd828a8b | ||
|
|
22727b5abe | ||
|
|
041cf1268d | ||
|
|
fb3afbdc76 | ||
|
|
1f6632146b | ||
|
|
944187f276 | ||
|
|
81eb2189ca | ||
|
|
4fc90984d8 | ||
|
|
0b1abc9ab0 | ||
|
|
212d81b93c | ||
|
|
238ac22b85 | ||
|
|
773f0be84a | ||
|
|
e45c988637 | ||
|
|
8305b49046 | ||
|
|
92b2601ba2 | ||
|
|
af8ab752ad | ||
|
|
e45105ccb3 | ||
|
|
9114b68659 | ||
|
|
cd2e091580 | ||
|
|
38d8f83587 | ||
|
|
5f3b6501d7 | ||
|
|
f35efbdd5b | ||
|
|
961954364a | ||
|
|
259725882a | ||
|
|
fb2288c4bc | ||
|
|
0220f4519d | ||
|
|
3432243acb | ||
|
|
27a32463d9 | ||
|
|
b47f7e8cf1 | ||
|
|
459bc69032 | ||
|
|
378b4bb8c1 | ||
|
|
6a5712070f | ||
|
|
48e125881b | ||
|
|
e6cec93f2c | ||
|
|
b5726393f3 | ||
|
|
82010e4fa3 | ||
|
|
eb99fe58dd | ||
|
|
a18e7ab2da | ||
|
|
e1f78f519c | ||
|
|
50a57727fe | ||
|
|
4bbb7f82b4 | ||
|
|
1da4cf8907 | ||
|
|
978a58391b | ||
|
|
b2be44e372 | ||
|
|
650fc6aa27 | ||
|
|
47bda7d789 | ||
|
|
64f41f004d | ||
|
|
68f69074cb | ||
|
|
4d3fb52956 | ||
|
|
18a23d6844 | ||
|
|
bdf653bc70 | ||
|
|
0aaa351797 | ||
|
|
1f39761f8c | ||
|
|
c7914fa8e4 | ||
|
|
a48cc2a7f3 | ||
|
|
3942409c9a | ||
|
|
6b0719db45 | ||
|
|
9728116836 | ||
|
|
6cffabe259 | ||
|
|
28b20cc8ba | ||
|
|
62b012941e | ||
|
|
1b94ac383c | ||
|
|
9a96ef2623 | ||
|
|
6480750757 | ||
|
|
df313560c2 | ||
|
|
be0e832589 | ||
|
|
9f87f551fd | ||
|
|
5804c57236 | ||
|
|
7efd81191a | ||
|
|
84bea20891 | ||
|
|
c2b9b6e162 | ||
|
|
3720b9481f | ||
|
|
b565d40ec7 | ||
|
|
0e1f2e721f | ||
|
|
951a22b90e | ||
|
|
1dd88a690b |
83
appveyor.yml
Normal file
83
appveyor.yml
Normal file
@@ -0,0 +1,83 @@
|
||||
image:
|
||||
- Visual Studio 2017
|
||||
- Ubuntu1804
|
||||
|
||||
branches:
|
||||
except:
|
||||
- l10n_master
|
||||
- gh-pages
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
stack: node 10
|
||||
|
||||
init:
|
||||
- ps: |
|
||||
if($isWindows) {
|
||||
Install-Product node 10
|
||||
}
|
||||
|
||||
install:
|
||||
- ps: |
|
||||
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\package.json | ConvertFrom-Json).version
|
||||
$env:PUSH_DOCKER = "false"
|
||||
$env:PROD_DEPLOY = "false"
|
||||
$env:TAG_NAME = ""
|
||||
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
|
||||
$env:PROD_DEPLOY = "true"
|
||||
$env:TAG_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
|
||||
echo "This is a production deployment for ${env:TAG_NAME}."
|
||||
}
|
||||
if("${env:DOCKER_USERNAME}" -ne "" -and "${env:DOCKER_PASSWORD}" -ne "") {
|
||||
$env:PUSH_DOCKER = "true"
|
||||
}
|
||||
if($isWindows) {
|
||||
choco install cloc --no-progress
|
||||
cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
||||
}
|
||||
|
||||
before_build:
|
||||
- node --version
|
||||
- npm --version
|
||||
- sh: |
|
||||
if [ "${PUSH_DOCKER}" == "true" ]
|
||||
then
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
fi
|
||||
- cmd: set "GIT_PATH=C:\Program Files\Git\mingw64\libexec\git-core"
|
||||
- cmd: set "PATH=%GIT_PATH%;%PATH%"
|
||||
|
||||
build_script:
|
||||
- sh: chmod +x ./build.sh
|
||||
- ps: |
|
||||
if($isLinux) {
|
||||
./build.sh
|
||||
./build.sh tag dev
|
||||
|
||||
if($env:PROD_DEPLOY -eq "true") {
|
||||
./build.sh tag beta
|
||||
./build.sh tag $env:TAG_NAME
|
||||
}
|
||||
|
||||
docker images
|
||||
|
||||
if($env:PUSH_DOCKER -eq "true") {
|
||||
./build.sh push dev
|
||||
|
||||
if($env:PROD_DEPLOY -eq "true") {
|
||||
./build.sh push beta
|
||||
./build.sh push latest
|
||||
./build.sh push $env:TAG_NAME
|
||||
}
|
||||
}
|
||||
}
|
||||
- cmd: npm install
|
||||
- cmd: npm run build:prod
|
||||
|
||||
after_build:
|
||||
- sh: |
|
||||
if [ "${PUSH_DOCKER}" == "true" ]
|
||||
then
|
||||
docker logout
|
||||
fi
|
||||
@@ -8,3 +8,4 @@ files:
|
||||
pt-BR: pt_BR
|
||||
zh-CN: zh_CN
|
||||
zh-TW: zh_TW
|
||||
en-GB: en_GB
|
||||
|
||||
10
gulpfile.js
10
gulpfile.js
@@ -5,6 +5,7 @@ const package = require('./package.json');
|
||||
const fs = require('fs');
|
||||
|
||||
const paths = {
|
||||
node_modules: './node_modules/',
|
||||
src: './src/',
|
||||
build: './build/',
|
||||
cssDir: './src/css/',
|
||||
@@ -29,8 +30,17 @@ function version(cb) {
|
||||
cb();
|
||||
}
|
||||
|
||||
// ref: https://github.com/t4t5/sweetalert/issues/890
|
||||
function fixSweetAlert(cb) {
|
||||
fs.writeFileSync(paths.node_modules + 'sweetalert/typings/sweetalert.d.ts',
|
||||
'import swal, { SweetAlert } from "./core";export default swal;export as namespace swal;');
|
||||
cb();
|
||||
}
|
||||
|
||||
exports.clean = clean;
|
||||
exports.webfonts = gulp.series(clean, webfonts);
|
||||
exports.prebuild = gulp.series(clean, webfonts);
|
||||
exports.version = version;
|
||||
exports.postdist = version;
|
||||
exports.fixSweetAlert = fixSweetAlert;
|
||||
exports.postinstall = fixSweetAlert;
|
||||
|
||||
2
jslib
2
jslib
Submodule jslib updated: 27566c3fd5...a884f77938
1944
package-lock.json
generated
1944
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
52
package.json
52
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "bitwarden-web",
|
||||
"version": "2.7.1",
|
||||
"version": "2.10.0",
|
||||
"scripts": {
|
||||
"sub:init": "git submodule update --init --recursive",
|
||||
"sub:update": "git submodule update --remote",
|
||||
"sub:pull": "git submodule foreach git pull",
|
||||
"postinstall": "npm run sub:init",
|
||||
"postinstall": "npm run sub:init && gulp postinstall",
|
||||
"build": "gulp prebuild && webpack",
|
||||
"build:watch": "gulp prebuild && webpack-dev-server",
|
||||
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack",
|
||||
@@ -23,8 +23,8 @@
|
||||
"lint:fix": "tslint src/**/*.ts --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^6.1.7",
|
||||
"@ngtools/webpack": "^6.2.1",
|
||||
"@angular/compiler-cli": "^7.2.1",
|
||||
"@ngtools/webpack": "^7.2.2",
|
||||
"@types/jquery": "^3.3.6",
|
||||
"@types/lunr": "^2.1.6",
|
||||
"@types/node-forge": "^0.7.5",
|
||||
@@ -48,25 +48,25 @@
|
||||
"node-sass": "^4.9.3",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"ts-loader": "^5.1.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-loader": "^3.6.0",
|
||||
"typescript": "^2.7.2",
|
||||
"webpack": "^4.18.0",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-dev-server": "^3.1.8"
|
||||
"ts-loader": "^5.3.3",
|
||||
"tslint": "^5.12.1",
|
||||
"tslint-loader": "^3.5.4",
|
||||
"typescript": "3.2.4",
|
||||
"webpack": "^4.29.0",
|
||||
"webpack-cli": "^3.2.1",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "6.1.7",
|
||||
"@angular/common": "6.1.7",
|
||||
"@angular/compiler": "6.1.7",
|
||||
"@angular/core": "6.1.7",
|
||||
"@angular/forms": "6.1.7",
|
||||
"@angular/http": "6.1.7",
|
||||
"@angular/platform-browser": "6.1.7",
|
||||
"@angular/platform-browser-dynamic": "6.1.7",
|
||||
"@angular/router": "6.1.7",
|
||||
"@angular/upgrade": "6.1.7",
|
||||
"@angular/animations": "7.2.1",
|
||||
"@angular/common": "7.2.1",
|
||||
"@angular/compiler": "7.2.1",
|
||||
"@angular/core": "7.2.1",
|
||||
"@angular/forms": "7.2.1",
|
||||
"@angular/http": "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",
|
||||
"@aspnet/signalr": "1.0.4",
|
||||
"@aspnet/signalr-protocol-msgpack": "1.0.4",
|
||||
"angular2-toaster": "6.1.0",
|
||||
@@ -74,22 +74,22 @@
|
||||
"big-integer": "1.6.36",
|
||||
"bootstrap": "4.1.3",
|
||||
"braintree-web-drop-in": "1.13.0",
|
||||
"core-js": "2.5.7",
|
||||
"core-js": "2.6.2",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.3.1",
|
||||
"lunr": "2.3.3",
|
||||
"ngx-infinite-scroll": "6.0.1",
|
||||
"ngx-infinite-scroll": "7.0.1",
|
||||
"node-forge": "0.7.6",
|
||||
"papaparse": "4.6.0",
|
||||
"popper.js": "1.14.4",
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "6.3.2",
|
||||
"sweetalert": "2.1.0",
|
||||
"rxjs": "6.3.3",
|
||||
"sweetalert": "2.1.2",
|
||||
"web-animations-js": "2.3.1",
|
||||
"webcrypto-shim": "0.1.4",
|
||||
"whatwg-fetch": "3.0.0",
|
||||
"zone.js": "0.8.26",
|
||||
"zone.js": "0.8.28",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
||||
{{'logIn' | i18n}}
|
||||
</a>
|
||||
<a routerLink="/register" [queryParams]="{email: email}" class="btn btn-primary btn-block ml-2 mt-0">
|
||||
<a routerLink="/register" [queryParams]="{email: email}"
|
||||
class="btn btn-primary btn-block ml-2 mt-0">
|
||||
{{'createAccount' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
appAutofocus inputmode="email" appInputVerbatim="false">
|
||||
<small class="form-text text-muted">{{'enterEmailToGetHint' | i18n}}</small>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
@@ -10,12 +10,16 @@
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
|
||||
required appAutofocus appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword()">
|
||||
<i class="fa fa-lg"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted form-text">{{'loggedInAsEmail' | i18n : email}}</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.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 { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { RouterService } from '../services/router.service';
|
||||
@@ -18,15 +17,18 @@ import { LockComponent as BaseLockComponent } from 'jslib/angular/components/loc
|
||||
selector: 'app-lock',
|
||||
templateUrl: 'lock.component.html',
|
||||
})
|
||||
export class LockComponent extends BaseLockComponent implements OnInit {
|
||||
export class LockComponent extends BaseLockComponent {
|
||||
constructor(router: Router, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
|
||||
userService: UserService, cryptoService: CryptoService,
|
||||
storageService: StorageService, lockService: LockService,
|
||||
private routerService: RouterService) {
|
||||
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService);
|
||||
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
|
||||
storageService, lockService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
const authed = await this.userService.isAuthenticated();
|
||||
if (!authed) {
|
||||
this.router.navigate(['/']);
|
||||
@@ -34,9 +36,12 @@ export class LockComponent extends BaseLockComponent implements OnInit {
|
||||
this.router.navigate(['vault']);
|
||||
}
|
||||
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
|
||||
this.successRoute = previousUrl;
|
||||
}
|
||||
this.onSuccessfulSubmit = () => {
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
|
||||
this.successRoute = previousUrl;
|
||||
}
|
||||
this.router.navigate([this.successRoute]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,19 @@
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required inputmode="email" appInputVerbatim="false">
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
inputmode="email" appInputVerbatim="false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
|
||||
[(ngModel)]="masterPassword" required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
|
||||
required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword()">
|
||||
<i class="fa fa-lg"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text">
|
||||
@@ -23,7 +27,8 @@
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="rememberEmail" name="RememberEmail" [(ngModel)]="rememberEmail">
|
||||
<input type="checkbox" class="form-check-input" id="rememberEmail" name="RememberEmail"
|
||||
[(ngModel)]="rememberEmail">
|
||||
<label class="form-check-label" for="rememberEmail">{{'rememberEmail' | i18n}}</label>
|
||||
</div>
|
||||
<hr>
|
||||
@@ -34,7 +39,8 @@
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
</button>
|
||||
<a routerLink="/register" [queryParams]="{email: email}" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
<a routerLink="/register" [queryParams]="{email: email}"
|
||||
class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
<i class="fa fa-pencil-square-o"></i> {{'createAccount' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
@@ -37,6 +37,9 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
|
||||
}
|
||||
await super.ngOnInit();
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<p>{{'deleteRecoverDesc' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
appAutofocus inputmode="email" appInputVerbatim="false">
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
|
||||
@@ -5,22 +5,23 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>{{'recoverAccountTwoStepDesc' | i18n}}
|
||||
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank" rel="noopener">{{'learnMore' | i18n}}</a>
|
||||
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank"
|
||||
rel="noopener">{{'learnMore' | i18n}}</a>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
appAutofocus inputmode="email" appInputVerbatim="false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPassword" class="form-control" [(ngModel)]="masterPassword" required
|
||||
appInputVerbatim>
|
||||
<input id="masterPassword" type="password" name="MasterPassword" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appInputVerbatim>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="recoveryCode">{{'recoveryCodeTitle' | i18n}}</label>
|
||||
<input id="recoveryCode" class="text-monospace form-control" type="text" name="RecoveryCode" [(ngModel)]="recoveryCode" required
|
||||
appInputVerbatim>
|
||||
<input id="recoveryCode" class="text-monospace form-control" type="text" name="RecoveryCode"
|
||||
[(ngModel)]="recoveryCode" required appInputVerbatim>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
|
||||
@@ -4,31 +4,38 @@
|
||||
<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">
|
||||
<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">
|
||||
<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 !== ''">
|
||||
<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">
|
||||
<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>
|
||||
<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" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword(false)">
|
||||
<i class="fa fa-lg"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
<div class="progress-bar invisible"></div>
|
||||
</div>
|
||||
@@ -38,10 +45,13 @@
|
||||
<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"
|
||||
<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" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword(true)">
|
||||
<i class="fa fa-lg"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,8 +72,10 @@
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe((qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
|
||||
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
@@ -44,6 +44,9 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
this.stateService.save('loginRedirect',
|
||||
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)" class="list-group-item list-group-item-action">
|
||||
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)"
|
||||
class="list-group-item list-group-item-action">
|
||||
<img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="pull-right">
|
||||
<h3>{{p.name}}</h3>
|
||||
{{p.description}}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate autocomplete="off">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5" [ngClass]="{'col-9': selectedProviderType === providerType.Duo || selectedProviderType === providerType.OrganizationDuo}">
|
||||
<div class="col-5"
|
||||
[ngClass]="{'col-9': selectedProviderType === providerType.Duo || selectedProviderType === providerType.OrganizationDuo}">
|
||||
<p class="lead text-center mb-4">{{title}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">{{'enterVerificationCodeApp' | i18n}}</p>
|
||||
<ng-container
|
||||
*ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{'enterVerificationCodeApp' | i18n}}</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="text" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus inputmode="tel" appInputVerbatim>
|
||||
<input id="code" type="text" name="Code" class="form-control" [(ngModel)]="token" required
|
||||
appAutofocus inputmode="tel" appInputVerbatim>
|
||||
<small class="form-text" *ngIf="selectedProviderType === providerType.Email">
|
||||
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise" *ngIf="selectedProviderType === providerType.Email">
|
||||
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise"
|
||||
*ngIf="selectedProviderType === providerType.Email">
|
||||
{{'sendVerificationCodeEmailAgain' | i18n}}
|
||||
</a>
|
||||
</small>
|
||||
@@ -24,8 +29,8 @@
|
||||
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="">
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token"
|
||||
required appAutofocus appInputVerbatim autocomplete="new-password">
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.U2f">
|
||||
@@ -43,9 +48,11 @@
|
||||
<iframe id="duo_iframe"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}" *ngIf="form.loading && selectedProviderType === providerType.U2f"></i>
|
||||
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
|
||||
*ngIf="form.loading && selectedProviderType === providerType.U2f"></i>
|
||||
<div class="form-check" *ngIf="selectedProviderType != null">
|
||||
<input id="remember" type="checkbox" name="Remember" class="form-check-input" [(ngModel)]="remember">
|
||||
<input id="remember" type="checkbox" name="Remember" class="form-check-input"
|
||||
[(ngModel)]="remember">
|
||||
<label for="remember" class="form-check-label">{{'rememberMe' | i18n}}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
@@ -54,7 +61,8 @@
|
||||
</ng-container>
|
||||
<hr>
|
||||
<div class="d-flex mb-3">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"
|
||||
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
|
||||
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.U2f">
|
||||
<span>
|
||||
<i class="fa fa-sign-in"></i> {{'continue' | i18n}}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/pe
|
||||
|
||||
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
|
||||
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
|
||||
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
|
||||
import { SettingsComponent as OrgSettingsComponent } from './organizations/settings/settings.component';
|
||||
import {
|
||||
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
|
||||
@@ -62,6 +63,7 @@ import { PremiumComponent } from './settings/premium.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
||||
import { UserBillingComponent } from './settings/user-billing.component';
|
||||
import { UserSubscriptionComponent } from './settings/user-subscription.component';
|
||||
|
||||
import { BreachReportComponent } from './tools/breach-report.component';
|
||||
import { ExportComponent } from './tools/export.component';
|
||||
@@ -145,7 +147,12 @@ const routes: Routes = [
|
||||
{ path: 'domain-rules', component: DomainRulesComponent, data: { titleId: 'domainRules' } },
|
||||
{ path: 'two-factor', component: TwoFactorSetupComponent, data: { titleId: 'twoStepLogin' } },
|
||||
{ path: 'premium', component: PremiumComponent, data: { titleId: 'goPremium' } },
|
||||
{ path: 'billing', component: UserBillingComponent, data: { titleId: 'billingAndLicensing' } },
|
||||
{ path: 'billing', component: UserBillingComponent, data: { titleId: 'billing' } },
|
||||
{
|
||||
path: 'subscription',
|
||||
component: UserSubscriptionComponent,
|
||||
data: { titleId: 'premiumMembership' },
|
||||
},
|
||||
{ path: 'organizations', component: OrganizationsComponent, data: { titleId: 'organizations' } },
|
||||
{
|
||||
path: 'create-organization',
|
||||
@@ -271,7 +278,12 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'billing',
|
||||
component: OrganizationBillingComponent,
|
||||
data: { titleId: 'billingAndLicensing' },
|
||||
data: { titleId: 'billing' },
|
||||
},
|
||||
{
|
||||
path: 'subscription',
|
||||
component: OrganizationSubscriptionComponent,
|
||||
data: { titleId: 'subscription' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -111,6 +111,9 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.notificationsService.updateConnection(false);
|
||||
this.router.navigate(['lock']);
|
||||
break;
|
||||
case 'lockedUrl':
|
||||
window.setTimeout(() => this.routerService.setPreviousUrl(message.url), 500);
|
||||
break;
|
||||
case 'syncStarted':
|
||||
break;
|
||||
case 'syncCompleted':
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'core-js';
|
||||
import { ToasterModule } from 'angular2-toaster';
|
||||
import { Angulartics2Module } from 'angulartics2';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
@@ -55,8 +56,13 @@ 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,
|
||||
@@ -89,6 +95,7 @@ import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vau
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.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 { ChangeEmailComponent } from './settings/change-email.component';
|
||||
@@ -99,6 +106,7 @@ import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.co
|
||||
import { DeleteAccountComponent } from './settings/delete-account.component';
|
||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||
import { OptionsComponent } from './settings/options.component';
|
||||
import { OrganizationPlansComponent } from './settings/organization-plans.component';
|
||||
import { OrganizationsComponent } from './settings/organizations.component';
|
||||
import { PaymentComponent } from './settings/payment.component';
|
||||
import { PremiumComponent } from './settings/premium.component';
|
||||
@@ -116,6 +124,7 @@ import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.compone
|
||||
import { UpdateKeyComponent } from './settings/update-key.component';
|
||||
import { UpdateLicenseComponent } from './settings/update-license.component';
|
||||
import { UserBillingComponent } from './settings/user-billing.component';
|
||||
import { UserSubscriptionComponent } from './settings/user-subscription.component';
|
||||
import { VerifyEmailComponent } from './settings/verify-email.component';
|
||||
|
||||
import { BreachReportComponent } from './tools/breach-report.component';
|
||||
@@ -149,6 +158,7 @@ import { AutofocusDirective } from 'jslib/angular/directives/autofocus.directive
|
||||
import { BlurClickDirective } from 'jslib/angular/directives/blur-click.directive';
|
||||
import { BoxRowDirective } from 'jslib/angular/directives/box-row.directive';
|
||||
import { FallbackSrcDirective } from 'jslib/angular/directives/fallback-src.directive';
|
||||
import { FlexCopyDirective } from 'jslib/angular/directives/flex-copy.directive';
|
||||
import { InputVerbatimDirective } from 'jslib/angular/directives/input-verbatim.directive';
|
||||
import { StopClickDirective } from 'jslib/angular/directives/stop-click.directive';
|
||||
import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive';
|
||||
@@ -160,13 +170,16 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
||||
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
|
||||
|
||||
import { registerLocaleData } 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 localeEnGb from '@angular/common/locales/en-GB';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import localeEt from '@angular/common/locales/et';
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
import localeIt from '@angular/common/locales/it';
|
||||
import localeJa from '@angular/common/locales/ja';
|
||||
import localeNb from '@angular/common/locales/nb';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
@@ -175,15 +188,20 @@ import localePtPt from '@angular/common/locales/pt-PT';
|
||||
import localeRu from '@angular/common/locales/ru';
|
||||
import localeSk from '@angular/common/locales/sk';
|
||||
import localeSv from '@angular/common/locales/sv';
|
||||
import localeUk from '@angular/common/locales/uk';
|
||||
import localeZhCn from '@angular/common/locales/zh-Hans';
|
||||
import localeZhTw from '@angular/common/locales/zh-Hant';
|
||||
|
||||
registerLocaleData(localeCa, 'ca');
|
||||
registerLocaleData(localeCs, 'cs');
|
||||
registerLocaleData(localeDa, 'da');
|
||||
registerLocaleData(localeDe, 'de');
|
||||
registerLocaleData(localeEnGb, 'en-GB');
|
||||
registerLocaleData(localeEs, 'es');
|
||||
registerLocaleData(localeEt, 'et');
|
||||
registerLocaleData(localeFr, 'fr');
|
||||
registerLocaleData(localeIt, 'it');
|
||||
registerLocaleData(localeJa, 'ja');
|
||||
registerLocaleData(localeNb, 'nb');
|
||||
registerLocaleData(localeNl, 'nl');
|
||||
registerLocaleData(localePl, 'pl');
|
||||
@@ -192,7 +210,9 @@ registerLocaleData(localePtPt, 'pt-PT');
|
||||
registerLocaleData(localeRu, 'ru');
|
||||
registerLocaleData(localeSk, 'sk');
|
||||
registerLocaleData(localeSv, 'sv');
|
||||
registerLocaleData(localeUk, 'uk');
|
||||
registerLocaleData(localeZhCn, 'zh-CN');
|
||||
registerLocaleData(localeZhTw, 'zh-TW');
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -207,10 +227,12 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
},
|
||||
}),
|
||||
ToasterModule.forRoot(),
|
||||
InfiniteScrollModule,
|
||||
],
|
||||
declarations: [
|
||||
AcceptOrganizationComponent,
|
||||
AccountComponent,
|
||||
AddCreditComponent,
|
||||
AddEditComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustSeatsComponent,
|
||||
@@ -230,6 +252,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
ChangeEmailComponent,
|
||||
ChangeKdfComponent,
|
||||
ChangePasswordComponent,
|
||||
ChangePlanComponent,
|
||||
CiphersComponent,
|
||||
CollectionsComponent,
|
||||
ColorPasswordPipe,
|
||||
@@ -238,9 +261,11 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
DeleteAccountComponent,
|
||||
DeleteOrganizationComponent,
|
||||
DomainRulesComponent,
|
||||
DownloadLicenseComponent,
|
||||
ExportComponent,
|
||||
ExposedPasswordsReportComponent,
|
||||
FallbackSrcDirective,
|
||||
FlexCopyDirective,
|
||||
FolderAddEditComponent,
|
||||
FooterComponent,
|
||||
FrontendLayoutComponent,
|
||||
@@ -258,7 +283,10 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
OptionsComponent,
|
||||
OrgAccountComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrganizationBillingComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrganizationSubscriptionComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCiphersComponent,
|
||||
OrgCollectionAddEditComponent,
|
||||
@@ -277,6 +305,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
OrgManageComponent,
|
||||
OrgPeopleComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgSettingComponent,
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
@@ -322,6 +351,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
UpdateLicenseComponent,
|
||||
UserBillingComponent,
|
||||
UserLayoutComponent,
|
||||
UserSubscriptionComponent,
|
||||
VaultComponent,
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
@@ -341,12 +371,14 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
FolderAddEditComponent,
|
||||
ModalComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCollectionAddEditComponent,
|
||||
OrgCollectionsComponent,
|
||||
OrgEntityEventsComponent,
|
||||
OrgEntityUsersComponent,
|
||||
OrgGroupAddEditComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="progress">
|
||||
<div class="progress-bar {{color}}" role="progressbar" [ngStyle]="{width: (scoreWidth + '%')}" attr.aria-valuenow="{{scoreWidth}}"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
<div class="progress-bar {{color}}" role="progressbar" [ngStyle]="{width: (scoreWidth + '%')}"
|
||||
attr.aria-valuenow="{{scoreWidth}}" aria-valuemin="0" aria-valuemax="100">
|
||||
<ng-container *ngIf="showText && text">
|
||||
{{text}}
|
||||
</ng-container>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<router-outlet></router-outlet>
|
||||
<div class="container my-5 text-muted text-center">
|
||||
© 2018, 8bit Solutions LLC
|
||||
© {{year}}, 8bit Solutions LLC
|
||||
<br> {{'versionNumber' | i18n : version}}
|
||||
</div>
|
||||
|
||||
@@ -12,10 +12,12 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
})
|
||||
export class FrontendLayoutComponent implements OnInit, OnDestroy {
|
||||
version: string;
|
||||
year: string = '2015';
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = this.platformUtilsService.getApplicationVersion();
|
||||
document.body.classList.add('layout_frontend');
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
</div>
|
||||
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-item nav-link dropdown-toggle" href="#" id="nav-profile" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="nav-item nav-link dropdown-toggle" href="#" id="nav-profile" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-user-circle fa-lg"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-profile">
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import {
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
|
||||
|
||||
@Component({
|
||||
selector: 'app-organization-layout',
|
||||
templateUrl: 'organization-layout.component.html',
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit {
|
||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
|
||||
private organizationId: string;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
|
||||
ngOnInit() {
|
||||
document.body.classList.remove('layout_frontend');
|
||||
@@ -25,6 +32,20 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'updatedOrgLicense':
|
||||
await this.load();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="externalId">{{'externalId' | i18n}}</label>
|
||||
<input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId">
|
||||
<small class="form-text text-muted">{{'externalIdDesc' | i18n}}</small>
|
||||
</div>
|
||||
<ng-container *ngIf="accessGroups">
|
||||
<h3 class="mt-4 d-flex mb-0">
|
||||
{{'groupAccess' | i18n}}
|
||||
@@ -41,14 +46,17 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let g of groups; let i = index">
|
||||
<td class="table-list-checkbox" (click)="check(g)">
|
||||
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" [disabled]="g.accessAll" appStopProp>
|
||||
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked"
|
||||
[disabled]="g.accessAll" appStopProp>
|
||||
</td>
|
||||
<td (click)="check(g)">
|
||||
{{g.name}}
|
||||
<i class="fa fa-th text-muted fa-fw" *ngIf="g.accessAll" title="This group can access all items"></i>
|
||||
<i class="fa fa-th text-muted fa-fw" *ngIf="g.accessAll"
|
||||
title="This group can access all items"></i>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly" [disabled]="!g.checked || g.accessAll">
|
||||
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly"
|
||||
[disabled]="!g.checked || g.accessAll">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -60,12 +68,15 @@
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<div class="ml-auto">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" title="{{'delete' | i18n}}"
|
||||
*ngIf="editMode" [disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
title="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
title="{{'loading' | i18n}}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
accessGroups: boolean = false;
|
||||
title: string;
|
||||
name: string;
|
||||
externalId: string;
|
||||
groups: GroupResponse[] = [];
|
||||
formPromise: Promise<any>;
|
||||
deletePromise: Promise<any>;
|
||||
@@ -65,6 +66,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
try {
|
||||
const collection = await this.apiService.getCollectionDetails(this.organizationId, this.collectionId);
|
||||
this.name = await this.cryptoService.decryptToUtf8(new CipherString(collection.name), this.orgKey);
|
||||
this.externalId = collection.externalId;
|
||||
if (collection.groups != null && this.groups.length > 0) {
|
||||
collection.groups.forEach((s) => {
|
||||
const group = this.groups.filter((g) => !g.accessAll && g.id === s.id);
|
||||
@@ -109,6 +111,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
|
||||
const request = new CollectionRequest();
|
||||
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));
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<div class="ml-auto d-flex">
|
||||
<div>
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}" [(ngModel)]="searchText">
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
@@ -22,7 +23,8 @@
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
|
||||
@@ -54,8 +54,11 @@ export class CollectionsComponent implements OnInit {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
<div class="d-flex">
|
||||
<div class="form-inline">
|
||||
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start" placeholder="{{'startDate' | i18n}}" [(ngModel)]="start"
|
||||
placeholder="YYYY-MM-DDTHH:MM">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start"
|
||||
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM">
|
||||
<span class="mx-2">-</span>
|
||||
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end" placeholder="{{'endDate' | i18n}}" [(ngModel)]="end"
|
||||
placeholder="YYYY-MM-DDTHH:MM">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end"
|
||||
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM">
|
||||
</div>
|
||||
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||
<button #refreshBtn [appApiAction]="refreshPromise" type="button"
|
||||
class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||
[disabled]="loaded && refreshBtn.loading">
|
||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
|
||||
{{'refresh' | i18n}}
|
||||
@@ -58,8 +59,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit" (click)="loadEvents(false)"
|
||||
[disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
|
||||
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'loadMore' | i18n}}</span>
|
||||
</button>
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
<div class="modal-body" *ngIf="loading || !users">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading && users && (users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<div class="modal-body"
|
||||
*ngIf="!loading && users && (users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<div class="d-flex">
|
||||
<div class="mr-3">
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
name="SearchText" [(ngModel)]="searchText">
|
||||
<input type="search" class="form-control form-control-sm" id="search"
|
||||
placeholder="{{'search' | i18n}}" name="SearchText" [(ngModel)]="searchText">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: !showSelected}"
|
||||
@@ -53,7 +54,8 @@
|
||||
<tr *ngFor="let u of searchedUsers">
|
||||
<td class="table-list-checkbox" (click)="check(u)">
|
||||
<input type="checkbox" [(ngModel)]="u.checked" name="{{u.id.substr(0,8)}}_Checked"
|
||||
[disabled]="entity === 'collection' && u.accessAll" (change)="selectedChanged(u)" appStopProp>
|
||||
[disabled]="entity === 'collection' && u.accessAll"
|
||||
(change)="selectedChanged(u)" appStopProp>
|
||||
</td>
|
||||
<td width="30" (click)="check(u)">
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
|
||||
@@ -61,9 +63,11 @@
|
||||
</td>
|
||||
<td>
|
||||
{{u.email}}
|
||||
<span class="badge badge-secondary" *ngIf="u.status === organizationUserStatusType.Invited">{{'invited'
|
||||
<span class="badge badge-secondary"
|
||||
*ngIf="u.status === organizationUserStatusType.Invited">{{'invited'
|
||||
| i18n}}</span>
|
||||
<span class="badge badge-warning" *ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted'
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted'
|
||||
| i18n}}</span>
|
||||
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||
</td>
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="form-inline">
|
||||
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start" placeholder="{{'startDate' | i18n}}" [(ngModel)]="start"
|
||||
placeholder="YYYY-MM-DDTHH:MM">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start"
|
||||
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM">
|
||||
<span class="mx-2">-</span>
|
||||
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end" placeholder="{{'endDate' | i18n}}" [(ngModel)]="end"
|
||||
placeholder="YYYY-MM-DDTHH:MM">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end"
|
||||
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM">
|
||||
</div>
|
||||
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||
[disabled]="loaded && refreshBtn.loading">
|
||||
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3"
|
||||
(click)="loadEvents(true)" [disabled]="loaded && refreshBtn.loading">
|
||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
|
||||
{{'refresh' | i18n}}
|
||||
</button>
|
||||
@@ -44,8 +44,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit" (click)="loadEvents(false)"
|
||||
[disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
|
||||
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'loadMore' | i18n}}</span>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="form-group">
|
||||
<label for="externalId">{{'externalId' | i18n}}</label>
|
||||
<input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId">
|
||||
<small class="form-text text-muted">{{'externalIdGroupDesc' | i18n}}</small>
|
||||
<small class="form-text text-muted">{{'externalIdDesc' | i18n}}</small>
|
||||
</div>
|
||||
<h3 class="mt-4 d-flex">
|
||||
<div class="mb-2">
|
||||
@@ -35,13 +35,15 @@
|
||||
</h3>
|
||||
<div class="form-group" [ngClass]="{'mb-0': access !== 'selected'}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all" [(ngModel)]="access">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all"
|
||||
[(ngModel)]="access">
|
||||
<label class="form-check-label" for="accessAll">
|
||||
{{'groupAccessAllItems' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected" [(ngModel)]="access">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected"
|
||||
[(ngModel)]="access">
|
||||
<label class="form-check-label" for="accessSelected">
|
||||
{{'groupAccessSelectedCollections' | i18n}}
|
||||
</label>
|
||||
@@ -62,13 +64,15 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index">
|
||||
<td class="table-list-checkbox" (click)="check(c)">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
|
||||
appStopProp>
|
||||
</td>
|
||||
<td (click)="check(c)">
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
|
||||
[disabled]="!c.checked">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -80,12 +84,15 @@
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<div class="ml-auto">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" title="{{'delete' | i18n}}" *ngIf="editMode"
|
||||
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
title="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
title="{{'loading' | i18n}}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<div class="ml-auto d-flex">
|
||||
<div>
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}" [(ngModel)]="searchText">
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
@@ -22,7 +23,8 @@
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
|
||||
@@ -56,8 +56,11 @@ export class GroupsComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
<div class="card" *ngIf="organization">
|
||||
<div class="card-header">{{'manage' | i18n}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="people" class="list-group-item" routerLinkActive="active" *ngIf="organization.isAdmin">
|
||||
<a routerLink="people" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin">
|
||||
{{'people' | i18n}}
|
||||
</a>
|
||||
<a routerLink="collections" class="list-group-item" routerLinkActive="active">
|
||||
{{'collections' | i18n}}
|
||||
</a>
|
||||
<a routerLink="groups" class="list-group-item" routerLinkActive="active" *ngIf="organization.isAdmin && accessGroups">
|
||||
<a routerLink="groups" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin && accessGroups">
|
||||
{{'groups' | i18n}}
|
||||
</a>
|
||||
<a routerLink="events" class="list-group-item" routerLinkActive="active" *ngIf="organization.isAdmin && accessEvents">
|
||||
<a routerLink="events" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin && accessEvents">
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
<h1>{{'people' | i18n}}</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}" (click)="filter(null)">
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}"
|
||||
(click)="filter(null)">
|
||||
{{'all' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == organizationUserStatusType.Invited}"
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
[ngClass]="{active: status == organizationUserStatusType.Invited}"
|
||||
(click)="filter(organizationUserStatusType.Invited)">
|
||||
{{'invited' | i18n}}
|
||||
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == organizationUserStatusType.Accepted}"
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
[ngClass]="{active: status == organizationUserStatusType.Accepted}"
|
||||
(click)="filter(organizationUserStatusType.Accepted)">
|
||||
{{'accepted' | i18n}}
|
||||
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
|
||||
@@ -18,7 +21,8 @@
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}" [(ngModel)]="searchText">
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
@@ -37,14 +41,20 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let u of searchedUsers">
|
||||
<td width="30">
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
|
||||
[fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
|
||||
<span class="badge badge-secondary" *ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
|
||||
<span class="badge badge-warning" *ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||
<span class="badge badge-secondary"
|
||||
*ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-lock" *ngIf="u.twoFactorEnabled" title="{{'userUsingTwoStep' | i18n}}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
|
||||
@@ -53,15 +63,18 @@
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)" *ngIf="u.status === organizationUserStatusType.Invited">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
|
||||
*ngIf="u.status === organizationUserStatusType.Invited">
|
||||
<i class="fa fa-fw fa-envelope-o"></i>
|
||||
{{'resendInvitation' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)" *ngIf="u.status === organizationUserStatusType.Accepted">
|
||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
|
||||
*ngIf="u.status === organizationUserStatusType.Accepted">
|
||||
<i class="fa fa-fw fa-check"></i>
|
||||
{{'confirm' | i18n}}
|
||||
</a>
|
||||
@@ -69,7 +82,8 @@
|
||||
<i class="fa fa-fw fa-sitemap"></i>
|
||||
{{'groups' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="events(u)" *ngIf="accessEvents && u.status === organizationUserStatusType.Confirmed">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
|
||||
*ngIf="accessEvents && u.status === organizationUserStatusType.Confirmed">
|
||||
<i class="fa fa-fw fa-file-text-o"></i>
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
|
||||
@@ -81,7 +81,7 @@ export class PeopleComponent implements OnInit {
|
||||
this.accessGroups = organization.useGroups;
|
||||
await this.load();
|
||||
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
if (qParams.viewEvents != null) {
|
||||
const user = this.users.filter((u) => u.id === qParams.viewEvents);
|
||||
@@ -89,6 +89,9 @@ export class PeopleComponent implements OnInit {
|
||||
this.events(user[0]);
|
||||
}
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -239,7 +242,7 @@ export class PeopleComponent implements OnInit {
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.groupsModalRef.createComponent(factory).instance;
|
||||
this.modal = this.confirmModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<UserConfirmComponent>(
|
||||
UserConfirmComponent, this.confirmModalRef);
|
||||
|
||||
|
||||
@@ -24,28 +24,32 @@
|
||||
</ng-container>
|
||||
<h3>{{'userType' | i18n}}</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">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeUser"
|
||||
[value]="organizationUserType.User" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeUser">
|
||||
{{'user' | i18n}}
|
||||
<small>{{'userDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeManager" [value]="organizationUserType.Manager" [(ngModel)]="type">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeManager"
|
||||
[value]="organizationUserType.Manager" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeManager">
|
||||
{{'manager' | i18n}}
|
||||
<small>{{'managerDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeAdmin" [value]="organizationUserType.Admin" [(ngModel)]="type">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeAdmin"
|
||||
[value]="organizationUserType.Admin" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeAdmin">
|
||||
{{'admin' | i18n}}
|
||||
<small>{{'adminDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeOwner" [value]="organizationUserType.Owner" [(ngModel)]="type">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeOwner"
|
||||
[value]="organizationUserType.Owner" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeOwner">
|
||||
{{'owner' | i18n}}
|
||||
<small>{{'ownerDesc' | i18n}}</small>
|
||||
@@ -66,13 +70,15 @@
|
||||
</h3>
|
||||
<div class="form-group" [ngClass]="{'mb-0': access !== 'selected'}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all" [(ngModel)]="access">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all"
|
||||
[(ngModel)]="access">
|
||||
<label class="form-check-label" for="accessAll">
|
||||
{{'userAccessAllItems' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected" [(ngModel)]="access">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected"
|
||||
[(ngModel)]="access">
|
||||
<label class="form-check-label" for="accessSelected">
|
||||
{{'userAccessSelectedCollections' | i18n}}
|
||||
</label>
|
||||
@@ -93,13 +99,15 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index">
|
||||
<td class="table-list-checkbox" (click)="check(c)">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
|
||||
appStopProp>
|
||||
</td>
|
||||
<td (click)="check(c)">
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
|
||||
[disabled]="!c.checked">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -111,12 +119,15 @@
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<div class="ml-auto">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" title="{{'delete' | i18n}}" *ngIf="editMode"
|
||||
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
title="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
title="{{'loading' | i18n}}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
</p>
|
||||
<p><code>{{fingerprint}}</code></p>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="dontAskAgain" name="DontAskAgain" [(ngModel)]="dontAskAgain">
|
||||
<input class="form-check-input" type="checkbox" id="dontAskAgain" name="DontAskAgain"
|
||||
[(ngModel)]="dontAskAgain">
|
||||
<label class="form-check-label" for="dontAskAgain">
|
||||
{{'dontAskFingerprintAgain' | i18n}}
|
||||
</label>
|
||||
@@ -29,7 +30,8 @@
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'confirm' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="org.billingEmail">
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
|
||||
[(ngModel)]="org.billingEmail">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="org.businessName">
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName"
|
||||
[(ngModel)]="org.businessName">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@@ -29,6 +31,19 @@
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="canUseApi">
|
||||
<div class="secondary-header border-0 mb-0">
|
||||
<h1>{{'apiKey' | i18n}}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{'apiKeyDesc' | i18n}}
|
||||
<a href="https://docs.bitwarden.com" target="_blank" rel="noopener">
|
||||
{{'learnMore' | i18n}}
|
||||
</a>
|
||||
</p>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="viewApiKey()">{{'viewApiKey' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="rotateApiKey()">{{'rotateApiKey' | i18n}}</button>
|
||||
</ng-container>
|
||||
<div class="secondary-header border-0 mb-0">
|
||||
<h1>{{'taxInformation' | i18n}}</h1>
|
||||
</div>
|
||||
@@ -42,9 +57,12 @@
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<p>{{'dangerZoneDesc' | i18n}}</p>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="deleteOrganization()">{{'deleteOrganization' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
(click)="deleteOrganization()">{{'deleteOrganization' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">{{'purgeVault' | i18n}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #deleteOrganizationTemplate></ng-template>
|
||||
<ng-template #purgeOrganizationTemplate></ng-template>
|
||||
<ng-template #apiKeyTemplate></ng-template>
|
||||
<ng-template #rotateApiKeyTemplate></ng-template>
|
||||
|
||||
@@ -18,7 +18,9 @@ import { OrganizationResponse } from 'jslib/models/response/organizationResponse
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
|
||||
import { ApiKeyComponent } from './api-key.component';
|
||||
import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
import { RotateApiKeyComponent } from './rotate-api-key.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-account',
|
||||
@@ -27,8 +29,11 @@ import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
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;
|
||||
|
||||
loading = true;
|
||||
canUseApi = false;
|
||||
org: OrganizationResponse;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
@@ -45,6 +50,7 @@ export class AccountComponent {
|
||||
this.organizationId = params.organizationId;
|
||||
try {
|
||||
this.org = await this.apiService.getOrganization(this.organizationId);
|
||||
this.canUseApi = this.org.useApi;
|
||||
} catch { }
|
||||
});
|
||||
this.loading = false;
|
||||
@@ -95,4 +101,34 @@ export class AccountComponent {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
viewApiKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
rotateApiKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
||||
(click)="cancel()"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{(add ? 'addSeats' : 'removeSeats') | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="seatAdjustment">{{(add ? 'seatsToAdd' : 'seatsToRemove') | i18n}}</label>
|
||||
<input id="seatAdjustment" class="form-control" type="number" name="SeatAdjustment" [(ngModel)]="seatAdjustment" min="0"
|
||||
step="1" required>
|
||||
<input id="seatAdjustment" class="form-control" type="number" name="SeatAdjustment"
|
||||
[(ngModel)]="seatAdjustment" min="0" step="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="add" class="mb-3">
|
||||
|
||||
48
src/app/organizations/settings/api-key.component.html
Normal file
48
src/app/organizations/settings/api-key.component.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{'apiKey' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'apiKeyDesc' | 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}}"></i>
|
||||
<span>{{'viewApiKey' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
50
src/app/organizations/settings/api-key.component.ts
Normal file
50
src/app/organizations/settings/api-key.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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-api-key',
|
||||
templateUrl: 'api-key.component.html',
|
||||
})
|
||||
export class ApiKeyComponent {
|
||||
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.postOrganizationApiKey(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: 'Viewed Organization API Key' });
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
11
src/app/organizations/settings/change-plan.component.html
Normal file
11
src/app/organizations/settings/change-plan.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="card card-org-plans">
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
||||
(click)="cancel()"><span 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>
|
||||
</div>
|
||||
</div>
|
||||
34
src/app/organizations/settings/change-plan.component.ts
Normal file
34
src/app/organizations/settings/change-plan.component.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-plan',
|
||||
templateUrl: 'change-plan.component.html',
|
||||
})
|
||||
export class ChangePlanComponent {
|
||||
@Input() organizationId: string;
|
||||
@Output() onChanged = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.platformUtilsService.eventTrack('Changed Plan');
|
||||
this.onChanged.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@
|
||||
<p>{{'deleteOrganizationDesc' | i18n}}</p>
|
||||
<app-callout type="warning">{{'deleteOrganizationWarning' | i18n}}</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
|
||||
appAutofocus appInputVerbatim>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
||||
(click)="cancel()"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{'downloadLicense' | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<div class="d-flex">
|
||||
<label for="installationId">{{'enterInstallationId' | i18n}}</label>
|
||||
<a class="ml-auto" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}"
|
||||
href="https://help.bitwarden.com/article/licensing-on-premise/#organization-account-sharing">
|
||||
<i class="fa fa-question-circle-o"></i>
|
||||
</a>
|
||||
</div>
|
||||
<input id="installationId" class="form-control" type="text" name="InstallationId"
|
||||
[(ngModel)]="installationId" required>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
43
src/app/organizations/settings/download-license.component.ts
Normal file
43
src/app/organizations/settings/download-license.component.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-download-license',
|
||||
templateUrl: 'download-license.component.html',
|
||||
})
|
||||
export class DownloadLicenseComponent {
|
||||
@Input() organizationId: string;
|
||||
@Output() onDownloaded = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
installationId: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async submit() {
|
||||
if (this.installationId == null || this.installationId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.apiService.getOrganizationLicense(this.organizationId, this.installationId);
|
||||
const license = await this.formPromise;
|
||||
const licenseString = JSON.stringify(license, null, 2);
|
||||
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json');
|
||||
this.platformUtilsService.eventTrack('Downloaded License');
|
||||
this.onDownloaded.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{'billingAndLicensing' | i18n}}
|
||||
<small>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="firstLoaded && loading" title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!firstLoaded && loading" title="{{'loading' | i18n}}"></i>
|
||||
<ng-container *ngIf="billing">
|
||||
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">{{'subscriptionCanceled' | i18n}}</app-callout>
|
||||
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
|
||||
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
|
||||
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()" [appApiAction]="reinstatePromise"
|
||||
[disabled]="reinstateBtn.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'reinstateSubscription' | i18n}}</span>
|
||||
</button>
|
||||
</app-callout>
|
||||
<dl *ngIf="selfHosted">
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{billing.plan}}</dd>
|
||||
<dt>{{'expiration' | i18n}}</dt>
|
||||
<dd *ngIf="billing.expiration">
|
||||
{{billing.expiration | date:'mediumDate'}}
|
||||
<span *ngIf="isExpired" class="text-danger ml-2">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
{{'licenseIsExpired' | i18n}}
|
||||
</span>
|
||||
</dd>
|
||||
<dd *ngIf="!billing.expiration">{{'neverExpires' | i18n}}</dd>
|
||||
</dl>
|
||||
<div class="row" *ngIf="!selfHosted">
|
||||
<div class="col-4">
|
||||
<dl>
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{billing.plan}}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{'status' | i18n}}</dt>
|
||||
<dd>
|
||||
<span class="text-capitalize">{{subscription.status || '-'}}</span>
|
||||
<span class="badge badge-warning" *ngIf="subscriptionMarkedForCancel">{{'pendingCancellation' | i18n}}</span>
|
||||
</dd>
|
||||
<dt>{{'nextCharge' | i18n}}</dt>
|
||||
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$'))
|
||||
: '-'}}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-8" *ngIf="subscription">
|
||||
<strong class="d-block mb-1">{{'details' | i18n}}</strong>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr *ngFor="let i of subscription.items">
|
||||
<td>
|
||||
{{i.name}} {{i.quantity > 1 ? '×' + i.quantity : ''}} @ {{i.amount | currency:'$'}}
|
||||
</td>
|
||||
<td>
|
||||
{{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
|
||||
{{'updateLicense' | i18n}}
|
||||
</button>
|
||||
<a href="https://vault.bitwarden.com" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
{{'manageSubscription' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card mt-3" *ngIf="showUpdateLicense">
|
||||
<div class="card-body">
|
||||
<h3 class="card-body-header">{{'updateLicense' | i18n}}</h3>
|
||||
<app-update-license [organizationId]="organizationId" (onUpdated)="closeUpdateLicense(true)" (onCanceled)="closeUpdateLicense(false)"></app-update-license>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selfHosted">
|
||||
<div class="d-flex">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="changePlan()">
|
||||
{{'changeBillingPlan' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-submit ml-1" #licenseBtn [appApiAction]="licensePromise" [disabled]="licenseBtn.loading"
|
||||
(click)="downloadLicense()" *ngIf="canDownloadLicense">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'downloadLicense' | i18n}}</span>
|
||||
</button>
|
||||
<button #cancelBtn type="button" class="btn btn-outline-danger btn-submit ml-auto" (click)="cancel()" [appApiAction]="cancelPromise"
|
||||
[disabled]="cancelBtn.loading" *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'cancelSubscription' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{'userSeats' | i18n}}</h2>
|
||||
<p>{{'subscriptionUserSeats' | i18n : billing.seats}}</p>
|
||||
<ng-container *ngIf="subscription && canAdjustSeats">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustSeats">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="adjustSeats(true)">
|
||||
{{'addSeats' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustSeats(false)">
|
||||
{{'removeSeats' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-seats [seatPrice]="seatPrice" [add]="adjustSeatsAdd" [organizationId]="organizationId" [interval]="billingInterval"
|
||||
(onAdjusted)="closeSeats(true)" (onCanceled)="closeSeats(false)" *ngIf="showAdjustSeats"></app-adjust-seats>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
|
||||
<p>{{'subscriptionStorage' | i18n : billing.maxStorageGb || 0 : billing.storageName || '0 MB'}}</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success" role="progressbar" [ngStyle]="{width: storageProgressWidth + '%' }" [attr.aria-valuenow]="storagePercentage"
|
||||
aria-valuemin="0" aria-valuemax="100">{{(storagePercentage / 100) | percent}}</div>
|
||||
</div>
|
||||
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel && paymentSource">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustStorage">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="adjustStorage(true)">
|
||||
{{'addStorage' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustStorage(false)">
|
||||
{{'removeStorage' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-storage [storageGbPrice]="storageGbPrice" [add]="adjustStorageAdd" [organizationId]="organizationId" [interval]="billingInterval"
|
||||
(onAdjusted)="closeStorage(true)" (onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="spaced-header">{{'paymentMethod' | i18n}}</h2>
|
||||
<p *ngIf="!paymentSource">{{'noPaymentMethod' | i18n}}</p>
|
||||
<ng-container *ngIf="paymentSource">
|
||||
<app-callout type="warning" title="{{'verifyBankAccount' | i18n}}" *ngIf="paymentSource.type === paymentMethodType.BankAccount && paymentSource.needsVerification">
|
||||
<p>{{'verifyBankAccountDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}</p>
|
||||
<form #verifyForm class="form-inline" (ngSubmit)="verifyBank()" [appApiAction]="verifyBankPromise" ngNativeValidate>
|
||||
<label class="sr-only" for="verifyAmount1">{{'amount' | i18n : '1'}}</label>
|
||||
<div class="input-group mr-2">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">$0.</div>
|
||||
</div>
|
||||
<input type="number" class="form-control" id="verifyAmount1" placeholder="xx" name="Amount1" [(ngModel)]="verifyAmount1"
|
||||
min="1" max="99" step="1" required>
|
||||
</div>
|
||||
<label class="sr-only" for="verifyAmount2">{{'amount' | i18n : '2'}}</label>
|
||||
<div class="input-group mr-2">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">$0.</div>
|
||||
</div>
|
||||
<input type="number" class="form-control" id="verifyAmount2" placeholder="xx" name="Amount2" [(ngModel)]="verifyAmount2"
|
||||
min="1" max="99" step="1" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary btn-submit" [disabled]="verifyForm.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'verifyBankAccount' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
</app-callout>
|
||||
<p>
|
||||
<i class="fa fa-fw" [ngClass]="{'fa-credit-card': paymentSource.type === paymentMethodType.Card,
|
||||
'fa-university': paymentSource.type === paymentMethodType.BankAccount,
|
||||
'fa-paypal text-primary': paymentSource.type === paymentMethodType.PayPal}"></i>
|
||||
{{paymentSource.description}}
|
||||
</p>
|
||||
</ng-container>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="changePayment()" *ngIf="!showAdjustPayment">
|
||||
{{(paymentSource ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}
|
||||
</button>
|
||||
<app-adjust-payment [currentType]="paymentSource != null ? paymentSource.type : null" [organizationId]="organizationId" (onAdjusted)="closePayment(true)"
|
||||
(onCanceled)="closePayment(false)" *ngIf="showAdjustPayment">
|
||||
</app-adjust-payment>
|
||||
<h2 class="spaced-header">{{'charges' | i18n}}</h2>
|
||||
<p *ngIf="!charges || !charges.length">{{'noCharges' | i18n}}</p>
|
||||
<table class="table mb-2" *ngIf="charges && charges.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of charges">
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="viewInvoice(c)" title="{{'invoice' | i18n}}">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{c.createdDate | date:'mediumDate'}}</td>
|
||||
<td>{{c.paymentSource ? c.paymentSource.description : '-'}}</td>
|
||||
<td class="text-capitalize">{{c.status}}</td>
|
||||
<td [ngClass]="{'text-strike':c.refunded}" title="{{(c.refunded ? 'refunded' : '') | i18n}}">{{c.amount | currency:'$'}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<small class="text-muted">* {{'chargesStatement' | i18n : 'BITWARDEN'}}</small>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -7,49 +7,20 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { VerifyBankRequest } from 'jslib/models/request/verifyBankRequest';
|
||||
|
||||
import { BillingChargeResponse } from 'jslib/models/response/billingResponse';
|
||||
import { OrganizationBillingResponse } from 'jslib/models/response/organizationBillingResponse';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
|
||||
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { UserBillingComponent } from '../../settings/user-billing.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-billing',
|
||||
templateUrl: 'organization-billing.component.html',
|
||||
templateUrl: '../../settings/user-billing.component.html',
|
||||
})
|
||||
export class OrganizationBillingComponent implements OnInit {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
organizationId: string;
|
||||
adjustSeatsAdd = true;
|
||||
showAdjustSeats = false;
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
showAdjustPayment = false;
|
||||
showUpdateLicense = false;
|
||||
billing: OrganizationBillingResponse;
|
||||
paymentMethodType = PaymentMethodType;
|
||||
selfHosted = false;
|
||||
verifyAmount1: number;
|
||||
verifyAmount2: number;
|
||||
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
licensePromise: Promise<any>;
|
||||
verifyBankPromise: Promise<any>;
|
||||
|
||||
constructor(private tokenService: TokenService, private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
export class OrganizationBillingComponent extends UserBillingComponent implements OnInit {
|
||||
constructor(apiService: ApiService, i18nService: I18nService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
private route: ActivatedRoute) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
super(apiService, i18nService, analytics, toasterService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -59,222 +30,4 @@ export class OrganizationBillingComponent implements OnInit {
|
||||
this.firstLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.billing = await this.apiService.getOrganizationBilling(this.organizationId);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async reinstate() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('reinstateConfirmation'),
|
||||
this.i18nService.t('reinstateSubscription'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.reinstatePromise = this.apiService.postOrganizationReinstate(this.organizationId);
|
||||
await this.reinstatePromise;
|
||||
this.analytics.eventTrack.next({ action: 'Reinstated Plan' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('reinstated'));
|
||||
this.load();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('cancelConfirmation'),
|
||||
this.i18nService.t('cancelSubscription'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.cancelPromise = this.apiService.postOrganizationCancel(this.organizationId);
|
||||
await this.cancelPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Canceled Plan' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription'));
|
||||
this.load();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async changePlan() {
|
||||
const contactSupport = await this.platformUtilsService.showDialog(this.i18nService.t('changeBillingPlanDesc'),
|
||||
this.i18nService.t('changeBillingPlan'), this.i18nService.t('contactSupport'), this.i18nService.t('close'));
|
||||
if (contactSupport) {
|
||||
this.platformUtilsService.launchUri('https://bitwarden.com/contact');
|
||||
}
|
||||
}
|
||||
|
||||
async downloadLicense() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installationId = window.prompt(this.i18nService.t('enterInstallationId'));
|
||||
if (installationId == null || installationId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.licensePromise = this.apiService.getOrganizationLicense(this.organizationId, installationId);
|
||||
const license = await this.licensePromise;
|
||||
const licenseString = JSON.stringify(license, null, 2);
|
||||
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json');
|
||||
} catch { }
|
||||
}
|
||||
|
||||
updateLicense() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.showUpdateLicense = true;
|
||||
}
|
||||
|
||||
async verifyBank() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const request = new VerifyBankRequest();
|
||||
request.amount1 = this.verifyAmount1;
|
||||
request.amount2 = this.verifyAmount2;
|
||||
this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(this.organizationId, request);
|
||||
await this.verifyBankPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Verified Bank Account' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('verifiedBankAccount'));
|
||||
this.load();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
closeUpdateLicense(load: boolean) {
|
||||
this.showUpdateLicense = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
adjustSeats(add: boolean) {
|
||||
this.adjustSeatsAdd = add;
|
||||
this.showAdjustSeats = true;
|
||||
}
|
||||
|
||||
closeSeats(load: boolean) {
|
||||
this.showAdjustSeats = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
adjustStorage(add: boolean) {
|
||||
this.adjustStorageAdd = add;
|
||||
this.showAdjustStorage = true;
|
||||
}
|
||||
|
||||
closeStorage(load: boolean) {
|
||||
this.showAdjustStorage = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
changePayment() {
|
||||
this.showAdjustPayment = true;
|
||||
}
|
||||
|
||||
closePayment(load: boolean) {
|
||||
this.showAdjustPayment = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
async viewInvoice(charge: BillingChargeResponse) {
|
||||
const token = await this.tokenService.getToken();
|
||||
const url = this.apiService.apiBaseUrl + '/organizations/' + this.organizationId +
|
||||
'/billing-invoice/' + charge.invoiceId + '?access_token=' + token;
|
||||
this.platformUtilsService.launchUri(url);
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return this.billing != null && this.billing.expiration != null &&
|
||||
new Date(this.billing.expiration) < new Date();
|
||||
}
|
||||
|
||||
get subscriptionMarkedForCancel() {
|
||||
return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
|
||||
}
|
||||
|
||||
get subscription() {
|
||||
return this.billing != null ? this.billing.subscription : null;
|
||||
}
|
||||
|
||||
get nextInvoice() {
|
||||
return this.billing != null ? this.billing.upcomingInvoice : null;
|
||||
}
|
||||
|
||||
get paymentSource() {
|
||||
return this.billing != null ? this.billing.paymentSource : null;
|
||||
}
|
||||
|
||||
get charges() {
|
||||
return this.billing != null ? this.billing.charges : null;
|
||||
}
|
||||
|
||||
get storagePercentage() {
|
||||
return this.billing != null && this.billing.maxStorageGb ?
|
||||
+(100 * (this.billing.storageGb / this.billing.maxStorageGb)).toFixed(2) : 0;
|
||||
}
|
||||
|
||||
get storageProgressWidth() {
|
||||
return this.storagePercentage < 5 ? 5 : 0;
|
||||
}
|
||||
|
||||
get billingInterval() {
|
||||
const monthly = this.billing.planType === PlanType.EnterpriseMonthly ||
|
||||
this.billing.planType === PlanType.TeamsMonthly;
|
||||
return monthly ? 'month' : 'year';
|
||||
}
|
||||
|
||||
get storageGbPrice() {
|
||||
return this.billingInterval === 'month' ? 0.5 : 4;
|
||||
}
|
||||
|
||||
get seatPrice() {
|
||||
switch (this.billing.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;
|
||||
}
|
||||
}
|
||||
|
||||
get canAdjustSeats() {
|
||||
return this.billing.planType === PlanType.EnterpriseMonthly ||
|
||||
this.billing.planType === PlanType.EnterpriseAnnually ||
|
||||
this.billing.planType === PlanType.TeamsMonthly || this.billing.planType === PlanType.TeamsAnnually;
|
||||
}
|
||||
|
||||
get canDownloadLicense() {
|
||||
return (this.billing.planType !== PlanType.Free && this.subscription == null) ||
|
||||
(this.subscription != null && !this.subscription.cancelled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{'subscription' | i18n}}
|
||||
<small>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="firstLoaded && loading" title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!firstLoaded && loading" title="{{'loading' | i18n}}"></i>
|
||||
<ng-container *ngIf="sub">
|
||||
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">
|
||||
{{'subscriptionCanceled' | i18n}}</app-callout>
|
||||
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
|
||||
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
|
||||
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()"
|
||||
[appApiAction]="reinstatePromise" [disabled]="reinstateBtn.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'reinstateSubscription' | i18n}}</span>
|
||||
</button>
|
||||
</app-callout>
|
||||
<dl *ngIf="selfHosted">
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<dt>{{'expiration' | i18n}}</dt>
|
||||
<dd *ngIf="sub.expiration">
|
||||
{{sub.expiration | date:'mediumDate'}}
|
||||
<span *ngIf="isExpired" class="text-danger ml-2">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
{{'licenseIsExpired' | i18n}}
|
||||
</span>
|
||||
</dd>
|
||||
<dd *ngIf="!sub.expiration">{{'neverExpires' | i18n}}</dd>
|
||||
</dl>
|
||||
<div class="row" *ngIf="!selfHosted">
|
||||
<div class="col-4">
|
||||
<dl>
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{'status' | i18n}}</dt>
|
||||
<dd>
|
||||
<span class="text-capitalize">{{subscription.status || '-'}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="subscriptionMarkedForCancel">{{'pendingCancellation' | i18n}}</span>
|
||||
</dd>
|
||||
<dt>{{'nextCharge' | i18n}}</dt>
|
||||
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$'))
|
||||
: '-'}}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-8" *ngIf="subscription">
|
||||
<strong class="d-block mb-1">{{'details' | i18n}}</strong>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr *ngFor="let i of subscription.items">
|
||||
<td>
|
||||
{{i.name}} {{i.quantity > 1 ? '×' + i.quantity : ''}} @ {{i.amount | currency:'$'}}
|
||||
</td>
|
||||
<td>
|
||||
{{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
|
||||
{{'updateLicense' | i18n}}
|
||||
</button>
|
||||
<a href="https://vault.bitwarden.com" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
{{'manageSubscription' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card mt-3" *ngIf="showUpdateLicense">
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
||||
(click)="closeUpdateLicense(false)"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{'updateLicense' | i18n}}</h3>
|
||||
<app-update-license [organizationId]="organizationId" (onUpdated)="closeUpdateLicense(true)"
|
||||
(onCanceled)="closeUpdateLicense(false)"></app-update-license>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selfHosted">
|
||||
<div class="d-flex">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="changePlan()" *ngIf="!showChangePlan">
|
||||
{{'changeBillingPlan' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="downloadLicense()"
|
||||
*ngIf="canDownloadLicense" [disabled]="showDownloadLicense">
|
||||
{{'downloadLicense' | i18n}}
|
||||
</button>
|
||||
<button #cancelBtn type="button" class="btn btn-outline-danger btn-submit ml-auto" (click)="cancel()"
|
||||
[appApiAction]="cancelPromise" [disabled]="cancelBtn.loading"
|
||||
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'cancelSubscription' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-change-plan [organizationId]="organizationId" (onChanged)="closeChangePlan(true)"
|
||||
(onCanceled)="closeChangePlan(false)" *ngIf="showChangePlan"></app-change-plan>
|
||||
<div class="mt-3" *ngIf="showDownloadLicense">
|
||||
<app-download-license [organizationId]="organizationId" (onDownloaded)="closeDownloadLicense()"
|
||||
(onCanceled)="closeDownloadLicense()"></app-download-license>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{'userSeats' | i18n}}</h2>
|
||||
<p>{{'subscriptionUserSeats' | i18n : sub.seats}}</p>
|
||||
<ng-container *ngIf="subscription && canAdjustSeats && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustSeats">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="adjustSeats(true)">
|
||||
{{'addSeats' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustSeats(false)">
|
||||
{{'removeSeats' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-seats [seatPrice]="seatPrice" [add]="adjustSeatsAdd" [organizationId]="organizationId"
|
||||
[interval]="billingInterval" (onAdjusted)="closeSeats(true)" (onCanceled)="closeSeats(false)"
|
||||
*ngIf="showAdjustSeats"></app-adjust-seats>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
|
||||
<p>{{'subscriptionStorage' | i18n : sub.maxStorageGb || 0 : sub.storageName || '0 MB'}}</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success" role="progressbar" [ngStyle]="{width: storageProgressWidth + '%' }"
|
||||
[attr.aria-valuenow]="storagePercentage" aria-valuemin="0" aria-valuemax="100">
|
||||
{{(storagePercentage / 100) | percent}}</div>
|
||||
</div>
|
||||
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustStorage">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="adjustStorage(true)">
|
||||
{{'addStorage' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustStorage(false)">
|
||||
{{'removeStorage' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-storage [storageGbPrice]="storageGbPrice" [add]="adjustStorageAdd"
|
||||
[organizationId]="organizationId" [interval]="billingInterval" (onAdjusted)="closeStorage(true)"
|
||||
(onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { OrganizationSubscriptionResponse } from 'jslib/models/response/organizationSubscriptionResponse';
|
||||
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-subscription',
|
||||
templateUrl: 'organization-subscription.component.html',
|
||||
})
|
||||
export class OrganizationSubscriptionComponent implements OnInit {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
organizationId: string;
|
||||
adjustSeatsAdd = true;
|
||||
showAdjustSeats = false;
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
showUpdateLicense = false;
|
||||
showDownloadLicense = false;
|
||||
showChangePlan = false;
|
||||
sub: OrganizationSubscriptionResponse;
|
||||
selfHosted = false;
|
||||
|
||||
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) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async reinstate() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('reinstateConfirmation'),
|
||||
this.i18nService.t('reinstateSubscription'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.reinstatePromise = this.apiService.postOrganizationReinstate(this.organizationId);
|
||||
await this.reinstatePromise;
|
||||
this.analytics.eventTrack.next({ action: 'Reinstated Plan' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('reinstated'));
|
||||
this.load();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('cancelConfirmation'),
|
||||
this.i18nService.t('cancelSubscription'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.cancelPromise = this.apiService.postOrganizationCancel(this.organizationId);
|
||||
await this.cancelPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Canceled Plan' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription'));
|
||||
this.load();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async changePlan() {
|
||||
if (this.subscription == null) {
|
||||
this.showChangePlan = !this.showChangePlan;
|
||||
return;
|
||||
}
|
||||
const contactSupport = await this.platformUtilsService.showDialog(this.i18nService.t('changeBillingPlanDesc'),
|
||||
this.i18nService.t('changeBillingPlan'), this.i18nService.t('contactSupport'), this.i18nService.t('close'));
|
||||
if (contactSupport) {
|
||||
this.platformUtilsService.launchUri('https://bitwarden.com/contact');
|
||||
}
|
||||
}
|
||||
|
||||
closeChangePlan(changed: boolean) {
|
||||
this.showChangePlan = false;
|
||||
}
|
||||
|
||||
downloadLicense() {
|
||||
this.showDownloadLicense = !this.showDownloadLicense;
|
||||
}
|
||||
|
||||
closeDownloadLicense() {
|
||||
this.showDownloadLicense = false;
|
||||
}
|
||||
|
||||
updateLicense() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.showUpdateLicense = true;
|
||||
}
|
||||
|
||||
closeUpdateLicense(updated: boolean) {
|
||||
this.showUpdateLicense = false;
|
||||
if (updated) {
|
||||
this.load();
|
||||
this.messagingService.send('updatedOrgLicense');
|
||||
}
|
||||
}
|
||||
|
||||
adjustSeats(add: boolean) {
|
||||
this.adjustSeatsAdd = add;
|
||||
this.showAdjustSeats = true;
|
||||
}
|
||||
|
||||
closeSeats(load: boolean) {
|
||||
this.showAdjustSeats = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
adjustStorage(add: boolean) {
|
||||
this.adjustStorageAdd = add;
|
||||
this.showAdjustStorage = true;
|
||||
}
|
||||
|
||||
closeStorage(load: boolean) {
|
||||
this.showAdjustStorage = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return this.sub != null && this.sub.expiration != null &&
|
||||
new Date(this.sub.expiration) < new Date();
|
||||
}
|
||||
|
||||
get subscriptionMarkedForCancel() {
|
||||
return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
|
||||
}
|
||||
|
||||
get subscription() {
|
||||
return this.sub != null ? this.sub.subscription : null;
|
||||
}
|
||||
|
||||
get nextInvoice() {
|
||||
return this.sub != null ? this.sub.upcomingInvoice : null;
|
||||
}
|
||||
|
||||
get storagePercentage() {
|
||||
return this.sub != null && this.sub.maxStorageGb ?
|
||||
+(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2) : 0;
|
||||
}
|
||||
|
||||
get storageProgressWidth() {
|
||||
return this.storagePercentage < 5 ? 5 : 0;
|
||||
}
|
||||
|
||||
get billingInterval() {
|
||||
const monthly = this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.TeamsMonthly;
|
||||
return monthly ? 'month' : 'year';
|
||||
}
|
||||
|
||||
get storageGbPrice() {
|
||||
return this.billingInterval === 'month' ? 0.5 : 4;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
get canAdjustSeats() {
|
||||
return this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually ||
|
||||
this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually;
|
||||
}
|
||||
|
||||
get canDownloadLicense() {
|
||||
return (this.sub.planType !== PlanType.Free && this.subscription == null) ||
|
||||
(this.subscription != null && !this.subscription.cancelled);
|
||||
}
|
||||
}
|
||||
48
src/app/organizations/settings/rotate-api-key.component.html
Normal file
48
src/app/organizations/settings/rotate-api-key.component.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{'rotateApiKey' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'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}}"></i>
|
||||
<span>{{'rotateApiKey' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
50
src/app/organizations/settings/rotate-api-key.component.ts
Normal file
50
src/app/organizations/settings/rotate-api-key.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,11 @@
|
||||
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
||||
{{'myOrganization' | i18n}}
|
||||
</a>
|
||||
<a routerLink="billing" class="list-group-item" routerLinkActive="active">
|
||||
{{'billingAndLicensing' | i18n}}
|
||||
<a routerLink="subscription" class="list-group-item" routerLinkActive="active">
|
||||
{{'subscription' | i18n}}
|
||||
</a>
|
||||
<a routerLink="billing" class="list-group-item" routerLinkActive="active" *ngIf="!selfHosted">
|
||||
{{'billing' | i18n}}
|
||||
</a>
|
||||
<a routerLink="two-factor" class="list-group-item" routerLinkActive="active" *ngIf="access2fa">
|
||||
{{'twoStepLogin' | i18n}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Component({
|
||||
@@ -9,11 +10,14 @@ import { UserService } from 'jslib/abstractions/user.service';
|
||||
})
|
||||
export class SettingsComponent {
|
||||
access2fa = false;
|
||||
selfHosted: boolean;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.selfHosted = await this.platformUtilsService.isSelfHost();
|
||||
const organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.access2fa = organization.use2fa;
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ExportComponent extends BaseExportComponent {
|
||||
}
|
||||
|
||||
getExportData() {
|
||||
return this.exportService.getOrganizationExport(this.organizationId, 'csv');
|
||||
return this.exportService.getOrganizationExport(this.organizationId, this.format);
|
||||
}
|
||||
|
||||
getFileName() {
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
<div class="card-header d-flex">
|
||||
{{'reports' | i18n}}
|
||||
<div class="ml-auto">
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports" (click)="upgradeOrganization()">
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
|
||||
(click)="upgradeOrganization()">
|
||||
{{'upgrade' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -34,8 +34,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
auditService: AuditService, stateService: StateService,
|
||||
userService: UserService, collectionService: CollectionService,
|
||||
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
|
||||
private apiService: ApiService,
|
||||
messagingService: MessagingService) {
|
||||
private apiService: ApiService, messagingService: MessagingService) {
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
|
||||
userService, collectionService, totpService, passwordGenerationService, messagingService);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
|
||||
import { CipherData } from 'jslib/models/data/cipherData';
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@@ -59,7 +57,7 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
}
|
||||
}
|
||||
|
||||
search(timeout: number = null) {
|
||||
async search(timeout: number = null) {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.search(timeout);
|
||||
}
|
||||
@@ -73,6 +71,7 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
} else {
|
||||
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText);
|
||||
}
|
||||
await this.resetPaging();
|
||||
}
|
||||
|
||||
checkCipher(c: CipherView) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false" (onAllClicked)="clearGroupingFilters()" (onCipherTypeClicked)="filterCipherType($event)"
|
||||
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false"
|
||||
(onAllClicked)="clearGroupingFilters()" (onCipherTypeClicked)="filterCipherType($event)"
|
||||
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)">
|
||||
</app-org-vault-groupings>
|
||||
</div>
|
||||
@@ -10,14 +11,16 @@
|
||||
<h1>
|
||||
{{'vault' | i18n}}
|
||||
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
|
||||
<i *ngIf="actionSpinner.loading" class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<i *ngIf="actionSpinner.loading" class="fa fa-spinner fa-spin text-muted"
|
||||
title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()">
|
||||
<i class="fa fa-plus fa-fw"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)" (onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
||||
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
||||
(onCollectionsClicked)="editCipherCollections($event)" (onEventsClicked)="viewEvents($event)">
|
||||
</app-org-vault-ciphers>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.groupingsComponent.organization = this.organization;
|
||||
this.ciphersComponent.organization = this.organization;
|
||||
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
|
||||
if (!this.organization.isAdmin) {
|
||||
await this.syncService.fullSync(false);
|
||||
@@ -90,7 +90,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (qParams == null) {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.load();
|
||||
await this.ciphersComponent.reload();
|
||||
} else {
|
||||
if (qParams.type) {
|
||||
const t = parseInt(qParams.type, null);
|
||||
@@ -101,7 +101,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
await this.filterCollection(qParams.collectionId, true);
|
||||
} else {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.load();
|
||||
await this.ciphersComponent.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.viewEvents(cipher[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -132,7 +136,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType');
|
||||
const filter = (c: CipherView) => c.type === type;
|
||||
if (load) {
|
||||
await this.ciphersComponent.load(filter);
|
||||
await this.ciphersComponent.reload(filter);
|
||||
} else {
|
||||
await this.ciphersComponent.applyFilter(filter);
|
||||
}
|
||||
@@ -152,7 +156,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
};
|
||||
if (load) {
|
||||
await this.ciphersComponent.load(filter);
|
||||
await this.ciphersComponent.reload(filter);
|
||||
} else {
|
||||
await this.ciphersComponent.applyFilter(filter);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ import 'core-js/es6';
|
||||
import 'core-js/es7/reflect';
|
||||
require('zone.js/dist/zone');
|
||||
|
||||
// IE11 fix, ref: https://github.com/angular/angular/issues/24769
|
||||
if (!Element.prototype.matches && (Element.prototype as any).msMatchesSelector) {
|
||||
Element.prototype.matches = (Element.prototype as any).msMatchesSelector;
|
||||
}
|
||||
|
||||
if (process.env.ENV === 'production') {
|
||||
// Production
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,7 @@ export class RouterService {
|
||||
private currentUrl: string = undefined;
|
||||
|
||||
constructor(private router: Router, private activatedRoute: ActivatedRoute,
|
||||
private titleService: Title, private i18nService: I18nService) {
|
||||
private titleService: Title, i18nService: I18nService) {
|
||||
this.currentUrl = this.router.url;
|
||||
router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
@@ -55,4 +55,8 @@ export class RouterService {
|
||||
getPreviousUrl() {
|
||||
return this.previousUrl;
|
||||
}
|
||||
|
||||
setPreviousUrl(url: string) {
|
||||
this.previousUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ const folderService = new FolderService(cryptoService, userService, apiService,
|
||||
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
|
||||
searchService = new SearchService(cipherService, platformUtilsService);
|
||||
const lockService = new LockService(cipherService, folderService, collectionService,
|
||||
cryptoService, platformUtilsService, storageService, messagingService, searchService, null);
|
||||
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, null);
|
||||
const syncService = new SyncService(userService, apiService, settingsService,
|
||||
folderService, cipherService, cryptoService, collectionService, storageService, messagingService,
|
||||
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
|
||||
@@ -116,7 +116,7 @@ const authService = new AuthService(cryptoService, apiService,
|
||||
const exportService = new ExportService(folderService, cipherService, apiService);
|
||||
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService);
|
||||
const notificationsService = new NotificationsService(userService, syncService, appIdService,
|
||||
apiService, cryptoService, async () => messagingService.send('logout', { expired: true }));
|
||||
apiService, lockService, async () => messagingService.send('logout', { expired: true }));
|
||||
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
|
||||
const auditService = new AuditService(cryptoFunctionService, apiService);
|
||||
|
||||
|
||||
@@ -4,19 +4,19 @@ import {
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { LockService } from 'jslib/abstractions/lock.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class UnauthGuardService implements CanActivate {
|
||||
constructor(private cryptoService: CryptoService, private userService: UserService,
|
||||
constructor(private lockService: LockService, private userService: UserService,
|
||||
private router: Router) { }
|
||||
|
||||
async canActivate() {
|
||||
const isAuthed = await this.userService.isAuthenticated();
|
||||
if (isAuthed) {
|
||||
const hasKey = await this.cryptoService.hasKey();
|
||||
if (!hasKey) {
|
||||
const locked = await this.lockService.isLocked();
|
||||
if (locked) {
|
||||
this.router.navigate(['lock']);
|
||||
} else {
|
||||
this.router.navigate(['vault']);
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<p>{{'dangerZoneDesc' | i18n}}</p>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="deauthorizeSessions()">{{'deauthorizeSessions' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
(click)="deauthorizeSessions()">{{'deauthorizeSessions' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">{{'purgeVault' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="deleteAccount()">{{'deleteAccount' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
(click)="deleteAccount()">{{'deleteAccount' | i18n}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
||||
|
||||
58
src/app/settings/add-credit.component.html
Normal file
58
src/app/settings/add-credit.component.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
||||
(click)="cancel()"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{'addCredit' | i18n}}</h3>
|
||||
<div class="mb-4 text-lg" *ngIf="showOptions">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="Method" id="credit-method-paypal"
|
||||
[value]="paymentMethodType.PayPal" [(ngModel)]="method">
|
||||
<label class="form-check-label" for="credit-method-paypal">
|
||||
<i class="fa fa-fw fa-paypal"></i> PayPal</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="Method" id="credit-method-bitcoin"
|
||||
[value]="paymentMethodType.BitPay" [(ngModel)]="method">
|
||||
<label class="form-check-label" for="credit-method-bitcoin">
|
||||
<i class="fa fa-fw fa-bitcoin"></i> Bitcoin</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<label for="creditAmount">{{'amount' | i18n}}</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">$USD</span></div>
|
||||
<input id="creditAmount" class="form-control" type="text" name="CreditAmount"
|
||||
[(ngModel)]="creditAmount" (blur)="formatAmount()" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{'creditDelayed' | i18n}}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading || ppLoading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form #ppButtonForm action="{{ppButtonFormAction}}" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_xclick">
|
||||
<input type="hidden" name="business" value="{{ppButtonBusinessId}}">
|
||||
<input type="hidden" name="button_subtype" value="services">
|
||||
<input type="hidden" name="no_note" value="1">
|
||||
<input type="hidden" name="no_shipping" value="1">
|
||||
<input type="hidden" name="rm" value="1">
|
||||
<input type="hidden" name="return" value="{{returnUrl}}">
|
||||
<input type="hidden" name="cancel_return" value="{{returnUrl}}">
|
||||
<input type="hidden" name="currency_code" value="USD">
|
||||
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png">
|
||||
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted">
|
||||
<input type="hidden" name="amount" value="{{creditAmount}}">
|
||||
<input type="hidden" name="custom" value="{{ppButtonCustomField}}">
|
||||
<input type="hidden" name="item_name" value="Bitwarden Account Credit">
|
||||
<input type="hidden" name="item_number" value="{{subject}}">
|
||||
</form>
|
||||
144
src/app/settings/add-credit.component.ts
Normal file
144
src/app/settings/add-credit.component.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
|
||||
|
||||
import { BitPayInvoiceRequest } from 'jslib/models/request/bitPayInvoiceRequest';
|
||||
|
||||
import { WebConstants } from '../../services/webConstants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-credit',
|
||||
templateUrl: 'add-credit.component.html',
|
||||
})
|
||||
export class AddCreditComponent implements OnInit {
|
||||
@Input() creditAmount: string;
|
||||
@Input() showOptions = true;
|
||||
@Input() method = PaymentMethodType.PayPal;
|
||||
@Input() organizationId: string;
|
||||
@Output() onAdded = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild('ppButtonForm', { read: ElementRef }) ppButtonFormRef: ElementRef;
|
||||
|
||||
paymentMethodType = PaymentMethodType;
|
||||
ppButtonFormAction = WebConstants.paypal.buttonActionProduction;
|
||||
ppButtonBusinessId = WebConstants.paypal.businessIdProduction;
|
||||
ppButtonCustomField: string;
|
||||
ppLoading = false;
|
||||
subject: string;
|
||||
returnUrl: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private userId: string;
|
||||
private name: string;
|
||||
private email: string;
|
||||
|
||||
constructor(private userService: UserService, private apiService: ApiService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private platformUtilsService: PlatformUtilsService) {
|
||||
if (platformUtilsService.isDev()) {
|
||||
this.ppButtonFormAction = WebConstants.paypal.buttonActionSandbox;
|
||||
this.ppButtonBusinessId = WebConstants.paypal.businessIdSandbox;
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.organizationId != null) {
|
||||
if (this.creditAmount == null) {
|
||||
this.creditAmount = '20.00';
|
||||
}
|
||||
this.ppButtonCustomField = 'organization_id:' + this.organizationId;
|
||||
const org = await this.userService.getOrganization(this.organizationId);
|
||||
if (org != null) {
|
||||
this.subject = org.name;
|
||||
this.name = org.name;
|
||||
}
|
||||
} else {
|
||||
if (this.creditAmount == null) {
|
||||
this.creditAmount = '10.00';
|
||||
}
|
||||
this.userId = await this.userService.getUserId();
|
||||
this.subject = await this.userService.getEmail();
|
||||
this.email = this.subject;
|
||||
this.ppButtonCustomField = 'user_id:' + this.userId;
|
||||
}
|
||||
this.ppButtonCustomField += ',account_credit:1';
|
||||
this.returnUrl = window.location.href;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.creditAmount == null || this.creditAmount === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.method === PaymentMethodType.PayPal) {
|
||||
this.ppButtonFormRef.nativeElement.submit();
|
||||
this.ppLoading = true;
|
||||
return;
|
||||
}
|
||||
if (this.method === PaymentMethodType.BitPay) {
|
||||
try {
|
||||
const req = new BitPayInvoiceRequest();
|
||||
req.email = this.email;
|
||||
req.name = this.name;
|
||||
req.credit = true;
|
||||
req.amount = this.creditAmountNumber;
|
||||
req.organizationId = this.organizationId;
|
||||
req.userId = this.userId;
|
||||
req.returnUrl = this.returnUrl;
|
||||
this.formPromise = this.apiService.postBitPayInvoice(req);
|
||||
const bitPayUrl: string = await this.formPromise;
|
||||
this.platformUtilsService.launchUri(bitPayUrl);
|
||||
} catch { }
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.analytics.eventTrack.next({
|
||||
action: 'Added Credit',
|
||||
});
|
||||
this.onAdded.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
|
||||
formatAmount() {
|
||||
try {
|
||||
if (this.creditAmount != null && this.creditAmount !== '') {
|
||||
const floatAmount = Math.abs(parseFloat(this.creditAmount));
|
||||
if (floatAmount > 0) {
|
||||
this.creditAmount = parseFloat((Math.round(floatAmount * 100) / 100).toString())
|
||||
.toFixed(2).toString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
this.creditAmount = '';
|
||||
}
|
||||
|
||||
get creditAmountNumber(): number {
|
||||
if (this.creditAmount != null && this.creditAmount !== '') {
|
||||
try {
|
||||
return parseFloat(this.creditAmount);
|
||||
} catch { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
<app-callout title="{{'contactSupport' | i18n}}" icon="fa-info-circle" *ngIf="!canChange">
|
||||
<p>{{'contactSupportPaymentMethod' | i18n}}</p>
|
||||
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
{{'contactSupport' | i18n}}
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{'close' | i18n}}
|
||||
</button>
|
||||
</app-callout>
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="canChange">
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
||||
(click)="cancel()"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}</h3>
|
||||
<app-payment [showOptions]="organizationId" [hidePaypal]="true" [hideBank]="!organizationId"></app-payment>
|
||||
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
|
||||
@@ -39,8 +39,9 @@ export class AdjustPaymentComponent {
|
||||
async submit() {
|
||||
try {
|
||||
const request = new PaymentRequest();
|
||||
this.formPromise = this.paymentComponent.createPaymentToken().then((token) => {
|
||||
request.paymentToken = token;
|
||||
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
|
||||
request.paymentToken = result[0];
|
||||
request.paymentMethodType = result[1];
|
||||
if (this.organizationId == null) {
|
||||
return this.apiService.postAccountPayment(request);
|
||||
} else {
|
||||
@@ -59,8 +60,4 @@ export class AdjustPaymentComponent {
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
|
||||
get canChange() {
|
||||
return this.currentType == null || this.currentType === PaymentMethodType.Card || this.organizationId != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
||||
(click)="cancel()"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{(add ? 'addStorage' : 'removeStorage') | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="storageAdjustment">{{(add ? 'gbStorageAdd' : 'gbStorageRemove') | i18n}}</label>
|
||||
<input id="storageAdjustment" class="form-control" type="number" name="StroageGbAdjustment" [(ngModel)]="storageAdjustment"
|
||||
min="0" max="99" step="1" required>
|
||||
<input id="storageAdjustment" class="form-control" type="number" name="StroageGbAdjustment"
|
||||
[(ngModel)]="storageAdjustment" min="0" max="99" step="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="add" class="mb-3">
|
||||
<strong>{{'total' | i18n}}:</strong> {{storageAdjustment || 0}} GB × {{storageGbPrice | currency:'$'}} = {{adjustedStorageTotal
|
||||
<strong>{{'total' | i18n}}:</strong> {{storageAdjustment || 0}} GB × {{storageGbPrice | currency:'$'}}
|
||||
= {{adjustedStorageTotal
|
||||
| currency:'$'}} /{{interval | i18n}}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
|
||||
[readonly]="tokenSent" appInputVerbatim>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required [readonly]="tokenSent" appInputVerbatim>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newEmail">{{'newEmail' | i18n}}</label>
|
||||
<input id="newEmail" class="form-control" type="text" name="NewEmail" [(ngModel)]="newEmail" required [readonly]="tokenSent"
|
||||
inputmode="email" appInputVerbatim="false">
|
||||
<input id="newEmail" class="form-control" type="text" name="NewEmail" [(ngModel)]="newEmail" required
|
||||
[readonly]="tokenSent" inputmode="email" appInputVerbatim="false">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,7 +21,8 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="token">{{'code' | i18n}}</label>
|
||||
<input id="token" class="form-control" type="text" name="Token" [(ngModel)]="token" required appInputVerbatim>
|
||||
<input id="token" class="form-control" type="text" name="Token" [(ngModel)]="token" required
|
||||
appInputVerbatim>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="kdfMasterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="kdfMasterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword"
|
||||
required appInputVerbatim>
|
||||
<input id="kdfMasterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appInputVerbatim>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,7 +13,8 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="kdf">{{'kdfAlgorithm' | i18n}}</label>
|
||||
<a class="ml-auto" href="https://en.wikipedia.org/wiki/Key_derivation_function" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
|
||||
<a class="ml-auto" href="https://en.wikipedia.org/wiki/Key_derivation_function" target="_blank"
|
||||
rel="noopener" title="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o"></i>
|
||||
</a>
|
||||
<select id="kdf" name="Kdf" [(ngModel)]="kdf" class="form-control" required>
|
||||
@@ -24,11 +25,12 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="kdfIterations">{{'kdfIterations' | i18n}}</label>
|
||||
<a class="ml-auto" href="https://en.wikipedia.org/wiki/PBKDF2" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
|
||||
<a class="ml-auto" href="https://en.wikipedia.org/wiki/PBKDF2" target="_blank" rel="noopener"
|
||||
title="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o"></i>
|
||||
</a>
|
||||
<input id="kdfIterations" type="number" min="5000" max="1000000" name="KdfIterations" class="form-control" [(ngModel)]="kdfIterations"
|
||||
required>
|
||||
<input id="kdfIterations" type="number" min="5000" max="1000000" name="KdfIterations"
|
||||
class="form-control" [(ngModel)]="kdfIterations" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
||||
@@ -22,15 +22,16 @@
|
||||
<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 autocomplete="new-password">
|
||||
<input id="confirmNewMasterPassword" type="password" name="ConfirmNewMasterPasswordHash"
|
||||
class="form-control" [(ngModel)]="confirmNewMasterPassword" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="rotateEncKey" name="RotateEncKey" [(ngModel)]="rotateEncKey"
|
||||
(change)="rotateEncKeyClicked()">
|
||||
<input class="form-check-input" type="checkbox" id="rotateEncKey" name="RotateEncKey"
|
||||
[(ngModel)]="rotateEncKey" (change)="rotateEncKeyClicked()">
|
||||
<label class="form-check-label" for="rotateEncKey">
|
||||
{{'rotateAccountEncKey' | i18n}}
|
||||
</label>
|
||||
|
||||
@@ -2,193 +2,4 @@
|
||||
<h1>{{'newOrganization' | i18n}}</h1>
|
||||
</div>
|
||||
<p>{{'newOrganizationDesc' | i18n}}</p>
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<p>{{'uploadLicenseFileOrg' | i18n}}</p>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="form-group">
|
||||
<label for="file">{{'licenseFile' | i18n}}</label>
|
||||
<input type="file" id="file" class="form-control-file" name="file" required>
|
||||
<small class="form-text text-muted">{{'licenseFileDesc' | i18n : 'bitwarden_organization_license.json'}}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
||||
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="name">{{'organizationName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input id="ownedBusiness" class="form-check-input" type="checkbox" name="OwnedBusiness" [(ngModel)]="ownedBusiness" (change)="changedOwnedBusiness()">
|
||||
<label for="ownedBusiness" class="form-check-label">{{'accountOwnedBusiness' | i18n}}</label>
|
||||
</div>
|
||||
<div class="row" *ngIf="ownedBusiness">
|
||||
<div class="form-group col-6">
|
||||
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="businessName">
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
|
||||
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
|
||||
<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>
|
||||
</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">
|
||||
<h2 class="mt-5">{{'users' | i18n}}</h2>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label for="additionalSeats">{{'userSeats' | i18n}}</label>
|
||||
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats" [(ngModel)]="additionalSeats" min="1"
|
||||
max="100000" placeholder="{{'userSeatsDesc' | i18n}}" required>
|
||||
<small class="text-muted form-text">{{'userSeatsHowManyDesc' | i18n}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="mt-5">{{'addons' | i18n}}</h2>
|
||||
<div class="row" *ngIf="!plans[plan].noAdditionalSeats && plans[plan].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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="additionalStorage">{{'additionalStorageGb' | i18n}}</label>
|
||||
<input id="additionalStorage" class="form-control" type="number" name="AdditionalStorageGb" [(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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
|
||||
<div class="form-check">
|
||||
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon" [(ngModel)]="premiumAccessAddon">
|
||||
<label for="premiumAccess" class="form-check-label bold">{{'premiumAccess' | i18n}}</label>
|
||||
</div>
|
||||
<small class="text-muted form-text">{{'premiumAccessDesc' | i18n : (3.33 | currency:'$') : ('month' | i18n)}}</small>
|
||||
</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)
|
||||
| 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)
|
||||
| currency:'$'}} /{{'month' | i18n}}
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<div class="text-lg">
|
||||
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
|
||||
</div>
|
||||
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
|
||||
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
|
||||
<app-payment [hidePaypal]="true"></app-payment>
|
||||
</ng-container>
|
||||
<div [ngClass]="{'mt-4': plans[plan].noPayment}">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<app-organization-plans></app-organization-plans>
|
||||
|
||||
@@ -3,240 +3,27 @@ import {
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { ActivatedRoute } 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 { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { PaymentComponent } from './payment.component';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
|
||||
import { OrganizationPlansComponent } from './organization-plans.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-organization',
|
||||
templateUrl: 'create-organization.component.html',
|
||||
})
|
||||
export class CreateOrganizationComponent implements OnInit {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(OrganizationPlansComponent) orgPlansComponent: OrganizationPlansComponent;
|
||||
|
||||
selfHosted = false;
|
||||
ownedBusiness = false;
|
||||
premiumAccessAddon = false;
|
||||
storageGbPriceMonthly = 0.33;
|
||||
additionalStorage = 0;
|
||||
additionalSeats = 0;
|
||||
plan = 'free';
|
||||
interval = 'year';
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
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 route: ActivatedRoute) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
constructor(private route: ActivatedRoute) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
if (qParams.plan === 'families' || qParams.plan === 'teams' || qParams.plan === 'enterprise') {
|
||||
this.plan = qParams.plan;
|
||||
this.orgPlansComponent.plan = qParams.plan;
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
let files: FileList = null;
|
||||
if (this.selfHosted) {
|
||||
const fileEl = document.getElementById('file') as HTMLInputElement;
|
||||
files = fileEl.files;
|
||||
if (files == null || files.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('selectFile'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let key: string = null;
|
||||
let collectionCt: string = null;
|
||||
|
||||
try {
|
||||
this.formPromise = this.cryptoService.makeShareKey().then((shareKey) => {
|
||||
key = shareKey[0].encryptedString;
|
||||
return this.cryptoService.encrypt(this.i18nService.t('defaultCollection'), shareKey[1]);
|
||||
}).then((collection) => {
|
||||
collectionCt = collection.encryptedString;
|
||||
if (this.selfHosted || this.plan === 'free') {
|
||||
return null;
|
||||
} else {
|
||||
return this.paymentComponent.createPaymentToken();
|
||||
}
|
||||
}).then((token: string) => {
|
||||
if (this.selfHosted) {
|
||||
const fd = new FormData();
|
||||
fd.append('license', files[0]);
|
||||
fd.append('key', key);
|
||||
fd.append('collectionName', collectionCt);
|
||||
return this.apiService.postOrganizationLicense(fd);
|
||||
} else {
|
||||
const request = new OrganizationCreateRequest();
|
||||
request.key = key;
|
||||
request.collectionName = collectionCt;
|
||||
request.name = this.name;
|
||||
request.billingEmail = this.billingEmail;
|
||||
|
||||
if (this.plan === 'free') {
|
||||
request.planType = PlanType.Free;
|
||||
} else {
|
||||
request.paymentToken = token;
|
||||
request.businessName = this.ownedBusiness ? this.businessName : null;
|
||||
request.additionalSeats = this.additionalSeats;
|
||||
request.additionalStorageGb = this.additionalStorage;
|
||||
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
||||
this.premiumAccessAddon;
|
||||
request.country = this.paymentComponent.getCountry();
|
||||
if (this.interval === 'month') {
|
||||
request.planType = this.plans[this.plan].monthPlanType;
|
||||
} else {
|
||||
request.planType = this.plans[this.plan].annualPlanType;
|
||||
}
|
||||
}
|
||||
return this.apiService.postOrganization(request);
|
||||
}
|
||||
}).then((response) => {
|
||||
return this.finalize(response.id);
|
||||
});
|
||||
await this.formPromise;
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async finalize(orgId: string) {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
this.analytics.eventTrack.next({ action: 'Created Organization' });
|
||||
this.toasterService.popAsync('success', this.i18nService.t('organizationCreated'),
|
||||
this.i18nService.t('organizationReadyToGo'));
|
||||
this.router.navigate(['/organizations/' + orgId]);
|
||||
}
|
||||
|
||||
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 (this.additionalStorage || 0) * this.storageGb.yearlyPrice;
|
||||
} else {
|
||||
return (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 * (this.additionalSeats || 0);
|
||||
} else {
|
||||
return this.plans[this.plan].monthlySeatPrice * (this.additionalSeats || 0);
|
||||
}
|
||||
}
|
||||
|
||||
baseTotal(annual: boolean): number {
|
||||
if (annual) {
|
||||
return (this.plans[this.plan].annualBasePrice || 0);
|
||||
} else {
|
||||
return (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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<p>{{'deauthorizeSessionsDesc' | i18n}}</p>
|
||||
<app-callout type="warning">{{'deauthorizeSessionsWarning' | i18n}}</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
|
||||
appAutoFocus appInputVerbatim>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutoFocus appInputVerbatim>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<p>{{'deleteAccountDesc' | i18n}}</p>
|
||||
<app-callout type="warning">{{'deleteAccountWarning' | i18n}}</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
|
||||
appAutofocus appInputVerbatim>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<div class="form-group d-flex" *ngFor="let d of custom; let i = index; trackBy: indexTrackBy">
|
||||
<div class="flex-fill">
|
||||
<label for="customDomain_{{i}}" class="sr-only">{{'customDomainX' | i18n : (i + 1)}}</label>
|
||||
<textarea class="form-control" name="CustomDomain[{{i}}]" id="customDomain_{{i}}" [(ngModel)]="custom[i]" placeholder="{{'ex' | i18n}} google.com, gmail.com"
|
||||
required></textarea>
|
||||
<textarea class="form-control" name="CustomDomain[{{i}}]" id="customDomain_{{i}}"
|
||||
[(ngModel)]="custom[i]" placeholder="{{'ex' | i18n}} google.com, gmail.com" required></textarea>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger ml-2" (click)="remove(i)" title="{{'remove' | i18n}}">
|
||||
<i class="fa fa-minus-circle fa-lg"></i>
|
||||
@@ -37,15 +37,18 @@
|
||||
<td [ngClass]="{'table-list-strike': d.excluded}">{{d.domains}}</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="toggleExcluded(d)" *ngIf="!d.excluded">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="toggleExcluded(d)"
|
||||
*ngIf="!d.excluded">
|
||||
<i class="fa fa-fw fa-close"></i>
|
||||
{{'exclude' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="toggleExcluded(d)" *ngIf="d.excluded">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="toggleExcluded(d)"
|
||||
*ngIf="d.excluded">
|
||||
<i class="fa fa-fw fa-plus"></i>
|
||||
{{'include' | i18n}}
|
||||
</a>
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
<div class="form-group">
|
||||
<div class="d-flex">
|
||||
<label for="locale">{{'language' | i18n}}</label>
|
||||
<a class="ml-auto" href="https://help.bitwarden.com/article/localization/" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
|
||||
<a class="ml-auto" href="https://help.bitwarden.com/article/localization/" target="_blank"
|
||||
rel="noopener" title="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -32,11 +33,13 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="disableIcons" name="DisableIcons" [(ngModel)]="disableIcons">
|
||||
<input class="form-check-input" type="checkbox" id="disableIcons" name="DisableIcons"
|
||||
[(ngModel)]="disableIcons">
|
||||
<label class="form-check-label" for="disableIcons">
|
||||
{{'disableIcons' | i18n}}
|
||||
</label>
|
||||
<a href="https://help.bitwarden.com/article/website-icons/" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
|
||||
<a href="https://help.bitwarden.com/article/website-icons/" target="_blank" rel="noopener"
|
||||
title="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -44,7 +47,8 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableGravatars" name="enableGravatars" [(ngModel)]="enableGravatars">
|
||||
<input class="form-check-input" type="checkbox" id="enableGravatars" name="enableGravatars"
|
||||
[(ngModel)]="enableGravatars">
|
||||
<label class="form-check-label" for="enableGravatars">
|
||||
{{'enableGravatars' | i18n}}
|
||||
</label>
|
||||
|
||||
218
src/app/settings/organization-plans.component.html
Normal file
218
src/app/settings/organization-plans.component.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<ng-container *ngIf="createOrganization && selfHosted">
|
||||
<p>{{'uploadLicenseFileOrg' | i18n}}</p>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="form-group">
|
||||
<label for="file">{{'licenseFile' | i18n}}</label>
|
||||
<input type="file" id="file" class="form-control-file" name="file" required>
|
||||
<small
|
||||
class="form-text text-muted">{{'licenseFileDesc' | i18n : 'bitwarden_organization_license.json'}}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
||||
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
||||
<div class="row" *ngIf="createOrganization">
|
||||
<div class="form-group col-6">
|
||||
<label for="name">{{'organizationName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input id="ownedBusiness" class="form-check-input" type="checkbox" name="OwnedBusiness"
|
||||
[(ngModel)]="ownedBusiness" (change)="changedOwnedBusiness()">
|
||||
<label for="ownedBusiness" class="form-check-label">{{'accountOwnedBusiness' | i18n}}</label>
|
||||
</div>
|
||||
<div class="row" *ngIf="ownedBusiness">
|
||||
<div class="form-group col-6">
|
||||
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="businessName">
|
||||
</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>
|
||||
</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">
|
||||
<h2 class="mt-5">{{'users' | i18n}}</h2>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label for="additionalSeats">{{'userSeats' | i18n}}</label>
|
||||
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
|
||||
[(ngModel)]="additionalSeats" min="1" max="100000" placeholder="{{'userSeatsDesc' | i18n}}"
|
||||
required>
|
||||
<small class="text-muted form-text">{{'userSeatsHowManyDesc' | i18n}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="mt-5">{{'addons' | i18n}}</h2>
|
||||
<div class="row" *ngIf="!plans[plan].noAdditionalSeats && plans[plan].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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="additionalStorage">{{'additionalStorageGb' | i18n}}</label>
|
||||
<input id="additionalStorage" class="form-control" type="number" name="AdditionalStorageGb"
|
||||
[(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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
|
||||
<div class="form-check">
|
||||
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon"
|
||||
[(ngModel)]="premiumAccessAddon">
|
||||
<label for="premiumAccess" class="form-check-label bold">{{'premiumAccess' | i18n}}</label>
|
||||
</div>
|
||||
<small
|
||||
class="text-muted form-text">{{'premiumAccessDesc' | i18n : (3.33 | currency:'$') : ('month' | i18n)}}</small>
|
||||
</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)
|
||||
| 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)
|
||||
| currency:'$'}} /{{'month' | i18n}}
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<div class="text-lg">
|
||||
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
|
||||
</div>
|
||||
<ng-container *ngIf="createOrganization">
|
||||
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
|
||||
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
|
||||
<app-payment [hideCredit]="true"></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}">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
271
src/app/settings/organization-plans.component.ts
Normal file
271
src/app/settings/organization-plans.component.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
|
||||
|
||||
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 { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { PaymentComponent } from './payment.component';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
|
||||
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'app-organization-plans',
|
||||
templateUrl: 'organization-plans.component.html',
|
||||
})
|
||||
export class OrganizationPlansComponent {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
|
||||
@Input() organizationId: string;
|
||||
@Input() showFree = true;
|
||||
@Input() showCancel = false;
|
||||
@Input() plan = 'free';
|
||||
@Output() onSuccess = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
selfHosted = false;
|
||||
ownedBusiness = false;
|
||||
premiumAccessAddon = false;
|
||||
storageGbPriceMonthly = 0.33;
|
||||
additionalStorage = 0;
|
||||
additionalSeats = 0;
|
||||
interval = 'year';
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
|
||||
private router: Router, private syncService: SyncService) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
let files: FileList = null;
|
||||
if (this.createOrganization && this.selfHosted) {
|
||||
const fileEl = document.getElementById('file') as HTMLInputElement;
|
||||
files = fileEl.files;
|
||||
if (files == null || files.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('selectFile'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const doSubmit = async () => {
|
||||
let orgId: string = null;
|
||||
if (this.createOrganization) {
|
||||
let tokenResult: [string, PaymentMethodType] = null;
|
||||
if (!this.selfHosted && this.plan !== 'free') {
|
||||
tokenResult = await this.paymentComponent.createPaymentToken();
|
||||
}
|
||||
const shareKey = await this.cryptoService.makeShareKey();
|
||||
const key = shareKey[0].encryptedString;
|
||||
const collection = await this.cryptoService.encrypt(
|
||||
this.i18nService.t('defaultCollection'), shareKey[1]);
|
||||
const collectionCt = collection.encryptedString;
|
||||
|
||||
if (this.selfHosted) {
|
||||
const fd = new FormData();
|
||||
fd.append('license', files[0]);
|
||||
fd.append('key', key);
|
||||
fd.append('collectionName', collectionCt);
|
||||
const response = await this.apiService.postOrganizationLicense(fd);
|
||||
orgId = response.id;
|
||||
} else {
|
||||
const request = new OrganizationCreateRequest();
|
||||
request.key = key;
|
||||
request.collectionName = collectionCt;
|
||||
request.name = this.name;
|
||||
request.billingEmail = this.billingEmail;
|
||||
|
||||
if (this.plan === 'free') {
|
||||
request.planType = PlanType.Free;
|
||||
} else {
|
||||
request.paymentToken = tokenResult[0];
|
||||
request.paymentMethodType = tokenResult[1];
|
||||
request.businessName = this.ownedBusiness ? this.businessName : null;
|
||||
request.additionalSeats = this.additionalSeats;
|
||||
request.additionalStorageGb = this.additionalStorage;
|
||||
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
||||
this.premiumAccessAddon;
|
||||
if (this.interval === 'month') {
|
||||
request.planType = this.plans[this.plan].monthPlanType;
|
||||
} else {
|
||||
request.planType = this.plans[this.plan].annualPlanType;
|
||||
}
|
||||
}
|
||||
const response = await this.apiService.postOrganization(request);
|
||||
orgId = response.id;
|
||||
}
|
||||
} else {
|
||||
const request = new OrganizationUpgradeRequest();
|
||||
request.businessName = this.ownedBusiness ? this.businessName : null;
|
||||
request.additionalSeats = this.additionalSeats;
|
||||
request.additionalStorageGb = this.additionalStorage;
|
||||
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
||||
this.premiumAccessAddon;
|
||||
if (this.interval === 'month') {
|
||||
request.planType = this.plans[this.plan].monthPlanType;
|
||||
} else {
|
||||
request.planType = this.plans[this.plan].annualPlanType;
|
||||
}
|
||||
await this.apiService.postOrganizationUpgrade(this.organizationId, request);
|
||||
orgId = this.organizationId;
|
||||
}
|
||||
|
||||
if (orgId != null) {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
if (this.createOrganization) {
|
||||
this.analytics.eventTrack.next({ action: 'Created Organization' });
|
||||
this.toasterService.popAsync('success',
|
||||
this.i18nService.t('organizationCreated'), this.i18nService.t('organizationReadyToGo'));
|
||||
} else {
|
||||
this.analytics.eventTrack.next({ action: 'Upgraded Organization' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('organizationUpgraded'));
|
||||
}
|
||||
this.router.navigate(['/organizations/' + orgId]);
|
||||
}
|
||||
};
|
||||
|
||||
this.formPromise = doSubmit();
|
||||
await this.formPromise;
|
||||
this.onSuccess.emit();
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
<li *ngFor="let o of organizations">
|
||||
<a [routerLink]="['/organizations', o.id]" class="text-body">
|
||||
<i class="fa-li fa fa-caret-right"></i> {{o.name}}
|
||||
<i *ngIf="!o.enabled" class="fa fa-exclamation-triangle text-danger" title="{{'organizationIsDisabled' | i18n}}"></i>
|
||||
<i *ngIf="!o.enabled" class="fa fa-exclamation-triangle text-danger"
|
||||
title="{{'organizationIsDisabled' | i18n}}"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -26,7 +27,8 @@
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="action.loading" title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
<a href="#" routerLink="/settings/create-organization" class="btn btn-sm btn-outline-primary ml-auto" *ngIf="!loaded || (organizations && organizations.length)">
|
||||
<a href="#" routerLink="/settings/create-organization" class="btn btn-sm btn-outline-primary ml-auto"
|
||||
*ngIf="!loaded || (organizations && organizations.length)">
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
{{'newOrganization' | i18n}}
|
||||
</a>
|
||||
@@ -48,11 +50,13 @@
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" [routerLink]="['/organizations', o.id]">{{o.name}}</a>
|
||||
<i *ngIf="!o.enabled" class="fa fa-exclamation-triangle text-danger" title="{{'organizationIsDisabled' | i18n}}"></i>
|
||||
<i *ngIf="!o.enabled" class="fa fa-exclamation-triangle text-danger"
|
||||
title="{{'organizationIsDisabled' | i18n}}"></i>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
|
||||
@@ -1,342 +1,79 @@
|
||||
<div class="mb-4 text-lg" *ngIf="showOptions">
|
||||
<div class="form-check form-check-inline mr-4">
|
||||
<input class="form-check-input" type="radio" name="Method" id="method-card" value="card" [(ngModel)]="method" (change)="changeMethod()">
|
||||
<input class="form-check-input" type="radio" name="Method" id="method-card" [value]="paymentMethodType.Card"
|
||||
[(ngModel)]="method" (change)="changeMethod()">
|
||||
<label class="form-check-label" for="method-card">
|
||||
<i class="fa fa-fw fa-credit-card"></i> {{'creditCard' | i18n}}</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mr-4" *ngIf="!hideBank">
|
||||
<input class="form-check-input" type="radio" name="Method" id="method-bank" value="bank" [(ngModel)]="method" (change)="changeMethod()">
|
||||
<input class="form-check-input" type="radio" name="Method" id="method-bank"
|
||||
[value]="paymentMethodType.BankAccount" [(ngModel)]="method" (change)="changeMethod()">
|
||||
<label class="form-check-label" for="method-bank">
|
||||
<i class="fa fa-fw fa-university"></i> {{'bankAccount' | i18n}}</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline" *ngIf="!hidePaypal">
|
||||
<input class="form-check-input" type="radio" name="Method" id="method-paypal" value="paypal" [(ngModel)]="method" (change)="changeMethod()">
|
||||
<input class="form-check-input" type="radio" name="Method" id="method-paypal" [value]="paymentMethodType.PayPal"
|
||||
[(ngModel)]="method" (change)="changeMethod()">
|
||||
<label class="form-check-label" for="method-paypal">
|
||||
<i class="fa fa-fw fa-paypal"></i> PayPal</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline" *ngIf="!hideCredit">
|
||||
<input class="form-check-input" type="radio" name="Method" id="method-credit" [value]="paymentMethodType.Credit"
|
||||
[(ngModel)]="method" (change)="changeMethod()">
|
||||
<label class="form-check-label" for="method-credit">
|
||||
<i class="fa fa-fw fa-dollar"></i> {{'accountCredit' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="method === 'card'">
|
||||
<ng-container *ngIf="method === paymentMethodType.Card">
|
||||
<div class="row">
|
||||
<div class="form-group col-5">
|
||||
<label for="card_number">{{'number' | i18n}}</label>
|
||||
<input id="card_number" class="form-control" type="text" name="card_number" [(ngModel)]="card.number" required pattern="[0-9]*"
|
||||
autocomplete="cc-number">
|
||||
<div class="form-group col-4">
|
||||
<label for="stripe-card-number-element">{{'number' | i18n}}</label>
|
||||
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
<div class="form-group col-7 d-flex align-items-end">
|
||||
<img src="../../images/cards.png" alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay" width="323" height="32">
|
||||
<div class="form-group col-8 d-flex align-items-end">
|
||||
<img src="../../images/cards.png" alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
|
||||
width="323" height="32">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="exp_month">{{'expirationMonth' | i18n}}</label>
|
||||
<select id="exp_month" class="form-control" name="exp_month" [(ngModel)]="card.exp_month" required autocomplete="cc-exp-month">
|
||||
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
<label for="stripe-card-expiry-element">{{'expiration' | i18n}}</label>
|
||||
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="exp_year">{{'expirationYear' | i18n}}</label>
|
||||
<select id="exp_year" class="form-control" name="exp_year" [(ngModel)]="card.exp_year" required autocomplete="cc-exp-year">
|
||||
<option *ngFor="let o of cardExpYearOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="cvc" class="d-flex">
|
||||
<label for="stripe-card-cvc-element" class="d-flex">
|
||||
{{'securityCode' | i18n}}
|
||||
<a href="https://www.cvvnumber.com/cvv.html" target="_blank" rel="noopener noreferrer" class="ml-auto" title="{{'learnMore' | i18n}}">
|
||||
<a href="https://www.cvvnumber.com/cvv.html" tabindex="-1" target="_blank" rel="noopener noreferrer"
|
||||
class="ml-auto" title="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o"></i>
|
||||
</a>
|
||||
</label>
|
||||
<input id="cvc" class="form-control" type="text" name="cvc" [(ngModel)]="card.cvc" required autocomplete="cc-csc">
|
||||
</div>
|
||||
<div class="form-group col-5">
|
||||
<label for="address_country">{{'country' | i18n}}</label>
|
||||
<select id="address_country" class="form-control" [(ngModel)]="card.address_country" required name="address_country" autocomplete="country">
|
||||
<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, Province of China</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 class="form-group col-4">
|
||||
<label for="address_zip">{{'zipPostalCode' | i18n}}</label>
|
||||
<input id="address_zip" class="form-control" type="text" name="address_zip" [(ngModel)]="card.address_zip" required autocomplete="postal-code">
|
||||
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="method === 'paypal'">
|
||||
<div class="mb-3">
|
||||
<div id="bt-dropin-container" class="mb-1"></div>
|
||||
<small class="text-muted">{{'paypalClickSubmit' | i18n}}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="method === 'bank'">
|
||||
<ng-container *ngIf="method === paymentMethodType.BankAccount">
|
||||
<app-callout type="warning" title="{{'verifyBankAccount' | i18n}}">
|
||||
{{'verifyBankAccountInitialDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}
|
||||
</app-callout>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="routing_number">{{'routingNumber' | i18n}}</label>
|
||||
<input id="routing_number" class="form-control" type="text" name="routing_number" [(ngModel)]="bank.routing_number" required
|
||||
appInputVerbatim>
|
||||
<input id="routing_number" class="form-control" type="text" name="routing_number"
|
||||
[(ngModel)]="bank.routing_number" required appInputVerbatim>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="account_number">{{'accountNumber' | i18n}}</label>
|
||||
<input id="account_number" class="form-control" type="text" name="account_number" [(ngModel)]="bank.account_number" required
|
||||
appInputVerbatim>
|
||||
<input id="account_number" class="form-control" type="text" name="account_number"
|
||||
[(ngModel)]="bank.account_number" required appInputVerbatim>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="account_holder_name">{{'accountHolderName' | i18n}}</label>
|
||||
<input id="account_holder_name" class="form-control" type="text" name="account_holder_name" [(ngModel)]="bank.account_holder_name"
|
||||
required>
|
||||
<input id="account_holder_name" class="form-control" type="text" name="account_holder_name"
|
||||
[(ngModel)]="bank.account_holder_name" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="account_holder_type">{{'bankAccountType' | i18n}}</label>
|
||||
<select id="account_holder_type" class="form-control" name="account_holder_type" [(ngModel)]="bank.account_holder_type" required>
|
||||
<select id="account_holder_type" class="form-control" name="account_holder_type"
|
||||
[(ngModel)]="bank.account_holder_type" required>
|
||||
<option value="">-- {{'select' | i18n}} --</option>
|
||||
<option value="company">{{'bankAccountTypeCompany' | i18n}}</option>
|
||||
<option value="individual">{{'bankAccountTypeIndividual' | i18n}}</option>
|
||||
@@ -344,3 +81,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="method === paymentMethodType.PayPal">
|
||||
<div class="mb-3">
|
||||
<div id="bt-dropin-container" class="mb-1"></div>
|
||||
<small class="text-muted">{{'paypalClickSubmit' | i18n}}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="method === paymentMethodType.Credit">
|
||||
<app-callout type="note">
|
||||
{{'makeSureEnoughCredit' | i18n}}
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
|
||||
@@ -4,14 +4,29 @@ import {
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
|
||||
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
const Keys = {
|
||||
stripeTest: 'pk_test_KPoCfZXu7mznb9uSCPZ2JpTD',
|
||||
stripeLive: 'pk_live_bpN0P37nMxrMQkcaHXtAybJk',
|
||||
btSandbox: 'sandbox_r72q8jq6_9pnxkwm75f87sdc2',
|
||||
btProduction: 'production_qfbsv8kc_njj2zjtyngtjmbjd',
|
||||
import { WebConstants } from '../../services/webConstants';
|
||||
|
||||
const StripeElementStyle = {
|
||||
base: {
|
||||
color: '#333333',
|
||||
fontFamily: '"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
|
||||
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
||||
fontSize: '14px',
|
||||
fontSmoothing: 'antialiased',
|
||||
},
|
||||
invalid: {
|
||||
color: '#333333',
|
||||
},
|
||||
};
|
||||
|
||||
const StripeElementClasses = {
|
||||
focus: 'is-focused',
|
||||
empty: 'is-empty',
|
||||
invalid: 'is-invalid',
|
||||
};
|
||||
|
||||
@Component({
|
||||
@@ -20,17 +35,11 @@ const Keys = {
|
||||
})
|
||||
export class PaymentComponent implements OnInit {
|
||||
@Input() showOptions = true;
|
||||
@Input() method: 'card' | 'paypal' | 'bank' = 'card';
|
||||
@Input() method = PaymentMethodType.Card;
|
||||
@Input() hideBank = false;
|
||||
@Input() hidePaypal = false;
|
||||
@Input() hideCredit = false;
|
||||
|
||||
card: any = {
|
||||
number: null,
|
||||
exp_month: null,
|
||||
exp_year: null,
|
||||
address_country: '',
|
||||
address_zip: null,
|
||||
};
|
||||
bank: any = {
|
||||
routing_number: null,
|
||||
account_number: null,
|
||||
@@ -39,54 +48,38 @@ export class PaymentComponent implements OnInit {
|
||||
currency: 'USD',
|
||||
country: 'US',
|
||||
};
|
||||
cardExpMonthOptions: any[];
|
||||
cardExpYearOptions: any[];
|
||||
|
||||
private stripeScript: HTMLScriptElement;
|
||||
paymentMethodType = PaymentMethodType;
|
||||
|
||||
private btScript: HTMLScriptElement;
|
||||
private btInstance: any = null;
|
||||
private stripeScript: HTMLScriptElement;
|
||||
private stripe: any = null;
|
||||
private stripeElements: any = null;
|
||||
private stripeCardNumberElement: any = null;
|
||||
private stripeCardExpiryElement: any = null;
|
||||
private stripeCardCvcElement: any = null;
|
||||
|
||||
constructor(i18nService: I18nService, private platformUtilsService: PlatformUtilsService) {
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {
|
||||
this.stripeScript = window.document.createElement('script');
|
||||
this.stripeScript.src = 'https://js.stripe.com/v2/';
|
||||
this.stripeScript.src = 'https://js.stripe.com/v3/';
|
||||
this.stripeScript.async = true;
|
||||
this.stripeScript.onload = () => {
|
||||
(window as any).Stripe.setPublishableKey(
|
||||
this.platformUtilsService.isDev() ? Keys.stripeTest : Keys.stripeLive);
|
||||
this.stripe = (window as any).Stripe(this.platformUtilsService.isDev() ?
|
||||
WebConstants.stripeTestKey : WebConstants.stripeLiveKey);
|
||||
this.stripeElements = this.stripe.elements();
|
||||
this.setStripeElement();
|
||||
};
|
||||
this.btScript = window.document.createElement('script');
|
||||
this.btScript.src = 'scripts/dropin.js';
|
||||
this.btScript.async = true;
|
||||
|
||||
this.cardExpMonthOptions = [
|
||||
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
|
||||
{ name: '01 - ' + i18nService.t('january'), value: '01' },
|
||||
{ name: '02 - ' + i18nService.t('february'), value: '02' },
|
||||
{ name: '03 - ' + i18nService.t('march'), value: '03' },
|
||||
{ name: '04 - ' + i18nService.t('april'), value: '04' },
|
||||
{ name: '05 - ' + i18nService.t('may'), value: '05' },
|
||||
{ name: '06 - ' + i18nService.t('june'), value: '06' },
|
||||
{ name: '07 - ' + i18nService.t('july'), value: '07' },
|
||||
{ name: '08 - ' + i18nService.t('august'), value: '08' },
|
||||
{ name: '09 - ' + i18nService.t('september'), value: '09' },
|
||||
{ name: '10 - ' + i18nService.t('october'), value: '10' },
|
||||
{ name: '11 - ' + i18nService.t('november'), value: '11' },
|
||||
{ name: '12 - ' + i18nService.t('december'), value: '12' },
|
||||
];
|
||||
|
||||
this.cardExpYearOptions = [
|
||||
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
|
||||
];
|
||||
const year = (new Date()).getFullYear();
|
||||
for (let i = year; i < (year + 15); i++) {
|
||||
this.cardExpYearOptions.push({ name: i.toString(), value: i.toString().slice(-2) });
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.showOptions) {
|
||||
this.hidePaypal = this.method !== 'paypal';
|
||||
this.hideBank = this.method !== 'bank';
|
||||
this.hidePaypal = this.method !== PaymentMethodType.PayPal;
|
||||
this.hideBank = this.method !== PaymentMethodType.BankAccount;
|
||||
this.hideCredit = this.method !== PaymentMethodType.Credit;
|
||||
}
|
||||
window.document.head.appendChild(this.stripeScript);
|
||||
if (!this.hidePaypal) {
|
||||
@@ -96,68 +89,92 @@ export class PaymentComponent implements OnInit {
|
||||
|
||||
ngOnDestroy() {
|
||||
window.document.head.removeChild(this.stripeScript);
|
||||
Array.from(window.document.querySelectorAll('iframe')).forEach((el) => {
|
||||
if (el.src != null && el.src.indexOf('stripe') > -1) {
|
||||
window.document.body.removeChild(el);
|
||||
}
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
Array.from(window.document.querySelectorAll('iframe')).forEach((el) => {
|
||||
if (el.src != null && el.src.indexOf('stripe') > -1) {
|
||||
try {
|
||||
window.document.body.removeChild(el);
|
||||
} catch { }
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
if (!this.hidePaypal) {
|
||||
window.document.head.removeChild(this.btScript);
|
||||
const btStylesheet = window.document.head.querySelector('#braintree-dropin-stylesheet');
|
||||
if (btStylesheet != null) {
|
||||
window.document.head.removeChild(btStylesheet);
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
Array.from(window.document.head.querySelectorAll('script')).forEach((el) => {
|
||||
if (el.src != null && el.src.indexOf('paypal') > -1) {
|
||||
try {
|
||||
window.document.head.removeChild(el);
|
||||
} catch { }
|
||||
}
|
||||
});
|
||||
const btStylesheet = window.document.head.querySelector('#braintree-dropin-stylesheet');
|
||||
if (btStylesheet != null) {
|
||||
try {
|
||||
window.document.head.removeChild(btStylesheet);
|
||||
} catch { }
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
changeMethod() {
|
||||
if (this.method !== 'paypal') {
|
||||
this.btInstance = null;
|
||||
return;
|
||||
}
|
||||
this.btInstance = null;
|
||||
|
||||
window.setTimeout(() => {
|
||||
(window as any).braintree.dropin.create({
|
||||
authorization: this.platformUtilsService.isDev() ? Keys.btSandbox : Keys.btProduction,
|
||||
container: '#bt-dropin-container',
|
||||
paymentOptionPriority: ['paypal'],
|
||||
paypal: {
|
||||
flow: 'vault',
|
||||
buttonStyle: {
|
||||
label: 'pay',
|
||||
size: 'medium',
|
||||
shape: 'pill',
|
||||
color: 'blue',
|
||||
if (this.method === PaymentMethodType.PayPal) {
|
||||
window.setTimeout(() => {
|
||||
(window as any).braintree.dropin.create({
|
||||
authorization: this.platformUtilsService.isDev() ?
|
||||
WebConstants.btSandboxKey : WebConstants.btProductionKey,
|
||||
container: '#bt-dropin-container',
|
||||
paymentOptionPriority: ['paypal'],
|
||||
paypal: {
|
||||
flow: 'vault',
|
||||
buttonStyle: {
|
||||
label: 'pay',
|
||||
size: 'medium',
|
||||
shape: 'pill',
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, (createErr: any, instance: any) => {
|
||||
if (createErr != null) {
|
||||
// tslint:disable-next-line
|
||||
console.error(createErr);
|
||||
return;
|
||||
}
|
||||
this.btInstance = instance;
|
||||
});
|
||||
}, 250);
|
||||
}, (createErr: any, instance: any) => {
|
||||
if (createErr != null) {
|
||||
// tslint:disable-next-line
|
||||
console.error(createErr);
|
||||
return;
|
||||
}
|
||||
this.btInstance = instance;
|
||||
});
|
||||
}, 250);
|
||||
} else {
|
||||
this.setStripeElement();
|
||||
}
|
||||
}
|
||||
|
||||
createPaymentToken(): Promise<string> {
|
||||
createPaymentToken(): Promise<[string, PaymentMethodType]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.method === 'paypal') {
|
||||
if (this.method === PaymentMethodType.Credit) {
|
||||
resolve([null, this.method]);
|
||||
} else if (this.method === PaymentMethodType.PayPal) {
|
||||
this.btInstance.requestPaymentMethod().then((payload: any) => {
|
||||
resolve(payload.nonce);
|
||||
resolve([payload.nonce, this.method]);
|
||||
}).catch((err: any) => {
|
||||
reject(err.message);
|
||||
});
|
||||
} else if (this.method === 'card' || this.method === 'bank') {
|
||||
const createObj: any = this.method === 'card' ? (window as any).Stripe.card :
|
||||
(window as any).Stripe.bankAccount;
|
||||
const sourceObj = this.method === 'card' ? this.card : this.bank;
|
||||
createObj.createToken(sourceObj, (status: number, response: any) => {
|
||||
if (status === 200 && response.id != null) {
|
||||
resolve(response.id);
|
||||
} else if (response.error != null) {
|
||||
reject(response.error.message);
|
||||
} else if (this.method === PaymentMethodType.Card || this.method === PaymentMethodType.BankAccount) {
|
||||
let sourceObj: any = null;
|
||||
let createObj: any = null;
|
||||
if (this.method === PaymentMethodType.Card) {
|
||||
sourceObj = this.stripeCardNumberElement;
|
||||
} else {
|
||||
sourceObj = 'bank_account';
|
||||
createObj = this.bank;
|
||||
}
|
||||
this.stripe.createToken(sourceObj, createObj).then((result: any) => {
|
||||
if (result.error) {
|
||||
reject(result.error.message);
|
||||
} else if (result.token && result.token.id != null) {
|
||||
resolve([result.token.id, this.method]);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
@@ -166,7 +183,33 @@ export class PaymentComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
getCountry(): string {
|
||||
return this.card.address_country;
|
||||
private setStripeElement() {
|
||||
window.setTimeout(() => {
|
||||
if (this.method === PaymentMethodType.Card) {
|
||||
if (this.stripeCardNumberElement == null) {
|
||||
this.stripeCardNumberElement = this.stripeElements.create('cardNumber', {
|
||||
style: StripeElementStyle,
|
||||
classes: StripeElementClasses,
|
||||
placeholder: '',
|
||||
});
|
||||
}
|
||||
if (this.stripeCardExpiryElement == null) {
|
||||
this.stripeCardExpiryElement = this.stripeElements.create('cardExpiry', {
|
||||
style: StripeElementStyle,
|
||||
classes: StripeElementClasses,
|
||||
});
|
||||
}
|
||||
if (this.stripeCardCvcElement == null) {
|
||||
this.stripeCardCvcElement = this.stripeElements.create('cardCvc', {
|
||||
style: StripeElementStyle,
|
||||
classes: StripeElementClasses,
|
||||
placeholder: '',
|
||||
});
|
||||
}
|
||||
this.stripeCardNumberElement.mount('#stripe-card-number-element');
|
||||
this.stripeCardExpiryElement.mount('#stripe-card-expiry-element');
|
||||
this.stripeCardCvcElement.mount('#stripe-card-cvc-element');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-lg" [ngClass]="{'mb-0':!selfHosted}">{{'premiumPrice' | i18n : (premiumPrice | currency:'$')}}</p>
|
||||
<a href="https://vault.bitwarden.com/#/settings/premium" target="_blank" rel="noopener" class="btn btn-outline-secondary"
|
||||
*ngIf="selfHosted">
|
||||
<a href="https://vault.bitwarden.com/#/settings/premium" target="_blank" rel="noopener"
|
||||
class="btn btn-outline-secondary" *ngIf="selfHosted">
|
||||
{{'purchasePremium' | i18n}}
|
||||
</a>
|
||||
</app-callout>
|
||||
@@ -57,9 +57,11 @@
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="additionalStorage">{{'additionalStorageGb' | i18n}}</label>
|
||||
<input id="additionalStorage" class="form-control" type="number" name="AdditionalStorageGb" [(ngModel)]="additionalStorage"
|
||||
min="0" max="99" step="1" placeholder="{{'additionalStorageGbDesc' | i18n}}">
|
||||
<small class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$') : ('year' | i18n)}}</small>
|
||||
<input id="additionalStorage" class="form-control" type="number" name="AdditionalStorageGb"
|
||||
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
|
||||
placeholder="{{'additionalStorageGbDesc' | i18n}}">
|
||||
<small
|
||||
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$') : ('year' | i18n)}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
|
||||
|
||||
@@ -45,7 +45,7 @@ export class PremiumComponent implements OnInit {
|
||||
this.canAccessPremium = await this.userService.canAccessPremium();
|
||||
const premium = await this.tokenService.getPremium();
|
||||
if (premium) {
|
||||
this.router.navigate(['/settings/billing']);
|
||||
this.router.navigate(['/settings/subscription']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -76,9 +76,12 @@ export class PremiumComponent implements OnInit {
|
||||
return this.finalizePremium();
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.paymentComponent.createPaymentToken().then((token) => {
|
||||
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
|
||||
const fd = new FormData();
|
||||
fd.append('paymentToken', token);
|
||||
fd.append('paymentMethodType', result[1].toString());
|
||||
if (result[0] != null) {
|
||||
fd.append('paymentToken', result[0]);
|
||||
}
|
||||
fd.append('additionalStorageGb', (this.additionalStorage || 0).toString());
|
||||
return this.apiService.postPremium(fd);
|
||||
}).then(() => {
|
||||
@@ -95,11 +98,11 @@ export class PremiumComponent implements OnInit {
|
||||
this.analytics.eventTrack.next({ action: 'Signed Up Premium' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('premiumUpdated'));
|
||||
this.messagingService.send('purchasedPremium');
|
||||
this.router.navigate(['/settings/billing']);
|
||||
this.router.navigate(['/settings/subscription']);
|
||||
}
|
||||
|
||||
get additionalStorageTotal(): number {
|
||||
return this.storageGbPrice * this.additionalStorage;
|
||||
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
|
||||
}
|
||||
|
||||
get total(): number {
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordHint">{{'masterPassHintLabel' | i18n}}</label>
|
||||
<input id="masterPasswordHint" class="form-control" type="text" name="MasterPasswordHint" [(ngModel)]="profile.masterPasswordHint">
|
||||
<input id="masterPasswordHint" class="form-control" type="text" name="MasterPasswordHint"
|
||||
[(ngModel)]="profile.masterPasswordHint">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@@ -25,7 +26,8 @@
|
||||
<hr>
|
||||
<p *ngIf="fingerprint">
|
||||
{{'yourAccountsFingerprint' | i18n}}:
|
||||
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
|
||||
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener"
|
||||
title="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o"></i></a><br>
|
||||
<code>{{fingerprint}}</code>
|
||||
</p>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<p>{{(organizationId ? 'purgeOrgVaultDesc' : 'purgeVaultDesc') | i18n}}</p>
|
||||
<app-callout type="warning">{{'purgeVaultWarning' | i18n}}</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
|
||||
appAutofocus appInputVerbatim>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
<a routerLink="organizations" class="list-group-item" routerLinkActive="active">
|
||||
{{'organizations' | i18n}}
|
||||
</a>
|
||||
<a routerLink="billing" class="list-group-item" routerLinkActive="active" *ngIf="premium">
|
||||
{{'billingAndLicensing' | i18n}}
|
||||
<a routerLink="subscription" class="list-group-item" routerLinkActive="active" *ngIf="premium">
|
||||
{{'premiumMembership' | i18n}}
|
||||
</a>
|
||||
<a routerLink="premium" class="list-group-item" routerLinkActive="active" *ngIf="!premium">
|
||||
{{'goPremium' | i18n}}
|
||||
</a>
|
||||
<a routerLink="billing" class="list-group-item" routerLinkActive="active" *ngIf="!selfHosted">
|
||||
{{'billing' | i18n}}
|
||||
</a>
|
||||
<a routerLink="two-factor" class="list-group-item" routerLinkActive="active">
|
||||
{{'twoStepLogin' | i18n}}
|
||||
</a>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
@@ -17,9 +18,10 @@ const BroadcasterSubscriptionId = 'SettingsComponent';
|
||||
})
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
premium: boolean;
|
||||
selfHosted: boolean;
|
||||
|
||||
constructor(private tokenService: TokenService, private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone) { }
|
||||
private ngZone: NgZone, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
@@ -33,6 +35,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
|
||||
this.selfHosted = await this.platformUtilsService.isSelfHost();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
|
||||
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
|
||||
*ngIf="!authed">
|
||||
</app-two-factor-verify>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
|
||||
<div class="modal-body">
|
||||
@@ -32,15 +33,18 @@
|
||||
<ul class="fa-ul">
|
||||
<li>
|
||||
<i class="fa-li fa fa-apple"></i>{{'iosDevices' | i18n}}:
|
||||
<a href="https://itunes.apple.com/us/app/authy/id494168017?mt=8" target="_blank" rel="noopener">Authy</a>
|
||||
<a href="https://itunes.apple.com/us/app/authy/id494168017?mt=8" target="_blank"
|
||||
rel="noopener">Authy</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa-li fa fa-android"></i>{{'androidDevices' | i18n}}:
|
||||
<a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank" rel="noopener">Authy</a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank"
|
||||
rel="noopener">Authy</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa-li fa fa-windows"></i>{{'windowsDevices' | i18n}}:
|
||||
<a href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj" target="_blank" rel="noopener">Microsoft Authenticator</a>
|
||||
<a href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj" target="_blank"
|
||||
rel="noopener">Microsoft Authenticator</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p>{{'twoStepAuthenticatorAppsRecommended' | i18n}}</p>
|
||||
@@ -54,7 +58,8 @@
|
||||
</p>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<label for="token">3. {{'twoStepAuthenticatorEnterCode' | i18n}}</label>
|
||||
<input id="token" type="text" name="Token" class="form-control" [(ngModel)]="token" required appInputVerbatim>
|
||||
<input id="token" type="text" name="Token" class="form-control" [(ngModel)]="token" required
|
||||
appInputVerbatim>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -63,7 +68,8 @@
|
||||
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
|
||||
<span *ngIf="enabled">{{'disable' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
|
||||
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
|
||||
*ngIf="!authed">
|
||||
</app-two-factor-verify>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed" autocomplete="off">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed"
|
||||
autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle">
|
||||
@@ -30,16 +32,18 @@
|
||||
<p>{{'twoFactorDuoDesc' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="ikey">{{'twoFactorDuoIntegrationKey' | i18n}}</label>
|
||||
<input id="ikey" type="text" name="IntegrationKey" class="form-control" [(ngModel)]="ikey" required appInputVerbatim>
|
||||
<input id="ikey" type="text" name="IntegrationKey" class="form-control" [(ngModel)]="ikey"
|
||||
required appInputVerbatim>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="skey">{{'twoFactorDuoSecretKey' | i18n}}</label>
|
||||
<input id="skey" type="password" name="SecretKey" class="form-control" [(ngModel)]="skey" required appInputVerbatim autocomplete="new-password">
|
||||
<input id="skey" type="password" name="SecretKey" class="form-control" [(ngModel)]="skey"
|
||||
required appInputVerbatim autocomplete="new-password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="host">{{'twoFactorDuoApiHostname' | i18n}}</label>
|
||||
<input id="host" type="text" name="Host" class="form-control" [(ngModel)]="host" placeholder="{{'ex' | i18n}} api-xxxxxxxx.duosecurity.com"
|
||||
required appInputVerbatim>
|
||||
<input id="host" type="text" name="Host" class="form-control" [(ngModel)]="host"
|
||||
placeholder="{{'ex' | i18n}} api-xxxxxxxx.duosecurity.com" required appInputVerbatim>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -49,7 +53,8 @@
|
||||
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
|
||||
<span *ngIf="enabled">{{'disable' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
|
||||
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
|
||||
*ngIf="!authed">
|
||||
</app-two-factor-verify>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
|
||||
<div class="modal-body">
|
||||
@@ -27,11 +28,13 @@
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="email">1. {{'twoFactorEmailEnterEmail' | i18n}}</label>
|
||||
<input id="email" type="text" name="Email" class="form-control" [(ngModel)]="email" required inputmode="email" appInputVerbatim="false">
|
||||
<input id="email" type="text" name="Email" class="form-control" [(ngModel)]="email" required
|
||||
inputmode="email" appInputVerbatim="false">
|
||||
</div>
|
||||
<div class="mb-3 d-flex">
|
||||
<button #sendBtn type="button" class="btn btn-outline-primary btn-sm btn-submit align-self-start" (click)="sendEmail()" [appApiAction]="emailPromise"
|
||||
[disabled]="sendBtn.loading">
|
||||
<button #sendBtn type="button"
|
||||
class="btn btn-outline-primary btn-sm btn-submit align-self-start" (click)="sendEmail()"
|
||||
[appApiAction]="emailPromise" [disabled]="sendBtn.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'sendEmail' | i18n}}</span>
|
||||
</button>
|
||||
@@ -41,7 +44,8 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="token">2. {{'twoFactorEmailEnterCode' | i18n}}</label>
|
||||
<input id="token" type="text" name="Token" class="form-control" [(ngModel)]="token" required appInputVerbatim>
|
||||
<input id="token" type="text" name="Token" class="form-control" [(ngModel)]="token" required
|
||||
appInputVerbatim>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -51,7 +55,8 @@
|
||||
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
|
||||
<span *ngIf="enabled">{{'disable' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
|
||||
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
|
||||
*ngIf="!authed">
|
||||
</app-two-factor-verify>
|
||||
<ng-container *ngIf="authed">
|
||||
<div class="modal-body text-center">
|
||||
@@ -23,8 +24,10 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="print()" *ngIf="code">{{'printCode' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="print()"
|
||||
*ngIf="code">{{'printCode' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user