mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b9c90f99b | ||
|
|
4050bc1da8 | ||
|
|
4bb9051136 | ||
|
|
ceca4fbe53 | ||
|
|
9b7c0288d4 | ||
|
|
392a90c02c | ||
|
|
7a58f6d967 | ||
|
|
35d1e51f9b | ||
|
|
9729a4c724 | ||
|
|
f13713a055 | ||
|
|
31655f7832 | ||
|
|
fb4bb81595 | ||
|
|
31cb6916c6 | ||
|
|
61d37615af | ||
|
|
eaa7701696 | ||
|
|
d6cff8e0b0 | ||
|
|
8ba761b33c | ||
|
|
05e7e452df | ||
|
|
3f0fd4f771 | ||
|
|
a587c1d1da | ||
|
|
c3355f7fe4 | ||
|
|
c182d874af | ||
|
|
ab9ebfb667 | ||
|
|
cb953eda61 | ||
|
|
93c291dba1 | ||
|
|
603a1ef046 | ||
|
|
5a504b00fb | ||
|
|
dfa59dc93d | ||
|
|
534bcdd52c | ||
|
|
ea032bf551 | ||
|
|
b44eee8d81 | ||
|
|
8f57ada128 | ||
|
|
3963990831 | ||
|
|
dc1ffafdf3 | ||
|
|
4a0b4de322 | ||
|
|
0ede65e9ca | ||
|
|
0ebf30b8b6 | ||
|
|
4222b192c4 | ||
|
|
4a301aaec3 | ||
|
|
97a3a97a15 | ||
|
|
c526d73e23 | ||
|
|
58baf137aa | ||
|
|
066ab1500f | ||
|
|
224a468712 | ||
|
|
dd282383d7 | ||
|
|
9a99a95b15 | ||
|
|
e814494e37 | ||
|
|
90a0155be1 | ||
|
|
555d40408d | ||
|
|
0c3fbeb0b7 | ||
|
|
2f27decaa1 | ||
|
|
867115659f | ||
|
|
6282ab58db | ||
|
|
95c25c1bcd | ||
|
|
74a62de7d0 | ||
|
|
7fc021648d | ||
|
|
95914ad312 | ||
|
|
5ed00e037a | ||
|
|
6f3074536a | ||
|
|
21f5cb36bb | ||
|
|
7b7592822f | ||
|
|
9c7b7b0d75 | ||
|
|
d88b23c42d | ||
|
|
1602c0aca2 | ||
|
|
384978a511 | ||
|
|
afd7a0494f | ||
|
|
ac1f8a69e1 | ||
|
|
05cfa99ea0 | ||
|
|
9b43ccbbc0 | ||
|
|
6d8b156455 | ||
|
|
1b9943a4c8 | ||
|
|
8232a4c9c8 | ||
|
|
9d4d64c95a | ||
|
|
2d0acc7663 | ||
|
|
4231ed74ba | ||
|
|
912e1cf89f | ||
|
|
26d4fb8005 | ||
|
|
4a6c0b39a8 | ||
|
|
9d01bba170 | ||
|
|
85c0ddba10 | ||
|
|
2664059812 | ||
|
|
b7e4d9c806 | ||
|
|
95b91f0ce2 | ||
|
|
f0407e4327 | ||
|
|
a7555f56e7 | ||
|
|
b093ed33b2 | ||
|
|
ec1a45ba18 | ||
|
|
def5dc3b0f | ||
|
|
24ec89c220 | ||
|
|
303e70bb58 | ||
|
|
ec3e92fc19 | ||
|
|
5ae776309d | ||
|
|
76dd606a48 | ||
|
|
8998798fa4 | ||
|
|
60ee82ca47 | ||
|
|
e1284002a9 | ||
|
|
8252512784 | ||
|
|
1390d7eb1d | ||
|
|
8da1bb13ff | ||
|
|
340e377b37 | ||
|
|
171589fb3d | ||
|
|
bcd07cce0d | ||
|
|
68880114b4 | ||
|
|
eb2360ae24 | ||
|
|
62712a352b | ||
|
|
745e6c1715 | ||
|
|
e20a75eb0c | ||
|
|
a24c41ff25 | ||
|
|
69f0339bd5 | ||
|
|
5e7c9a7278 | ||
|
|
726c323fe1 | ||
|
|
e96cbe2710 | ||
|
|
323e54b4bd | ||
|
|
7ab132bbf6 | ||
|
|
6b09210a80 | ||
|
|
be80d62c01 | ||
|
|
30587d625a | ||
|
|
af43cd407e | ||
|
|
647388e475 | ||
|
|
329e06ac30 | ||
|
|
5d96138720 | ||
|
|
66b275605c | ||
|
|
9b7478c0c7 | ||
|
|
668271bb31 | ||
|
|
1aa93e7737 | ||
|
|
a0864f5f67 | ||
|
|
6e9f71f942 | ||
|
|
65211372df | ||
|
|
2ca8d8817a | ||
|
|
ec266ea657 | ||
|
|
d117aa5139 | ||
|
|
4534b7d4dc | ||
|
|
707fe01d77 | ||
|
|
0e09ba0dd5 | ||
|
|
989560f23c | ||
|
|
844a9f934f | ||
|
|
b5348c593a | ||
|
|
7f809ba541 | ||
|
|
f9058fcddc | ||
|
|
05c9957fd2 | ||
|
|
675739d24f | ||
|
|
10be0867ad | ||
|
|
d2a4b85bdd | ||
|
|
782061ac5e | ||
|
|
8d98e9e6f9 | ||
|
|
4aa75e9376 | ||
|
|
c6d6eecb43 | ||
|
|
1d6d7b8aa8 | ||
|
|
68ed8e51bd | ||
|
|
d4dd962193 | ||
|
|
7dfb70eb8e | ||
|
|
53675eeba7 | ||
|
|
6399973bfa | ||
|
|
f1384f5dc1 | ||
|
|
027cad9e52 | ||
|
|
dffcff48a0 | ||
|
|
0391f31b3a | ||
|
|
eb7b0ba92f | ||
|
|
3a136e1464 | ||
|
|
8792bcabcb | ||
|
|
c362fc4677 | ||
|
|
f471fe62ea | ||
|
|
f19aa96f3e | ||
|
|
2d6b4f1216 | ||
|
|
22a8f766c7 | ||
|
|
86bc6fa807 | ||
|
|
14b094cfe0 | ||
|
|
d90b36bd33 | ||
|
|
18b800ff7a | ||
|
|
7ab56a9616 | ||
|
|
6f8352033b | ||
|
|
188ac5051a | ||
|
|
335e0dd575 | ||
|
|
67b187f884 | ||
|
|
c2d262ea1d | ||
|
|
8683465d70 | ||
|
|
8c9705eec0 | ||
|
|
d14a8bc301 | ||
|
|
72aceedab4 | ||
|
|
7c5ee1bd00 | ||
|
|
26aa79db1a | ||
|
|
2629aaf368 | ||
|
|
3973ebc00f | ||
|
|
6cddb5f3ba | ||
|
|
7c55da8cc6 | ||
|
|
0c9f122719 | ||
|
|
1d941baff1 | ||
|
|
e5226d7ffc | ||
|
|
aa3d69cb94 | ||
|
|
e68d386d3d | ||
|
|
b322f20c81 | ||
|
|
7d7a9f3dc6 | ||
|
|
977a5e868f | ||
|
|
41ff511165 | ||
|
|
aadbb970b6 | ||
|
|
1873ce41b6 | ||
|
|
ff51e4cc36 | ||
|
|
73b87f2e97 | ||
|
|
1444c99458 | ||
|
|
85c3056223 | ||
|
|
4a815b0bdf | ||
|
|
b7525e1e7e | ||
|
|
c3f64fe9c4 | ||
|
|
34f6bc2403 | ||
|
|
9ecec972ca | ||
|
|
80febf97d3 | ||
|
|
71073874eb | ||
|
|
f3dfeac125 | ||
|
|
f12e73519e | ||
|
|
19f7dda4cc | ||
|
|
221397b159 | ||
|
|
91766ecea3 | ||
|
|
191be134f9 | ||
|
|
56d279ae1e | ||
|
|
3e61464dac | ||
|
|
85ca10dbb3 | ||
|
|
eaf08c45d9 | ||
|
|
bcb44e8cf7 | ||
|
|
d215e0716e | ||
|
|
f635162832 | ||
|
|
c892480086 | ||
|
|
ea49d17c47 | ||
|
|
39c32b0e62 | ||
|
|
5cdfa35a76 | ||
|
|
147b3ff993 | ||
|
|
662c229de1 | ||
|
|
c90cb2ae6e | ||
|
|
864d070656 | ||
|
|
e8ac2b561a | ||
|
|
c71a432ce4 | ||
|
|
e3ca470a6a |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.sh eol=lf
|
||||
.dockerignore eol=lf
|
||||
dockerfile eol=lf
|
||||
13
gulpfile.js
13
gulpfile.js
@@ -24,12 +24,13 @@ function webfonts() {
|
||||
.pipe(gulp.dest(paths.cssDir));
|
||||
};
|
||||
|
||||
function version() {
|
||||
function version(cb) {
|
||||
fs.writeFileSync(paths.build + 'version.json', '{"version":"' + package.version + '"}');
|
||||
cb();
|
||||
}
|
||||
|
||||
gulp.task('clean', clean);
|
||||
gulp.task('webfonts', ['clean'], webfonts);
|
||||
gulp.task('prebuild', ['webfonts']);
|
||||
gulp.task('version', version);
|
||||
gulp.task('postdist', ['version']);
|
||||
exports.clean = clean;
|
||||
exports.webfonts = gulp.series(clean, webfonts);
|
||||
exports.prebuild = gulp.series(clean, webfonts);
|
||||
exports.version = version;
|
||||
exports.postdist = version;
|
||||
|
||||
2
jslib
2
jslib
Submodule jslib updated: 4ca7a9709e...27566c3fd5
7697
package-lock.json
generated
7697
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
112
package.json
112
package.json
@@ -1,91 +1,95 @@
|
||||
{
|
||||
"name": "bitwarden-web",
|
||||
"version": "2.1.1",
|
||||
"version": "2.7.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",
|
||||
"build": "gulp prebuild && webpack --config webpack.config.js",
|
||||
"build:watch": "gulp prebuild && webpack-serve --config webpack.config.js",
|
||||
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack --config webpack.config.js",
|
||||
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production webpack-serve --config webpack.config.js",
|
||||
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack-serve --config webpack.config.js",
|
||||
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack-serve --config webpack.config.js",
|
||||
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack --config webpack.config.js",
|
||||
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack-serve --config webpack.config.js",
|
||||
"build": "gulp prebuild && webpack",
|
||||
"build:watch": "gulp prebuild && webpack-dev-server",
|
||||
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack",
|
||||
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production webpack-dev-server",
|
||||
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
|
||||
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
|
||||
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack",
|
||||
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack-dev-server",
|
||||
"clean:l10n": "git push origin --delete l10n_master",
|
||||
"dist": "npm run build:prod && gulp postdist",
|
||||
"dist:selfhost": "npm run build:selfhost:prod && gulp postdist",
|
||||
"deploy": "npm run dist && gh-pages -d build",
|
||||
"deploy:preview": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/web-preview.git",
|
||||
"deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
|
||||
"lint": "tslint src/**/*.ts || true",
|
||||
"lint:fix": "tslint src/**/*.ts --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "5.2.0",
|
||||
"@ngtools/webpack": "1.10.2",
|
||||
"@types/jquery": "^3.3.2",
|
||||
"@types/lunr": "2.1.5",
|
||||
"@types/node": "8.0.19",
|
||||
"@types/node-forge": "0.6.10",
|
||||
"@types/papaparse": "4.1.33",
|
||||
"@angular/compiler-cli": "^6.1.7",
|
||||
"@ngtools/webpack": "^6.2.1",
|
||||
"@types/jquery": "^3.3.6",
|
||||
"@types/lunr": "^2.1.6",
|
||||
"@types/node-forge": "^0.7.5",
|
||||
"@types/papaparse": "^4.5.3",
|
||||
"@types/webcrypto": "^0.0.28",
|
||||
"@types/webpack": "^4.4.11",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"angular2-template-loader": "^0.6.2",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"cross-env": "^5.1.4",
|
||||
"css-loader": "^0.28.11",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"del": "^3.0.0",
|
||||
"extract-text-webpack-plugin": "next",
|
||||
"file-loader": "^1.1.11",
|
||||
"file-loader": "^2.0.0",
|
||||
"gh-pages": "^1.2.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-google-webfonts": "^2.0.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"node-sass": "^4.9.2",
|
||||
"sass-loader": "^7.0.2",
|
||||
"style-loader": "^0.21.0",
|
||||
"ts-loader": "^4.3.1",
|
||||
"tslint": "^5.10.0",
|
||||
"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.10.2",
|
||||
"webpack-cli": "^3.0.2",
|
||||
"webpack-serve": "^1.0.2"
|
||||
"webpack": "^4.18.0",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-dev-server": "^3.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "5.2.0",
|
||||
"@angular/common": "5.2.0",
|
||||
"@angular/compiler": "5.2.0",
|
||||
"@angular/core": "5.2.0",
|
||||
"@angular/forms": "5.2.0",
|
||||
"@angular/http": "5.2.0",
|
||||
"@angular/platform-browser": "5.2.0",
|
||||
"@angular/platform-browser-dynamic": "5.2.0",
|
||||
"@angular/router": "5.2.0",
|
||||
"@angular/upgrade": "5.2.0",
|
||||
"angular2-toaster": "4.0.2",
|
||||
"angulartics2": "5.0.1",
|
||||
"bootstrap": "4.1.1",
|
||||
"core-js": "2.4.1",
|
||||
"@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",
|
||||
"@aspnet/signalr": "1.0.4",
|
||||
"@aspnet/signalr-protocol-msgpack": "1.0.4",
|
||||
"angular2-toaster": "6.1.0",
|
||||
"angulartics2": "6.3.0",
|
||||
"big-integer": "1.6.36",
|
||||
"bootstrap": "4.1.3",
|
||||
"braintree-web-drop-in": "1.13.0",
|
||||
"core-js": "2.5.7",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.3.1",
|
||||
"lunr": "2.1.6",
|
||||
"mousetrap": "1.6.1",
|
||||
"ngx-infinite-scroll": "0.8.4",
|
||||
"node-forge": "0.7.1",
|
||||
"papaparse": "4.3.5",
|
||||
"popper.js": "1.14.3",
|
||||
"lunr": "2.3.3",
|
||||
"ngx-infinite-scroll": "6.0.1",
|
||||
"node-forge": "0.7.6",
|
||||
"papaparse": "4.6.0",
|
||||
"popper.js": "1.14.4",
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "5.5.6",
|
||||
"rxjs": "6.3.2",
|
||||
"sweetalert": "2.1.0",
|
||||
"tldjs": "2.0.0",
|
||||
"web-animations-js": "2.3.1",
|
||||
"webcrypto-shim": "0.1.4",
|
||||
"whatwg-fetch": "^2.0.4",
|
||||
"zone.js": "0.8.19"
|
||||
"whatwg-fetch": "3.0.0",
|
||||
"zone.js": "0.8.26",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
0
src/.nojekyll
Normal file
0
src/.nojekyll
Normal file
@@ -1,11 +1,9 @@
|
||||
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 { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { HintComponent as BaseHintComponent } from 'jslib/angular/components/hint.component';
|
||||
|
||||
@@ -14,9 +12,8 @@ import { HintComponent as BaseHintComponent } from 'jslib/angular/components/hin
|
||||
templateUrl: 'hint.component.html',
|
||||
})
|
||||
export class HintComponent extends BaseHintComponent {
|
||||
constructor(router: Router, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
apiService: ApiService) {
|
||||
super(router, analytics, toasterService, i18nService, apiService);
|
||||
constructor(router: Router, i18nService: I18nService,
|
||||
apiService: ApiService, platformUtilsService: PlatformUtilsService) {
|
||||
super(router, i18nService, apiService, platformUtilsService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
@@ -22,13 +19,11 @@ import { LockComponent as BaseLockComponent } from 'jslib/angular/components/loc
|
||||
templateUrl: 'lock.component.html',
|
||||
})
|
||||
export class LockComponent extends BaseLockComponent implements OnInit {
|
||||
constructor(router: Router, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
constructor(router: Router, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
|
||||
userService: UserService, cryptoService: CryptoService,
|
||||
private routerService: RouterService) {
|
||||
super(router, analytics, toasterService, i18nService, platformUtilsService,
|
||||
messagingService, userService, cryptoService);
|
||||
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -4,11 +4,9 @@ import {
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
@@ -20,10 +18,10 @@ import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/l
|
||||
})
|
||||
export class LoginComponent extends BaseLoginComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, private route: ActivatedRoute,
|
||||
storageService: StorageService, private stateService: StateService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, storageService);
|
||||
storageService: StorageService, private stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, platformUtilsService, i18nService, storageService);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export class RecoverDeleteComponent {
|
||||
async submit() {
|
||||
try {
|
||||
const request = new DeleteRecoverRequest();
|
||||
request.email = this.email.toLowerCase();
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
this.formPromise = this.apiService.postAccountRecoverDelete(request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Started Delete Recovery' });
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
@@ -22,15 +23,15 @@ export class RecoverTwoFactorComponent {
|
||||
|
||||
constructor(private router: Router, private apiService: ApiService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private cryptoService: CryptoService) {
|
||||
}
|
||||
private i18nService: I18nService, private cryptoService: CryptoService,
|
||||
private authService: AuthService) { }
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new TwoFactorRecoveryRequest();
|
||||
request.recoveryCode = this.recoveryCode.replace(/\s/g, '').toLowerCase();
|
||||
request.email = this.email.toLowerCase();
|
||||
const key = await this.cryptoService.makeKey(this.masterPassword, request.email);
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
const key = await this.authService.makePreloginKey(this.masterPassword, request.email);
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||
this.formPromise = this.apiService.postTwoFactorRecover(request);
|
||||
await this.formPromise;
|
||||
|
||||
@@ -21,11 +21,17 @@
|
||||
<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(false)">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
<div class="w-100">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control mb-1"
|
||||
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required appInputVerbatim>
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true"></app-password-strength>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="ml-1 btn btn-link" 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>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,11 @@ import {
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
|
||||
@@ -25,11 +23,12 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
showTerms = true;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, cryptoService: CryptoService,
|
||||
apiService: ApiService, private route: ActivatedRoute,
|
||||
stateService: StateService, platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, cryptoService, apiService, stateService);
|
||||
stateService: StateService, platformUtilsService: PlatformUtilsService,
|
||||
passwordGenerationService: PasswordGenerationService) {
|
||||
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
|
||||
passwordGenerationService);
|
||||
this.showTerms = !platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
@@ -18,8 +15,7 @@ import {
|
||||
})
|
||||
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, platformUtilsService, window);
|
||||
super(authService, router, i18nService, platformUtilsService, window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ import {
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { TwoFactorOptionsComponent } from './two-factor-options.component';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
@@ -33,12 +30,10 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, private stateService: StateService,
|
||||
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(authService, router, analytics, toasterService, i18nService, apiService,
|
||||
platformUtilsService, window, environmentService);
|
||||
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,23 @@ import {
|
||||
} from './organizations/settings/two-factor-setup.component';
|
||||
|
||||
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
|
||||
import {
|
||||
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
|
||||
} from './organizations/tools/exposed-passwords-report.component';
|
||||
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
|
||||
import {
|
||||
InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent,
|
||||
} from './organizations/tools/inactive-two-factor-report.component';
|
||||
import {
|
||||
ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent,
|
||||
} from './organizations/tools/reused-passwords-report.component';
|
||||
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
|
||||
import {
|
||||
UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent,
|
||||
} from './organizations/tools/unsecured-websites-report.component';
|
||||
import {
|
||||
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
|
||||
} from './organizations/tools/weak-passwords-report.component';
|
||||
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
|
||||
|
||||
@@ -50,9 +65,14 @@ import { UserBillingComponent } from './settings/user-billing.component';
|
||||
|
||||
import { BreachReportComponent } from './tools/breach-report.component';
|
||||
import { ExportComponent } from './tools/export.component';
|
||||
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
|
||||
import { ImportComponent } from './tools/import.component';
|
||||
import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
|
||||
import { PasswordGeneratorComponent } from './tools/password-generator.component';
|
||||
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
|
||||
import { ToolsComponent } from './tools/tools.component';
|
||||
import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component';
|
||||
import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component';
|
||||
|
||||
import { VaultComponent } from './vault/vault.component';
|
||||
|
||||
@@ -148,6 +168,31 @@ const routes: Routes = [
|
||||
data: { titleId: 'passwordGenerator' },
|
||||
},
|
||||
{ path: 'breach-report', component: BreachReportComponent, data: { titleId: 'dataBreachReport' } },
|
||||
{
|
||||
path: 'reused-passwords-report',
|
||||
component: ReusedPasswordsReportComponent,
|
||||
data: { titleId: 'reusedPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'unsecured-websites-report',
|
||||
component: UnsecuredWebsitesReportComponent,
|
||||
data: { titleId: 'unsecuredWebsitesReport' },
|
||||
},
|
||||
{
|
||||
path: 'weak-passwords-report',
|
||||
component: WeakPasswordsReportComponent,
|
||||
data: { titleId: 'weakPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'exposed-passwords-report',
|
||||
component: ExposedPasswordsReportComponent,
|
||||
data: { titleId: 'exposedPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'inactive-two-factor-report',
|
||||
component: InactiveTwoFactorReportComponent,
|
||||
data: { titleId: 'inactive2faReport' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -168,13 +213,44 @@ const routes: Routes = [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'import' },
|
||||
{ path: 'import', component: OrgImportComponent, data: { titleId: 'importData' } },
|
||||
{ path: 'export', component: OrgExportComponent, data: { titleId: 'exportVault' } },
|
||||
{
|
||||
path: 'exposed-passwords-report',
|
||||
component: OrgExposedPasswordsReportComponent,
|
||||
data: { titleId: 'exposedPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'inactive-two-factor-report',
|
||||
component: OrgInactiveTwoFactorReportComponent,
|
||||
data: { titleId: 'inactive2faReport' },
|
||||
},
|
||||
{
|
||||
path: 'reused-passwords-report',
|
||||
component: OrgReusedPasswordsReportComponent,
|
||||
data: { titleId: 'reusedPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'unsecured-websites-report',
|
||||
component: OrgUnsecuredWebsitesReportComponent,
|
||||
data: { titleId: 'unsecuredWebsitesReport' },
|
||||
},
|
||||
{
|
||||
path: 'weak-passwords-report',
|
||||
component: OrgWeakPasswordsReportComponent,
|
||||
data: { titleId: 'weakPasswordsReport' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'manage',
|
||||
component: OrgManageComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] },
|
||||
data: {
|
||||
allowedTypes: [
|
||||
OrganizationUserType.Owner,
|
||||
OrganizationUserType.Admin,
|
||||
OrganizationUserType.Manager,
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'people' },
|
||||
{ path: 'collections', component: OrgManageCollectionsComponent, data: { titleId: 'collections' } },
|
||||
|
||||
@@ -3,7 +3,10 @@ import * as _swal from 'sweetalert';
|
||||
import { SweetAlert } from 'sweetalert/typings/core';
|
||||
|
||||
import {
|
||||
BodyOutputType,
|
||||
Toast,
|
||||
ToasterConfig,
|
||||
ToasterContainerComponent,
|
||||
ToasterService,
|
||||
} from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
@@ -14,7 +17,9 @@ import {
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SecurityContext,
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import {
|
||||
NavigationEnd,
|
||||
Router,
|
||||
@@ -31,8 +36,10 @@ import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LockService } from 'jslib/abstractions/lock.service';
|
||||
import { NotificationsService } from 'jslib/abstractions/notifications.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { SettingsService } from 'jslib/abstractions/settings.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
@@ -45,6 +52,7 @@ import { RouterService } from './services/router.service';
|
||||
const BroadcasterSubscriptionId = 'AppComponent';
|
||||
// Hack due to Angular 5.2 bug
|
||||
const swal: SweetAlert = _swal as any;
|
||||
const IdleTimeout = 60000 * 10; // 10 minutes
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -59,6 +67,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
|
||||
private lastActivity: number = null;
|
||||
private idleTimer: number = null;
|
||||
private isIdle = false;
|
||||
|
||||
constructor(private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private broadcasterService: BroadcasterService, private userService: UserService,
|
||||
@@ -70,7 +80,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
|
||||
private lockService: LockService, private storageService: StorageService,
|
||||
private cryptoService: CryptoService, private collectionService: CollectionService,
|
||||
private routerService: RouterService) { }
|
||||
private sanitizer: DomSanitizer, private searchService: SearchService,
|
||||
private notificationsService: NotificationsService, private routerService: RouterService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
@@ -86,8 +97,9 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'loggedIn':
|
||||
case 'unlocked':
|
||||
case 'loggedOut':
|
||||
case 'unlocked':
|
||||
this.notificationsService.updateConnection(false);
|
||||
break;
|
||||
case 'logout':
|
||||
this.logOut(!!message.expired);
|
||||
@@ -96,6 +108,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
await this.lockService.lock();
|
||||
break;
|
||||
case 'locked':
|
||||
this.notificationsService.updateConnection(false);
|
||||
this.router.navigate(['lock']);
|
||||
break;
|
||||
case 'syncStarted':
|
||||
@@ -118,6 +131,15 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.router.navigate(['settings/premium']);
|
||||
}
|
||||
break;
|
||||
case 'showToast':
|
||||
this.showToast(message);
|
||||
break;
|
||||
case 'analyticsEventTrack':
|
||||
this.analytics.eventTrack.next({
|
||||
action: message.action,
|
||||
properties: { label: message.label },
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -157,6 +179,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.passwordGenerationService.clear(),
|
||||
]);
|
||||
|
||||
this.searchService.clearIndex();
|
||||
this.authService.logOut(async () => {
|
||||
this.analytics.eventTrack.next({ action: 'Logged Out' });
|
||||
if (expired) {
|
||||
@@ -175,5 +198,56 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.lastActivity = now;
|
||||
this.storageService.save(ConstantsService.lastActiveKey, now);
|
||||
|
||||
// Idle states
|
||||
if (this.isIdle) {
|
||||
this.isIdle = false;
|
||||
this.idleStateChanged();
|
||||
}
|
||||
if (this.idleTimer != null) {
|
||||
window.clearTimeout(this.idleTimer);
|
||||
this.idleTimer = null;
|
||||
}
|
||||
this.idleTimer = window.setTimeout(() => {
|
||||
if (!this.isIdle) {
|
||||
this.isIdle = true;
|
||||
this.idleStateChanged();
|
||||
}
|
||||
}, IdleTimeout);
|
||||
}
|
||||
|
||||
private showToast(msg: any) {
|
||||
const toast: Toast = {
|
||||
type: msg.type,
|
||||
title: msg.title,
|
||||
};
|
||||
if (typeof (msg.text) === 'string') {
|
||||
toast.body = msg.text;
|
||||
} else if (msg.text.length === 1) {
|
||||
toast.body = msg.text[0];
|
||||
} else {
|
||||
let message = '';
|
||||
msg.text.forEach((t: string) =>
|
||||
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
|
||||
toast.body = message;
|
||||
toast.bodyOutputType = BodyOutputType.TrustedHtml;
|
||||
}
|
||||
if (msg.options != null) {
|
||||
if (msg.options.trustedHtml === true) {
|
||||
toast.bodyOutputType = BodyOutputType.TrustedHtml;
|
||||
}
|
||||
if (msg.options.timeout != null && msg.options.timeout > 0) {
|
||||
toast.timeout = msg.options.timeout;
|
||||
}
|
||||
}
|
||||
this.toasterService.popAsync(toast);
|
||||
}
|
||||
|
||||
private idleStateChanged() {
|
||||
if (this.isIdle) {
|
||||
this.notificationsService.disconnectFromInactivity();
|
||||
} else {
|
||||
this.notificationsService.reconnectFromActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ModalComponent } from './modal.component';
|
||||
|
||||
import { AvatarComponent } from './components/avatar.component';
|
||||
import { CalloutComponent } from './components/callout.component';
|
||||
import { PasswordStrengthComponent } from './components/password-strength.component';
|
||||
|
||||
import { FooterComponent } from './layouts/footer.component';
|
||||
import { FrontendLayoutComponent } from './layouts/frontend-layout.component';
|
||||
@@ -49,6 +50,7 @@ import { GroupsComponent as OrgGroupsComponent } from './organizations/manage/gr
|
||||
import { ManageComponent as OrgManageComponent } from './organizations/manage/manage.component';
|
||||
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
|
||||
import { UserAddEditComponent as OrgUserAddEditComponent } from './organizations/manage/user-add-edit.component';
|
||||
import { UserConfirmComponent as OrgUserConfirmComponent } from './organizations/manage/user-confirm.component';
|
||||
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
|
||||
|
||||
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
|
||||
@@ -61,8 +63,23 @@ import {
|
||||
} from './organizations/settings/two-factor-setup.component';
|
||||
|
||||
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
|
||||
import {
|
||||
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
|
||||
} from './organizations/tools/exposed-passwords-report.component';
|
||||
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
|
||||
import {
|
||||
InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent,
|
||||
} from './organizations/tools/inactive-two-factor-report.component';
|
||||
import {
|
||||
ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent,
|
||||
} from './organizations/tools/reused-passwords-report.component';
|
||||
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
|
||||
import {
|
||||
UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent,
|
||||
} from './organizations/tools/unsecured-websites-report.component';
|
||||
import {
|
||||
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
|
||||
} from './organizations/tools/weak-passwords-report.component';
|
||||
|
||||
import { AddEditComponent as OrgAddEditComponent } from './organizations/vault/add-edit.component';
|
||||
import { AttachmentsComponent as OrgAttachmentsComponent } from './organizations/vault/attachments.component';
|
||||
@@ -75,6 +92,7 @@ import { AccountComponent } from './settings/account.component';
|
||||
import { AdjustPaymentComponent } from './settings/adjust-payment.component';
|
||||
import { AdjustStorageComponent } from './settings/adjust-storage.component';
|
||||
import { ChangeEmailComponent } from './settings/change-email.component';
|
||||
import { ChangeKdfComponent } from './settings/change-kdf.component';
|
||||
import { ChangePasswordComponent } from './settings/change-password.component';
|
||||
import { CreateOrganizationComponent } from './settings/create-organization.component';
|
||||
import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.component';
|
||||
@@ -102,10 +120,15 @@ import { VerifyEmailComponent } from './settings/verify-email.component';
|
||||
|
||||
import { BreachReportComponent } from './tools/breach-report.component';
|
||||
import { ExportComponent } from './tools/export.component';
|
||||
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
|
||||
import { ImportComponent } from './tools/import.component';
|
||||
import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
|
||||
import { PasswordGeneratorHistoryComponent } from './tools/password-generator-history.component';
|
||||
import { PasswordGeneratorComponent } from './tools/password-generator.component';
|
||||
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
|
||||
import { ToolsComponent } from './tools/tools.component';
|
||||
import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component';
|
||||
import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component';
|
||||
|
||||
import { AddEditComponent } from './vault/add-edit.component';
|
||||
import { AttachmentsComponent } from './vault/attachments.component';
|
||||
@@ -131,6 +154,7 @@ import { StopClickDirective } from 'jslib/angular/directives/stop-click.directiv
|
||||
import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive';
|
||||
import { TrueFalseValueDirective } from 'jslib/angular/directives/true-false-value.directive';
|
||||
|
||||
import { ColorPasswordPipe } from 'jslib/angular/pipes/color-password.pipe';
|
||||
import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
|
||||
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
||||
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
|
||||
@@ -140,8 +164,11 @@ import localeCs from '@angular/common/locales/cs';
|
||||
import localeDa from '@angular/common/locales/da';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
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 localeNb from '@angular/common/locales/nb';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
import localePtBr from '@angular/common/locales/pt';
|
||||
import localePtPt from '@angular/common/locales/pt-PT';
|
||||
@@ -154,8 +181,11 @@ registerLocaleData(localeCs, 'cs');
|
||||
registerLocaleData(localeDa, 'da');
|
||||
registerLocaleData(localeDe, 'de');
|
||||
registerLocaleData(localeEs, 'es');
|
||||
registerLocaleData(localeEt, 'et');
|
||||
registerLocaleData(localeFr, 'fr');
|
||||
registerLocaleData(localeIt, 'it');
|
||||
registerLocaleData(localeNb, 'nb');
|
||||
registerLocaleData(localeNl, 'nl');
|
||||
registerLocaleData(localePl, 'pl');
|
||||
registerLocaleData(localePtBr, 'pt-BR');
|
||||
registerLocaleData(localePtPt, 'pt-PT');
|
||||
@@ -176,7 +206,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
clearQueryParams: true,
|
||||
},
|
||||
}),
|
||||
ToasterModule,
|
||||
ToasterModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
AcceptOrganizationComponent,
|
||||
@@ -198,24 +228,28 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
BulkShareComponent,
|
||||
CalloutComponent,
|
||||
ChangeEmailComponent,
|
||||
ChangeKdfComponent,
|
||||
ChangePasswordComponent,
|
||||
CiphersComponent,
|
||||
CollectionsComponent,
|
||||
ColorPasswordPipe,
|
||||
CreateOrganizationComponent,
|
||||
DeauthorizeSessionsComponent,
|
||||
DeleteAccountComponent,
|
||||
DeleteOrganizationComponent,
|
||||
DomainRulesComponent,
|
||||
ExportComponent,
|
||||
ExposedPasswordsReportComponent,
|
||||
FallbackSrcDirective,
|
||||
FolderAddEditComponent,
|
||||
FooterComponent,
|
||||
FrontendLayoutComponent,
|
||||
GroupingsComponent,
|
||||
HintComponent,
|
||||
IconComponent,
|
||||
I18nPipe,
|
||||
IconComponent,
|
||||
ImportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
InputVerbatimDirective,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
@@ -233,23 +267,30 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
OrgEntityUsersComponent,
|
||||
OrgEventsComponent,
|
||||
OrgExportComponent,
|
||||
OrgExposedPasswordsReportComponent,
|
||||
OrgImportComponent,
|
||||
OrgInactiveTwoFactorReportComponent,
|
||||
OrgGroupAddEditComponent,
|
||||
OrgGroupingsComponent,
|
||||
OrgGroupsComponent,
|
||||
OrgManageCollectionsComponent,
|
||||
OrgManageComponent,
|
||||
OrgPeopleComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgSettingComponent,
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
OrganizationsComponent,
|
||||
OrganizationLayoutComponent,
|
||||
OrgUnsecuredWebsitesReportComponent,
|
||||
OrgVaultComponent,
|
||||
OrgWeakPasswordsReportComponent,
|
||||
PasswordGeneratorComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PasswordStrengthComponent,
|
||||
PaymentComponent,
|
||||
PremiumComponent,
|
||||
ProfileComponent,
|
||||
@@ -257,6 +298,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
RecoverDeleteComponent,
|
||||
RecoverTwoFactorComponent,
|
||||
RegisterComponent,
|
||||
ReusedPasswordsReportComponent,
|
||||
SearchCiphersPipe,
|
||||
SearchPipe,
|
||||
SettingsComponent,
|
||||
@@ -275,6 +317,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
TwoFactorU2fComponent,
|
||||
TwoFactorVerifyComponent,
|
||||
TwoFactorYubiKeyComponent,
|
||||
UnsecuredWebsitesReportComponent,
|
||||
UpdateKeyComponent,
|
||||
UpdateLicenseComponent,
|
||||
UserBillingComponent,
|
||||
@@ -283,6 +326,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
WeakPasswordsReportComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddEditComponent,
|
||||
@@ -304,6 +348,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
OrgEntityUsersComponent,
|
||||
OrgGroupAddEditComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PurgeVaultComponent,
|
||||
|
||||
8
src/app/components/password-strength.component.html
Normal file
8
src/app/components/password-strength.component.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="progress">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
44
src/app/components/password-strength.component.ts
Normal file
44
src/app/components/password-strength.component.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-strength',
|
||||
templateUrl: 'password-strength.component.html',
|
||||
})
|
||||
export class PasswordStrengthComponent implements OnChanges {
|
||||
@Input() score?: number;
|
||||
@Input() showText = false;
|
||||
|
||||
scoreWidth = 0;
|
||||
color = 'bg-danger';
|
||||
text: string;
|
||||
|
||||
constructor(private i18nService: I18nService) { }
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.scoreWidth = this.score == null ? 0 : (this.score + 1) * 20;
|
||||
switch (this.score) {
|
||||
case 4:
|
||||
this.color = 'bg-success';
|
||||
this.text = this.i18nService.t('strong');
|
||||
break;
|
||||
case 3:
|
||||
this.color = 'bg-primary';
|
||||
this.text = this.i18nService.t('good');
|
||||
break;
|
||||
case 2:
|
||||
this.color = 'bg-warning';
|
||||
this.text = this.i18nService.t('weak');
|
||||
break;
|
||||
default:
|
||||
this.color = 'bg-danger';
|
||||
this.text = this.score != null ? this.i18nService.t('weak') : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="organization.isAdmin">
|
||||
<ul class="nav nav-tabs" *ngIf="organization.isManager">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||
<i class="fa fa-lock"></i>
|
||||
@@ -27,7 +27,7 @@
|
||||
{{'manage' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *ngIf="organization.isAdmin">
|
||||
<a class="nav-link" routerLink="tools" routerLinkActive="active">
|
||||
<i class="fa fa-wrench"></i>
|
||||
{{'tools' | i18n}}
|
||||
|
||||
@@ -14,4 +14,4 @@ if (process.env.ENV === 'production') {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||
|
||||
@@ -41,13 +41,11 @@
|
||||
<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">
|
||||
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" [disabled]="g.accessAll" appStopProp>
|
||||
</td>
|
||||
<td (click)="check(g)">
|
||||
<span appStopProp>
|
||||
{{g.name}}
|
||||
<i class="fa fa-th text-muted fa-fw" *ngIf="g.accessAll" title="This group can access all items"></i>
|
||||
</span>
|
||||
{{g.name}}
|
||||
<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">
|
||||
@@ -64,8 +62,8 @@
|
||||
</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>
|
||||
</button>
|
||||
|
||||
@@ -103,6 +103,10 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.orgKey == null) {
|
||||
throw new Error('No encryption key for this organization.');
|
||||
}
|
||||
|
||||
const request = new CollectionRequest();
|
||||
request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString;
|
||||
request.groups = this.groups.filter((g) => (g as any).checked && !g.accessAll)
|
||||
|
||||
@@ -14,10 +14,15 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CollectionData } from 'jslib/models/data/collectionData';
|
||||
import { Collection } from 'jslib/models/domain/collection';
|
||||
import { CollectionDetailsResponse } from 'jslib/models/response/collectionResponse';
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from 'jslib/models/response/collectionResponse';
|
||||
import { ListResponse } from 'jslib/models/response/listResponse';
|
||||
import { CollectionView } from 'jslib/models/view/collectionView';
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
@@ -42,7 +47,8 @@ export class CollectionsComponent implements OnInit {
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService) { }
|
||||
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
|
||||
private userService: UserService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
@@ -55,8 +61,14 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getCollections(this.organizationId);
|
||||
const collections = response.data.map((r) =>
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
let response: ListResponse<CollectionResponse>;
|
||||
if (organization.isAdmin) {
|
||||
response = await this.apiService.getCollections(this.organizationId);
|
||||
} else {
|
||||
response = await this.apiService.getUserCollections();
|
||||
}
|
||||
const collections = response.data.filter((c) => c.organizationId === this.organizationId).map((r) =>
|
||||
new Collection(new CollectionData(r as CollectionDetailsResponse)));
|
||||
this.collections = await this.collectionService.decryptMany(collections);
|
||||
this.loading = false;
|
||||
@@ -123,6 +135,10 @@ export class CollectionsComponent implements OnInit {
|
||||
childComponent.entityId = collection.id;
|
||||
childComponent.entityName = collection.name;
|
||||
|
||||
childComponent.onEditedUsers.subscribe(() => {
|
||||
this.load();
|
||||
this.modal.close();
|
||||
});
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
{{'userAccess' | i18n}}
|
||||
@@ -10,48 +10,88 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<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">
|
||||
<ng-container *ngIf="!users || !users.length">
|
||||
<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">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: !showSelected}"
|
||||
(click)="filterSelected(false)">
|
||||
{{'all' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: showSelected}"
|
||||
(click)="filterSelected(true)">
|
||||
{{'selected' | i18n}}
|
||||
<span class="badge badge-pill badge-info" *ngIf="selectedCount">{{selectedCount}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="!searchedUsers.length">
|
||||
<hr>
|
||||
{{'noUsersInList' | i18n}}
|
||||
</ng-container>
|
||||
<table class="table table-hover table-list mb-0" *ngIf="users && users.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let u of users">
|
||||
<td width="30">
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{u.email}}
|
||||
<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 *ngIf="entity === 'collection'">
|
||||
<i class="fa fa-th" *ngIf="u.accessAll" title="{{'userAccessAllItems' | i18n}}"></i>
|
||||
<i class="fa fa-eye" *ngIf="u.readOnly" title="{{'readOnly' | i18n}}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
|
||||
</td>
|
||||
<td class="table-list-options wider">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-submit" (click)="remove(u)" #removeBtn [disabled]="removeBtn.loading"
|
||||
[appApiAction]="actionPromise" *ngIf="entity !== 'collection' || !u.accessAll">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'remove' | i18n}}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
<table class="table table-hover table-list mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th *ngIf="entity === 'collection'"> </th>
|
||||
<th>{{'userType' | i18n}}</th>
|
||||
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'readOnly' |
|
||||
i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
</td>
|
||||
<td width="30" (click)="check(u)">
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
|
||||
[fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{u.email}}
|
||||
<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 *ngIf="entity === 'collection'">
|
||||
<i class="fa fa-th" *ngIf="u.accessAll" title="{{'userAccessAllItems' | i18n}}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
|
||||
</td>
|
||||
<td class="text-center" *ngIf="entity === 'collection'">
|
||||
<input type="checkbox" [(ngModel)]="u.readOnly" name="{{u.id.substr(0,8)}}_ReadOnly"
|
||||
[disabled]="u.accessAll || !u.checked">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,10 +11,11 @@ import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
|
||||
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
|
||||
import { SelectionReadOnlyRequest } from 'jslib/models/request/selectionReadOnlyRequest';
|
||||
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
@@ -27,68 +28,110 @@ export class EntityUsersComponent implements OnInit {
|
||||
@Input() entityId: string;
|
||||
@Input() entityName: string;
|
||||
@Input() organizationId: string;
|
||||
@Output() onRemovedUser = new EventEmitter();
|
||||
@Output() onEditedUsers = new EventEmitter();
|
||||
|
||||
organizationUserType = OrganizationUserType;
|
||||
organizationUserStatusType = OrganizationUserStatusType;
|
||||
|
||||
showSelected = false;
|
||||
loading = true;
|
||||
users: any[] = [];
|
||||
actionPromise: Promise<any>;
|
||||
formPromise: Promise<any>;
|
||||
selectedCount = 0;
|
||||
searchText: string;
|
||||
|
||||
private allUsers: OrganizationUserUserDetailsResponse[] = [];
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
private analytics: Angulartics2, private toasterService: ToasterService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
await this.loadUsers();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async loadUsers() {
|
||||
let users: any[] = [];
|
||||
if (this.entity === 'group') {
|
||||
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
|
||||
users = response.data.map((r) => r);
|
||||
} else if (this.entity === 'collection') {
|
||||
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
|
||||
users = response.data.map((r) => r);
|
||||
get users() {
|
||||
if (this.showSelected) {
|
||||
return this.allUsers.filter((u) => (u as any).checked);
|
||||
} else {
|
||||
return this.allUsers;
|
||||
}
|
||||
users.sort(Utils.getSortFunction(this.i18nService, 'email'));
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
async remove(user: any) {
|
||||
if (this.actionPromise != null || (this.entity === 'collection' && user.accessAll)) {
|
||||
async loadUsers() {
|
||||
const users = await this.apiService.getOrganizationUsers(this.organizationId);
|
||||
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'email'));
|
||||
if (this.entity === 'group') {
|
||||
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
|
||||
if (response != null && users.data.length > 0) {
|
||||
response.forEach((s) => {
|
||||
const user = users.data.filter((u) => u.id === s);
|
||||
if (user != null && user.length > 0) {
|
||||
(user[0] as any).checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (this.entity === 'collection') {
|
||||
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
|
||||
if (response != null && users.data.length > 0) {
|
||||
response.forEach((s) => {
|
||||
const user = users.data.filter((u) => !u.accessAll && u.id === s.id);
|
||||
if (user != null && user.length > 0) {
|
||||
(user[0] as any).checked = true;
|
||||
(user[0] as any).readOnly = s.readOnly;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.allUsers.forEach((u) => {
|
||||
if (this.entity === 'collection' && u.accessAll) {
|
||||
(u as any).checked = true;
|
||||
}
|
||||
if ((u as any).checked) {
|
||||
this.selectedCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
check(u: OrganizationUserUserDetailsResponse) {
|
||||
if (this.entity === 'collection' && u.accessAll) {
|
||||
return;
|
||||
}
|
||||
(u as any).checked = !(u as any).checked;
|
||||
this.selectedChanged(u);
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('removeUserConfirmation'), user.email,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
selectedChanged(u: OrganizationUserUserDetailsResponse) {
|
||||
if ((u as any).checked) {
|
||||
this.selectedCount++;
|
||||
} else {
|
||||
if (this.entity === 'collection') {
|
||||
(u as any).readOnly = false;
|
||||
}
|
||||
this.selectedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
filterSelected(showSelected: boolean) {
|
||||
this.showSelected = showSelected;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (this.entity === 'group') {
|
||||
this.actionPromise = this.apiService.deleteGroupUser(this.organizationId, this.entityId,
|
||||
user.organizationUserId);
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Removed User From Group' });
|
||||
} else if (this.entity === 'collection') {
|
||||
this.actionPromise = this.apiService.deleteCollectionUser(this.organizationId, this.entityId,
|
||||
user.organizationUserId);
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Removed User From Collection' });
|
||||
}
|
||||
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', user.email));
|
||||
this.onRemovedUser.emit();
|
||||
const index = this.users.indexOf(user);
|
||||
if (index > -1) {
|
||||
this.users.splice(index, 1);
|
||||
const selections = this.users.filter((u) => (u as any).checked).map((u) => u.id);
|
||||
this.formPromise = this.apiService.putGroupUsers(this.organizationId, this.entityId, selections);
|
||||
} else {
|
||||
const selections = this.users.filter((u) => (u as any).checked && !u.accessAll)
|
||||
.map((u) => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly));
|
||||
this.formPromise = this.apiService.putCollectionUsers(this.organizationId, this.entityId, selections);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({
|
||||
action: this.entity === 'group' ? 'Edited Group Users' : 'Edited Collection Users',
|
||||
});
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('updatedUsers'));
|
||||
this.onEditedUsers.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,10 @@
|
||||
<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">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
|
||||
</td>
|
||||
<td (click)="check(c)">
|
||||
<span appStopProp>{{c.name}}</span>
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
|
||||
|
||||
@@ -131,6 +131,9 @@ export class GroupsComponent implements OnInit {
|
||||
childComponent.entityId = group.id;
|
||||
childComponent.entityName = group.name;
|
||||
|
||||
childComponent.onEditedUsers.subscribe(() => {
|
||||
this.modal.close();
|
||||
});
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card">
|
||||
<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">
|
||||
<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="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="accessEvents">
|
||||
<a routerLink="events" class="list-group-item" routerLinkActive="active" *ngIf="organization.isAdmin && accessEvents">
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,14 @@ import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-manage',
|
||||
templateUrl: 'manage.component.html',
|
||||
})
|
||||
export class ManageComponent implements OnInit {
|
||||
organization: Organization;
|
||||
accessGroups = false;
|
||||
accessEvents = false;
|
||||
|
||||
@@ -18,9 +21,9 @@ export class ManageComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
const organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.accessEvents = organization.useEvents;
|
||||
this.accessGroups = organization.useGroups;
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.accessEvents = this.organization.useEvents;
|
||||
this.accessGroups = this.organization.useGroups;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<td>
|
||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
@@ -87,3 +88,4 @@
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #groupsTemplate></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
<ng-template #confirmTemplate></ng-template>
|
||||
|
||||
@@ -5,15 +5,21 @@ import {
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
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 { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
|
||||
@@ -28,6 +34,7 @@ import { Utils } from 'jslib/misc/utils';
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
import { EntityEventsComponent } from './entity-events.component';
|
||||
import { UserAddEditComponent } from './user-add-edit.component';
|
||||
import { UserConfirmComponent } from './user-confirm.component';
|
||||
import { UserGroupsComponent } from './user-groups.component';
|
||||
|
||||
@Component({
|
||||
@@ -38,6 +45,7 @@ export class PeopleComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('groupsTemplate', { read: ViewContainerRef }) groupsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild('confirmTemplate', { read: ViewContainerRef }) confirmModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
@@ -58,12 +66,17 @@ export class PeopleComponent implements OnInit {
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private cryptoService: CryptoService,
|
||||
private userService: UserService) { }
|
||||
private userService: UserService, private router: Router,
|
||||
private storageService: StorageService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
if (!organization.isAdmin) {
|
||||
this.router.navigate(['../collections'], { relativeTo: this.route });
|
||||
return;
|
||||
}
|
||||
this.accessEvents = organization.useEvents;
|
||||
this.accessGroups = organization.useGroups;
|
||||
await this.load();
|
||||
@@ -206,17 +219,48 @@ export class PeopleComponent implements OnInit {
|
||||
}
|
||||
|
||||
async confirm(user: OrganizationUserUserDetailsResponse) {
|
||||
function updateUser(self: PeopleComponent) {
|
||||
user.status = OrganizationUserStatusType.Confirmed;
|
||||
const mapIndex = self.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
|
||||
if (mapIndex > -1) {
|
||||
self.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
|
||||
self.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const autoConfirm = await this.storageService.get<boolean>(ConstantsService.autoConfirmFingerprints);
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.groupsModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<UserConfirmComponent>(
|
||||
UserConfirmComponent, this.confirmModalRef);
|
||||
|
||||
childComponent.name = user != null ? user.name || user.email : null;
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.organizationUserId = user != null ? user.id : null;
|
||||
childComponent.userId = user != null ? user.userId : null;
|
||||
childComponent.onConfirmedUser.subscribe(() => {
|
||||
this.modal.close();
|
||||
updateUser(this);
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.doConfirmation(user);
|
||||
await this.actionPromise;
|
||||
user.status = OrganizationUserStatusType.Confirmed;
|
||||
const mapIndex = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
|
||||
if (mapIndex > -1) {
|
||||
this.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
|
||||
this.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
|
||||
}
|
||||
updateUser(this);
|
||||
this.analytics.eventTrack.next({ action: 'Confirmed User' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
|
||||
this.actionPromise = null;
|
||||
@@ -247,6 +291,11 @@ export class PeopleComponent implements OnInit {
|
||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
try {
|
||||
// tslint:disable-next-line
|
||||
console.log('User\'s fingerprint: ' +
|
||||
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
|
||||
} catch { }
|
||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
<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">
|
||||
<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">
|
||||
<label class="form-check-label" for="userTypeAdmin">
|
||||
@@ -86,10 +93,10 @@
|
||||
<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">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
|
||||
</td>
|
||||
<td (click)="check(c)">
|
||||
<span appStopProp>{{c.name}}</span>
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
|
||||
|
||||
36
src/app/organizations/manage/user-confirm.component.html
Normal file
36
src/app/organizations/manage/user-confirm.component.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
{{'confirmUser' | i18n}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</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>
|
||||
{{'fingerprintEnsureIntegrityVerify' | i18n}}
|
||||
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener">
|
||||
{{'learnMore' | i18n}}</a>
|
||||
</p>
|
||||
<p><code>{{fingerprint}}</code></p>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="dontAskAgain" name="DontAskAgain" [(ngModel)]="dontAskAgain">
|
||||
<label class="form-check-label" for="dontAskAgain">
|
||||
{{'dontAskFingerprintAgain' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'confirm' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
84
src/app/organizations/manage/user-confirm.component.ts
Normal file
84
src/app/organizations/manage/user-confirm.component.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-confirm',
|
||||
templateUrl: 'user-confirm.component.html',
|
||||
})
|
||||
export class UserConfirmComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() userId: string;
|
||||
@Input() organizationUserId: string;
|
||||
@Input() organizationId: string;
|
||||
@Output() onConfirmedUser = new EventEmitter();
|
||||
|
||||
dontAskAgain = false;
|
||||
loading = true;
|
||||
fingerprint: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private publicKey: Uint8Array = null;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private storageService: StorageService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
|
||||
if (publicKeyResponse != null) {
|
||||
this.publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
const fingerprint = await this.cryptoService.getFingerprint(this.userId, this.publicKey.buffer);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join('-');
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dontAskAgain) {
|
||||
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.doConfirmation();
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Confirmed User' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', this.name));
|
||||
this.onConfirmedUser.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
private async doConfirmation() {
|
||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, this.publicKey.buffer);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
await this.apiService.postOrganizationUserConfirm(this.organizationId, this.organizationUserId, request);
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,10 @@
|
||||
<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">
|
||||
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" appStopProp>
|
||||
</td>
|
||||
<td (click)="check(g)">
|
||||
<span appStopProp>{{g.name}}</span>
|
||||
{{g.name}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -32,13 +32,6 @@
|
||||
<div class="secondary-header border-0 mb-0">
|
||||
<h1>{{'taxInformation' | i18n}}</h1>
|
||||
</div>
|
||||
<div class="mb-3" *ngIf="org && (org.businessAddress1 || org.businessTaxNumber)">
|
||||
<div>{{org.businessAddress1}}</div>
|
||||
<div>{{org.businessAddress2}}</div>
|
||||
<div>{{org.businessAddress3}}</div>
|
||||
<div>{{org.businessCountry}}</div>
|
||||
<div>{{org.businessTaxNumber}}</div>
|
||||
</div>
|
||||
<p>{{'taxInformationDesc' | i18n}}</p>
|
||||
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
{{'contactSupport' | i18n}}
|
||||
@@ -50,6 +43,8 @@
|
||||
<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)="purgeVault()">{{'purgeVault' | i18n}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #deleteOrganizationTemplate></ng-template>
|
||||
<ng-template #purgeOrganizationTemplate></ng-template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpda
|
||||
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
|
||||
import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
|
||||
@Component({
|
||||
@@ -25,6 +26,7 @@ import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
org: OrganizationResponse;
|
||||
@@ -78,4 +80,19 @@ export class AccountComponent {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
purgeVault() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.purgeModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<PurgeVaultComponent>(PurgeVaultComponent, this.purgeModalRef);
|
||||
childComponent.organizationId = this.organizationId;
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,6 @@ export class AdjustSeatsComponent {
|
||||
}
|
||||
|
||||
get adjustedSeatTotal(): number {
|
||||
return this.seatAdjustment * this.seatAdjustment;
|
||||
return this.seatAdjustment * this.seatPrice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,8 @@ export class OrganizationBillingComponent implements OnInit {
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return this.billing != null && this.billing.expiration != null && this.billing.expiration < new Date();
|
||||
return this.billing != null && this.billing.expiration != null &&
|
||||
new Date(this.billing.expiration) < new Date();
|
||||
}
|
||||
|
||||
get subscriptionMarkedForCancel() {
|
||||
@@ -256,11 +257,11 @@ export class OrganizationBillingComponent implements OnInit {
|
||||
case PlanType.EnterpriseMonthly:
|
||||
return 4;
|
||||
case PlanType.EnterpriseAnnually:
|
||||
return 3;
|
||||
return 36;
|
||||
case PlanType.TeamsMonthly:
|
||||
return 2.5;
|
||||
case PlanType.TeamsAnnually:
|
||||
return 2;
|
||||
return 24;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||
|
||||
@@ -18,10 +18,10 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from '../../se
|
||||
templateUrl: '../../settings/two-factor-setup.component.html',
|
||||
})
|
||||
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
||||
constructor(apiService: ApiService, tokenService: TokenService,
|
||||
constructor(apiService: ApiService, userService: UserService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
private route: ActivatedRoute) {
|
||||
super(apiService, tokenService, componentFactoryResolver, messagingService);
|
||||
super(apiService, userService, componentFactoryResolver, messagingService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@@ -8,7 +5,6 @@ import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { ExportService } from 'jslib/abstractions/export.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { ExportComponent as BaseExportComponent } from '../../tools/export.component';
|
||||
|
||||
@@ -19,12 +15,10 @@ import { ExportComponent as BaseExportComponent } from '../../tools/export.compo
|
||||
export class ExportComponent extends BaseExportComponent {
|
||||
organizationId: string;
|
||||
|
||||
constructor(analytics: Angulartics2, toasterService: ToasterService,
|
||||
cryptoService: CryptoService, userService: UserService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
|
||||
exportService: ExportService, private route: ActivatedRoute) {
|
||||
super(analytics, toasterService, cryptoService, userService, i18nService, platformUtilsService,
|
||||
exportService);
|
||||
constructor(cryptoService: CryptoService, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, exportService: ExportService,
|
||||
private route: ActivatedRoute) {
|
||||
super(cryptoService, i18nService, platformUtilsService, exportService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent,
|
||||
} from '../../tools/exposed-passwords-report.component';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@Component({
|
||||
selector: 'app-exposed-passwords-report',
|
||||
templateUrl: '../../tools/exposed-passwords-report.component.html',
|
||||
})
|
||||
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
|
||||
constructor(cipherService: CipherService, auditService: AuditService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
userService: UserService, private route: ActivatedRoute) {
|
||||
super(cipherService, auditService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent,
|
||||
} from '../../tools/inactive-two-factor-report.component';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@Component({
|
||||
selector: 'app-inactive-two-factor-report',
|
||||
templateUrl: '../../tools/inactive-two-factor-report.component.html',
|
||||
})
|
||||
export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent {
|
||||
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService,
|
||||
private route: ActivatedRoute) {
|
||||
super(cipherService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import {
|
||||
ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent,
|
||||
} from '../../tools/reused-passwords-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reused-passwords-report',
|
||||
templateUrl: '../../tools/reused-passwords-report.component.html',
|
||||
})
|
||||
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
|
||||
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService,
|
||||
private route: ActivatedRoute) {
|
||||
super(cipherService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">{{'tools' | i18n}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="import" class="list-group-item" routerLinkActive="active">
|
||||
@@ -12,6 +12,33 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex">
|
||||
{{'reports' | i18n}}
|
||||
<div class="ml-auto">
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports" (click)="upgradeOrganization()">
|
||||
{{'upgrade' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'exposedPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'reusedPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'weakPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'unsecuredWebsitesReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'inactive2faReport' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-tools',
|
||||
templateUrl: 'tools.component.html',
|
||||
})
|
||||
export class ToolsComponent { }
|
||||
export class ToolsComponent {
|
||||
organization: Organization;
|
||||
accessReports = false;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private messagingService: MessagingService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare use2fa for now
|
||||
// since all paid plans include use2fa
|
||||
this.accessReports = this.organization.use2fa;
|
||||
});
|
||||
}
|
||||
|
||||
upgradeOrganization() {
|
||||
this.messagingService.send('upgradeOrganization', { organizationId: this.organization.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponent,
|
||||
} from '../../tools/unsecured-websites-report.component';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unsecured-websites-report',
|
||||
templateUrl: '../../tools/unsecured-websites-report.component.html',
|
||||
})
|
||||
export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent {
|
||||
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService,
|
||||
private route: ActivatedRoute) {
|
||||
super(cipherService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import {
|
||||
WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent,
|
||||
} from '../../tools/weak-passwords-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-weak-passwords-report',
|
||||
templateUrl: '../../tools/weak-passwords-report.component.html',
|
||||
})
|
||||
export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent {
|
||||
constructor(cipherService: CipherService, passwordGenerationService: PasswordGenerationService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
userService: UserService, private route: ActivatedRoute) {
|
||||
super(cipherService, passwordGenerationService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,22 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { TotpService } from 'jslib/abstractions/totp.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherData } from 'jslib/models/data/cipherData';
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
import { CipherCreateRequest } from 'jslib/models/request/cipherCreateRequest';
|
||||
import { CipherRequest } from 'jslib/models/request/cipherRequest';
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from '../../vault/add-edit.component';
|
||||
@@ -29,19 +25,26 @@ import { AddEditComponent as BaseAddEditComponent } from '../../vault/add-edit.c
|
||||
selector: 'app-org-vault-add-edit',
|
||||
templateUrl: '../../vault/add-edit.component.html',
|
||||
})
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
export class AddEditComponent extends BaseAddEditComponent {
|
||||
organization: Organization;
|
||||
originalCipher: Cipher = null;
|
||||
|
||||
constructor(cipherService: CipherService, folderService: FolderService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
auditService: AuditService, stateService: StateService,
|
||||
tokenService: TokenService, totpService: TotpService,
|
||||
passwordGenerationService: PasswordGenerationService, private apiService: ApiService,
|
||||
userService: UserService, collectionService: CollectionService,
|
||||
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
|
||||
private apiService: ApiService,
|
||||
messagingService: MessagingService) {
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, analytics,
|
||||
toasterService, auditService, stateService, tokenService, totpService, passwordGenerationService,
|
||||
messagingService);
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
|
||||
userService, collectionService, totpService, passwordGenerationService, messagingService);
|
||||
}
|
||||
|
||||
protected loadCollections() {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.loadCollections();
|
||||
}
|
||||
return Promise.resolve(this.collections);
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
@@ -49,24 +52,27 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
return await super.loadCipher();
|
||||
}
|
||||
const response = await this.apiService.getCipherAdmin(this.cipherId);
|
||||
return new Cipher(new CipherData(response));
|
||||
const data = new CipherData(response);
|
||||
this.originalCipher = new Cipher(data);
|
||||
return new Cipher(data);
|
||||
}
|
||||
|
||||
protected encryptCipher() {
|
||||
if (!this.editMode) {
|
||||
this.cipher.organizationId = this.organization.id;
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.encryptCipher();
|
||||
}
|
||||
return super.encryptCipher();
|
||||
return this.cipherService.encrypt(this.cipher, null, this.originalCipher);
|
||||
}
|
||||
|
||||
protected async saveCipher(cipher: Cipher) {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.saveCipher(cipher);
|
||||
}
|
||||
const request = new CipherRequest(cipher);
|
||||
if (this.editMode) {
|
||||
const request = new CipherRequest(cipher);
|
||||
return this.apiService.putCipherAdmin(this.cipherId, request);
|
||||
} else {
|
||||
const request = new CipherCreateRequest(cipher);
|
||||
return this.apiService.postCipherAdmin(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherData } from 'jslib/models/data/cipherData';
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
import { AttachmentView } from 'jslib/models/view/attachmentView';
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/attachments.component';
|
||||
|
||||
@Component({
|
||||
@@ -23,12 +22,16 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/at
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
organization: Organization;
|
||||
|
||||
constructor(cipherService: CipherService, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, tokenService: TokenService,
|
||||
constructor(cipherService: CipherService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, userService: UserService,
|
||||
platformUtilsService: PlatformUtilsService, private apiService: ApiService) {
|
||||
super(cipherService, analytics, toasterService, i18nService, cryptoService, tokenService,
|
||||
platformUtilsService);
|
||||
super(cipherService, i18nService, cryptoService, userService, platformUtilsService);
|
||||
}
|
||||
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
if (this.organization.isAdmin && this.showFixOldAttachments(attachment)) {
|
||||
await super.reuploadCipherAttachment(attachment, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
@@ -49,4 +52,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
}
|
||||
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return attachment.key == null && this.organization.isAdmin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
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';
|
||||
@@ -29,43 +30,48 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
organization: Organization;
|
||||
accessEvents = false;
|
||||
|
||||
constructor(cipherService: CipherService, analytics: Angulartics2,
|
||||
protected allCiphers: CipherView[] = [];
|
||||
|
||||
constructor(searchService: SearchService, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, private apiService: ApiService) {
|
||||
super(cipherService, analytics, toasterService, i18nService, platformUtilsService);
|
||||
platformUtilsService: PlatformUtilsService, cipherService: CipherService,
|
||||
private apiService: ApiService) {
|
||||
super(searchService, analytics, toasterService, i18nService, platformUtilsService, cipherService);
|
||||
}
|
||||
|
||||
async load(filter: (cipher: CipherView) => boolean = null) {
|
||||
if (!this.organization.isAdmin) {
|
||||
await super.load();
|
||||
await super.load(filter);
|
||||
return;
|
||||
}
|
||||
this.accessEvents = this.organization.useEvents;
|
||||
const ciphers = await this.apiService.getCiphersOrganization(this.organization.id);
|
||||
if (ciphers != null && ciphers.data != null && ciphers.data.length) {
|
||||
const decCiphers: CipherView[] = [];
|
||||
const promises: any[] = [];
|
||||
ciphers.data.forEach((r) => {
|
||||
const data = new CipherData(r);
|
||||
const cipher = new Cipher(data);
|
||||
promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
|
||||
this.allCiphers = decCiphers;
|
||||
} else {
|
||||
this.allCiphers = [];
|
||||
}
|
||||
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
this.applyFilter(filter);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
applyFilter(filter: (cipher: CipherView) => boolean = null) {
|
||||
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
|
||||
if (this.organization.isAdmin) {
|
||||
super.applyFilter(filter);
|
||||
await super.applyFilter(filter);
|
||||
} else {
|
||||
const f = (c: CipherView) => c.organizationId === this.organization.id && (filter == null || filter(c));
|
||||
super.applyFilter(f);
|
||||
await super.applyFilter(f);
|
||||
}
|
||||
}
|
||||
|
||||
search(timeout: number = null) {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.search(timeout);
|
||||
}
|
||||
this.searchPending = false;
|
||||
let filteredCiphers = this.allCiphers;
|
||||
if (this.filter != null) {
|
||||
filteredCiphers = filteredCiphers.filter(this.filter);
|
||||
}
|
||||
if (this.searchText == null || this.searchText.trim().length < 2) {
|
||||
this.ciphers = filteredCiphers;
|
||||
} else {
|
||||
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,4 +89,8 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
}
|
||||
return this.apiService.deleteCipherAdmin(id);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(c: CipherView) {
|
||||
return this.organization.isAdmin && c.hasOldAttachments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { CipherData } from 'jslib/models/data/cipherData';
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
@@ -22,10 +20,10 @@ import { CollectionsComponent as BaseCollectionsComponent } from '../../vault/co
|
||||
export class CollectionsComponent extends BaseCollectionsComponent {
|
||||
organization: Organization;
|
||||
|
||||
constructor(collectionService: CollectionService, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
cipherService: CipherService, private apiService: ApiService) {
|
||||
super(collectionService, analytics, toasterService, i18nService, cipherService);
|
||||
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService, cipherService: CipherService,
|
||||
private apiService: ApiService) {
|
||||
super(collectionService, platformUtilsService, i18nService, cipherService);
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CollectionData } from 'jslib/models/data/collectionData';
|
||||
import { Collection } from 'jslib/models/domain/collection';
|
||||
@@ -21,8 +23,9 @@ export class GroupingsComponent extends BaseGroupingsComponent {
|
||||
organization: Organization;
|
||||
|
||||
constructor(collectionService: CollectionService, folderService: FolderService,
|
||||
storageService: StorageService, userService: UserService,
|
||||
private apiService: ApiService, private i18nService: I18nService) {
|
||||
super(collectionService, folderService);
|
||||
super(collectionService, folderService, storageService, userService);
|
||||
}
|
||||
|
||||
async loadCollections() {
|
||||
@@ -30,6 +33,7 @@ export class GroupingsComponent extends BaseGroupingsComponent {
|
||||
await super.loadCollections(this.organization.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const collections = await this.apiService.getCollections(this.organization.id);
|
||||
if (collections != null && collections.data != null && collections.data.length) {
|
||||
const collectionDomains = collections.data.map((r) =>
|
||||
@@ -45,5 +49,14 @@ export class GroupingsComponent extends BaseGroupingsComponent {
|
||||
unassignedCollection.organizationId = this.organization.id;
|
||||
unassignedCollection.readOnly = true;
|
||||
this.collections.push(unassignedCollection);
|
||||
this.nestedCollections = await this.collectionService.getAllNested(this.collections);
|
||||
}
|
||||
|
||||
collapse(grouping: CollectionView) {
|
||||
super.collapse(grouping, 'org_');
|
||||
}
|
||||
|
||||
isCollapsed(grouping: CollectionView) {
|
||||
return super.isCollapsed(grouping, 'org_');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<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()" *ngIf="showAdd">
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
@@ -16,6 +18,8 @@ import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@@ -30,11 +34,13 @@ import { CiphersComponent } from './ciphers.component';
|
||||
import { CollectionsComponent } from './collections.component';
|
||||
import { GroupingsComponent } from './groupings.component';
|
||||
|
||||
const BroadcasterSubscriptionId = 'OrgVaultComponent';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-vault',
|
||||
templateUrl: 'vault.component.html',
|
||||
})
|
||||
export class VaultComponent implements OnInit {
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
|
||||
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
|
||||
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
|
||||
@@ -45,25 +51,40 @@ export class VaultComponent implements OnInit {
|
||||
organization: Organization;
|
||||
collectionId: string;
|
||||
type: CipherType;
|
||||
showAdd = true;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private location: Location, private router: Router,
|
||||
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
||||
private syncService: SyncService, private i18nService: I18nService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService) { }
|
||||
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.showAdd = this.organization.isAdmin;
|
||||
this.groupingsComponent.organization = this.organization;
|
||||
this.ciphersComponent.organization = this.organization;
|
||||
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
|
||||
if (!this.organization.isAdmin) {
|
||||
await this.syncService.fullSync(false);
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'syncCompleted':
|
||||
if (message.successfully) {
|
||||
await Promise.all([
|
||||
this.groupingsComponent.load(),
|
||||
this.ciphersComponent.refresh(),
|
||||
]);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
await this.groupingsComponent.load();
|
||||
|
||||
@@ -84,7 +105,6 @@ export class VaultComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
|
||||
if (qParams.viewEvents != null) {
|
||||
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
|
||||
if (cipher.length > 0) {
|
||||
@@ -95,6 +115,10 @@ export class VaultComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async clearGroupingFilters() {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
|
||||
@@ -118,13 +142,13 @@ export class VaultComponent implements OnInit {
|
||||
}
|
||||
|
||||
async filterCollection(collectionId: string, load = false) {
|
||||
this.ciphersComponent.showAddNew = false;
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
|
||||
const filter = (c: CipherView) => {
|
||||
if (collectionId === 'unassigned') {
|
||||
return c.collectionIds == null || c.collectionIds.length === 0;
|
||||
} else {
|
||||
return c.collectionIds.indexOf(collectionId) > -1;
|
||||
return c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1;
|
||||
}
|
||||
};
|
||||
if (load) {
|
||||
@@ -139,6 +163,7 @@ export class VaultComponent implements OnInit {
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.ciphersComponent.searchText = searchText;
|
||||
this.ciphersComponent.search(200);
|
||||
}
|
||||
|
||||
editCipherAttachments(cipher: CipherView) {
|
||||
@@ -197,7 +222,14 @@ export class VaultComponent implements OnInit {
|
||||
|
||||
addCipher() {
|
||||
const component = this.editCipher(null);
|
||||
component.organizationId = this.organization.id;
|
||||
component.type = this.type;
|
||||
if (this.organization.isAdmin) {
|
||||
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
|
||||
}
|
||||
if (this.collectionId != null) {
|
||||
component.collectionIds = [this.collectionId];
|
||||
}
|
||||
}
|
||||
|
||||
editCipher(cipher: CipherView) {
|
||||
@@ -261,8 +293,10 @@ export class VaultComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
const url = this.router.createUrlTree(['organizations', this.organization.id, 'vault'],
|
||||
{ queryParams: queryParams }).toString();
|
||||
this.location.go(url);
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ export class EventService {
|
||||
case EventType.User_ChangedPassword:
|
||||
msg = this.i18nService.t('changedPassword');
|
||||
break;
|
||||
case EventType.User_Enabled2fa:
|
||||
msg = this.i18nService.t('enabled2fa');
|
||||
case EventType.User_Updated2fa:
|
||||
msg = this.i18nService.t('enabledUpdated2fa');
|
||||
break;
|
||||
case EventType.User_Disabled2fa:
|
||||
msg = this.i18nService.t('disabled2fa');
|
||||
@@ -124,6 +124,9 @@ export class EventService {
|
||||
case EventType.Organization_Updated:
|
||||
msg = this.i18nService.t('editedOrgSettings');
|
||||
break;
|
||||
case EventType.Organization_PurgedVault:
|
||||
msg = this.i18nService.t('purgedOrganizationVault');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ import { ExportService } from 'jslib/services/export.service';
|
||||
import { FolderService } from 'jslib/services/folder.service';
|
||||
import { ImportService } from 'jslib/services/import.service';
|
||||
import { LockService } from 'jslib/services/lock.service';
|
||||
import { NotificationsService } from 'jslib/services/notifications.service';
|
||||
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
|
||||
import { SearchService } from 'jslib/services/search.service';
|
||||
import { SettingsService } from 'jslib/services/settings.service';
|
||||
import { StateService } from 'jslib/services/state.service';
|
||||
import { SyncService } from 'jslib/services/sync.service';
|
||||
@@ -63,10 +65,12 @@ import { ImportService as ImportServiceAbstraction } from 'jslib/abstractions/im
|
||||
import { LockService as LockServiceAbstraction } from 'jslib/abstractions/lock.service';
|
||||
import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service';
|
||||
import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service';
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from 'jslib/abstractions/notifications.service';
|
||||
import {
|
||||
PasswordGenerationService as PasswordGenerationServiceAbstraction,
|
||||
} from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.service';
|
||||
import { SettingsService as SettingsServiceAbstraction } from 'jslib/abstractions/settings.service';
|
||||
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
|
||||
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
|
||||
@@ -79,7 +83,7 @@ const i18nService = new I18nService(window.navigator.language, 'locales');
|
||||
const stateService = new StateService();
|
||||
const broadcasterService = new BroadcasterService();
|
||||
const messagingService = new BroadcasterMessagingService(broadcasterService);
|
||||
const platformUtilsService = new WebPlatformUtilsService(i18nService);
|
||||
const platformUtilsService = new WebPlatformUtilsService(i18nService, messagingService);
|
||||
const storageService: StorageServiceAbstraction = new HtmlStorageService(platformUtilsService);
|
||||
const secureStorageService: StorageServiceAbstraction = new MemoryStorageService();
|
||||
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new WebCryptoFunctionService(window,
|
||||
@@ -90,26 +94,30 @@ const tokenService = new TokenService(storageService);
|
||||
const appIdService = new AppIdService(storageService);
|
||||
const apiService = new ApiService(tokenService, platformUtilsService,
|
||||
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
|
||||
const environmentService = new EnvironmentService(apiService, storageService);
|
||||
const userService = new UserService(tokenService, storageService);
|
||||
const settingsService = new SettingsService(userService, storageService);
|
||||
export let searchService: SearchService = null;
|
||||
const cipherService = new CipherService(cryptoService, userService, settingsService,
|
||||
apiService, storageService, i18nService, platformUtilsService);
|
||||
apiService, storageService, i18nService, () => searchService);
|
||||
const folderService = new FolderService(cryptoService, userService, apiService, storageService,
|
||||
i18nService, cipherService);
|
||||
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
|
||||
searchService = new SearchService(cipherService, platformUtilsService);
|
||||
const lockService = new LockService(cipherService, folderService, collectionService,
|
||||
cryptoService, platformUtilsService, storageService, messagingService, null);
|
||||
cryptoService, platformUtilsService, storageService, messagingService, searchService, null);
|
||||
const syncService = new SyncService(userService, apiService, settingsService,
|
||||
folderService, cipherService, cryptoService, collectionService, storageService, messagingService,
|
||||
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
|
||||
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService);
|
||||
const totpService = new TotpService(storageService, cryptoFunctionService);
|
||||
const containerService = new ContainerService(cryptoService, platformUtilsService);
|
||||
const containerService = new ContainerService(cryptoService);
|
||||
const authService = new AuthService(cryptoService, apiService,
|
||||
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService);
|
||||
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 }));
|
||||
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
|
||||
const auditService = new AuditService(cryptoFunctionService, apiService);
|
||||
|
||||
const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost(),
|
||||
@@ -122,8 +130,11 @@ export function initFactory(): Function {
|
||||
const isDev = platformUtilsService.isDev();
|
||||
if (!isDev && platformUtilsService.isSelfHost()) {
|
||||
environmentService.baseUrl = window.location.origin;
|
||||
} else {
|
||||
environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
|
||||
'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
|
||||
}
|
||||
await apiService.setUrls({
|
||||
apiService.setUrls({
|
||||
base: isDev ? null : window.location.origin,
|
||||
api: isDev ? 'http://localhost:4000' : null,
|
||||
identity: isDev ? 'http://localhost:33656' : null,
|
||||
@@ -135,11 +146,12 @@ export function initFactory(): Function {
|
||||
// api: 'https://api.bitwarden.com',
|
||||
// identity: 'https://identity.bitwarden.com',
|
||||
});
|
||||
setTimeout(() => notificationsService.init(environmentService), 3000);
|
||||
|
||||
lockService.init(true);
|
||||
const locale = await storageService.get<string>(ConstantsService.localeKey);
|
||||
await i18nService.init(locale);
|
||||
await authService.init();
|
||||
authService.init();
|
||||
const htmlEl = window.document.documentElement;
|
||||
htmlEl.classList.add('locale_' + i18nService.translationLocale);
|
||||
let theme = await storageService.get<string>(ConstantsService.themeKey);
|
||||
@@ -188,7 +200,9 @@ export function initFactory(): Function {
|
||||
{ provide: StorageServiceAbstraction, useValue: storageService },
|
||||
{ provide: StateServiceAbstraction, useValue: stateService },
|
||||
{ provide: ExportServiceAbstraction, useValue: exportService },
|
||||
{ provide: SearchServiceAbstraction, useValue: searchService },
|
||||
{ provide: ImportServiceAbstraction, useValue: importService },
|
||||
{ provide: NotificationsServiceAbstraction, useValue: notificationsService },
|
||||
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
<h1>{{'changeMasterPassword' | i18n}}</h1>
|
||||
</div>
|
||||
<app-change-password></app-change-password>
|
||||
<div class="secondary-header">
|
||||
<h1>{{'encKeySettings' | i18n}}</h1>
|
||||
</div>
|
||||
<app-change-kdf></app-change-kdf>
|
||||
<div class="secondary-header text-danger border-0 mb-0">
|
||||
<h1>{{'dangerZone' | i18n}}</h1>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { EmailRequest } from 'jslib/models/request/emailRequest';
|
||||
import { EmailTokenRequest } from 'jslib/models/request/emailTokenRequest';
|
||||
@@ -27,7 +28,8 @@ export class ChangeEmailComponent {
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private messagingService: MessagingService) { }
|
||||
private cryptoService: CryptoService, private messagingService: MessagingService,
|
||||
private userService: UserService) { }
|
||||
|
||||
async submit() {
|
||||
const hasEncKey = await this.cryptoService.hasEncKey();
|
||||
@@ -36,7 +38,7 @@ export class ChangeEmailComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.newEmail = this.newEmail.toLowerCase();
|
||||
this.newEmail = this.newEmail.trim().toLowerCase();
|
||||
if (!this.tokenSent) {
|
||||
const request = new EmailTokenRequest();
|
||||
request.newEmail = this.newEmail;
|
||||
@@ -51,11 +53,12 @@ export class ChangeEmailComponent {
|
||||
request.token = this.token;
|
||||
request.newEmail = this.newEmail;
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
const newKey = await this.cryptoService.makeKey(this.masterPassword, this.newEmail);
|
||||
const kdf = await this.userService.getKdf();
|
||||
const kdfIterations = await this.userService.getKdfIterations();
|
||||
const newKey = await this.cryptoService.makeKey(this.masterPassword, this.newEmail, kdf, kdfIterations);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey);
|
||||
const encKey = await this.cryptoService.getEncKey();
|
||||
const newEncKey = await this.cryptoService.encrypt(encKey.key, newKey);
|
||||
request.key = newEncKey.encryptedString;
|
||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
try {
|
||||
this.formPromise = this.apiService.postEmail(request);
|
||||
await this.formPromise;
|
||||
|
||||
47
src/app/settings/change-kdf.component.html
Normal file
47
src/app/settings/change-kdf.component.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<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}}">
|
||||
<i class="fa fa-question-circle-o"></i>
|
||||
</a>
|
||||
<select id="kdf" name="Kdf" [(ngModel)]="kdf" class="form-control" required>
|
||||
<option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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}}">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<div class="small form-text text-muted">
|
||||
<p>{{'kdfIterationsDesc' | i18n : (100000 | number)}}</p>
|
||||
<strong>{{'warning' | i18n}}</strong>: {{'kdfIterationsWarning' | i18n : (50000 | number)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'changeKdf' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
69
src/app/settings/change-kdf.component.ts
Normal file
69
src/app/settings/change-kdf.component.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
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 { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { KdfRequest } from 'jslib/models/request/kdfRequest';
|
||||
|
||||
import { KdfType } from 'jslib/enums/kdfType';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-kdf',
|
||||
templateUrl: 'change-kdf.component.html',
|
||||
})
|
||||
export class ChangeKdfComponent implements OnInit {
|
||||
masterPassword: string;
|
||||
kdfIterations: number;
|
||||
kdf = KdfType.PBKDF2_SHA256;
|
||||
kdfOptions: any[] = [];
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private messagingService: MessagingService,
|
||||
private userService: UserService) {
|
||||
this.kdfOptions = [
|
||||
{ name: 'PBKDF2 SHA-256', value: KdfType.PBKDF2_SHA256 },
|
||||
];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.kdf = await this.userService.getKdf();
|
||||
this.kdfIterations = await this.userService.getKdfIterations();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const hasEncKey = await this.cryptoService.hasEncKey();
|
||||
if (!hasEncKey) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('updateKey'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new KdfRequest();
|
||||
request.kdf = this.kdf;
|
||||
request.kdfIterations = this.kdfIterations;
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
const email = await this.userService.getEmail();
|
||||
const newKey = await this.cryptoService.makeKey(this.masterPassword, email, this.kdf, this.kdfIterations);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey);
|
||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
try {
|
||||
this.formPromise = this.apiService.postAccountKdf(request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Changed KDF' });
|
||||
this.toasterService.popAsync('success', this.i18nService.t('encKeySettingsChanged'),
|
||||
this.i18nService.t('logBackIn'));
|
||||
this.messagingService.send('logout');
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,42 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="currentMasterPassword">{{'currentMasterPass' | i18n}}</label>
|
||||
<input id="currentMasterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="currentMasterPassword"
|
||||
required appInputVerbatim>
|
||||
<input id="currentMasterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="currentMasterPassword" required appInputVerbatim>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="newMasterPassword">{{'newMasterPass' | i18n}}</label>
|
||||
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control" [(ngModel)]="newMasterPassword"
|
||||
required appInputVerbatim autocomplete="new-password">
|
||||
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
|
||||
[(ngModel)]="newMasterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true"></app-password-strength>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="confirmNewMasterPassword">{{'confirmNewMasterPass' | i18n}}</label>
|
||||
<input id="confirmNewMasterPassword" type="password" name="ConfirmNewMasterPasswordHash" class="form-control" [(ngModel)]="confirmNewMasterPassword"
|
||||
required appInputVerbatim 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()">
|
||||
<label class="form-check-label" for="rotateEncKey">
|
||||
{{'rotateAccountEncKey' | i18n}}
|
||||
</label>
|
||||
<a href="https://help.bitwarden.com/article/change-your-master-password/#rotating-your-accounts-encryption-key"
|
||||
target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o"></i>
|
||||
</a>
|
||||
</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>{{'changeMasterPassword' | i18n}}</span>
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherString } from 'jslib/models/domain/cipherString';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
|
||||
import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest';
|
||||
import { FolderWithIdRequest } from 'jslib/models/request/folderWithIdRequest';
|
||||
import { PasswordRequest } from 'jslib/models/request/passwordRequest';
|
||||
import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-password',
|
||||
templateUrl: 'change-password.component.html',
|
||||
})
|
||||
export class ChangePasswordComponent {
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
currentMasterPassword: string;
|
||||
newMasterPassword: string;
|
||||
confirmNewMasterPassword: string;
|
||||
formPromise: Promise<any>;
|
||||
masterPasswordScore: number;
|
||||
rotateEncKey = false;
|
||||
|
||||
private masterPasswordStrengthTimeout: any;
|
||||
private email: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private messagingService: MessagingService,
|
||||
private userService: UserService) { }
|
||||
private userService: UserService, private passwordGenerationService: PasswordGenerationService,
|
||||
private platformUtilsService: PlatformUtilsService, private folderService: FolderService,
|
||||
private cipherService: CipherService, private syncService: SyncService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await this.userService.getEmail();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const hasEncKey = await this.cryptoService.hasEncKey();
|
||||
@@ -51,16 +75,39 @@ export class ChangePasswordComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
|
||||
this.getPasswordStrengthUserInput());
|
||||
if (strengthResult != null && strengthResult.score < 3) {
|
||||
const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'),
|
||||
this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'),
|
||||
'warning');
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.rotateEncKey) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
|
||||
const email = await this.userService.getEmail();
|
||||
const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email);
|
||||
const kdf = await this.userService.getKdf();
|
||||
const kdfIterations = await this.userService.getKdfIterations();
|
||||
const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email.trim().toLowerCase(),
|
||||
kdf, kdfIterations);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.newMasterPassword, newKey);
|
||||
const encKey = await this.cryptoService.getEncKey();
|
||||
const newEncKey = await this.cryptoService.encrypt(encKey.key, newKey);
|
||||
request.key = newEncKey.encryptedString;
|
||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
try {
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
if (this.rotateEncKey) {
|
||||
this.formPromise = this.apiService.postPassword(request).then(() => {
|
||||
return this.updateKey(newKey, request.newMasterPasswordHash);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Changed Password' });
|
||||
this.toasterService.popAsync('success', this.i18nService.t('masterPasswordChanged'),
|
||||
@@ -68,4 +115,93 @@ export class ChangePasswordComponent {
|
||||
this.messagingService.send('logout');
|
||||
} catch { }
|
||||
}
|
||||
|
||||
updatePasswordStrength() {
|
||||
if (this.masterPasswordStrengthTimeout != null) {
|
||||
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||
}
|
||||
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
|
||||
this.getPasswordStrengthUserInput());
|
||||
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
async rotateEncKeyClicked() {
|
||||
if (this.rotateEncKey) {
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
let hasOldAttachments = false;
|
||||
if (ciphers != null) {
|
||||
for (let i = 0; i < ciphers.length; i++) {
|
||||
if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) {
|
||||
hasOldAttachments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOldAttachments) {
|
||||
const learnMore = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('oldAttachmentsNeedFixDesc'), null,
|
||||
this.i18nService.t('learnMore'), this.i18nService.t('close'), 'warning');
|
||||
if (learnMore) {
|
||||
this.platformUtilsService.launchUri(
|
||||
'https://help.bitwarden.com/article/attachments/#fixing-old-attachments');
|
||||
}
|
||||
this.rotateEncKey = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('updateEncryptionKeyWarning') + ' ' +
|
||||
this.i18nService.t('rotateEncKeyConfirmation'), this.i18nService.t('rotateEncKeyTitle'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!result) {
|
||||
this.rotateEncKey = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPasswordStrengthUserInput() {
|
||||
let userInput: string[] = [];
|
||||
const atPosition = this.email.indexOf('@');
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/));
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
|
||||
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {
|
||||
const encKey = await this.cryptoService.makeEncKey(key);
|
||||
const privateKey = await this.cryptoService.getPrivateKey();
|
||||
let encPrivateKey: CipherString = null;
|
||||
if (privateKey != null) {
|
||||
encPrivateKey = await this.cryptoService.encrypt(privateKey, encKey[0]);
|
||||
}
|
||||
const request = new UpdateKeyRequest();
|
||||
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
|
||||
request.key = encKey[1].encryptedString;
|
||||
request.masterPasswordHash = masterPasswordHash;
|
||||
|
||||
const folders = await this.folderService.getAllDecrypted();
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
if (folders[i].id == null) {
|
||||
continue;
|
||||
}
|
||||
const folder = await this.folderService.encrypt(folders[i], encKey[0]);
|
||||
request.folders.push(new FolderWithIdRequest(folder));
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
for (let i = 0; i < ciphers.length; i++) {
|
||||
if (ciphers[i].organizationId != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cipher = await this.cipherService.encrypt(ciphers[i], encKey[0]);
|
||||
request.ciphers.push(new CipherWithIdRequest(cipher));
|
||||
}
|
||||
|
||||
await this.apiService.postAccountKey(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
<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>
|
||||
@@ -120,7 +121,16 @@
|
||||
<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">{{'additionalStorageDesc' | i18n : '1 GB' : (storageGb.price | currency:'$')}}</small>
|
||||
<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>
|
||||
@@ -142,6 +152,10 @@
|
||||
{{'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">
|
||||
|
||||
@@ -31,6 +31,7 @@ export class CreateOrganizationComponent implements OnInit {
|
||||
|
||||
selfHosted = false;
|
||||
ownedBusiness = false;
|
||||
premiumAccessAddon = false;
|
||||
storageGbPriceMonthly = 0.33;
|
||||
additionalStorage = 0;
|
||||
additionalSeats = 0;
|
||||
@@ -58,6 +59,7 @@ export class CreateOrganizationComponent implements OnInit {
|
||||
baseSeats: 5,
|
||||
noAdditionalSeats: true,
|
||||
annualPlanType: PlanType.FamiliesAnnually,
|
||||
canBuyPremiumAccessAddon: true,
|
||||
},
|
||||
teams: {
|
||||
basePrice: 5,
|
||||
@@ -144,6 +146,8 @@ export class CreateOrganizationComponent implements OnInit {
|
||||
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;
|
||||
@@ -170,6 +174,10 @@ export class CreateOrganizationComponent implements OnInit {
|
||||
}
|
||||
|
||||
changedPlan() {
|
||||
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
|
||||
this.premiumAccessAddon = false;
|
||||
}
|
||||
|
||||
if (this.plans[this.plan].monthPlanType == null) {
|
||||
this.interval = 'year';
|
||||
}
|
||||
@@ -217,8 +225,18 @@ export class CreateOrganizationComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
|
||||
this.premiumAccessTotal(annual);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class PaymentComponent implements OnInit {
|
||||
this.platformUtilsService.isDev() ? Keys.stripeTest : Keys.stripeLive);
|
||||
};
|
||||
this.btScript = window.document.createElement('script');
|
||||
this.btScript.src = 'https://js.braintreegateway.com/web/dropin/1.4.0/js/dropin.min.js';
|
||||
this.btScript.src = 'scripts/dropin.js';
|
||||
this.btScript.async = true;
|
||||
|
||||
this.cardExpMonthOptions = [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<div class="page-header">
|
||||
<h1>{{'goPremium' | i18n}}</h1>
|
||||
</div>
|
||||
<app-callout type="info" *ngIf="canAccessPremium" title="{{'youHavePremiumAccess' | i18n}}" icon="fa-star">
|
||||
{{'alreadyPremiumFromOrg' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="success">
|
||||
<p>{{'premiumUpgradeUnlockFeatures' | i18n}}</p>
|
||||
<ul class="fa-ul">
|
||||
@@ -12,6 +15,10 @@
|
||||
<i class="fa fa-check text-success fa-li"></i>
|
||||
{{'premiumSignUpTwoStep' | i18n}}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-check text-success fa-li"></i>
|
||||
{{'premiumSignUpReports' | i18n}}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-check text-success fa-li"></i>
|
||||
{{'premiumSignUpTotp' | i18n}}
|
||||
@@ -52,7 +59,7 @@
|
||||
<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">{{'additionalStorageDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$')}}</small>
|
||||
<small class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$') : ('year' | i18n)}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { PaymentComponent } from './payment.component';
|
||||
|
||||
@@ -24,6 +25,7 @@ import { PaymentComponent } from './payment.component';
|
||||
export class PremiumComponent implements OnInit {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
|
||||
canAccessPremium = false;
|
||||
selfHosted = false;
|
||||
premiumPrice = 10;
|
||||
storageGbPrice = 4;
|
||||
@@ -35,11 +37,12 @@ export class PremiumComponent implements OnInit {
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
platformUtilsService: PlatformUtilsService, private tokenService: TokenService,
|
||||
private router: Router, private messagingService: MessagingService,
|
||||
private syncService: SyncService) {
|
||||
private syncService: SyncService, private userService: UserService) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.canAccessPremium = await this.userService.canAccessPremium();
|
||||
const premium = await this.tokenService.getPremium();
|
||||
if (premium) {
|
||||
this.router.navigate(['/settings/billing']);
|
||||
|
||||
@@ -18,7 +18,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<app-avatar data="{{profile.name || profile.email}}" [email]="profile.email" dynamic="true" size="75" fontSize="35"></app-avatar>
|
||||
<div class="mb-3">
|
||||
<app-avatar data="{{profile.name || profile.email}}" [email]="profile.email" dynamic="true" size="75"
|
||||
fontSize="35"></app-avatar>
|
||||
</div>
|
||||
<hr>
|
||||
<p *ngIf="fingerprint">
|
||||
{{'yourAccountsFingerprint' | 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>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -7,7 +7,9 @@ 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 { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { UpdateProfileRequest } from 'jslib/models/request/updateProfileRequest';
|
||||
|
||||
@@ -20,15 +22,21 @@ import { ProfileResponse } from 'jslib/models/response/profileResponse';
|
||||
export class ProfileComponent implements OnInit {
|
||||
loading = true;
|
||||
profile: ProfileResponse;
|
||||
fingerprint: string;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService) { }
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private userService: UserService, private cryptoService: CryptoService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.profile = await this.apiService.getProfile();
|
||||
this.loading = false;
|
||||
const fingerprint = await this.cryptoService.getFingerprint(await this.userService.getUserId());
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join('-');
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'purgeVaultDesc' | i18n}}</p>
|
||||
<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
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
@@ -15,6 +18,8 @@ import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerifi
|
||||
templateUrl: 'purge-vault.component.html',
|
||||
})
|
||||
export class PurgeVaultComponent {
|
||||
@Input() organizationId?: string = null;
|
||||
|
||||
masterPassword: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
@@ -32,11 +37,17 @@ export class PurgeVaultComponent {
|
||||
const request = new PasswordVerificationRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
try {
|
||||
this.formPromise = this.apiService.postPurgeCiphers(request);
|
||||
this.formPromise = this.apiService.postPurgeCiphers(request, this.organizationId);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Purged Vault' });
|
||||
this.analytics.eventTrack.next({
|
||||
action: this.organizationId != null ? 'Purged Organization Vault' : 'Purged Vault',
|
||||
});
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('vaultPurged'));
|
||||
this.router.navigate(['vault']);
|
||||
if (this.organizationId != null) {
|
||||
this.router.navigate(['organizations', this.organizationId, 'vault']);
|
||||
} else {
|
||||
this.router.navigate(['vault']);
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
<div class="mx-4">
|
||||
<h3 class="mb-0">
|
||||
{{p.name}}
|
||||
<i class="fa fa-check text-success fa-fw" *ngIf="p.enabled" title="{{'enabled' | i18n}}"></i>
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!premium && p.premium" (click)="premiumRequired()">
|
||||
<i class="fa fa-check text-success fa-fw" *ngIf="p.enabled && canAccessPremium" title="{{'enabled' | i18n}}"></i>
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!canAccessPremium && p.premium" (click)="premiumRequired()">
|
||||
{{'premium' | i18n}}
|
||||
</a>
|
||||
</h3>
|
||||
{{p.description}}
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" [disabled]="!premium && p.premium" (click)="manage(p.type)">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" [disabled]="!canAccessPremium && p.premium" (click)="manage(p.type)">
|
||||
{{'manage' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { TwoFactorProviders } from 'jslib/services/auth.service';
|
||||
|
||||
@@ -38,16 +38,16 @@ export class TwoFactorSetupComponent implements OnInit {
|
||||
|
||||
organizationId: string;
|
||||
providers: any[] = [];
|
||||
premium: boolean;
|
||||
canAccessPremium: boolean;
|
||||
loading = true;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(protected apiService: ApiService, protected tokenService: TokenService,
|
||||
constructor(protected apiService: ApiService, protected userService: UserService,
|
||||
protected componentFactoryResolver: ComponentFactoryResolver, protected messagingService: MessagingService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.premium = this.tokenService.getPremium();
|
||||
this.canAccessPremium = await this.userService.canAccessPremium();
|
||||
|
||||
for (const key in TwoFactorProviders) {
|
||||
if (!TwoFactorProviders.hasOwnProperty(key)) {
|
||||
@@ -128,8 +128,7 @@ export class TwoFactorSetupComponent implements OnInit {
|
||||
}
|
||||
|
||||
async premiumRequired() {
|
||||
const premium = await this.tokenService.getPremium();
|
||||
if (!premium) {
|
||||
if (!this.canAccessPremium) {
|
||||
this.messagingService.send('premiumRequired');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
@@ -23,43 +23,66 @@
|
||||
<li>{{'twoFactorU2fSupportWeb' | i18n}}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<img src="../../images/two-factor/4.png" class="float-right ml-5" alt="">
|
||||
<p>{{'twoFactorU2fAdd' | i18n}}:</p>
|
||||
<ol>
|
||||
<li>{{'twoFactorU2fPlugIn' | i18n}}</li>
|
||||
<li>{{'twoFactorU2fTouchButton' | i18n}}</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<ng-container *ngIf="u2fListening">
|
||||
<p>
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
</p>
|
||||
{{'twoFactorU2fWaiting' | i18n}}...
|
||||
</ng-container>
|
||||
<ng-container *ngIf="u2fResponse">
|
||||
<p>
|
||||
<i class="fa fa-check-circle fa-2x text-success"></i>
|
||||
</p>
|
||||
{{'twoFactorU2fClickEnable' | i18n}}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="u2fError">
|
||||
<p>
|
||||
<i class="fa fa-warning fa-2x text-danger"></i>
|
||||
</p>
|
||||
{{'twoFactorU2fProblemReading' | i18n}}
|
||||
<img src="../../images/two-factor/4.png" class="float-right ml-5" alt="">
|
||||
<ul class="fa-ul">
|
||||
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
|
||||
<i class="fa-li fa fa-key"></i>
|
||||
<strong *ngIf="!k.configured || !k.name">{{'u2fkeyX' | i18n : i + 1}}</strong>
|
||||
<strong *ngIf="k.configured && k.name">{{k.name}}</strong>
|
||||
<i class="fa fa-fw" [ngClass]="{'fa-check text-success': !k.compromised, 'fa-exclamation-triangle text-warning': k.compromised}"
|
||||
*ngIf="k.configured && !removeKeyBtn.loading" title="{{(k.compromised ? 'keyCompromised' : 'enabled') | i18n}}"></i>
|
||||
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
||||
<i class="fa fa-spin fa-spinner text-muted fa-fw" title="{{'loading' | i18n}}" *ngIf="removeKeyBtn.loading"></i>
|
||||
-
|
||||
<a href="#" appStopClick (click)="remove(k)">{{'remove' | i18n}}</a>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<p>{{'twoFactorU2fAdd' | i18n}}:</p>
|
||||
<ol>
|
||||
<li>{{'twoFactorU2fGiveName' | i18n}}</li>
|
||||
<li>{{'twoFactorU2fPlugInReadKey' | i18n}}</li>
|
||||
<li>{{'twoFactorU2fTouchButton' | i18n}}</li>
|
||||
<li>{{'twoFactorU2fSaveForm' | i18n}}</li>
|
||||
</ol>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" type="text" name="Name" class="form-control" [(ngModel)]="name" [disabled]="!keyIdAvailable">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" (click)="readKey()" class="btn btn-outline-secondary mr-2" [disabled]="readKeyBtn.loading || u2fListening || !keyIdAvailable"
|
||||
#readKeyBtn [appApiAction]="challengePromise">
|
||||
{{'readKey' | i18n}}
|
||||
</button>
|
||||
<ng-container *ngIf="readKeyBtn.loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted"></i>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!readKeyBtn.loading">
|
||||
<ng-container *ngIf="u2fListening">
|
||||
<i class="fa fa-spinner fa-spin text-muted"></i>
|
||||
{{'twoFactorU2fWaiting' | i18n}}...
|
||||
</ng-container>
|
||||
<ng-container *ngIf="u2fResponse">
|
||||
<i class="fa fa-check-circle text-success"></i>
|
||||
{{'twoFactorU2fClickSave' | i18n}}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="u2fError">
|
||||
<i class="fa fa-warning text-danger"></i>
|
||||
{{'twoFactorU2fProblemReadingTryAgain' | i18n}}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" [disabled]="form.loading || (!enabled && !u2fResponse)">
|
||||
<button type="submit" class="btn btn-primary" [disabled]="form.loading || !u2fResponse">
|
||||
<i class="fa fa-spinner fa-spin" *ngIf="form.loading" title="{{'loading' | i18n}}"></i>
|
||||
<ng-container *ngIf="!form.loading">
|
||||
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
|
||||
<span *ngIf="enabled">{{'disable' | i18n}}</span>
|
||||
</ng-container>
|
||||
<span *ngIf="!form.loading">{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button #disableBtn type="button" class="btn btn-outline-secondary btn-submit" [appApiAction]="disablePromise"
|
||||
[disabled]="disableBtn.loading" (click)="disable()" *ngIf="enabled">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'disableAllKeys' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
@@ -12,6 +13,9 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||
|
||||
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
|
||||
import { UpdateTwoFactorU2fDeleteRequest } from 'jslib/models/request/updateTwoFactorU2fDeleteRequest';
|
||||
import { UpdateTwoFactorU2fRequest } from 'jslib/models/request/updateTwoFactorU2fRequest';
|
||||
import {
|
||||
ChallengeResponse,
|
||||
@@ -26,18 +30,21 @@ import { TwoFactorBaseComponent } from './two-factor-base.component';
|
||||
})
|
||||
export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnInit, OnDestroy {
|
||||
type = TwoFactorProviderType.U2f;
|
||||
u2fChallenge: ChallengeResponse;
|
||||
name: string;
|
||||
keys: any[];
|
||||
keyIdAvailable: number = null;
|
||||
keysConfiguredCount = 0;
|
||||
u2fError: boolean;
|
||||
u2fListening: boolean;
|
||||
u2fResponse: string;
|
||||
challengePromise: Promise<ChallengeResponse>;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private closed = false;
|
||||
private u2fScript: HTMLScriptElement;
|
||||
|
||||
constructor(apiService: ApiService, i18nService: I18nService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
platformUtilsService: PlatformUtilsService, private ngZone: NgZone) {
|
||||
super(apiService, i18nService, analytics, toasterService, platformUtilsService);
|
||||
this.u2fScript = window.document.createElement('script');
|
||||
this.u2fScript.src = 'scripts/u2f.js';
|
||||
@@ -49,28 +56,24 @@ export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnI
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.closed = true;
|
||||
window.document.body.removeChild(this.u2fScript);
|
||||
}
|
||||
|
||||
auth(authResponse: any) {
|
||||
super.auth(authResponse);
|
||||
this.processResponse(authResponse.response);
|
||||
this.readDevice();
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.enabled) {
|
||||
return super.disable(this.formPromise);
|
||||
} else {
|
||||
return this.enable();
|
||||
if (this.u2fResponse == null || this.keyIdAvailable == null) {
|
||||
// Should never happen.
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
protected enable() {
|
||||
const request = new UpdateTwoFactorU2fRequest();
|
||||
request.masterPasswordHash = this.masterPasswordHash;
|
||||
request.deviceResponse = this.u2fResponse;
|
||||
request.id = this.keyIdAvailable;
|
||||
request.name = this.name;
|
||||
|
||||
return super.enable(async () => {
|
||||
this.formPromise = this.apiService.putTwoFactorU2f(request);
|
||||
@@ -79,38 +82,97 @@ export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnI
|
||||
});
|
||||
}
|
||||
|
||||
private readDevice() {
|
||||
if (this.closed || this.enabled) {
|
||||
disable() {
|
||||
return super.disable(this.formPromise);
|
||||
}
|
||||
|
||||
async remove(key: any) {
|
||||
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
|
||||
return;
|
||||
}
|
||||
const name = key.name != null ? key.name : this.i18nService.t('u2fkeyX', key.id);
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('removeU2fConfirmation'), name,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
const request = new UpdateTwoFactorU2fDeleteRequest();
|
||||
request.id = key.id;
|
||||
request.masterPasswordHash = this.masterPasswordHash;
|
||||
try {
|
||||
key.removePromise = this.apiService.deleteTwoFactorU2f(request);
|
||||
const response = await key.removePromise;
|
||||
key.removePromise = null;
|
||||
await this.processResponse(response);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async readKey() {
|
||||
if (this.keyIdAvailable == null) {
|
||||
return;
|
||||
}
|
||||
const request = new PasswordVerificationRequest();
|
||||
request.masterPasswordHash = this.masterPasswordHash;
|
||||
try {
|
||||
this.challengePromise = this.apiService.getTwoFactorU2fChallenge(request);
|
||||
const challenge = await this.challengePromise;
|
||||
this.readDevice(challenge);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
private readDevice(u2fChallenge: ChallengeResponse) {
|
||||
// tslint:disable-next-line
|
||||
console.log('listening for key...');
|
||||
this.resetU2f(true);
|
||||
(window as any).u2f.register(u2fChallenge.appId, [{
|
||||
version: u2fChallenge.version,
|
||||
challenge: u2fChallenge.challenge,
|
||||
}], [], (data: any) => {
|
||||
this.ngZone.run(() => {
|
||||
this.u2fListening = false;
|
||||
if (data.errorCode) {
|
||||
this.u2fError = true;
|
||||
// tslint:disable-next-line
|
||||
console.log('error: ' + data.errorCode);
|
||||
return;
|
||||
}
|
||||
this.u2fResponse = JSON.stringify(data);
|
||||
});
|
||||
}, 15);
|
||||
}
|
||||
|
||||
private resetU2f(listening = false) {
|
||||
this.u2fResponse = null;
|
||||
this.u2fError = false;
|
||||
this.u2fListening = true;
|
||||
|
||||
(window as any).u2f.register(this.u2fChallenge.appId, [{
|
||||
version: this.u2fChallenge.version,
|
||||
challenge: this.u2fChallenge.challenge,
|
||||
}], [], (data: any) => {
|
||||
this.u2fListening = false;
|
||||
if (data.errorCode === 5) {
|
||||
this.readDevice();
|
||||
return;
|
||||
} else if (data.errorCode) {
|
||||
this.u2fError = true;
|
||||
// tslint:disable-next-line
|
||||
console.log('error: ' + data.errorCode);
|
||||
return;
|
||||
}
|
||||
this.u2fResponse = JSON.stringify(data);
|
||||
}, 10);
|
||||
this.u2fListening = listening;
|
||||
}
|
||||
|
||||
private processResponse(response: TwoFactorU2fResponse) {
|
||||
this.u2fChallenge = response.challenge;
|
||||
this.resetU2f();
|
||||
this.keys = [];
|
||||
this.keyIdAvailable = null;
|
||||
this.name = null;
|
||||
this.keysConfiguredCount = 0;
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
if (response.keys != null) {
|
||||
const key = response.keys.filter((k) => k.id === i);
|
||||
if (key.length > 0) {
|
||||
this.keysConfiguredCount++;
|
||||
this.keys.push({
|
||||
id: i, name: key[0].name,
|
||||
configured: true,
|
||||
compromised: key[0].compromised,
|
||||
removePromise: null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.keys.push({ id: i, name: null, configured: false, compromised: false, removePromise: null });
|
||||
if (this.keyIdAvailable == null) {
|
||||
this.keyIdAvailable = i;
|
||||
}
|
||||
}
|
||||
this.enabled = response.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ export class UserBillingComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const premium = this.tokenService.getPremium();
|
||||
if (premium) {
|
||||
if (this.tokenService.getPremium()) {
|
||||
this.loading = true;
|
||||
this.billing = await this.apiService.getUserBilling();
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<li *ngFor="let a of breachedAccounts" class="list-group-item min-height-fix">
|
||||
<div class="row">
|
||||
<div class="col-2 text-center">
|
||||
<img [src]="'https://haveibeenpwned.com/Content/Images/PwnedLogos/' + a.name + '.' + a.logoType" alt="" class="img-fluid">
|
||||
<img [src]="a.logoPath" alt="" class="img-fluid">
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<h3 class="text-lg">{{a.title}}</h3>
|
||||
|
||||
95
src/app/tools/cipher-report.component.ts
Normal file
95
src/app/tools/cipher-report.component.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
ComponentFactoryResolver,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
import { AddEditComponent as OrgAddEditComponent } from '../organizations/vault/add-edit.component';
|
||||
import { AddEditComponent } from '../vault/add-edit.component';
|
||||
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
export class CipherReportComponent {
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
||||
|
||||
loading = false;
|
||||
hasLoaded = false;
|
||||
ciphers: CipherView[] = [];
|
||||
organization: Organization;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver, protected userService: UserService,
|
||||
protected messagingService: MessagingService, public requiresPaid: boolean) { }
|
||||
|
||||
async load() {
|
||||
this.loading = true;
|
||||
await this.setCiphers();
|
||||
this.loading = false;
|
||||
this.hasLoaded = true;
|
||||
}
|
||||
|
||||
selectCipher(cipher: CipherView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.cipherAddEditModalRef.createComponent(factory).instance;
|
||||
let childComponent: OrgAddEditComponent | AddEditComponent;
|
||||
if (this.organization != null) {
|
||||
childComponent = this.modal.show<OrgAddEditComponent>(OrgAddEditComponent, this.cipherAddEditModalRef);
|
||||
(childComponent as OrgAddEditComponent).organization = this.organization;
|
||||
} else {
|
||||
childComponent = this.modal.show<AddEditComponent>(AddEditComponent, this.cipherAddEditModalRef);
|
||||
}
|
||||
|
||||
childComponent.cipherId = cipher == null ? null : cipher.id;
|
||||
if (this.organization != null) {
|
||||
childComponent.organizationId = this.organization.id;
|
||||
}
|
||||
childComponent.onSavedCipher.subscribe(async (c: CipherView) => {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
childComponent.onDeletedCipher.subscribe(async (c: CipherView) => {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
protected async checkAccess(): Promise<boolean> {
|
||||
if (this.organization != null) {
|
||||
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare use2fa for now
|
||||
// since all paid plans include use2fa
|
||||
if (this.requiresPaid && !this.organization.use2fa) {
|
||||
this.messagingService.send('upgradeOrganization', { organizationId: this.organization.id });
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const accessPremium = await this.userService.canAccessPremium();
|
||||
if (this.requiresPaid && !accessPremium) {
|
||||
this.messagingService.send('premiumRequired');
|
||||
this.loading = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async setCiphers() {
|
||||
this.ciphers = [];
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { ExportService } from 'jslib/abstractions/export.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { ExportComponent as BaseExportComponent } from 'jslib/angular/components/export.component';
|
||||
|
||||
@@ -16,17 +12,14 @@ import { ExportComponent as BaseExportComponent } from 'jslib/angular/components
|
||||
templateUrl: 'export.component.html',
|
||||
})
|
||||
export class ExportComponent extends BaseExportComponent {
|
||||
constructor(analytics: Angulartics2, toasterService: ToasterService,
|
||||
cryptoService: CryptoService, userService: UserService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
|
||||
exportService: ExportService) {
|
||||
super(analytics, toasterService, cryptoService, userService, i18nService, platformUtilsService,
|
||||
exportService, window);
|
||||
constructor(cryptoService: CryptoService, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, exportService: ExportService) {
|
||||
super(cryptoService, i18nService, platformUtilsService, exportService, window);
|
||||
}
|
||||
|
||||
protected saved() {
|
||||
super.saved();
|
||||
this.masterPassword = null;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('exportSuccess'));
|
||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('exportSuccess'));
|
||||
}
|
||||
}
|
||||
|
||||
40
src/app/tools/exposed-passwords-report.component.html
Normal file
40
src/app/tools/exposed-passwords-report.component.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<div class="page-header">
|
||||
<h1>{{'exposedPasswordsReport' | i18n}}</h1>
|
||||
</div>
|
||||
<p>{{'exposedPasswordsReportDesc' | i18n}}</p>
|
||||
<button type="button" class="btn btn-primary btn-submit" [disabled]="loading" (click)="load()">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'checkExposedPasswords' | i18n}}</span>
|
||||
</button>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
|
||||
{{'noExposedPasswords' | i18n}}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{'exposedPasswordsFound' | i18n}}">
|
||||
{{'exposedPasswordsFoundDesc' | i18n : (ciphers.length | number)}}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
|
||||
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
|
||||
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
|
||||
<br>
|
||||
<small>{{c.subTitle}}</small>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="badge badge-warning">
|
||||
{{'exposedXTimes' | i18n : (exposedPasswordMap.get(c.id) | number)}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
64
src/app/tools/exposed-passwords-report.component.ts
Normal file
64
src/app/tools/exposed-passwords-report.component.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { CipherReportComponent } from './cipher-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-exposed-passwords-report',
|
||||
templateUrl: 'exposed-passwords-report.component.html',
|
||||
})
|
||||
export class ExposedPasswordsReportComponent extends CipherReportComponent implements OnInit {
|
||||
exposedPasswordMap = new Map<string, number>();
|
||||
|
||||
constructor(protected cipherService: CipherService, protected auditService: AuditService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
userService: UserService) {
|
||||
super(componentFactoryResolver, userService, messagingService, true);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.checkAccess();
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (await this.checkAccess()) {
|
||||
super.load();
|
||||
}
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const exposedPasswordCiphers: CipherView[] = [];
|
||||
const promises: Array<Promise<void>> = [];
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
|
||||
return;
|
||||
}
|
||||
const promise = this.auditService.passwordLeaked(c.login.password).then((exposedCount) => {
|
||||
if (exposedCount > 0) {
|
||||
exposedPasswordCiphers.push(c);
|
||||
this.exposedPasswordMap.set(c.id, exposedCount);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
this.ciphers = exposedPasswordCiphers;
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
}
|
||||
@@ -84,13 +84,17 @@
|
||||
and save the CSV file. Note that the importer only supports files exported while Enpass is set to the English
|
||||
language, so adjust your settings accordingly.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'enpassjson'">
|
||||
Using the Enpass 6 desktop application, click the menu button and navigate to "File" → "Export".
|
||||
Select the ".json" file format option and save the JSON file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'pwsafexml'">
|
||||
Using the Password Safe desktop application, navigate to "File" → "Export To" → "XML format..." and save the XML
|
||||
file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'dashlanecsv'">
|
||||
Using the Dashlane desktop application, navigate to "File" → "Export" → "Unsecured archive (readable) in CSV format"
|
||||
and save the CSV file.
|
||||
<ng-container *ngIf="format === 'dashlanejson'">
|
||||
Using the Dashlane desktop application, navigate to "File" → "Export" → "Unsecured archive (readable) in JSON format"
|
||||
and save the JSON file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'msecurecsv'">
|
||||
Using the mSecure desktop application, navigate to "File" → "Export" → "CSV File..." and save the CSV file.
|
||||
@@ -153,6 +157,15 @@
|
||||
check all of the fields, change the "Output format" to "CSV", and then click the "Start" button to save the CSV
|
||||
file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'passpackcsv'">
|
||||
Log into the Passpack website vault and navigate to "Settings" → "Export", then click the "Download" button to save
|
||||
the CSV file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'passmanjson'">
|
||||
Open your Passman vault and click on "Settings" in the bottom left corner. In the "Settings" window switch to the
|
||||
"Export credentials" tab and choose "JSON" as the export type. Enter your vault's passphrase and click the "Export"
|
||||
button to save the JSON file.
|
||||
</ng-container>
|
||||
</app-callout>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
|
||||
@@ -129,7 +129,7 @@ export class ImportComponent implements OnInit {
|
||||
reader.onload = (evt) => {
|
||||
if (this.format === 'lastpasscsv' && file.type === 'text/html') {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(evt.target.result, 'text/html');
|
||||
const doc = parser.parseFromString((evt.target as any).result, 'text/html');
|
||||
const pre = doc.querySelector('pre');
|
||||
if (pre != null) {
|
||||
resolve(pre.textContent);
|
||||
@@ -139,7 +139,7 @@ export class ImportComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(evt.target.result);
|
||||
resolve((evt.target as any).result);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject();
|
||||
|
||||
44
src/app/tools/inactive-two-factor-report.component.html
Normal file
44
src/app/tools/inactive-two-factor-report.component.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{'inactive2faReport' | i18n}}
|
||||
<small *ngIf="hasLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<p>{{'inactive2faReportDesc' | i18n}}</p>
|
||||
<div *ngIf="!hasLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</div>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
|
||||
{{'noInactive2fa' | i18n}}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{'inactive2faFound' | i18n}}">
|
||||
{{'inactive2faFoundDesc' | i18n : (ciphers.length | number)}}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
|
||||
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
|
||||
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
|
||||
<br>
|
||||
<small>{{c.subTitle}}</small>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a class="badge badge-primary" href="{{cipherDocs.get(c.id)}}" target="_blank" rel="noopener"
|
||||
*ngIf="cipherDocs.has(c.id)">
|
||||
{{'instructions' | i18n}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
101
src/app/tools/inactive-two-factor-report.component.ts
Normal file
101
src/app/tools/inactive-two-factor-report.component.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
import { CipherReportComponent } from './cipher-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-inactive-two-factor-report',
|
||||
templateUrl: 'inactive-two-factor-report.component.html',
|
||||
})
|
||||
export class InactiveTwoFactorReportComponent extends CipherReportComponent implements OnInit {
|
||||
services = new Map<string, string>();
|
||||
cipherDocs = new Map<string, string>();
|
||||
|
||||
constructor(protected cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService) {
|
||||
super(componentFactoryResolver, userService, messagingService, true);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (await this.checkAccess()) {
|
||||
await super.load();
|
||||
}
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
try {
|
||||
await this.load2fa();
|
||||
} catch { }
|
||||
if (this.services.size > 0) {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const inactive2faCiphers: CipherView[] = [];
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const docs = new Map<string, string>();
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < c.login.uris.length; i++) {
|
||||
const u = c.login.uris[i];
|
||||
if (u.uri != null && u.uri !== '') {
|
||||
const hostname = Utils.getHostname(u.uri);
|
||||
if (hostname != null && this.services.has(hostname)) {
|
||||
if (this.services.get(hostname) != null) {
|
||||
docs.set(c.id, this.services.get(hostname));
|
||||
}
|
||||
inactive2faCiphers.push(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
this.ciphers = inactive2faCiphers;
|
||||
this.cipherDocs = docs;
|
||||
}
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
|
||||
private async load2fa() {
|
||||
if (this.services.size > 0) {
|
||||
return;
|
||||
}
|
||||
const response = await fetch(new Request('https://twofactorauth.org/data.json'));
|
||||
if (response.status !== 200) {
|
||||
throw new Error();
|
||||
}
|
||||
const responseJson = await response.json();
|
||||
for (const categoryName in responseJson) {
|
||||
if (responseJson.hasOwnProperty(categoryName)) {
|
||||
const category = responseJson[categoryName];
|
||||
for (const serviceName in category) {
|
||||
if (category.hasOwnProperty(serviceName)) {
|
||||
const service = category[serviceName];
|
||||
if (service.tfa && service.url != null) {
|
||||
const hostname = Utils.getHostname(service.url);
|
||||
if (hostname != null) {
|
||||
this.services.set(hostname, service.doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<ul class="list-group list-group-flush" *ngIf="history.length">
|
||||
<li class="list-group-item d-flex" *ngFor="let h of history">
|
||||
<div>
|
||||
<div class="password">{{h.password}}</div>
|
||||
<div class="text-monospace password-wrapper" [innerHTML]="h.password | colorPassword"></div>
|
||||
<small class="text-muted">{{h.date | date:'medium'}}</small>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -16,9 +13,8 @@ import {
|
||||
templateUrl: 'password-generator-history.component.html',
|
||||
})
|
||||
export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent {
|
||||
constructor(passwordGenerationService: PasswordGenerationService, analytics: Angulartics2,
|
||||
platformUtilsService: PlatformUtilsService, i18nService: I18nService,
|
||||
toasterService: ToasterService) {
|
||||
super(passwordGenerationService, analytics, platformUtilsService, i18nService, toasterService, window);
|
||||
constructor(passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService) {
|
||||
super(passwordGenerationService, platformUtilsService, i18nService, window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +2,71 @@
|
||||
<h1>{{'passwordGenerator' | i18n}}</h1>
|
||||
</div>
|
||||
<div class="card card-password bg-light my-4">
|
||||
<div class="card-body">
|
||||
{{password}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-4">
|
||||
<label for="length">{{'length' | i18n}}</label>
|
||||
<input id="length" class="form-control" type="number" min="5" max="128" [(ngModel)]="options.length" (input)="saveOptions()">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="min-number">{{'minNumbers' | i18n}}</label>
|
||||
<input id="min-number" class="form-control" type="number" min="0" max="9" (input)="saveOptions()" [(ngModel)]="options.minNumber">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="min-special">{{'minSpecial' | i18n}}</label>
|
||||
<input id="min-special" class="form-control" type="number" min="0" max="9" (input)="saveOptions()" [(ngModel)]="options.minSpecial">
|
||||
</div>
|
||||
<div class="card-body" [innerHTML]="password | colorPassword"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="uppercase" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.uppercase">
|
||||
<label for="uppercase" class="form-check-label">A-Z</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input id="generate-password" name="type" value="password" class="form-check-input" type="radio" (change)="saveOptions()"
|
||||
[(ngModel)]="options.type">
|
||||
<label for="generate-password" class="form-check-label">{{'password' | i18n}}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="lowercase" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.lowercase">
|
||||
<label for="lowercase" class="form-check-label">a-z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="numbers" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.number">
|
||||
<label for="numbers" class="form-check-label">0-9</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="special" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.special">
|
||||
<label for="special" class="form-check-label">!@#$%^&*</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="ambiguous" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="avoidAmbiguous">
|
||||
<label for="ambiguous" class="form-check-label">{{'ambiguous' | i18n}}</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input id="generate-passphrase" name="type" value="passphrase" class="form-check-input" type="radio" (change)="saveOptions()"
|
||||
[(ngModel)]="options.type">
|
||||
<label for="generate-passphrase" class="form-check-label">{{'passphrase' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" *ngIf="options.type === 'passphrase'">
|
||||
<div class="form-group col-4">
|
||||
<label for="num-words">{{'numWords' | i18n}}</label>
|
||||
<input id="num-words" class="form-control" type="number" min="3" max="20" [(ngModel)]="options.numWords" (blur)="saveOptions()">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="word-separator">{{'wordSeparator' | i18n}}</label>
|
||||
<input id="word-separator" class="form-control" type="text" maxlength="1" [(ngModel)]="options.wordSeparator"
|
||||
(blur)="saveOptions()">
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="options.type === 'password'">
|
||||
<div class="row">
|
||||
<div class="form-group col-4">
|
||||
<label for="length">{{'length' | i18n}}</label>
|
||||
<input id="length" class="form-control" type="number" min="5" max="128" [(ngModel)]="options.length" (blur)="saveOptions()">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="min-number">{{'minNumbers' | i18n}}</label>
|
||||
<input id="min-number" class="form-control" type="number" min="0" max="9" (input)="saveOptions()"
|
||||
[(ngModel)]="options.minNumber">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="min-special">{{'minSpecial' | i18n}}</label>
|
||||
<input id="min-special" class="form-control" type="number" min="0" max="9" (input)="saveOptions()"
|
||||
[(ngModel)]="options.minSpecial">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="uppercase" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.uppercase">
|
||||
<label for="uppercase" class="form-check-label">A-Z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="lowercase" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.lowercase">
|
||||
<label for="lowercase" class="form-check-label">a-z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="numbers" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.number">
|
||||
<label for="numbers" class="form-check-label">0-9</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="special" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.special">
|
||||
<label for="special" class="form-check-label">!@#$%^&*</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="ambiguous" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="avoidAmbiguous">
|
||||
<label for="ambiguous" class="form-check-label">{{'ambiguous' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" (click)="regenerate()">
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
@@ -28,10 +25,9 @@ export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(passwordGenerationService: PasswordGenerationService, analytics: Angulartics2,
|
||||
platformUtilsService: PlatformUtilsService, i18nService: I18nService,
|
||||
toasterService: ToasterService, private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(passwordGenerationService, analytics, platformUtilsService, i18nService, toasterService, window);
|
||||
constructor(passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(passwordGenerationService, platformUtilsService, i18nService, window);
|
||||
}
|
||||
|
||||
history() {
|
||||
|
||||
44
src/app/tools/reused-passwords-report.component.html
Normal file
44
src/app/tools/reused-passwords-report.component.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{'reusedPasswordsReport' | i18n}}
|
||||
<small *ngIf="hasLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<p>{{'reusedPasswordsReportDesc' | i18n}}</p>
|
||||
<div *ngIf="!hasLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</div>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
|
||||
{{'noReusedPasswords' | i18n}}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{'reusedPasswordsFound' | i18n}}">
|
||||
{{'reusedPasswordsFoundDesc' | i18n : (ciphers.length | number)}}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
|
||||
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
|
||||
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
|
||||
<br>
|
||||
<small>{{c.subTitle}}</small>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="badge badge-warning">
|
||||
{{'reusedXTimes' | i18n : passwordUseMap.get(c.login.password)}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
58
src/app/tools/reused-passwords-report.component.ts
Normal file
58
src/app/tools/reused-passwords-report.component.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { CipherReportComponent } from './cipher-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reused-passwords-report',
|
||||
templateUrl: 'reused-passwords-report.component.html',
|
||||
})
|
||||
export class ReusedPasswordsReportComponent extends CipherReportComponent implements OnInit {
|
||||
passwordUseMap: Map<string, number>;
|
||||
|
||||
constructor(protected cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService) {
|
||||
super(componentFactoryResolver, userService, messagingService, true);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (await this.checkAccess()) {
|
||||
await super.load();
|
||||
}
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const ciphersWithPasswords: CipherView[] = [];
|
||||
this.passwordUseMap = new Map<string, number>();
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
|
||||
return;
|
||||
}
|
||||
ciphersWithPasswords.push(c);
|
||||
if (this.passwordUseMap.has(c.login.password)) {
|
||||
this.passwordUseMap.set(c.login.password, this.passwordUseMap.get(c.login.password) + 1);
|
||||
} else {
|
||||
this.passwordUseMap.set(c.login.password, 1);
|
||||
}
|
||||
});
|
||||
const reusedPasswordCiphers = ciphersWithPasswords.filter((c) =>
|
||||
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1);
|
||||
this.ciphers = reusedPasswordCiphers;
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,37 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">{{'reports' | i18n}}</div>
|
||||
<div class="card-header d-flex">
|
||||
{{'reports' | i18n}}
|
||||
<div class="ml-auto">
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!canAccessPremium" (click)="premiumRequired()">
|
||||
{{'premium' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="breach-report" class="list-group-item" routerLinkActive="active">
|
||||
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'exposedPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'reusedPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'weakPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'unsecuredWebsitesReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'inactive2faReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="breach-report" class="list-group-item d-flex" routerLinkActive="active">
|
||||
{{'dataBreachReport' | i18n}}
|
||||
<div class="ml-auto">
|
||||
<span class="badge badge-success" *ngIf="!canAccessPremium">
|
||||
{{'free' | i18n | uppercase}}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tools',
|
||||
templateUrl: 'tools.component.html',
|
||||
})
|
||||
export class ToolsComponent { }
|
||||
export class ToolsComponent implements OnInit {
|
||||
canAccessPremium = false;
|
||||
|
||||
constructor(private userService: UserService, private messagingService: MessagingService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.canAccessPremium = await this.userService.canAccessPremium();
|
||||
}
|
||||
|
||||
premiumRequired() {
|
||||
if (!this.canAccessPremium) {
|
||||
this.messagingService.send('premiumRequired');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
39
src/app/tools/unsecured-websites-report.component.html
Normal file
39
src/app/tools/unsecured-websites-report.component.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{'unsecuredWebsitesReport' | i18n}}
|
||||
<small *ngIf="hasLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<p>{{'unsecuredWebsitesReportDesc' | i18n}}</p>
|
||||
<div *ngIf="!hasLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</div>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
|
||||
{{'noUnsecuredWebsites' | i18n}}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{'unsecuredWebsitesFound' | i18n}}">
|
||||
{{'unsecuredWebsitesFoundDesc' | i18n : (ciphers.length | number)}}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
|
||||
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
|
||||
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
|
||||
<br>
|
||||
<small>{{c.subTitle}}</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
47
src/app/tools/unsecured-websites-report.component.ts
Normal file
47
src/app/tools/unsecured-websites-report.component.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { CipherReportComponent } from './cipher-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unsecured-websites-report',
|
||||
templateUrl: 'unsecured-websites-report.component.html',
|
||||
})
|
||||
export class UnsecuredWebsitesReportComponent extends CipherReportComponent implements OnInit {
|
||||
constructor(protected cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService) {
|
||||
super(componentFactoryResolver, userService, messagingService, true);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (await this.checkAccess()) {
|
||||
await super.load();
|
||||
}
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const unsecuredCiphers = allCiphers.filter((c) => {
|
||||
if (c.type !== CipherType.Login || !c.login.hasUris) {
|
||||
return false;
|
||||
}
|
||||
return c.login.uris.find((u) => u.uri != null && u.uri.indexOf('http://') === 0) != null;
|
||||
});
|
||||
this.ciphers = unsecuredCiphers;
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
}
|
||||
44
src/app/tools/weak-passwords-report.component.html
Normal file
44
src/app/tools/weak-passwords-report.component.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{'weakPasswordsReport' | i18n}}
|
||||
<small *ngIf="hasLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<p>{{'weakPasswordsReportDesc' | i18n}}</p>
|
||||
<div *ngIf="!hasLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
</div>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
|
||||
{{'noWeakPasswords' | i18n}}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{'weakPasswordsFound' | i18n}}">
|
||||
{{'weakPasswordsFoundDesc' | i18n : (ciphers.length | number)}}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
|
||||
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
|
||||
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
|
||||
<br>
|
||||
<small>{{c.subTitle}}</small>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="badge badge-{{passwordStrengthMap.get(c.id)[1]}}">
|
||||
{{passwordStrengthMap.get(c.id)[0] | i18n}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
90
src/app/tools/weak-passwords-report.component.ts
Normal file
90
src/app/tools/weak-passwords-report.component.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { CipherReportComponent } from './cipher-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-weak-passwords-report',
|
||||
templateUrl: 'weak-passwords-report.component.html',
|
||||
})
|
||||
export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit {
|
||||
passwordStrengthMap = new Map<string, [string, string]>();
|
||||
|
||||
private passwordStrengthCache = new Map<string, number>();
|
||||
|
||||
constructor(protected cipherService: CipherService, protected passwordGenerationService: PasswordGenerationService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
userService: UserService) {
|
||||
super(componentFactoryResolver, userService, messagingService, true);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (await this.checkAccess()) {
|
||||
await super.load();
|
||||
}
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const weakPasswordCiphers: CipherView[] = [];
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
|
||||
return;
|
||||
}
|
||||
const hasUsername = c.login.username != null && c.login.username.trim() !== '';
|
||||
const cacheKey = c.login.password + '_____' + (hasUsername ? c.login.username : '');
|
||||
if (!this.passwordStrengthCache.has(cacheKey)) {
|
||||
let userInput: string[] = [];
|
||||
if (hasUsername) {
|
||||
const atPosition = c.login.username.indexOf('@');
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput.concat(
|
||||
c.login.username.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/))
|
||||
.filter((i) => i.length >= 3);
|
||||
} else {
|
||||
userInput = c.login.username.trim().toLowerCase().split(/[^A-Za-z0-9]/)
|
||||
.filter((i) => i.length >= 3);
|
||||
}
|
||||
}
|
||||
const result = this.passwordGenerationService.passwordStrength(c.login.password,
|
||||
userInput.length > 0 ? userInput : null);
|
||||
this.passwordStrengthCache.set(cacheKey, result.score);
|
||||
}
|
||||
const score = this.passwordStrengthCache.get(cacheKey);
|
||||
if (score != null && score <= 3) {
|
||||
this.passwordStrengthMap.set(c.id, this.scoreKey(score));
|
||||
weakPasswordCiphers.push(c);
|
||||
}
|
||||
});
|
||||
this.ciphers = weakPasswordCiphers;
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
|
||||
private scoreKey(score: number): [string, string] {
|
||||
switch (score) {
|
||||
case 4:
|
||||
return ['strong', 'success'];
|
||||
case 3:
|
||||
return ['good', 'primary'];
|
||||
case 2:
|
||||
return ['weak', 'warning'];
|
||||
default:
|
||||
return ['veryWeak', 'danger'];
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user