mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e7c6fbf423 | ||
|
|
6cef5e614d | ||
|
|
33cf77559f | ||
|
|
b5085d8004 | ||
|
|
80af20ef54 |
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: 3429b57db4...739d308498
11100
package-lock.json
generated
11100
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.0",
|
||||
"version": "2.6.2",
|
||||
"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.7.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,13 @@ const routes: Routes = [
|
||||
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) { }
|
||||
|
||||
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';
|
||||
@@ -75,6 +77,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';
|
||||
@@ -140,8 +143,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 +160,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 +185,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
clearQueryParams: true,
|
||||
},
|
||||
}),
|
||||
ToasterModule,
|
||||
ToasterModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
AcceptOrganizationComponent,
|
||||
@@ -198,6 +207,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
BulkShareComponent,
|
||||
CalloutComponent,
|
||||
ChangeEmailComponent,
|
||||
ChangeKdfComponent,
|
||||
ChangePasswordComponent,
|
||||
CiphersComponent,
|
||||
CollectionsComponent,
|
||||
@@ -244,6 +254,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
OrganizationsComponent,
|
||||
OrganizationLayoutComponent,
|
||||
@@ -283,6 +294,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
PasswordStrengthComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddEditComponent,
|
||||
@@ -304,6 +316,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() {
|
||||
|
||||
@@ -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,15 +30,18 @@ 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;
|
||||
@@ -60,12 +64,28 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
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 +103,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,10 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
@@ -16,6 +19,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 +35,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 +52,41 @@ 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 syncService: SyncService, private i18nService: I18nService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService) { }
|
||||
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone,
|
||||
private changeDetectorRef: ChangeDetectorRef) { }
|
||||
|
||||
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 +107,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 +117,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 +144,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 +165,7 @@ export class VaultComponent implements OnInit {
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.ciphersComponent.searchText = searchText;
|
||||
this.ciphersComponent.search(200);
|
||||
}
|
||||
|
||||
editCipherAttachments(cipher: CipherView) {
|
||||
@@ -197,7 +224,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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -161,10 +165,8 @@ export class CreateOrganizationComponent implements OnInit {
|
||||
}
|
||||
|
||||
async finalize(orgId: string) {
|
||||
await Promise.all([
|
||||
this.apiService.refreshIdentityToken(),
|
||||
this.syncService.fullSync(true),
|
||||
]);
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
this.analytics.eventTrack.next({ action: 'Created Organization' });
|
||||
this.toasterService.popAsync('success', this.i18nService.t('organizationCreated'),
|
||||
this.i18nService.t('organizationReadyToGo'));
|
||||
@@ -172,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';
|
||||
}
|
||||
@@ -219,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">
|
||||
@@ -52,7 +55,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']);
|
||||
@@ -87,10 +90,8 @@ export class PremiumComponent implements OnInit {
|
||||
}
|
||||
|
||||
async finalizePremium() {
|
||||
await Promise.all([
|
||||
this.apiService.refreshIdentityToken(),
|
||||
this.syncService.fullSync(true),
|
||||
]);
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
this.analytics.eventTrack.next({ action: 'Signed Up Premium' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('premiumUpdated'));
|
||||
this.messagingService.send('purchasedPremium');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,42 +6,69 @@
|
||||
{{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>
|
||||
<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() {
|
||||
|
||||
@@ -80,10 +80,10 @@
|
||||
<div class="col-6 form-group totp d-flex align-items-end" [ngClass]="{'low': totpLow}">
|
||||
<div *ngIf="!cipher.login.totp || !totpCode">
|
||||
<img src="../../images/totp-countdown.png" title="{{'verificationCodeTotp' | i18n}}" class="ml-2">
|
||||
<a href="#" appStopClick class="badge badge-primary ml-3" (click)="premiumRequired()" *ngIf="!organization && !cipher.organizationId && !isPremium">
|
||||
<a href="#" appStopClick class="badge badge-primary ml-3" (click)="premiumRequired()" *ngIf="!organization && !cipher.organizationId && !canAccessPremium">
|
||||
{{'premium' | i18n}}
|
||||
</a>
|
||||
<a href="#" appStopClick class="badge badge-primary ml-3" (click)="upgradeOrganization()" *ngIf="(organization && !organization.useTotp) || (cipher.organizationId && !cipher.organizationUseTotp)">
|
||||
<a href="#" appStopClick class="badge badge-primary ml-3" (click)="upgradeOrganization()" *ngIf="(organization && !organization.useTotp) || (!organization && !canAccessPremium && cipher.organizationId && !cipher.organizationUseTotp)">
|
||||
{{'upgrade' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
@@ -366,6 +366,30 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="!editMode && !organization && ownershipOptions && ownershipOptions.length > 1">
|
||||
<h3 class="mt-4">{{'ownership' | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<label for="organizationId">{{'whoOwnsThisItem' | i18n}}</label>
|
||||
<select id="organizationId" class="form-control" name="OrganizationId" [(ngModel)]="cipher.organizationId"
|
||||
(change)="organizationChanged()">
|
||||
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!editMode && cipher.organizationId">
|
||||
<h3 class="mt-4">{{'collections' | i18n}}</h3>
|
||||
<div *ngIf="!collections || !collections.length">
|
||||
{{'noCollectionsInList' | i18n}}
|
||||
</div>
|
||||
<ng-container *ngIf="collections && collections.length">
|
||||
<div class="form-check" *ngFor="let c of collections; let i = index">
|
||||
<input class="form-check-input" type="checkbox" [(ngModel)]="c.checked" id="collection-{{i}}" name="Collection[{{i}}].Checked">
|
||||
<label class="form-check-label" for="collection-{{i}}">{{c.name}}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="editMode">
|
||||
<div class="small text-muted mt-4">
|
||||
<div>
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
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 { AddEditComponent as BaseAddEditComponent } from 'jslib/angular/components/add-edit.component';
|
||||
import { LoginUriView } from 'jslib/models/view/loginUriView';
|
||||
@@ -26,8 +21,8 @@ import { LoginUriView } from 'jslib/models/view/loginUriView';
|
||||
selector: 'app-vault-add-edit',
|
||||
templateUrl: 'add-edit.component.html',
|
||||
})
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
isPremium: boolean;
|
||||
export class AddEditComponent extends BaseAddEditComponent {
|
||||
canAccessPremium: boolean;
|
||||
totpCode: string;
|
||||
totpCodeFormatted: string;
|
||||
totpDash: number;
|
||||
@@ -41,23 +36,24 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
|
||||
constructor(cipherService: CipherService, folderService: FolderService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
auditService: AuditService, stateService: StateService,
|
||||
protected tokenService: TokenService, protected totpService: TotpService,
|
||||
protected passwordGenerationService: PasswordGenerationService, protected messagingService: MessagingService) {
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, analytics,
|
||||
toasterService, auditService, stateService);
|
||||
userService: UserService, collectionService: CollectionService,
|
||||
protected totpService: TotpService, protected passwordGenerationService: PasswordGenerationService,
|
||||
protected messagingService: MessagingService) {
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
|
||||
userService, collectionService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.load();
|
||||
await super.ngOnInit();
|
||||
await this.load();
|
||||
this.showRevisionDate = this.cipher.passwordRevisionDisplayDate != null;
|
||||
this.hasPasswordHistory = this.cipher.hasPasswordHistory;
|
||||
this.cleanUp();
|
||||
|
||||
this.isPremium = this.tokenService.getPremium();
|
||||
this.canAccessPremium = await this.userService.canAccessPremium();
|
||||
if (this.cipher.type === CipherType.Login && this.cipher.login.totp &&
|
||||
(this.cipher.organizationUseTotp || this.isPremium)) {
|
||||
(this.cipher.organizationUseTotp || this.canAccessPremium)) {
|
||||
await this.totpUpdateCode();
|
||||
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
||||
await this.totpTick(interval);
|
||||
@@ -77,7 +73,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.analytics.eventTrack.next({ action: 'Launched Login URI' });
|
||||
this.platformUtilsService.eventTrack('Launched Login URI');
|
||||
this.platformUtilsService.launchUri(uri.uri);
|
||||
}
|
||||
|
||||
@@ -86,9 +82,9 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.analytics.eventTrack.next({ action: 'Copied ' + aType });
|
||||
this.platformUtilsService.copyToClipboard(value, { doc: window.document });
|
||||
this.toasterService.popAsync('info', null,
|
||||
this.platformUtilsService.eventTrack('Copied ' + aType);
|
||||
this.platformUtilsService.copyToClipboard(value, { window: window });
|
||||
this.platformUtilsService.showToast('info', null,
|
||||
this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey)));
|
||||
}
|
||||
|
||||
@@ -102,8 +98,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
async premiumRequired() {
|
||||
const premium = await this.tokenService.getPremium();
|
||||
if (!premium) {
|
||||
if (!this.canAccessPremium) {
|
||||
this.messagingService.send('premiumRequired');
|
||||
return;
|
||||
}
|
||||
@@ -158,5 +153,4 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
await this.totpUpdateCode();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,13 +19,22 @@
|
||||
<i class="fa fa-spinner fa-lg fa-fw fa-spin" *ngIf="a.downloading"></i>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="download(a)">{{a.fileName}}</a>
|
||||
<br>
|
||||
<div class="d-flex">
|
||||
<a href="#" appStopClick (click)="download(a)">{{a.fileName}}</a>
|
||||
<div *ngIf="showFixOldAttachments(a)" class="ml-2">
|
||||
<a href="https://help.bitwarden.com/article/attachments/#fixing-old-attachments"
|
||||
target="_blank" rel="noopener">
|
||||
<i class="fa fa-exclamation-triangle text-warning" title="{{'attachmentFixDesc' | i18n}}"></i></a>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm m-0 py-0 px-2"
|
||||
(click)="reupload(a)" #reuploadBtn [appApiAction]="reuploadPromises[a.id]"
|
||||
[disabled]="reuploadBtn.loading">{{'fix' | i18n}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<small>{{a.sizeName}}</small>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<button class="btn btn-outline-danger" type="button" appStopClick title="{{'delete' | i18n}}" (click)="delete(a)" #deleteBtn
|
||||
[appApiAction]="deletePromises[a.id]" [disabled]="deleteBtn.loading">
|
||||
<button class="btn btn-outline-danger" type="button" appStopClick title="{{'delete' | i18n}}"
|
||||
(click)="delete(a)" #deleteBtn [appApiAction]="deletePromises[a.id]" [disabled]="deleteBtn.loading">
|
||||
<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>
|
||||
@@ -43,7 +52,8 @@
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal" title="{{'close' | i18n}}">{{'close' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal" title="{{'close' | i18n}}">{{'close'
|
||||
| i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
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 { AttachmentView } from 'jslib/models/view/attachmentView';
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib/angular/components/attachments.component';
|
||||
|
||||
@@ -16,11 +15,19 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib/angular/
|
||||
templateUrl: 'attachments.component.html',
|
||||
})
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
constructor(cipherService: CipherService, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, tokenService: TokenService,
|
||||
constructor(cipherService: CipherService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, userService: UserService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
super(cipherService, analytics, toasterService, i18nService, cryptoService, tokenService,
|
||||
platformUtilsService, window);
|
||||
super(cipherService, i18nService, cryptoService, userService, platformUtilsService, window);
|
||||
}
|
||||
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
if (this.showFixOldAttachments(attachment)) {
|
||||
await this.reuploadCipherAttachment(attachment, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return attachment.key == null && this.cipher.organizationId == null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'shareManyDesc' | i18n}}</p>
|
||||
<p>{{'shareSelectedItemsDesc' | i18n: this.ciphers.length : shareableCiphers.length : nonShareableCount}}</p>
|
||||
<p>{{'shareSelectedItemsCountDesc' | i18n: this.ciphers.length : shareableCiphers.length : nonShareableCount}}</p>
|
||||
<div class="form-group">
|
||||
<label for="organization">{{'organization' | i18n}}</label>
|
||||
<select id="organization" name="OrganizationId" [(ngModel)]="organizationId" class="form-control" (change)="filterCollections()">
|
||||
@@ -36,17 +36,17 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
|
||||
<td class="table-list-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
|
||||
</td>
|
||||
<td>
|
||||
<span appStopProp>{{c.name}}</span>
|
||||
{{c.name}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<button type="submit" class="btn btn-primary btn-submit manual" [disabled]="form.loading || !canSave" [ngClass]="{loading:form.loading}">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
|
||||
@@ -41,7 +41,7 @@ export class BulkShareComponent implements OnInit {
|
||||
private collectionService: CollectionService, private userService: UserService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.shareableCiphers = this.ciphers.filter((c) => !c.hasAttachments && c.organizationId == null);
|
||||
this.shareableCiphers = this.ciphers.filter((c) => !c.hasOldAttachments && c.organizationId == null);
|
||||
this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length;
|
||||
const allCollections = await this.collectionService.getAllDecrypted();
|
||||
this.writeableCollections = allCollections.filter((c) => !c.readOnly);
|
||||
@@ -67,12 +67,14 @@ export class BulkShareComponent implements OnInit {
|
||||
|
||||
async submit() {
|
||||
const checkedCollectionIds = this.collections.filter((c) => (c as any).checked).map((c) => c.id);
|
||||
this.formPromise = this.cipherService.shareManyWithServer(this.shareableCiphers, this.organizationId,
|
||||
checkedCollectionIds);
|
||||
await this.formPromise;
|
||||
this.onShared.emit();
|
||||
this.analytics.eventTrack.next({ action: 'Bulk Shared Items' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('sharedItems'));
|
||||
try {
|
||||
this.formPromise = this.cipherService.shareManyWithServer(this.shareableCiphers, this.organizationId,
|
||||
checkedCollectionIds);
|
||||
await this.formPromise;
|
||||
this.onShared.emit();
|
||||
this.analytics.eventTrack.next({ action: 'Bulk Shared Items' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('sharedItems'));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
check(c: CollectionView, select?: boolean) {
|
||||
@@ -83,4 +85,15 @@ export class BulkShareComponent implements OnInit {
|
||||
const collections = select ? this.collections : this.writeableCollections;
|
||||
collections.forEach((c) => this.check(c, select));
|
||||
}
|
||||
|
||||
get canSave() {
|
||||
if (this.shareableCiphers != null && this.shareableCiphers.length > 0 && this.collections != null) {
|
||||
for (let i = 0; i < this.collections.length; i++) {
|
||||
if ((this.collections[i] as any).checked) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
<ng-container *ngIf="(ciphers | searchCiphers: searchText) as searchedCiphers">
|
||||
<table class="table table-hover table-list table-ciphers" *ngIf="searchedCiphers.length > 0">
|
||||
<ng-container *ngIf="ciphers">
|
||||
<table class="table table-hover table-list table-ciphers" *ngIf="ciphers.length > 0">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of searchedCiphers">
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td (click)="checkCipher(c)" class="table-list-checkbox" *ngIf="!organization">
|
||||
<input type="checkbox" [(ngModel)]="c.checked">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" appStopProp>
|
||||
</td>
|
||||
<td (click)="checkCipher(c)" class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td (click)="checkCipher(c)" class="reduced-lh">
|
||||
<td (click)="checkCipher(c)" class="reduced-lh wrap">
|
||||
<a href="#" appStopClick appStopProp (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
|
||||
<i class="fa fa-share-alt" appStopProp *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
|
||||
<i class="fa fa-paperclip" appStopProp *ngIf="c.hasAttachments" title="{{'attachments' | i18n}}"></i>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i class="fa fa-paperclip" appStopProp title="{{'attachments' | i18n}}"></i>
|
||||
<i class="fa fa-exclamation-triangle text-warning" appStopProp *ngIf="showFixOldAttachments(c)"
|
||||
title="{{'attachmentsNeedFix' | i18n}}"></i>
|
||||
</ng-container>
|
||||
<br>
|
||||
<small appStopProp>{{c.subTitle}}</small>
|
||||
</td>
|
||||
@@ -52,7 +56,7 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="no-items" *ngIf="searchedCiphers.length === 0">
|
||||
<div class="no-items" *ngIf="ciphers.length === 0">
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!loaded" title="{{'loading' | i18n}}"></i>
|
||||
<ng-container *ngIf="loaded">
|
||||
<p>{{'noItemsInList' | i18n}}</p>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Angulartics2 } from 'angulartics2';
|
||||
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 { CiphersComponent as BaseCiphersComponent } from 'jslib/angular/components/ciphers.component';
|
||||
|
||||
@@ -38,10 +39,10 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
||||
|
||||
private searchPipe: SearchCiphersPipe;
|
||||
|
||||
constructor(cipherService: CipherService, protected analytics: Angulartics2,
|
||||
constructor(searchService: SearchService, protected analytics: Angulartics2,
|
||||
protected toasterService: ToasterService, protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService) {
|
||||
super(cipherService);
|
||||
protected platformUtilsService: PlatformUtilsService, protected cipherService: CipherService) {
|
||||
super(searchService);
|
||||
this.searchPipe = new SearchCiphersPipe(platformUtilsService);
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
||||
}
|
||||
|
||||
this.analytics.eventTrack.next({ action: 'Copied ' + aType.toLowerCase() + ' from listing.' });
|
||||
this.platformUtilsService.copyToClipboard(value, { doc: window.document });
|
||||
this.platformUtilsService.copyToClipboard(value, { window: window });
|
||||
this.toasterService.popAsync('info', null,
|
||||
this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey)));
|
||||
}
|
||||
@@ -126,4 +127,8 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
||||
protected deleteCipher(id: string) {
|
||||
return this.cipherService.deleteWithServer(id);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(c: CipherView) {
|
||||
return c.hasOldAttachments && c.organizationId == null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
|
||||
<td class="table-list-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
|
||||
</td>
|
||||
<td>
|
||||
<span appStopProp>{{c.name}}</span>
|
||||
{{c.name}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -1,72 +1,31 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
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 { CipherView } from 'jslib/models/view/cipherView';
|
||||
import { CollectionView } from 'jslib/models/view/collectionView';
|
||||
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
import { CollectionsComponent as BaseCollectionsComponent } from 'jslib/angular/components/collections.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-collections',
|
||||
templateUrl: 'collections.component.html',
|
||||
})
|
||||
export class CollectionsComponent implements OnInit, OnDestroy {
|
||||
@Input() cipherId: string;
|
||||
@Output() onSavedCollections = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
cipher: CipherView;
|
||||
collectionIds: string[];
|
||||
collections: CollectionView[] = [];
|
||||
|
||||
protected cipherDomain: Cipher;
|
||||
|
||||
constructor(protected collectionService: CollectionService, protected analytics: Angulartics2,
|
||||
protected toasterService: ToasterService, protected i18nService: I18nService,
|
||||
protected cipherService: CipherService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
this.collectionIds = this.loadCipherCollections();
|
||||
this.cipher = await this.cipherDomain.decrypt();
|
||||
this.collections = await this.loadCollections();
|
||||
|
||||
this.selectAll(false);
|
||||
if (this.collectionIds != null) {
|
||||
this.collections.forEach((c) => {
|
||||
(c as any).checked = this.collectionIds.indexOf(c.id) > -1;
|
||||
});
|
||||
}
|
||||
export class CollectionsComponent extends BaseCollectionsComponent implements OnDestroy {
|
||||
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService, cipherService: CipherService) {
|
||||
super(collectionService, platformUtilsService, i18nService, cipherService);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.selectAll(false);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.cipherDomain.collectionIds = this.collections
|
||||
.filter((c) => !!(c as any).checked)
|
||||
.map((c) => c.id);
|
||||
this.formPromise = this.saveCollections();
|
||||
await this.formPromise;
|
||||
this.onSavedCollections.emit();
|
||||
this.analytics.eventTrack.next({ action: 'Edited Cipher Collections' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('editedItem'));
|
||||
}
|
||||
|
||||
check(c: CollectionView, select?: boolean) {
|
||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||
}
|
||||
@@ -74,21 +33,4 @@ export class CollectionsComponent implements OnInit, OnDestroy {
|
||||
selectAll(select: boolean) {
|
||||
this.collections.forEach((c) => this.check(c, select));
|
||||
}
|
||||
|
||||
protected loadCipher() {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
}
|
||||
|
||||
protected loadCipherCollections() {
|
||||
return this.cipherDomain.collectionIds;
|
||||
}
|
||||
|
||||
protected async loadCollections() {
|
||||
const allCollections = await this.collectionService.getAllDecrypted();
|
||||
return allCollections.filter((c) => !c.readOnly && c.organizationId === this.cipher.organizationId);
|
||||
}
|
||||
|
||||
protected saveCollections() {
|
||||
return this.cipherService.saveCollectionsWithServer(this.cipherDomain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
@@ -17,8 +14,7 @@ import {
|
||||
})
|
||||
export class FolderAddEditComponent extends BaseFolderAddEditComponent {
|
||||
constructor(folderService: FolderService, i18nService: I18nService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
super(folderService, i18nService, analytics, toasterService, platformUtilsService);
|
||||
super(folderService, i18nService, platformUtilsService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{{'filters' | i18n}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="search" placeholder="{{searchPlaceholder || ('searchVault' | i18n)}}" id="search" class="form-control" [(ngModel)]="searchText"
|
||||
(input)="searchTextChanged()" appAutofocus>
|
||||
<input type="search" placeholder="{{searchPlaceholder || ('searchVault' | i18n)}}" id="search" class="form-control"
|
||||
[(ngModel)]="searchText" (input)="searchTextChanged()" appAutofocus>
|
||||
<ul class="fa-ul card-ul">
|
||||
<li [ngClass]="{active: selectedAll}">
|
||||
<a href="#" appStopClick (click)="selectAll()">
|
||||
@@ -52,22 +52,37 @@
|
||||
</a>
|
||||
</h3>
|
||||
<ul class="fa-ul card-ul carets">
|
||||
<li *ngFor="let f of folders" class="d-flex" [ngClass]="{active: selectedFolder && f.id === selectedFolderId}">
|
||||
<a href="#" appStopClick (click)="selectFolder(f)">
|
||||
<i class="fa-li fa fa-caret-right"></i> {{f.name}}</a>
|
||||
<a href="#" class="text-muted ml-auto show-active" appStopClick (click)="editFolder(f)" title="{{'editFolder' | i18n}}" *ngIf="f.id">
|
||||
<i class="fa fa-pencil fa-fw"></i>
|
||||
</a>
|
||||
</li>
|
||||
<ng-template #recursiveFolders let-folders>
|
||||
<li *ngFor="let f of folders" [ngClass]="{active: selectedFolder && f.node.id === selectedFolderId}">
|
||||
<div class="d-flex">
|
||||
<i class="fa-li fa" title="{{'toggleCollapse' | i18n}}" [ngClass]="{'fa-caret-right': isCollapsed(f.node), 'fa-caret-down': !isCollapsed(f.node)}" (click)="collapse(f.node)"></i>
|
||||
<a href="#" appStopClick (click)="selectFolder(f.node)">{{f.node.name}}</a>
|
||||
<a href="#" class="text-muted ml-auto show-active" appStopClick (click)="editFolder(f.node)"
|
||||
title="{{'editFolder' | i18n}}" *ngIf="f.node.id">
|
||||
<i class="fa fa-pencil fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="fa-ul card-ul carets" *ngIf="f.children.length && !isCollapsed(f.node)">
|
||||
<ng-container *ngTemplateOutlet="recursiveFolders; context:{ $implicit: f.children }"></ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-container *ngTemplateOutlet="recursiveFolders; context:{ $implicit: nestedFolders }"></ng-container>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showCollections && collections && collections.length">
|
||||
<h3>{{'collections' | i18n}}</h3>
|
||||
<ul class="fa-ul card-ul carets">
|
||||
<li *ngFor="let c of collections" [ngClass]="{active: c.id === selectedCollectionId}">
|
||||
<a href="#" appStopClick (click)="selectCollection(c)">
|
||||
<i class="fa-li fa fa-caret-right"></i> {{c.name}}</a>
|
||||
</li>
|
||||
<ng-template #recursiveCollections let-collections>
|
||||
<li *ngFor="let c of collections" [ngClass]="{active: c.node.id === selectedCollectionId}">
|
||||
<i class="fa-li fa" title="{{'toggleCollapse' | i18n}}" [ngClass]="{'fa-caret-right': isCollapsed(c.node), 'fa-caret-down': !isCollapsed(c.node)}" (click)="collapse(c.node)"></i>
|
||||
<a href="#" appStopClick (click)="selectCollection(c.node)">{{c.node.name}}</a>
|
||||
<ul class="fa-ul card-ul carets" *ngIf="c.children.length && !isCollapsed(c.node)">
|
||||
<ng-container *ngTemplateOutlet="recursiveCollections; context:{ $implicit: c.children }"></ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-container *ngTemplateOutlet="recursiveCollections; context:{ $implicit: nestedCollections }"></ng-container>
|
||||
</ul>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { GroupingsComponent as BaseGroupingsComponent } from 'jslib/angular/components/groupings.component';
|
||||
|
||||
@@ -19,8 +21,9 @@ export class GroupingsComponent extends BaseGroupingsComponent {
|
||||
searchText: string = '';
|
||||
searchPlaceholder: string = null;
|
||||
|
||||
constructor(collectionService: CollectionService, folderService: FolderService) {
|
||||
super(collectionService, folderService);
|
||||
constructor(collectionService: CollectionService, folderService: FolderService,
|
||||
storageService: StorageService, userService: UserService) {
|
||||
super(collectionService, folderService, storageService, userService);
|
||||
}
|
||||
|
||||
searchTextChanged() {
|
||||
|
||||
@@ -39,17 +39,17 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
|
||||
<td class="table-list-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
|
||||
</td>
|
||||
<td>
|
||||
<span appStopProp>{{c.name}}</span>
|
||||
{{c.name}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading" *ngIf="organizations && organizations.length">
|
||||
<button type="submit" class="btn btn-primary btn-submit manual" [disabled]="form.loading || !canSave" [ngClass]="{loading:form.loading}" *ngIf="organizations && organizations.length">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
|
||||
@@ -1,94 +1,33 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
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 { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
import { CollectionView } from 'jslib/models/view/collectionView';
|
||||
|
||||
import { ShareComponent as BaseShareComponent } from 'jslib/angular/components/share.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-share',
|
||||
templateUrl: 'share.component.html',
|
||||
})
|
||||
export class ShareComponent implements OnInit, OnDestroy {
|
||||
@Input() cipherId: string;
|
||||
@Input() organizationId: string;
|
||||
@Output() onSharedCipher = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
cipher: CipherView;
|
||||
collections: CollectionView[] = [];
|
||||
organizations: Organization[] = [];
|
||||
|
||||
private writeableCollections: CollectionView[] = [];
|
||||
|
||||
constructor(private collectionService: CollectionService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private userService: UserService, private cipherService: CipherService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
this.cipher = await cipherDomain.decrypt();
|
||||
const allCollections = await this.collectionService.getAllDecrypted();
|
||||
this.writeableCollections = allCollections.filter((c) => !c.readOnly);
|
||||
this.organizations = await this.userService.getAllOrganizations();
|
||||
if (this.organizationId == null && this.organizations.length > 0) {
|
||||
this.organizationId = this.organizations[0].id;
|
||||
}
|
||||
this.filterCollections();
|
||||
export class ShareComponent extends BaseShareComponent implements OnDestroy {
|
||||
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService, userService: UserService,
|
||||
cipherService: CipherService) {
|
||||
super(collectionService, platformUtilsService, i18nService, userService, cipherService);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.selectAll(false);
|
||||
}
|
||||
|
||||
filterCollections() {
|
||||
this.selectAll(false);
|
||||
if (this.organizationId == null || this.writeableCollections.length === 0) {
|
||||
this.collections = [];
|
||||
} else {
|
||||
this.collections = this.writeableCollections.filter((c) => c.organizationId === this.organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
const cipherView = await cipherDomain.decrypt();
|
||||
|
||||
const attachmentPromises: Array<Promise<any>> = [];
|
||||
if (cipherView.attachments != null) {
|
||||
for (const attachment of cipherView.attachments) {
|
||||
const promise = this.cipherService.shareAttachmentWithServer(attachment,
|
||||
cipherView.id, this.organizationId);
|
||||
attachmentPromises.push(promise);
|
||||
}
|
||||
}
|
||||
|
||||
const checkedCollectionIds = this.collections.filter((c) => (c as any).checked).map((c) => c.id);
|
||||
try {
|
||||
this.formPromise = Promise.all(attachmentPromises).then(async () => {
|
||||
await this.cipherService.shareWithServer(cipherView, this.organizationId, checkedCollectionIds);
|
||||
this.onSharedCipher.emit();
|
||||
this.analytics.eventTrack.next({ action: 'Shared Cipher' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('sharedItem'));
|
||||
});
|
||||
await this.formPromise;
|
||||
} catch { }
|
||||
}
|
||||
|
||||
check(c: CollectionView, select?: boolean) {
|
||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
@@ -40,11 +43,15 @@ import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
const BroadcasterSubscriptionId = 'VaultComponent';
|
||||
|
||||
@Component({
|
||||
selector: 'app-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(OrganizationsComponent) organizationsComponent: OrganizationsComponent;
|
||||
@@ -74,25 +81,27 @@ export class VaultComponent implements OnInit {
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private tokenService: TokenService, private cryptoService: CryptoService,
|
||||
private messagingService: MessagingService, private userService: UserService,
|
||||
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService) { }
|
||||
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone,
|
||||
private changeDetectorRef: ChangeDetectorRef) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
||||
this.showBrowserOutdated = window.navigator.userAgent.indexOf('MSIE') !== -1;
|
||||
const hasEncKey = await this.cryptoService.hasEncKey();
|
||||
this.showUpdateKey = !hasEncKey;
|
||||
const isPremium = await this.tokenService.getPremium();
|
||||
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
await this.syncService.fullSync(false);
|
||||
|
||||
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
|
||||
const canAccessPremium = await this.userService.canAccessPremium();
|
||||
this.showPremiumCallout = !this.showVerifyEmail && !canAccessPremium &&
|
||||
!this.platformUtilsService.isSelfHost();
|
||||
|
||||
await Promise.all([
|
||||
this.groupingsComponent.load(),
|
||||
this.organizationsComponent.load(),
|
||||
]);
|
||||
|
||||
this.showPremiumCallout = !this.showVerifyEmail && !isPremium &&
|
||||
!this.platformUtilsService.isSelfHost() && !(await this.inOrgWithPremium());
|
||||
|
||||
if (params == null) {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.load();
|
||||
@@ -117,9 +126,30 @@ export class VaultComponent implements OnInit {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.load();
|
||||
}
|
||||
|
||||
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.organizationsComponent.load(),
|
||||
this.ciphersComponent.load(this.ciphersComponent.filter),
|
||||
]);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async clearGroupingFilters() {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
|
||||
@@ -157,9 +187,9 @@ export class VaultComponent implements OnInit {
|
||||
}
|
||||
|
||||
async filterCollection(collectionId: string) {
|
||||
this.ciphersComponent.showAddNew = false;
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
|
||||
await this.ciphersComponent.load((c) => c.collectionIds.indexOf(collectionId) > -1);
|
||||
await this.ciphersComponent.load((c) => c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1);
|
||||
this.clearFilters();
|
||||
this.collectionId = collectionId;
|
||||
this.go();
|
||||
@@ -167,11 +197,12 @@ export class VaultComponent implements OnInit {
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.ciphersComponent.searchText = searchText;
|
||||
this.ciphersComponent.search(200);
|
||||
}
|
||||
|
||||
async editCipherAttachments(cipher: CipherView) {
|
||||
const premium = await this.tokenService.getPremium();
|
||||
if (cipher.organizationId == null && !premium) {
|
||||
const canAccessPremium = await this.userService.canAccessPremium();
|
||||
if (cipher.organizationId == null && !canAccessPremium) {
|
||||
this.messagingService.send('premiumRequired');
|
||||
return;
|
||||
} else if (cipher.organizationId != null) {
|
||||
@@ -194,6 +225,7 @@ export class VaultComponent implements OnInit {
|
||||
let madeAttachmentChanges = false;
|
||||
childComponent.onUploadedAttachment.subscribe(() => madeAttachmentChanges = true);
|
||||
childComponent.onDeletedAttachment.subscribe(() => madeAttachmentChanges = true);
|
||||
childComponent.onReuploadedAttachment.subscribe(() => madeAttachmentChanges = true);
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
@@ -296,6 +328,13 @@ export class VaultComponent implements OnInit {
|
||||
const component = this.editCipher(null);
|
||||
component.type = this.type;
|
||||
component.folderId = this.folderId === 'none' ? null : this.folderId;
|
||||
if (this.collectionId != null) {
|
||||
const collection = this.groupingsComponent.collections.filter((c) => c.id === this.collectionId);
|
||||
if (collection.length > 0) {
|
||||
component.organizationId = collection[0].organizationId;
|
||||
component.collectionIds = [this.collectionId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editCipher(cipher: CipherView) {
|
||||
@@ -444,14 +483,4 @@ export class VaultComponent implements OnInit {
|
||||
const url = this.router.createUrlTree(['vault'], { queryParams: queryParams }).toString();
|
||||
this.location.go(url);
|
||||
}
|
||||
|
||||
private async inOrgWithPremium() {
|
||||
const orgs = await this.userService.getAllOrganizations();
|
||||
for (let i = 0; i < orgs.length; i++) {
|
||||
if (orgs[i].usersGetPremium) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Heslo"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Heslová fráze"
|
||||
},
|
||||
"notes": {
|
||||
"message": "Poznámky"
|
||||
},
|
||||
@@ -204,6 +207,10 @@
|
||||
"toggleVisibility": {
|
||||
"message": "Přepnout viditelnost"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Přepnout sbalení",
|
||||
"description": "Toggling an expand\/collapse state."
|
||||
},
|
||||
"generatePassword": {
|
||||
"message": "Vygenerovat heslo"
|
||||
},
|
||||
@@ -433,10 +440,10 @@
|
||||
"message": "Položka byla upravena"
|
||||
},
|
||||
"sharedItem": {
|
||||
"message": "Položka byla nasdílena"
|
||||
"message": "Položka byla sdílena"
|
||||
},
|
||||
"sharedItems": {
|
||||
"message": "Položky byly nasdíleny"
|
||||
"message": "Položky byly sdíleny"
|
||||
},
|
||||
"deleteItem": {
|
||||
"message": "Smazat položku"
|
||||
@@ -544,7 +551,7 @@
|
||||
"message": "Zadejte e-mailovou adresu pro zaslání nápovědy k hlavnímu heslu."
|
||||
},
|
||||
"getMasterPasswordHint": {
|
||||
"message": "Získat nápovědu pro hlavní heslo"
|
||||
"message": "Zaslat nápovědu k hlavnímu heslu"
|
||||
},
|
||||
"emailRequired": {
|
||||
"message": "E-mailová adresa je povinná."
|
||||
@@ -616,7 +623,7 @@
|
||||
}
|
||||
},
|
||||
"enterVerificationCodeApp": {
|
||||
"message": "Zadejte 6-místný kód z ověřovací aplikace."
|
||||
"message": "Zadejte 6místný kód z ověřovací aplikace."
|
||||
},
|
||||
"enterVerificationCodeEmail": {
|
||||
"message": "Zadejte 6místný kód z e-mailu, který byl zaslán na $EMAIL$.",
|
||||
@@ -640,7 +647,7 @@
|
||||
"message": "Pamatuj si mě"
|
||||
},
|
||||
"sendVerificationCodeEmailAgain": {
|
||||
"message": "Zaslat znovu ověřovací kód na e-mail"
|
||||
"message": "Znovu zaslat ověřovací kód na e-mail"
|
||||
},
|
||||
"useAnotherTwoStepMethod": {
|
||||
"message": "Použít jinou metodu dvoufázového přihlášení"
|
||||
@@ -680,7 +687,7 @@
|
||||
"message": "YubiKey OTP bezpečnostní klíč"
|
||||
},
|
||||
"yubiKeyDesc": {
|
||||
"message": "Použít YubiKey pro přístup k vašemu trezoru. Podporuje YubiKey 4, 4 Nano, 4C a NEO zařízení."
|
||||
"message": "Použít YubiKey pro přístup k vašemu trezoru. Podporuje YubiKey 4. série, 5. série a zařízení řady NEO."
|
||||
},
|
||||
"duoDesc": {
|
||||
"message": "Ověřit pomocí Duo Security prostřednictvím aplikace Duo Mobile, SMS, telefonního hovoru nebo U2F bezpečnostního kódu.",
|
||||
@@ -712,10 +719,10 @@
|
||||
"message": "Organizace"
|
||||
},
|
||||
"shareDesc": {
|
||||
"message": "Vyberte organizaci se kterou chcete sdílet tuto položku. Sdílením přenese vlastnictví položky na organizaci a již nadále nebudete přímým vlastníkem této položky."
|
||||
"message": "Vyberte organizaci se kterou chcete tuto položku sdílet. Sdílením přenese vlastnictví položky na organizaci a již nadále nebudete přímým vlastníkem."
|
||||
},
|
||||
"shareManyDesc": {
|
||||
"message": "Vyberte organizaci se kterou chcete sdílet tyto položky. Sdílením přenese vlastnictví položek na organizaci a již nadále nebudete přímým vlastníkem těchto položek."
|
||||
"message": "Vyberte organizaci se kterou chcete tyto položky sdílet. Sdílením přenese vlastnictví položek na organizaci a již nadále nebudete přímým vlastníkem."
|
||||
},
|
||||
"collectionsDesc": {
|
||||
"message": "Upravit kolekce, ve kterých je tato položka sdílená. Pouze uživatelé organizace, kteří mají přístup k těmto kolekcím, budou moci tuto položku vidět."
|
||||
@@ -738,8 +745,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shareSelectedItemsDesc": {
|
||||
"message": "Vybrali jste $COUNT$ položek. $SHAREABLE_COUNT$ položek je možné sdílet a $NONSHAREABLE_COUNT$ položek ne. Položky s přílohami musí být sdíleny jednotlivě.",
|
||||
"shareSelectedItemsCountDesc": {
|
||||
"message": "You have selected $COUNT$ item(s). $SHAREABLE_COUNT$ items are sharable, $NONSHAREABLE_COUNT$ are not.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -795,6 +802,12 @@
|
||||
"length": {
|
||||
"message": "Délka"
|
||||
},
|
||||
"numWords": {
|
||||
"message": "Počet slov"
|
||||
},
|
||||
"wordSeparator": {
|
||||
"message": "Oddělovač slov"
|
||||
},
|
||||
"passwordHistory": {
|
||||
"message": "Historie hesel"
|
||||
},
|
||||
@@ -852,11 +865,44 @@
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Potvrzení nového hesla"
|
||||
},
|
||||
"encKeySettings": {
|
||||
"message": "Nastavení šifrovacího klíče"
|
||||
},
|
||||
"kdfAlgorithm": {
|
||||
"message": "KDF algoritmus"
|
||||
},
|
||||
"kdfIterations": {
|
||||
"message": "KDF iterace"
|
||||
},
|
||||
"kdfIterationsDesc": {
|
||||
"message": "Vyšší hodnota KDF iterací pomáhá chránit vaše hlavní heslo před útokem hrubou silou (brute force attack). Doporučujeme nastavit hodnotu $VALUE$ nebo vyšší.",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1",
|
||||
"example": "100,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kdfIterationsWarning": {
|
||||
"message": "Nastavení příliš vysoké hodnoty počtu KDF iterací může mít za následek špatný výkon při přihlašování (a odemykání) aplikace Bitwarden na zařízeních se slabým procesorem. Doporučujeme hodnotu navyšovat přírůstkově po $INCREMENT$ a vyzkoušet všechna vaše zařízení.",
|
||||
"placeholders": {
|
||||
"increment": {
|
||||
"content": "$1",
|
||||
"example": "50,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"changeKdf": {
|
||||
"message": "Změnit KDF"
|
||||
},
|
||||
"encKeySettingsChanged": {
|
||||
"message": "Nastavení šifrovacího klíče bylo změněno"
|
||||
},
|
||||
"dangerZone": {
|
||||
"message": "Nebezpečná zóna"
|
||||
},
|
||||
"dangerZoneDesc": {
|
||||
"message": "Opatrně tyto akce se nedají vrátit!"
|
||||
"message": "Opatrně. Tyto akce se nedají vrátit!"
|
||||
},
|
||||
"deauthorizeSessions": {
|
||||
"message": "Zrušit autorizaci relací"
|
||||
@@ -873,8 +919,14 @@
|
||||
"purgeVault": {
|
||||
"message": "Vymazat celý trezor"
|
||||
},
|
||||
"purgedOrganizationVault": {
|
||||
"message": "Trezor organizace byl vyprázdněn."
|
||||
},
|
||||
"purgeVaultDesc": {
|
||||
"message": "Pokračujte níže ke smazání všech položek a složek ve vašem trezoru. Položky sdílené s organizací nebudou smazány."
|
||||
"message": "Chcete-li smazat všechny položky a složky ve vašem trezoru, pokračujte podle následujících pokynů. Položky sdílené s organizací nebudou smazány."
|
||||
},
|
||||
"purgeOrgVaultDesc": {
|
||||
"message": "Chcete-li smazat všechny položky v trezoru organizace, pokračujte podle následujících pokynů."
|
||||
},
|
||||
"purgeVaultWarning": {
|
||||
"message": "Vymazání trezoru je trvalé. Tuto akci nelze vrátit zpět."
|
||||
@@ -969,7 +1021,7 @@
|
||||
"message": "Doménová pravidla"
|
||||
},
|
||||
"domainRulesDesc": {
|
||||
"message": "Pokud máte stejné přihlašovací údaje napříč různými doménami, můžete je označit jako \"ekvivalentní\". Bitwarden za vás již vytvořil seznam globálních domén."
|
||||
"message": "Pokud máte stejné přihlašovací údaje napříč různými doménami, můžete je označit jako „ekvivalentní“. Bitwarden za vás již vytvořil seznam globálních domén."
|
||||
},
|
||||
"globalEqDomains": {
|
||||
"message": "Globální ekvivalentní domény"
|
||||
@@ -990,7 +1042,7 @@
|
||||
"message": "Přidat vlastní doménu"
|
||||
},
|
||||
"newCustomDomainDesc": {
|
||||
"message": "Zadejte seznam domén oddělených čárkou. Povolené jsou pouze \"základní\" domény a proto nezadávejte subdomény. Například zadejte \"bitwarden.com\" místo \"vault.bitwarden.com\". Můžete také zadat \"androidapp:\/\/package.name\" pokud chcete přiřadit Android aplikaci k ostatním webovým doménám."
|
||||
"message": "Zadejte seznam domén oddělených čárkou. Povolené jsou pouze „základní“ domény. Nezadávejte subdomény. Například zadejte „bitwarden.com“, nikoliv „vault.bitwarden.com“. Můžete také zadat „androidapp:\/\/package.name“ pokud chcete přiřadit Android aplikaci k ostatním webovým doménám."
|
||||
},
|
||||
"customDomainX": {
|
||||
"message": "Vlastní doména $INDEX$",
|
||||
@@ -1042,6 +1094,12 @@
|
||||
"premiumRequiredDesc": {
|
||||
"message": "Pro použití této funkce je potřebné prémiové členství."
|
||||
},
|
||||
"youHavePremiumAccess": {
|
||||
"message": "Máte prémiový přístup"
|
||||
},
|
||||
"alreadyPremiumFromOrg": {
|
||||
"message": "Již máte přístup k prémiovým funkcím díky organizaci, které jste členem."
|
||||
},
|
||||
"manage": {
|
||||
"message": "Správa"
|
||||
},
|
||||
@@ -1082,7 +1140,7 @@
|
||||
"message": "Klíč"
|
||||
},
|
||||
"twoStepAuthenticatorEnterCode": {
|
||||
"message": "Zadejte 6-místný kód z ověřovací aplikace"
|
||||
"message": "Zadejte 6místný kód z ověřovací aplikace"
|
||||
},
|
||||
"twoStepAuthenticatorReaddDesc": {
|
||||
"message": "V případě potřeby přidání do jiného zařízení, je níže zobrazen QR kód (nebo klíč) vyžadovaný ověřovací aplikací."
|
||||
@@ -1097,7 +1155,7 @@
|
||||
"message": "Přidání nového YubiKey k vašemu účtu"
|
||||
},
|
||||
"twoFactorYubikeyPlugIn": {
|
||||
"message": "Připojte YubiKey (NEO nebo 4. řada) do USB portu počítače."
|
||||
"message": "Připojte YubiKey do USB portu počítače."
|
||||
},
|
||||
"twoFactorYubikeySelectKey": {
|
||||
"message": "Vyberte první prázdné pole YubiKey níže."
|
||||
@@ -1126,6 +1184,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"u2fkeyX": {
|
||||
"message": "U2F klíč $INDEX$",
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nfcSupport": {
|
||||
"message": "Podpora NFC"
|
||||
},
|
||||
@@ -1160,7 +1227,7 @@
|
||||
"message": "Zadejte e-mail, na který si přejete dostávat ověřovací kódy"
|
||||
},
|
||||
"twoFactorEmailEnterCode": {
|
||||
"message": "Zadejte 6-místný kód zaslaný na e-mail"
|
||||
"message": "Zadejte 6místný kód zaslaný na e-mail"
|
||||
},
|
||||
"sendEmail": {
|
||||
"message": "Odeslat e-mail"
|
||||
@@ -1168,12 +1235,27 @@
|
||||
"twoFactorU2fAdd": {
|
||||
"message": "Přidání bezpečnostního klíče FIDO U2F k vašemu účtu"
|
||||
},
|
||||
"twoFactorU2fPlugIn": {
|
||||
"message": "Připojte bezpečnostní klíč do portu USB počítače."
|
||||
"removeU2fConfirmation": {
|
||||
"message": "Opravdu chcete odebrat tento bezpečnostní klíč?"
|
||||
},
|
||||
"readKey": {
|
||||
"message": "Přečíst klíč"
|
||||
},
|
||||
"keyCompromised": {
|
||||
"message": "Klíč je prozrazen."
|
||||
},
|
||||
"twoFactorU2fGiveName": {
|
||||
"message": "Give the security key a friendly name to identify it."
|
||||
},
|
||||
"twoFactorU2fPlugInReadKey": {
|
||||
"message": "Plug the security key into your computer's USB port and click the \"Read Key\" button."
|
||||
},
|
||||
"twoFactorU2fTouchButton": {
|
||||
"message": "Pokud má bezpečnostní klíč tlačítko, zmáčkněte jej."
|
||||
},
|
||||
"twoFactorU2fSaveForm": {
|
||||
"message": "Uložte formulář."
|
||||
},
|
||||
"twoFactorU2fWarning": {
|
||||
"message": "Z důvodu omezení různých platforem, nemůže být FIDO U2F použit ve všech aplikacích Bitwarden. Měli byste povolit jiný způsob dvoufázového přihlášení pro případy, kdy nelze FIDO U2F použít. Podporované platformy:"
|
||||
},
|
||||
@@ -1183,11 +1265,11 @@
|
||||
"twoFactorU2fWaiting": {
|
||||
"message": "Čeká se na stisknutí tlačítka na bezpečnostním klíči"
|
||||
},
|
||||
"twoFactorU2fClickEnable": {
|
||||
"message": "Klepněte níže na tlačítko \"Povolit\" pro povolení tohoto bezpečnostního klíče pro dvoufázové přihlášení."
|
||||
"twoFactorU2fClickSave": {
|
||||
"message": "Click the \"Save\" button below to enable this security key for two-step login."
|
||||
},
|
||||
"twoFactorU2fProblemReading": {
|
||||
"message": "Došlo k potížím při čtení bezpečnostního klíče."
|
||||
"twoFactorU2fProblemReadingTryAgain": {
|
||||
"message": "There was a problem reading the security key. Try again."
|
||||
},
|
||||
"twoFactorRecoveryYourCode": {
|
||||
"message": "Váše kód pro obnovení dvoufázového přihlášení"
|
||||
@@ -1206,7 +1288,7 @@
|
||||
"message": "Hlášení o úniku dat"
|
||||
},
|
||||
"breachDesc": {
|
||||
"message": "\"Únik\" je incident, při kterém se podařilo hackerům nelegální cestou získat údaje z webových stránek a následně je také zveřejnit. Zkontrolujte prosím kompromitovaná data (e-mailové adresy, hesla, kreditní karty, ...) a proveďte příslušná opatření (např. změňte heslo)."
|
||||
"message": "„Únik“ je incident, při kterém se podařilo hackerům nelegální cestou získat údaje z webových stránek a následně je také zveřejnit. Zkontrolujte prosím kompromitovaná data (e-mailové adresy, hesla, kreditní karty, …) a proveďte příslušná opatření (např. změňte heslo)."
|
||||
},
|
||||
"breachCheckUsernameEmail": {
|
||||
"message": "Zkontrolujte všechna uživatelská jména nebo e-mailové adresy, které používáte."
|
||||
@@ -1269,10 +1351,10 @@
|
||||
"description": "Another way of saying \"Get a premium membership\""
|
||||
},
|
||||
"premiumUpdated": {
|
||||
"message": "You've upgraded to premium."
|
||||
"message": "Povýšili jste na premium."
|
||||
},
|
||||
"premiumUpgradeUnlockFeatures": {
|
||||
"message": "Upgrade your account to a premium membership and unlock some great additional features."
|
||||
"message": "Povyšte svůj účet na prémiové členství a odemkněte další skvělé funkce."
|
||||
},
|
||||
"premiumSignUpStorage": {
|
||||
"message": "1 GB šifrovaného uložiště."
|
||||
@@ -1301,14 +1383,30 @@
|
||||
"addons": {
|
||||
"message": "Doplňky"
|
||||
},
|
||||
"premiumAccess": {
|
||||
"message": "Premium Access"
|
||||
},
|
||||
"premiumAccessDesc": {
|
||||
"message": "You can add premium access to all members of your organization for $PRICE$ \/$INTERVAL$.",
|
||||
"placeholders": {
|
||||
"price": {
|
||||
"content": "$1",
|
||||
"example": "$3.33"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$2",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalStorageGb": {
|
||||
"message": "Další úložiště (GB)"
|
||||
},
|
||||
"additionalStorageGbDesc": {
|
||||
"message": "# dalších GB"
|
||||
},
|
||||
"additionalStorageDesc": {
|
||||
"message": "Vybraný plán obsahuje $SIZE$ šifrovaného uložiště. Můžete si přikoupit další prostor za $PRICE$ za každý další 1 GB ročně.",
|
||||
"additionalStorageIntervalDesc": {
|
||||
"message": "Your plan comes with $SIZE$ of encrypted file storage. You can add additional storage for $PRICE$ per GB \/$INTERVAL$.",
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"content": "$1",
|
||||
@@ -1317,6 +1415,10 @@
|
||||
"price": {
|
||||
"content": "$2",
|
||||
"example": "$4.00"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$3",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1340,7 +1442,7 @@
|
||||
"message": "Částka bude stržena okamžitě a poté opakovaně každý rok. Plán můžete kdykoli zrušit."
|
||||
},
|
||||
"paymentChargedWithTrial": {
|
||||
"message": "Vybraný plán obsahuje bezplatnou 7denní zkušební dobu. Částka z vašeho účtu nebude stržena, dokud tato zkušební doba neuplyne. Stržení platby a fakturace bude následně probíhat opakovaně každý $ INTERVAL $. Plán můžete kdykoli zrušit.",
|
||||
"message": "Vybraný plán obsahuje bezplatnou 7denní zkušební dobu. Částka z vašeho účtu nebude stržena, dokud tato zkušební doba neuplyne. Stržení platby a fakturace bude následně probíhat opakovaně každý $INTERVAL$. Plán můžete kdykoli zrušit.",
|
||||
"placeholders": {
|
||||
"interval": {
|
||||
"content": "$1",
|
||||
@@ -1367,19 +1469,19 @@
|
||||
"message": "Čeká na zrušení"
|
||||
},
|
||||
"subscriptionPendingCanceled": {
|
||||
"message": "The subscription has been marked for cancellation at the end of the current billing period."
|
||||
"message": "Předplatné bude zrušeno na konci aktuálního fakturačního období."
|
||||
},
|
||||
"reinstateSubscription": {
|
||||
"message": "Obnovit předplatné"
|
||||
},
|
||||
"reinstateConfirmation": {
|
||||
"message": "Are you sure you want to remove the pending cancellation request and reinstate your subscription?"
|
||||
"message": "Opravdu chcete zrušit požadavek na ukončení předplatného a obnovit původní předplatné?"
|
||||
},
|
||||
"reinstated": {
|
||||
"message": "Předplatné bylo obnoveno"
|
||||
},
|
||||
"cancelConfirmation": {
|
||||
"message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle."
|
||||
"message": "Opravdu chcete zrušit předplatné? Na konci fakturačního období přijdete o veškeré výhody plynoucí z vybraného plánu."
|
||||
},
|
||||
"canceledSubscription": {
|
||||
"message": "Předplatné bylo zrušeno"
|
||||
@@ -1418,7 +1520,7 @@
|
||||
"message": "Odebrat uložiště"
|
||||
},
|
||||
"subscriptionStorage": {
|
||||
"message": "Your subscription has a total of $MAX_STORAGE$ GB of encrypted file storage. You are currently using $USED_STORAGE$.",
|
||||
"message": "Vaše předplatné zahrnuje celkem $MAX_STORAGE$ GB místa v šifrovaném úložišti. Aktuálně používáte $USED_STORAGE$.",
|
||||
"placeholders": {
|
||||
"max_storage": {
|
||||
"content": "$1",
|
||||
@@ -1431,7 +1533,7 @@
|
||||
}
|
||||
},
|
||||
"paymentMethod": {
|
||||
"message": "Způsob Platby"
|
||||
"message": "Způsob platby"
|
||||
},
|
||||
"noPaymentMethod": {
|
||||
"message": "No payment method on file."
|
||||
@@ -1450,7 +1552,7 @@
|
||||
"message": "Žádné platby."
|
||||
},
|
||||
"chargesStatement": {
|
||||
"message": "Any charges will appear on your statement as $STATEMENT_NAME$.",
|
||||
"message": " Veškeré platby se na výpisu objeví jako $STATEMENT_NAME$.",
|
||||
"placeholders": {
|
||||
"statement_name": {
|
||||
"content": "$1",
|
||||
@@ -1507,13 +1609,13 @@
|
||||
"message": "To upgrade your account to a premium membership you need to upload a valid license file."
|
||||
},
|
||||
"uploadLicenseFileOrg": {
|
||||
"message": "To create an on-premise hosted organization you need to upload a valid license file."
|
||||
"message": "Pro vytvoření organizace hostované na vlastní doméně, musíte nahrát platný licenční soubor."
|
||||
},
|
||||
"accountEmailMustBeVerified": {
|
||||
"message": "E-mailová adresa vašeho účtu musí být ověřena."
|
||||
},
|
||||
"newOrganizationDesc": {
|
||||
"message": "Organizations allow you to share parts of your vault with others as well as manage related users for a specific entity such as a family, small team, or large company."
|
||||
"message": "Organizace umožňují sdílení položek vašeho trezoru s ostatními uživateli a také správu uživatelů jako členů rodiny, malého týmu nebo velké organizace."
|
||||
},
|
||||
"generalInformation": {
|
||||
"message": "Obecné informace"
|
||||
@@ -1522,13 +1624,13 @@
|
||||
"message": "Název organizace"
|
||||
},
|
||||
"accountOwnedBusiness": {
|
||||
"message": "Tento účet je vlastněn společností,"
|
||||
"message": "Tento účet je vlastněn firmou."
|
||||
},
|
||||
"billingEmail": {
|
||||
"message": "E-mailová adresa pro fakturaci"
|
||||
},
|
||||
"businessName": {
|
||||
"message": "Název společnosti"
|
||||
"message": "Název firmy"
|
||||
},
|
||||
"chooseYourPlan": {
|
||||
"message": "Vyberte plán"
|
||||
@@ -1537,10 +1639,10 @@
|
||||
"message": "Uživatelé"
|
||||
},
|
||||
"userSeats": {
|
||||
"message": "User Seats"
|
||||
"message": "Počet uživatelů"
|
||||
},
|
||||
"additionalUserSeats": {
|
||||
"message": "Additional User Seats"
|
||||
"message": "Další uživatelé"
|
||||
},
|
||||
"userSeatsDesc": {
|
||||
"message": "# of user seats"
|
||||
@@ -1566,7 +1668,7 @@
|
||||
"description": "Free as in 'free beer'."
|
||||
},
|
||||
"planDescFree": {
|
||||
"message": "For testing or personal users to share with $COUNT$ other user.",
|
||||
"message": "Pro testování nebo osobní použití a sdílení s $COUNT$ dalším uživatelem.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -1578,19 +1680,19 @@
|
||||
"message": "Rodiny"
|
||||
},
|
||||
"planDescFamilies": {
|
||||
"message": "For personal use, to share with family & friends."
|
||||
"message": "Pro osobní použití a sdílení s rodinou či přáteli."
|
||||
},
|
||||
"planNameTeams": {
|
||||
"message": "Týmy"
|
||||
},
|
||||
"planDescTeams": {
|
||||
"message": "For businesses and other team organizations."
|
||||
"message": "Pro firmy a různé týmy."
|
||||
},
|
||||
"planNameEnterprise": {
|
||||
"message": "Enterprise"
|
||||
},
|
||||
"planDescEnterprise": {
|
||||
"message": "For businesses and other large organizations."
|
||||
"message": "Pro firmy a velké organizace."
|
||||
},
|
||||
"freeForever": {
|
||||
"message": "Navždy zdarma"
|
||||
@@ -1605,7 +1707,7 @@
|
||||
}
|
||||
},
|
||||
"additionalUsers": {
|
||||
"message": "Additional Users"
|
||||
"message": "Další uživatelé"
|
||||
},
|
||||
"costPerUser": {
|
||||
"message": "$COST$ za uživatele",
|
||||
@@ -1617,7 +1719,7 @@
|
||||
}
|
||||
},
|
||||
"limitedUsers": {
|
||||
"message": "Limited to $COUNT$ users (including you)",
|
||||
"message": "Omezeno na $COUNT$ uživatele (včetně vás)",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -1626,7 +1728,7 @@
|
||||
}
|
||||
},
|
||||
"limitedCollections": {
|
||||
"message": "Limited to $COUNT$ collections",
|
||||
"message": "Omezeno na $COUNT$ kolekce",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -1644,13 +1746,13 @@
|
||||
}
|
||||
},
|
||||
"addShareUnlimitedUsers": {
|
||||
"message": "Add and share with unlimited users"
|
||||
"message": "Neomezený počet uživatelů"
|
||||
},
|
||||
"createUnlimitedCollections": {
|
||||
"message": "Create unlimited collections"
|
||||
"message": "Neomezený počet kolekcí"
|
||||
},
|
||||
"gbEncryptedFileStorage": {
|
||||
"message": "$SIZE$ encrypted file storage",
|
||||
"message": "$SIZE$ šifrovaného úložiště",
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"content": "$1",
|
||||
@@ -1659,7 +1761,10 @@
|
||||
}
|
||||
},
|
||||
"onPremHostingOptional": {
|
||||
"message": "On-premise hosting (optional)"
|
||||
"message": "Možnost hostování na vlastní doméně"
|
||||
},
|
||||
"usersGetPremium": {
|
||||
"message": "Users get access to premium membership features"
|
||||
},
|
||||
"controlAccessWithGroups": {
|
||||
"message": "Control user access with groups"
|
||||
@@ -1677,7 +1782,7 @@
|
||||
"message": "Přednostní zákaznická podpora"
|
||||
},
|
||||
"xDayFreeTrial": {
|
||||
"message": "$COUNT$ day free trial, cancel anytime",
|
||||
"message": "$COUNT$denní zkušební verze, možno kdykoliv zrušit",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -1707,16 +1812,16 @@
|
||||
"message": "Opravdu chcete tuto organizaci opustit?"
|
||||
},
|
||||
"leftOrganization": {
|
||||
"message": "Organizace byla opuštěna"
|
||||
"message": "Organizace byla opuštěna."
|
||||
},
|
||||
"defaultCollection": {
|
||||
"message": "Výchozí kolekce"
|
||||
},
|
||||
"getHelp": {
|
||||
"message": "Získat nápovědu"
|
||||
"message": "Nápověda"
|
||||
},
|
||||
"getApps": {
|
||||
"message": "Získat aplikaci"
|
||||
"message": "Stáhnout aplikaci"
|
||||
},
|
||||
"loggedInAs": {
|
||||
"message": "Přihlášen jako"
|
||||
@@ -1725,7 +1830,7 @@
|
||||
"message": "Protokol událostí"
|
||||
},
|
||||
"people": {
|
||||
"message": "Lidé"
|
||||
"message": "Uživatelé"
|
||||
},
|
||||
"groups": {
|
||||
"message": "Skupiny"
|
||||
@@ -1752,13 +1857,13 @@
|
||||
"message": "Externí ID slouží k propojení této skupiny s externím systémem, například s adresářem uživatelů."
|
||||
},
|
||||
"accessControl": {
|
||||
"message": "Správa přístupů"
|
||||
"message": "Správa přístupu"
|
||||
},
|
||||
"groupAccessAllItems": {
|
||||
"message": "This group can access and modify all items."
|
||||
"message": "Tato skupina může vidět a upravovat vše."
|
||||
},
|
||||
"groupAccessSelectedCollections": {
|
||||
"message": "This group can access only the selected collections."
|
||||
"message": "Tato skupina může vidět a upravovat pouze vybrané kolekce."
|
||||
},
|
||||
"readOnly": {
|
||||
"message": "Pouze pro čtení"
|
||||
@@ -1794,16 +1899,16 @@
|
||||
}
|
||||
},
|
||||
"userAccessAllItems": {
|
||||
"message": "This user can access and modify all items."
|
||||
"message": "Tento uživatel může vidět a upravovat vše."
|
||||
},
|
||||
"userAccessSelectedCollections": {
|
||||
"message": "This user can access only the selected collections."
|
||||
"message": "Tento uživatel může vidět a upravovat pouze vybrané kolekce."
|
||||
},
|
||||
"search": {
|
||||
"message": "Hledat"
|
||||
},
|
||||
"invited": {
|
||||
"message": "Pozván\/a"
|
||||
"message": "Pozváno"
|
||||
},
|
||||
"accepted": {
|
||||
"message": "Přijato"
|
||||
@@ -1827,7 +1932,13 @@
|
||||
"message": "Uživatel"
|
||||
},
|
||||
"userDesc": {
|
||||
"message": "A regular user with access to your organization's collections."
|
||||
"message": "Běžný uživatel s přístupem k přiřazeným kolekcím vaší organizace."
|
||||
},
|
||||
"manager": {
|
||||
"message": "Správce"
|
||||
},
|
||||
"managerDesc": {
|
||||
"message": "Správci mohou přistupovat a spravovat přiřazené kolekce ve vaší organizaci."
|
||||
},
|
||||
"all": {
|
||||
"message": "Vše"
|
||||
@@ -1868,8 +1979,8 @@
|
||||
"changedPassword": {
|
||||
"message": "Heslo účtu bylo změněno"
|
||||
},
|
||||
"enabled2fa": {
|
||||
"message": "Dvoufázové přihlášení bylo povoleno"
|
||||
"enabledUpdated2fa": {
|
||||
"message": "Enabled\/updated two-step login."
|
||||
},
|
||||
"disabled2fa": {
|
||||
"message": "Dvoufázové přihlášení bylo zakázáno"
|
||||
@@ -2076,10 +2187,10 @@
|
||||
"message": "Invited user(s)."
|
||||
},
|
||||
"resendInvitation": {
|
||||
"message": "Resend Invitation"
|
||||
"message": "Znovu poslat pozvánku"
|
||||
},
|
||||
"hasBeenReinvited": {
|
||||
"message": "$USER$ has been reinvited.",
|
||||
"message": "Uživatel $USER$ byl znovu pozván.",
|
||||
"placeholders": {
|
||||
"user": {
|
||||
"content": "$1",
|
||||
@@ -2088,10 +2199,13 @@
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"message": "Confirm"
|
||||
"message": "Potvrdit"
|
||||
},
|
||||
"confirmUser": {
|
||||
"message": "Potvrdit uživatele"
|
||||
},
|
||||
"hasBeenConfirmed": {
|
||||
"message": "$USER$ has been confirmed.",
|
||||
"message": "Uživatel $USER$ byl potvrzen.",
|
||||
"placeholders": {
|
||||
"user": {
|
||||
"content": "$1",
|
||||
@@ -2100,16 +2214,16 @@
|
||||
}
|
||||
},
|
||||
"confirmUsers": {
|
||||
"message": "Confirm Users"
|
||||
"message": "Potvrdit uživatele"
|
||||
},
|
||||
"usersNeedConfirmed": {
|
||||
"message": "You have users that have accepted their invitation, but still need to be confirmed. Users will not have access to the organization until they are confirmed."
|
||||
"message": "Někteří uživatelé přijali pozvánku, ale nejdříve musí být potvrzeni. Dokud nedojde k potvrzení, nebudou mít tito uživatelé přístup k organizaci."
|
||||
},
|
||||
"startDate": {
|
||||
"message": "Start Date"
|
||||
"message": "Datum začátku"
|
||||
},
|
||||
"endDate": {
|
||||
"message": "End Date"
|
||||
"message": "Datum konce"
|
||||
},
|
||||
"verifyEmail": {
|
||||
"message": "Ověřit e-mail"
|
||||
@@ -2118,7 +2232,7 @@
|
||||
"message": "Verify your account's email address to unlock access to all features."
|
||||
},
|
||||
"verifyEmailFirst": {
|
||||
"message": "Your account's email address first must be verified."
|
||||
"message": "E-mailová adresa vašeho účtu musí být ověřena."
|
||||
},
|
||||
"checkInboxForVerification": {
|
||||
"message": "Check your email inbox for a verification link."
|
||||
@@ -2127,22 +2241,22 @@
|
||||
"message": "Vaše e-mailová adresa byla ověřena"
|
||||
},
|
||||
"emailVerifiedFailed": {
|
||||
"message": "Unable to verify your email. Try sending a new verification email."
|
||||
"message": "Váš e-mail se nepodařilo ověřit. Zkuste odeslat nový ověřovací e-mail."
|
||||
},
|
||||
"updateBrowser": {
|
||||
"message": "Update Browser"
|
||||
"message": "Aktualizace prohlížeče"
|
||||
},
|
||||
"updateBrowserDesc": {
|
||||
"message": "You are using an unsupported web browser. The web vault may not function properly."
|
||||
"message": "Používáte nepodporovaný webový prohlížeč. Aplikace nemusí pracovat správně."
|
||||
},
|
||||
"joinOrganization": {
|
||||
"message": "Přidat se k organizaci"
|
||||
},
|
||||
"joinOrganizationDesc": {
|
||||
"message": "You've been invited to join the organization listed above. To accept the invitation, you need to log in or create a new Bitwarden account."
|
||||
"message": "Byly jste pozváni do výše uvedené organizace. Pro přijetí pozvánky se musíte přihlásit nebo si založit nový účet."
|
||||
},
|
||||
"inviteAccepted": {
|
||||
"message": "Pozvánka byla přijata"
|
||||
"message": "Pozvánka byla přijata."
|
||||
},
|
||||
"inviteAcceptedDesc": {
|
||||
"message": "You can access this organization once an administrator confirms your membership. We'll send you an email when that happens."
|
||||
@@ -2184,7 +2298,7 @@
|
||||
"message": "Proceed below to delete this organization and all associated data. Individual user accounts will remain, though they will not be associated to this organization anymore. "
|
||||
},
|
||||
"deleteOrganizationWarning": {
|
||||
"message": "Deleting the organization is permanent. It cannot be undone."
|
||||
"message": "Smazání organizace je trvalé. Tuto akci nelze vrátit zpět."
|
||||
},
|
||||
"organizationDeleted": {
|
||||
"message": "Organizace byla smazána."
|
||||
@@ -2193,13 +2307,13 @@
|
||||
"message": "Organizace a veškerá související data byla smazána."
|
||||
},
|
||||
"organizationUpdated": {
|
||||
"message": "Organizace byla aktualizována"
|
||||
"message": "Organizace byla aktualizována."
|
||||
},
|
||||
"taxInformation": {
|
||||
"message": "Tax Information"
|
||||
"message": "Daňové údaje"
|
||||
},
|
||||
"taxInformationDesc": {
|
||||
"message": "Please contact support to provide (or update) tax information for your invoices."
|
||||
"message": "Pro poskytnutí nebo aktualizaci daňových údajů pro vaše faktury kontaktujte zákaznickou podporu."
|
||||
},
|
||||
"billingPlan": {
|
||||
"message": "Plán",
|
||||
@@ -2210,14 +2324,14 @@
|
||||
"description": "A billing plan\/package. For example: families, teams, enterprise, etc."
|
||||
},
|
||||
"changeBillingPlanDesc": {
|
||||
"message": "Contact customer support if you would like to change your plan. Please ensure that you have an active payment method added to the account.",
|
||||
"message": "Pokud chcete změnit svůj plán, kontaktujte zákaznickou podporu. Ujistěte se prosím, zda máte k účtu přidán způsob platby.",
|
||||
"description": "A billing plan\/package. For example: families, teams, enterprise, etc."
|
||||
},
|
||||
"invoice": {
|
||||
"message": "Faktura"
|
||||
},
|
||||
"verifyBankAccount": {
|
||||
"message": "Verify Bank Account"
|
||||
"message": "Ověření bankovního účtu"
|
||||
},
|
||||
"verifyBankAccountDesc": {
|
||||
"message": "We have made two micro-deposits to your bank account (it may take 1-2 business days to show up). Enter these amounts to verify the bank account."
|
||||
@@ -2229,13 +2343,13 @@
|
||||
"message": "Failure to verify the bank account will result in a missed payment and your subscription being disabled."
|
||||
},
|
||||
"verifiedBankAccount": {
|
||||
"message": "Bank account has been verified."
|
||||
"message": "Bankovní účet byl ověřen."
|
||||
},
|
||||
"bankAccount": {
|
||||
"message": "Bank Account"
|
||||
"message": "Bankovní účet"
|
||||
},
|
||||
"amountX": {
|
||||
"message": "Amount $COUNT$",
|
||||
"message": "Částka $COUNT$",
|
||||
"description": "Used in bank account verification of micro-deposits. Amount, as in a currency amount. Ex. Amount 1 is $2.00, Amount 2 is $1.50",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -2245,26 +2359,26 @@
|
||||
}
|
||||
},
|
||||
"routingNumber": {
|
||||
"message": "Routing Number",
|
||||
"message": "Směrovací číslo",
|
||||
"description": "Bank account routing number"
|
||||
},
|
||||
"accountNumber": {
|
||||
"message": "Account Number"
|
||||
"message": "Číslo účtu"
|
||||
},
|
||||
"accountHolderName": {
|
||||
"message": "Account Holder Name"
|
||||
"message": "Jméno majitele účtu"
|
||||
},
|
||||
"bankAccountType": {
|
||||
"message": "Account Type"
|
||||
"message": "Typ účtu"
|
||||
},
|
||||
"bankAccountTypeCompany": {
|
||||
"message": "Company (Business)"
|
||||
"message": "Společnost (firma)"
|
||||
},
|
||||
"bankAccountTypeIndividual": {
|
||||
"message": "Individual (Personal)"
|
||||
},
|
||||
"enterInstallationId": {
|
||||
"message": "Enter your installation id"
|
||||
"message": "Zadejte ID instalace"
|
||||
},
|
||||
"addSeats": {
|
||||
"message": "Add Seats",
|
||||
@@ -2305,22 +2419,22 @@
|
||||
}
|
||||
},
|
||||
"keyUpdated": {
|
||||
"message": "Key Updated"
|
||||
"message": "Klíč byl upraven."
|
||||
},
|
||||
"updateKeyTitle": {
|
||||
"message": "Update Key"
|
||||
"message": "Aktualizovat klíč"
|
||||
},
|
||||
"updateEncryptionKey": {
|
||||
"message": "Update Encryption Key"
|
||||
"message": "Aktualizovat šifrovací klíč"
|
||||
},
|
||||
"updateEncryptionKeyShortDesc": {
|
||||
"message": "You are currently using an outdated encryption scheme."
|
||||
"message": "Používáte zastaralé šifrovací schéma."
|
||||
},
|
||||
"updateEncryptionKeyDesc": {
|
||||
"message": "We've moved to larger encryption keys that provide better security and access to newer features. Updating your encryption key is quick and easy. Just type your master password below. This update will eventually become mandatory."
|
||||
"message": "Přešli jsme na delší šifrovací klíče, které poskytují vyšší úroveň zabezpečení a přístup k novým funkcím. Aktualizace vašeho šifrovacího klíče je rychlá a snadná. Stačí níže zadat vaše hlavní heslo. Provedení této aktualizace může být v budoucnu povinné."
|
||||
},
|
||||
"updateEncryptionKeyWarning": {
|
||||
"message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed."
|
||||
"message": "Po aktualizace šifrovacího klíče dojde k odhlášení a budete se muset opětovně přihlásit do všech Bitwarden aplikací, které aktuálně používáte (např. mobilní aplikace či rozšíření pro prohlížeč). Nezdaří-li se odhlášení a opětovné přihlášení (během něhož bude stažen nový šifrovací klíč), může dojít k poškození údajů. Pokusíme se vás automaticky odhlásit, nicméně, může to chvíli trvat."
|
||||
},
|
||||
"subscription": {
|
||||
"message": "Odběr"
|
||||
@@ -2329,25 +2443,25 @@
|
||||
"message": "Načítání"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
"message": "Povýšit"
|
||||
},
|
||||
"upgradeOrganization": {
|
||||
"message": "Upgrade Organization"
|
||||
},
|
||||
"upgradeOrganizationDesc": {
|
||||
"message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features."
|
||||
"message": "Tato funkce je nedostupná pro bezplatné organizace. Přejděte na placené členství a odemkněte více funkcí."
|
||||
},
|
||||
"createOrganizationStep1": {
|
||||
"message": "Create Organization: Step 1"
|
||||
"message": "Vytvoření organizace: Krok 1"
|
||||
},
|
||||
"createOrganizationCreatePersonalAccount": {
|
||||
"message": "Before creating your organization, you first need to create a free personal account."
|
||||
"message": "Před vytvořením organizace si musíte nejdříve vytvořit bezplatný osobní účet."
|
||||
},
|
||||
"refunded": {
|
||||
"message": "Platba vrácena"
|
||||
},
|
||||
"nothingSelected": {
|
||||
"message": "Nic jste nevybrali."
|
||||
"message": "Nevybrali jste žádné položky."
|
||||
},
|
||||
"submitAgreePolicies": {
|
||||
"message": "Klepnutím na tlačítko \"Odeslat\" souhlasíte s následující podmínkami:",
|
||||
@@ -2366,7 +2480,7 @@
|
||||
"message": "Možnosti zamknutí"
|
||||
},
|
||||
"lockOptionsDesc": {
|
||||
"message": "Vyberte kdy dojde k automatickému uzamčení trezoru. Pro opětovný přístup do uzamčeného trezoru bude potřeba znovu zadat hlavního heslo."
|
||||
"message": "Vyberte kdy dojde k automatickému uzamčení trezoru. Pro opětovný přístup do trezoru bude potřeba znovu zadat hlavního heslo."
|
||||
},
|
||||
"oneMinute": {
|
||||
"message": "Po 1 minutě"
|
||||
@@ -2402,5 +2516,69 @@
|
||||
},
|
||||
"licenseIsExpired": {
|
||||
"message": "Licence vypršela."
|
||||
},
|
||||
"updatedUsers": {
|
||||
"message": "Updated users"
|
||||
},
|
||||
"selected": {
|
||||
"message": "Vybrané"
|
||||
},
|
||||
"ownership": {
|
||||
"message": "Vlastnictví"
|
||||
},
|
||||
"whoOwnsThisItem": {
|
||||
"message": "Kdo vlastní tuto položku?"
|
||||
},
|
||||
"strong": {
|
||||
"message": "Silné",
|
||||
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"good": {
|
||||
"message": "Dobré",
|
||||
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weak": {
|
||||
"message": "Slabé",
|
||||
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weakMasterPassword": {
|
||||
"message": "Slabé hlavní heslo"
|
||||
},
|
||||
"weakMasterPasswordDesc": {
|
||||
"message": "Hlavní heslo, které jste si vybrali, je slabé. Pro správnou ochranu účtu Bitwarden byste měli použít silné hlavní heslo (nebo heslovou frázi). Opravdu chcete toto heslo použít?"
|
||||
},
|
||||
"rotateAccountEncKey": {
|
||||
"message": "Also rotate my account's encryption key"
|
||||
},
|
||||
"rotateEncKeyTitle": {
|
||||
"message": "Rotate Encryption Key"
|
||||
},
|
||||
"rotateEncKeyConfirmation": {
|
||||
"message": "Are you sure you want to rotate your account's encryption key?"
|
||||
},
|
||||
"attachmentsNeedFix": {
|
||||
"message": "This item has old file attachments that need to be fixed."
|
||||
},
|
||||
"attachmentFixDesc": {
|
||||
"message": "This is an old file attachment the needs to be fixed. Click to learn more."
|
||||
},
|
||||
"fix": {
|
||||
"message": "Opravit",
|
||||
"description": "This is a verb. ex. 'Fix The Car'"
|
||||
},
|
||||
"oldAttachmentsNeedFixDesc": {
|
||||
"message": "There are old file attachments in your vault that need to be fixed before you can rotate your account's encryption key."
|
||||
},
|
||||
"yourAccountsFingerprint": {
|
||||
"message": "Fráze otisku prstu vašeho účtu",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"fingerprintEnsureIntegrityVerify": {
|
||||
"message": "To ensure the integrity of your encryption keys, please verify the user's fingerprint phrase before continuing.",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"dontAskFingerprintAgain": {
|
||||
"message": "Don't ask to verify fingerprint phrase again",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Adgangskode"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Adgangssætning"
|
||||
},
|
||||
"notes": {
|
||||
"message": "Noter"
|
||||
},
|
||||
@@ -74,7 +77,7 @@
|
||||
"message": "Kørekortnummer"
|
||||
},
|
||||
"email": {
|
||||
"message": "Email"
|
||||
"message": "E-mail"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
@@ -171,7 +174,7 @@
|
||||
"message": "Tilføj mappe"
|
||||
},
|
||||
"editFolder": {
|
||||
"message": "Rediger mappe"
|
||||
"message": "Redigér mappe"
|
||||
},
|
||||
"baseDomain": {
|
||||
"message": "Grund-domæne"
|
||||
@@ -204,8 +207,12 @@
|
||||
"toggleVisibility": {
|
||||
"message": "Slå synlighed til\/fra"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Fold sammen\/fold ud",
|
||||
"description": "Toggling an expand\/collapse state."
|
||||
},
|
||||
"generatePassword": {
|
||||
"message": "Generer adgangskode"
|
||||
"message": "Generér adgangskode"
|
||||
},
|
||||
"checkPassword": {
|
||||
"message": "Undersøg om adgangskoden er blevet afsløret."
|
||||
@@ -226,7 +233,7 @@
|
||||
"message": "Gem"
|
||||
},
|
||||
"cancel": {
|
||||
"message": "Annuller"
|
||||
"message": "Annullér"
|
||||
},
|
||||
"canceled": {
|
||||
"message": "Annulleret"
|
||||
@@ -244,7 +251,7 @@
|
||||
"message": "Fjern favorit"
|
||||
},
|
||||
"edit": {
|
||||
"message": "Rediger"
|
||||
"message": "Redigér"
|
||||
},
|
||||
"searchCollection": {
|
||||
"message": "Søg i samling"
|
||||
@@ -332,7 +339,7 @@
|
||||
"message": "Tilføj element"
|
||||
},
|
||||
"editItem": {
|
||||
"message": "Rediger element"
|
||||
"message": "Redigér element"
|
||||
},
|
||||
"ex": {
|
||||
"message": "eks.",
|
||||
@@ -355,27 +362,27 @@
|
||||
}
|
||||
},
|
||||
"copyValue": {
|
||||
"message": "Kopier værdi",
|
||||
"message": "Kopiér værdi",
|
||||
"description": "Copy value to clipboard"
|
||||
},
|
||||
"copyPassword": {
|
||||
"message": "Kopier adgangskode",
|
||||
"message": "Kopiér adgangskode",
|
||||
"description": "Copy password to clipboard"
|
||||
},
|
||||
"copyUsername": {
|
||||
"message": "Kopier brugernavn",
|
||||
"message": "Kopiér brugernavn",
|
||||
"description": "Copy username to clipboard"
|
||||
},
|
||||
"copyNumber": {
|
||||
"message": "Kopier nummer",
|
||||
"message": "Kopiér nummer",
|
||||
"description": "Copy credit card number"
|
||||
},
|
||||
"copySecurityCode": {
|
||||
"message": "Kopier sikkerhedskode",
|
||||
"message": "Kopiér sikkerhedskode",
|
||||
"description": "Copy credit card security code (CVV)"
|
||||
},
|
||||
"copyUri": {
|
||||
"message": "Kopier URI",
|
||||
"message": "Kopiér URI",
|
||||
"description": "Copy URI to clipboard"
|
||||
},
|
||||
"myVault": {
|
||||
@@ -475,16 +482,16 @@
|
||||
"message": "Slettede mappe"
|
||||
},
|
||||
"loggedOut": {
|
||||
"message": "Logget af"
|
||||
"message": "Logget ud"
|
||||
},
|
||||
"loginExpired": {
|
||||
"message": "Din login-session er udløbet."
|
||||
},
|
||||
"logOutConfirmation": {
|
||||
"message": "Er du sikker på, at du vil logge af?"
|
||||
"message": "Er du sikker på, at du vil logge ud?"
|
||||
},
|
||||
"logOut": {
|
||||
"message": "Log af"
|
||||
"message": "Log ud"
|
||||
},
|
||||
"ok": {
|
||||
"message": "Ok"
|
||||
@@ -508,7 +515,7 @@
|
||||
"message": "Indsend"
|
||||
},
|
||||
"emailAddressDesc": {
|
||||
"message": "Du bruger din emailadresse til at logge ind."
|
||||
"message": "Du bruger din e-mailadresse til at logge ind."
|
||||
},
|
||||
"yourName": {
|
||||
"message": "Dit navn"
|
||||
@@ -541,16 +548,16 @@
|
||||
"message": "Adgangskodetip"
|
||||
},
|
||||
"enterEmailToGetHint": {
|
||||
"message": "Indtast din kontos emailadresse for at modtage dit hovedadgangskodetip."
|
||||
"message": "Indtast din kontos e-mailadresse for at modtage dit hovedadgangskodetip."
|
||||
},
|
||||
"getMasterPasswordHint": {
|
||||
"message": "Få hovedadgangskodetip"
|
||||
},
|
||||
"emailRequired": {
|
||||
"message": "Emailadresse er påkrævet."
|
||||
"message": "E-mailadresse er påkrævet."
|
||||
},
|
||||
"invalidEmail": {
|
||||
"message": "Ugyldig emailadresse."
|
||||
"message": "Ugyldig e-mailadresse."
|
||||
},
|
||||
"masterPassRequired": {
|
||||
"message": "Hovedadgangskode er påkrævet."
|
||||
@@ -565,13 +572,13 @@
|
||||
"message": "Din nye konto er oprettet! Du kan nu logge ind."
|
||||
},
|
||||
"masterPassSent": {
|
||||
"message": "Vi har sendt dig en email med dit hovedadgangskodetip."
|
||||
"message": "Vi har sendt dig en e-mail med dit hovedadgangskodetip."
|
||||
},
|
||||
"unexpectedError": {
|
||||
"message": "Der opstod en uventet fejl."
|
||||
},
|
||||
"emailAddress": {
|
||||
"message": "Emailadresse"
|
||||
"message": "E-mailadresse"
|
||||
},
|
||||
"yourVaultIsLocked": {
|
||||
"message": "Din boks er låst. Bekræft din hovedadgangskode for at fortsætte."
|
||||
@@ -680,7 +687,7 @@
|
||||
"message": "YubiKey OTP sikkerhedsnøgle"
|
||||
},
|
||||
"yubiKeyDesc": {
|
||||
"message": "Brug en YubiKey til at få adgang til din konto. Virker med YubiKey 4, 4 Nano, 4C og NEO enheder."
|
||||
"message": "Brug en YubiKey til at få adgang til din konto. Virker med YubiKey 4 serien, 5 serien og NEO enheder."
|
||||
},
|
||||
"duoDesc": {
|
||||
"message": "Bekræft med Duo Security ved hjælp af Duo Mobile app, SMS, telefonopkald eller U2F sikkerhedsnøgle.",
|
||||
@@ -697,10 +704,10 @@
|
||||
"message": "FIDO U2F sikkerhedsnøgle"
|
||||
},
|
||||
"emailTitle": {
|
||||
"message": "Email"
|
||||
"message": "E-mail"
|
||||
},
|
||||
"emailDesc": {
|
||||
"message": "Bekræftelseskoder vil blive emailet til dig."
|
||||
"message": "Bekræftelseskoder vil blive e-mailet til dig."
|
||||
},
|
||||
"continue": {
|
||||
"message": "Fortsæt"
|
||||
@@ -718,7 +725,7 @@
|
||||
"message": "Vælg en organisation, som du ønsker at dele disse elementer med. Deling overfører ejerskab af elementerne til organisationen. Du vil ikke længere være den direkte ejer af disse elementer, når de er blevet delt."
|
||||
},
|
||||
"collectionsDesc": {
|
||||
"message": "Rediger de samlinger, som dette element deles med. Kun organisationsbrugere med adgang til disse samlinger vil kunne se dette element."
|
||||
"message": "Redigér de samlinger, som dette element deles med. Kun organisationsbrugere med adgang til disse samlinger vil kunne se dette element."
|
||||
},
|
||||
"deleteSelectedItemsDesc": {
|
||||
"message": "Du har valgt $COUNT$ element(er), der skal slettes. Er du sikker på, at du vil slette alle disse elementer?",
|
||||
@@ -738,8 +745,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shareSelectedItemsDesc": {
|
||||
"message": "Du har valgt $COUNT$ element(er). $SHAREABLE_COUNT$ element(er) er delbare, $NONSHAREABLE_COUNT$ er ikke. Elementer med vedhæftninger skal deles individuelt.",
|
||||
"shareSelectedItemsCountDesc": {
|
||||
"message": "Du har valgt $COUNT$ element(er). $SHAREABLE_COUNT$ element(er) kan deles, $NONSHAREABLE_COUNT$ kan ikke.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -759,19 +766,19 @@
|
||||
"message": "Bekræftelseskode (TOTP)"
|
||||
},
|
||||
"copyVerificationCode": {
|
||||
"message": "Kopier bekræftelseskoden"
|
||||
"message": "Kopiér verifikationskoden"
|
||||
},
|
||||
"warning": {
|
||||
"message": "Advarsel"
|
||||
},
|
||||
"exportWarning": {
|
||||
"message": "Denne eksport indeholder dine ikke-krypterede data i .csv-format. Du bør ikke gemme eller sende den over usikre kanaler (f.eks. email). Slet den umiddelbart efter at du er færdig med at bruge den."
|
||||
"message": "Denne eksport indeholder dine ikke-krypterede data i .csv-format. Du bør ikke gemme eller sende den over usikre kanaler (f.eks. e-mail). Slet den umiddelbart efter at du er færdig med at bruge den."
|
||||
},
|
||||
"exportMasterPassword": {
|
||||
"message": "Indtast din hovedadgangskode for at eksportere dine data fra boksen."
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Eksporter boks"
|
||||
"message": "Eksportér boks"
|
||||
},
|
||||
"exportSuccess": {
|
||||
"message": "Dine boksdata er blevet eksporteret."
|
||||
@@ -790,11 +797,17 @@
|
||||
"message": "Undgå tvetydige tegn"
|
||||
},
|
||||
"regeneratePassword": {
|
||||
"message": "Regenerer adgangskode"
|
||||
"message": "Regenerér adgangskode"
|
||||
},
|
||||
"length": {
|
||||
"message": "Længde"
|
||||
},
|
||||
"numWords": {
|
||||
"message": "Antal ord"
|
||||
},
|
||||
"wordSeparator": {
|
||||
"message": "Ordseparator"
|
||||
},
|
||||
"passwordHistory": {
|
||||
"message": "Adgangskodehistorik"
|
||||
},
|
||||
@@ -808,16 +821,16 @@
|
||||
"message": "Konto opdateret"
|
||||
},
|
||||
"changeEmail": {
|
||||
"message": "Skift email"
|
||||
"message": "Skift e-mail"
|
||||
},
|
||||
"newEmail": {
|
||||
"message": "Ny email"
|
||||
"message": "Ny e-mail"
|
||||
},
|
||||
"code": {
|
||||
"message": "Kode"
|
||||
},
|
||||
"changeEmailDesc": {
|
||||
"message": "Vi har sendt en bekræftelseskode til $EMAIL$. Tjek venligst din email for denne kode og indtast den nedenfor for at afslutte emailadresse-ændringen.",
|
||||
"message": "Vi har sendt en bekræftelseskode til $EMAIL$. Tjek venligst din e-mail for denne kode og indtast den nedenfor for at afslutte e-mailadresseændringen.",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@@ -829,7 +842,7 @@
|
||||
"message": "Ved at fortsætte vil du blive logget ud af din nuværende session, og du skal logge ind igen. Aktive sessioner på andre enheder kan fortsat forblive aktive i op til én time."
|
||||
},
|
||||
"emailChanged": {
|
||||
"message": "Email ændret"
|
||||
"message": "E-mail ændret"
|
||||
},
|
||||
"logBackIn": {
|
||||
"message": "Log venligst ind igen."
|
||||
@@ -852,6 +865,39 @@
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Bekræft ny hovedadgangskode"
|
||||
},
|
||||
"encKeySettings": {
|
||||
"message": "Indstillinger for krypteringsnøgle"
|
||||
},
|
||||
"kdfAlgorithm": {
|
||||
"message": "KDF algoritme"
|
||||
},
|
||||
"kdfIterations": {
|
||||
"message": "KDF iterationer"
|
||||
},
|
||||
"kdfIterationsDesc": {
|
||||
"message": "Flere KDF iterationer kan hjælpe med at beskytte din hovedadgangskode imod brute force angreb. Vi anbefaler en værdi på $VALUE$ eller mere.",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1",
|
||||
"example": "100,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kdfIterationsWarning": {
|
||||
"message": "Hvis du indstiller dine KDF iterationer for højt, kan det resultere i dårlig ydeevne, når du logger ind på (og låser op for) Bitwarden på enheder med langsomme CPU'er. Vi anbefaler at du øger værdien i trin på $INCREMENT$ og derefter tester alle dine enheder.",
|
||||
"placeholders": {
|
||||
"increment": {
|
||||
"content": "$1",
|
||||
"example": "50,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"changeKdf": {
|
||||
"message": "Ændre KDF"
|
||||
},
|
||||
"encKeySettingsChanged": {
|
||||
"message": "Krypteringsnøgleindstillinger ændret"
|
||||
},
|
||||
"dangerZone": {
|
||||
"message": "Farezone"
|
||||
},
|
||||
@@ -873,9 +919,15 @@
|
||||
"purgeVault": {
|
||||
"message": "Tøm boks"
|
||||
},
|
||||
"purgedOrganizationVault": {
|
||||
"message": "Organisationsboks tømt."
|
||||
},
|
||||
"purgeVaultDesc": {
|
||||
"message": "Fortsæt nedenfor for at slette alle elementer og mapper i din boks. Elementer, der tilhører en organisation du deler med, slettes ikke."
|
||||
},
|
||||
"purgeOrgVaultDesc": {
|
||||
"message": "Fortsæt nedenfor for at slette alle elementer i organisationens boks."
|
||||
},
|
||||
"purgeVaultWarning": {
|
||||
"message": "Tømning af din boks er permanent. Det kan ikke fortrydes."
|
||||
},
|
||||
@@ -904,7 +956,7 @@
|
||||
"message": "Værktøjer"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Importer data"
|
||||
"message": "Importér data"
|
||||
},
|
||||
"importSuccess": {
|
||||
"message": "Data er blevet importeret til din boks med success."
|
||||
@@ -922,7 +974,7 @@
|
||||
"message": "Vælg importfil"
|
||||
},
|
||||
"orCopyPasteFileContents": {
|
||||
"message": "eller kopier\/indsæt importfilens indhold"
|
||||
"message": "eller kopiér\/indsæt importfilens indhold"
|
||||
},
|
||||
"instructionsFor": {
|
||||
"message": "$NAME$ Instruktioner",
|
||||
@@ -956,7 +1008,7 @@
|
||||
"message": "Webikoner vises som et genkendeligt billede ved siden af hvert loginelement i din boks."
|
||||
},
|
||||
"enableGravatars": {
|
||||
"message": "Aktiver Gravatars",
|
||||
"message": "Aktivér Gravatars",
|
||||
"description": "'Gravatar' is the name of a service. See www.gravatar.com"
|
||||
},
|
||||
"enableGravatarsDesc": {
|
||||
@@ -1024,7 +1076,7 @@
|
||||
"description": "Two-step login providers such as YubiKey, Duo, Authenticator apps, Email, etc."
|
||||
},
|
||||
"enable": {
|
||||
"message": "Aktiver"
|
||||
"message": "Aktivér"
|
||||
},
|
||||
"enabled": {
|
||||
"message": "Aktiveret"
|
||||
@@ -1042,11 +1094,17 @@
|
||||
"premiumRequiredDesc": {
|
||||
"message": "Premium-medlemskab kræves for at anvende denne funktion."
|
||||
},
|
||||
"youHavePremiumAccess": {
|
||||
"message": "Du har premium adgang"
|
||||
},
|
||||
"alreadyPremiumFromOrg": {
|
||||
"message": "Du har allerede adgang til premium-funktioner på grund af en organisation, du er medlem af."
|
||||
},
|
||||
"manage": {
|
||||
"message": "Administrer"
|
||||
"message": "Håndtér"
|
||||
},
|
||||
"disable": {
|
||||
"message": "Deaktiver"
|
||||
"message": "Deaktivér"
|
||||
},
|
||||
"twoStepLoginProviderEnabled": {
|
||||
"message": "Denne to-trins-login udbyder er aktiveret på din konto."
|
||||
@@ -1097,7 +1155,7 @@
|
||||
"message": "Tilføj en ny YubiKey til din konto"
|
||||
},
|
||||
"twoFactorYubikeyPlugIn": {
|
||||
"message": "Sæt YubiKey'en (NEO eller 4-serien) i din computers USB-port."
|
||||
"message": "Sæt YubiKey'en i din computers USB-port."
|
||||
},
|
||||
"twoFactorYubikeySelectKey": {
|
||||
"message": "Vælg det første tomme YubiKey-indtastningsfelt nedenfor."
|
||||
@@ -1126,6 +1184,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"u2fkeyX": {
|
||||
"message": "U2F nøgle $INDEX$",
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nfcSupport": {
|
||||
"message": "NFC understøttelse"
|
||||
},
|
||||
@@ -1139,7 +1206,7 @@
|
||||
"message": "YubiKeys opdateret"
|
||||
},
|
||||
"disableAllKeys": {
|
||||
"message": "Deaktiver alle nøgler"
|
||||
"message": "Deaktivér alle nøgler"
|
||||
},
|
||||
"twoFactorDuoDesc": {
|
||||
"message": "Indtast Bitwarden-programoplysningerne fra dit Duo-administrationspanel."
|
||||
@@ -1154,26 +1221,41 @@
|
||||
"message": "API værtsnavn"
|
||||
},
|
||||
"twoFactorEmailDesc": {
|
||||
"message": "Følg disse trin for at konfigurere to-trins-login med email:"
|
||||
"message": "Følg disse trin for at konfigurere to-trins-login med e-mail:"
|
||||
},
|
||||
"twoFactorEmailEnterEmail": {
|
||||
"message": "Indtast den email, som du ønsker skal modtage verifikationskoder"
|
||||
"message": "Indtast den e-mail, som du ønsker skal modtage verifikationskoder"
|
||||
},
|
||||
"twoFactorEmailEnterCode": {
|
||||
"message": "Indtast den 6-cifrede verifikationskode fra emailen"
|
||||
"message": "Indtast den 6-cifrede verifikationskode fra e-mailen"
|
||||
},
|
||||
"sendEmail": {
|
||||
"message": "Send email"
|
||||
"message": "Send e-mail"
|
||||
},
|
||||
"twoFactorU2fAdd": {
|
||||
"message": "Tilføj en FIDO U2F sikkerhedsnøgle til din konto"
|
||||
},
|
||||
"twoFactorU2fPlugIn": {
|
||||
"message": "Sæt sikkerhedsnøglen i computerens USB-port."
|
||||
"removeU2fConfirmation": {
|
||||
"message": "Er du sikker på, at du vil fjerne denne sikkerhedsnøgle?"
|
||||
},
|
||||
"readKey": {
|
||||
"message": "Læs nøgle"
|
||||
},
|
||||
"keyCompromised": {
|
||||
"message": "Nøglen er kompromitteret."
|
||||
},
|
||||
"twoFactorU2fGiveName": {
|
||||
"message": "Giv sikkerhedsnøglen et brugervenligt navn til at identificere den."
|
||||
},
|
||||
"twoFactorU2fPlugInReadKey": {
|
||||
"message": "Sæt sikkerhedsnøglen i computerens USB-port, og klik på knappen \"Læs nøgle\"."
|
||||
},
|
||||
"twoFactorU2fTouchButton": {
|
||||
"message": "Hvis sikkerhedsnøglen har en knap, skal du trykke på den."
|
||||
},
|
||||
"twoFactorU2fSaveForm": {
|
||||
"message": "Gem formularen."
|
||||
},
|
||||
"twoFactorU2fWarning": {
|
||||
"message": "På grund af platformbegrænsninger kan FIDO U2F ikke bruges på alle Bitwarden-applikationer. Du bør aktivere en anden to-trins login udbyder, så du kan få adgang til din konto, når FIDO U2F ikke kan benyttes. Understøttede platforme:"
|
||||
},
|
||||
@@ -1183,11 +1265,11 @@
|
||||
"twoFactorU2fWaiting": {
|
||||
"message": "Venter på at du trykker på knappen på din sikkerhedsnøgle"
|
||||
},
|
||||
"twoFactorU2fClickEnable": {
|
||||
"message": "Klik på knappen \"Aktiver\" nedenfor for at benytte denne sikkerhedsnøgle til to-trins-login."
|
||||
"twoFactorU2fClickSave": {
|
||||
"message": "Klik på \"Gem\" knappen nedenfor for at aktivere denne sikkerhedsnøgle til to-trins login."
|
||||
},
|
||||
"twoFactorU2fProblemReading": {
|
||||
"message": "Der opstod et problem med at læse sikkerhedsnøglen."
|
||||
"twoFactorU2fProblemReadingTryAgain": {
|
||||
"message": "Der opstod et problem med at læse sikkerhedsnøglen. Prøv igen."
|
||||
},
|
||||
"twoFactorRecoveryYourCode": {
|
||||
"message": "Din Bitwarden to-trins-login gendannelseskode"
|
||||
@@ -1206,10 +1288,10 @@
|
||||
"message": "Datalæk rapport"
|
||||
},
|
||||
"breachDesc": {
|
||||
"message": "Et \"læk\" er en hændelse, hvor et websteds data er blevet ulovligt tilgået til af hackere og derefter offentliggjort. Gennemgå de typer af data, der blev kompromitteret (emailadresser, adgangskoder, kreditkort osv.) og træf passende foranstaltninger, som f.eks. ændring af adgangskoder."
|
||||
"message": "Et \"læk\" er en hændelse, hvor et websteds data er blevet ulovligt tilgået til af hackere og derefter offentliggjort. Gennemgå de typer af data, der blev kompromitteret (e-mailadresser, adgangskoder, kreditkort osv.) og træf passende foranstaltninger, som f.eks. ændring af adgangskoder."
|
||||
},
|
||||
"breachCheckUsernameEmail": {
|
||||
"message": "Kontroller eventuelle brugernavne eller emailadresser, som du bruger."
|
||||
"message": "Kontroller eventuelle brugernavne eller e-mailadresser, som du bruger."
|
||||
},
|
||||
"checkBreaches": {
|
||||
"message": "Kontroller læk"
|
||||
@@ -1301,14 +1383,30 @@
|
||||
"addons": {
|
||||
"message": "Tilføjelser"
|
||||
},
|
||||
"premiumAccess": {
|
||||
"message": "Premium adgang"
|
||||
},
|
||||
"premiumAccessDesc": {
|
||||
"message": "Du kan tilføje premium adgang til alle medlemmer af din organisation for $PRICE$ \/$INTERVAL$.",
|
||||
"placeholders": {
|
||||
"price": {
|
||||
"content": "$1",
|
||||
"example": "$3.33"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$2",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalStorageGb": {
|
||||
"message": "Ekstra lagerplads (GB)"
|
||||
},
|
||||
"additionalStorageGbDesc": {
|
||||
"message": "# af ekstra GB"
|
||||
},
|
||||
"additionalStorageDesc": {
|
||||
"message": "Dit abonnement indeholder $SIZE$ krypteret fillagring. Du kan tilføje ekstra lagerplads til $PRICE$ per GB \/ år.",
|
||||
"additionalStorageIntervalDesc": {
|
||||
"message": "Dit abonnement indeholder $SIZE$ krypteret fillagring. Du kan tilføje ekstra lagerplads til $PRICE$ per GB \/ $INTERVAL$.",
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"content": "$1",
|
||||
@@ -1317,6 +1415,10 @@
|
||||
"price": {
|
||||
"content": "$2",
|
||||
"example": "$4.00"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$3",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1406,7 +1508,7 @@
|
||||
"message": "Opdaterede licens"
|
||||
},
|
||||
"manageSubscription": {
|
||||
"message": "Administrer abonnement"
|
||||
"message": "Håndtér abonnement"
|
||||
},
|
||||
"storage": {
|
||||
"message": "Lager"
|
||||
@@ -1510,10 +1612,10 @@
|
||||
"message": "For at oprette en lokal-hosted organisation, skal du uploade en gyldig licensfil."
|
||||
},
|
||||
"accountEmailMustBeVerified": {
|
||||
"message": "Din kontos emailadresse skal verificeres."
|
||||
"message": "Din kontos e-mailadresse skal verificeres."
|
||||
},
|
||||
"newOrganizationDesc": {
|
||||
"message": "Organisationer giver dig mulighed for at dele dele af din boks med andre såvel som at administrere relaterede brugere i en bestemt enhed som f.eks. en familie, et lille team eller et stort firma."
|
||||
"message": "Organisationer giver dig mulighed for at dele dele af din boks med andre såvel som at håndtere relaterede brugere i en bestemt enhed som f.eks. en familie, et lille team eller et stort firma."
|
||||
},
|
||||
"generalInformation": {
|
||||
"message": "Generelle oplysninger"
|
||||
@@ -1661,6 +1763,9 @@
|
||||
"onPremHostingOptional": {
|
||||
"message": "Lokal-hosting (valgfri)"
|
||||
},
|
||||
"usersGetPremium": {
|
||||
"message": "Brugere får adgang til premium-medlemskabsfunktioner"
|
||||
},
|
||||
"controlAccessWithGroups": {
|
||||
"message": "Kontroller brugeradgang med grupper"
|
||||
},
|
||||
@@ -1737,7 +1842,7 @@
|
||||
"message": "Tilføj gruppe"
|
||||
},
|
||||
"editGroup": {
|
||||
"message": "Rediger Gruppe"
|
||||
"message": "Redigér Gruppe"
|
||||
},
|
||||
"deleteGroupConfirmation": {
|
||||
"message": "Er du sikker på, at du vil slette denne gruppe?"
|
||||
@@ -1770,22 +1875,22 @@
|
||||
"message": "Tilføj samling"
|
||||
},
|
||||
"editCollection": {
|
||||
"message": "Rediger samling"
|
||||
"message": "Redigér samling"
|
||||
},
|
||||
"deleteCollectionConfirmation": {
|
||||
"message": "Er du sikker på, at du vil slette denne samling?"
|
||||
},
|
||||
"editUser": {
|
||||
"message": "Rediger bruger"
|
||||
"message": "Redigér bruger"
|
||||
},
|
||||
"inviteUser": {
|
||||
"message": "Inviter bruger"
|
||||
},
|
||||
"inviteUserDesc": {
|
||||
"message": "Inviter en ny bruger til din organisation ved at indtaste emailadressen på deres Bitwarden-konto nedenfor. Hvis de ikke allerede har en Bitwarden-konto, bliver de bedt om at oprette en ny konto."
|
||||
"message": "Inviter en ny bruger til din organisation ved at indtaste e-mailadressen på deres Bitwarden-konto nedenfor. Hvis de ikke allerede har en Bitwarden-konto, bliver de bedt om at oprette en ny konto."
|
||||
},
|
||||
"inviteMultipleEmailDesc": {
|
||||
"message": "Du kan invitere op til $COUNT$ brugere ad gangen ved at kommaseparere en liste med emailadresser.",
|
||||
"message": "Du kan invitere op til $COUNT$ brugere ad gangen ved at kommaseparere en liste med e-mailadresser.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -1815,19 +1920,25 @@
|
||||
"message": "Ejer"
|
||||
},
|
||||
"ownerDesc": {
|
||||
"message": "Den bruger med højeste rettigheder, som kan administrere alle aspekter af din organisation."
|
||||
"message": "Den bruger med højeste rettigheder, som kan håndtere alle aspekter af din organisation."
|
||||
},
|
||||
"admin": {
|
||||
"message": "Administrator"
|
||||
},
|
||||
"adminDesc": {
|
||||
"message": "Administratorer kan få adgang til og styre alle elementer, samlinger og brugere i din organisation."
|
||||
"message": "Administratorer kan få adgang til og håndtere alle elementer, samlinger og brugere i din organisation."
|
||||
},
|
||||
"user": {
|
||||
"message": "Bruger"
|
||||
},
|
||||
"userDesc": {
|
||||
"message": "En almindelig bruger med adgang til organisationens samlinger."
|
||||
"message": "En almindelig bruger med adgang til tildelte samlinger i din organisation."
|
||||
},
|
||||
"manager": {
|
||||
"message": "Administrator"
|
||||
},
|
||||
"managerDesc": {
|
||||
"message": "Administratorer kan få adgang til og håndtere tildelte samlinger i din organisation."
|
||||
},
|
||||
"all": {
|
||||
"message": "Alle"
|
||||
@@ -1868,8 +1979,8 @@
|
||||
"changedPassword": {
|
||||
"message": "Ændrede konto kodeord."
|
||||
},
|
||||
"enabled2fa": {
|
||||
"message": "Aktiverede to-trins-login."
|
||||
"enabledUpdated2fa": {
|
||||
"message": "Aktiverede\/opdaterede to-trins login."
|
||||
},
|
||||
"disabled2fa": {
|
||||
"message": "Deaktiverede to-trins-login."
|
||||
@@ -2070,7 +2181,7 @@
|
||||
"message": "Gruppeadgang"
|
||||
},
|
||||
"groupAccessUserDesc": {
|
||||
"message": "Rediger de grupper, som denne bruger tilhører."
|
||||
"message": "Redigér de grupper, som denne bruger tilhører."
|
||||
},
|
||||
"invitedUsers": {
|
||||
"message": "Inviterede bruger(e)."
|
||||
@@ -2090,6 +2201,9 @@
|
||||
"confirm": {
|
||||
"message": "Bekræft"
|
||||
},
|
||||
"confirmUser": {
|
||||
"message": "Bekræft bruger"
|
||||
},
|
||||
"hasBeenConfirmed": {
|
||||
"message": "$USER$ er blevet bekræftet.",
|
||||
"placeholders": {
|
||||
@@ -2112,22 +2226,22 @@
|
||||
"message": "Slutdato"
|
||||
},
|
||||
"verifyEmail": {
|
||||
"message": "Bekræft email"
|
||||
"message": "Bekræft e-mail"
|
||||
},
|
||||
"verifyEmailDesc": {
|
||||
"message": "Bekræft din emailadresse for at låse op for adgangen til alle funktioner."
|
||||
"message": "Bekræft din e-mailadresse for at låse op for adgangen til alle funktioner."
|
||||
},
|
||||
"verifyEmailFirst": {
|
||||
"message": "Din kontos emailadresse skal først verificeres."
|
||||
"message": "Din kontos e-mailadresse skal først verificeres."
|
||||
},
|
||||
"checkInboxForVerification": {
|
||||
"message": "Tjek din email-indbakke for et bekræftelseslink."
|
||||
"message": "Tjek din e-mail indbakke for et bekræftelseslink."
|
||||
},
|
||||
"emailVerified": {
|
||||
"message": "Din email er blevet bekræftet."
|
||||
"message": "Din e-mail er blevet bekræftet."
|
||||
},
|
||||
"emailVerifiedFailed": {
|
||||
"message": "Kan ikke bekræfte din email. Prøv at sende en ny verifikations-email."
|
||||
"message": "Kan ikke bekræfte din e-mail. Prøv at sende en ny verifikations-email."
|
||||
},
|
||||
"updateBrowser": {
|
||||
"message": "Opdater browser"
|
||||
@@ -2145,13 +2259,13 @@
|
||||
"message": "Invitation accepteret"
|
||||
},
|
||||
"inviteAcceptedDesc": {
|
||||
"message": "Du kan få adgang til denne organisation, når en administrator bekræfter dit medlemskab. Vi sender dig en email, når dette sker."
|
||||
"message": "Du kan få adgang til denne organisation, når en administrator bekræfter dit medlemskab. Vi sender dig en e-mail, når dette sker."
|
||||
},
|
||||
"inviteAcceptFailed": {
|
||||
"message": "Kan ikke acceptere invitationen. Bed en organisations-administrator om at sende en ny invitation."
|
||||
},
|
||||
"rememberEmail": {
|
||||
"message": "Husk email"
|
||||
"message": "Husk e-mail"
|
||||
},
|
||||
"recoverAccountTwoStepDesc": {
|
||||
"message": "Hvis du ikke kan få adgang til din konto via dine normale to-trins-login metoder, kan du bruge din to-trins-login gendannelseskode til at deaktivere alle to-trins-udbydere på din konto."
|
||||
@@ -2166,10 +2280,10 @@
|
||||
"message": "Få mere at vide"
|
||||
},
|
||||
"deleteRecoverDesc": {
|
||||
"message": "Indtast din emailadresse nedenfor for at gendanne og slette din konto."
|
||||
"message": "Indtast din e-mailadresse nedenfor for at gendanne og slette din konto."
|
||||
},
|
||||
"deleteRecoverEmailSent": {
|
||||
"message": "Hvis din konto findes, har vi sendt dig en email med yderligere instruktioner."
|
||||
"message": "Hvis din konto findes, har vi sendt dig en e-mail med yderligere instruktioner."
|
||||
},
|
||||
"deleteRecoverConfirmDesc": {
|
||||
"message": "Du har bedt om at få slettet din Bitwarden-konto. Klik på knappen nedenfor for at bekræfte."
|
||||
@@ -2402,5 +2516,69 @@
|
||||
},
|
||||
"licenseIsExpired": {
|
||||
"message": "Licensen er udløbet."
|
||||
},
|
||||
"updatedUsers": {
|
||||
"message": "Opdaterede brugere"
|
||||
},
|
||||
"selected": {
|
||||
"message": "Valgt"
|
||||
},
|
||||
"ownership": {
|
||||
"message": "Ejerskab"
|
||||
},
|
||||
"whoOwnsThisItem": {
|
||||
"message": "Hvem ejer dette element?"
|
||||
},
|
||||
"strong": {
|
||||
"message": "Stærk",
|
||||
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"good": {
|
||||
"message": "God",
|
||||
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weak": {
|
||||
"message": "Svag",
|
||||
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weakMasterPassword": {
|
||||
"message": "Svag hovedadgangskode"
|
||||
},
|
||||
"weakMasterPasswordDesc": {
|
||||
"message": "Hovedadgangskoden du har valgt er svag. Du skal bruge en stærk hovedadgangskode (eller en adgangssætning) for at beskytte din Bitwarden-konto korrekt. Er du sikker på, at du vil bruge denne hovedadgangskode?"
|
||||
},
|
||||
"rotateAccountEncKey": {
|
||||
"message": "Rotér også min kontos krypteringsnøgle"
|
||||
},
|
||||
"rotateEncKeyTitle": {
|
||||
"message": "Rotér krypteringsnøgle"
|
||||
},
|
||||
"rotateEncKeyConfirmation": {
|
||||
"message": "Er du sikker på, at du vil rotere din kontos krypteringsnøgle?"
|
||||
},
|
||||
"attachmentsNeedFix": {
|
||||
"message": "Dette element har gamle filvedhæftninger, der skal repareres."
|
||||
},
|
||||
"attachmentFixDesc": {
|
||||
"message": "Dette er en gammel filvedhæftning, der skal repareres. Klik for at lære mere."
|
||||
},
|
||||
"fix": {
|
||||
"message": "Reparér",
|
||||
"description": "This is a verb. ex. 'Fix The Car'"
|
||||
},
|
||||
"oldAttachmentsNeedFixDesc": {
|
||||
"message": "Der er gamle filvedhæftninger i din boks, der skal repareres, før du kan rotere din kontos krypteringsnøgle."
|
||||
},
|
||||
"yourAccountsFingerprint": {
|
||||
"message": "Din kontos fingeraftrykssætning",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"fingerprintEnsureIntegrityVerify": {
|
||||
"message": "For at sikre integriteten af dine krypteringsnøgler, bedes du bekræfte brugerens fingeraftrykssætning, inden du fortsætter.",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"dontAskFingerprintAgain": {
|
||||
"message": "Spørg ikke om at bekræfte fingeraftrykssætning igen",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Passwort"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Passphrase"
|
||||
},
|
||||
"notes": {
|
||||
"message": "Notizen"
|
||||
},
|
||||
@@ -204,6 +207,10 @@
|
||||
"toggleVisibility": {
|
||||
"message": "Sichtbarkeit umschalten"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Toggle Collapse",
|
||||
"description": "Toggling an expand\/collapse state."
|
||||
},
|
||||
"generatePassword": {
|
||||
"message": "Passwort generieren"
|
||||
},
|
||||
@@ -680,7 +687,7 @@
|
||||
"message": "YubiKey OTP Sicherheitsschlüssel"
|
||||
},
|
||||
"yubiKeyDesc": {
|
||||
"message": "Verwenden Sie einen YubiKey um auf Ihr Konto zuzugreifen. Funtioniert mit YubiKey 4, Nano 4, 4C und NEO Geräten."
|
||||
"message": "Verwende einen YubiKey um auf dein Konto zuzugreifen. Funtioniert mit YubiKey 4, Nano 4, 4C und NEO Geräten."
|
||||
},
|
||||
"duoDesc": {
|
||||
"message": "Verifizieren Sie mit Duo Security, indem Sie die Duo Mobile App, SMS, Anrufe oder U2F Sicherheitsschlüssel benutzen.",
|
||||
@@ -738,8 +745,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shareSelectedItemsDesc": {
|
||||
"message": "Sie haben $COUNT$ Objekt(e) ausgewählt.\n$SHAREABLE_COUNT$ Objekt(e) können geteilt werden, davon $NONSHAREABLE_COUNT$ jedoch nicht. Objekte mit Anhängen müssen individuell geteilt werden.",
|
||||
"shareSelectedItemsCountDesc": {
|
||||
"message": "You have selected $COUNT$ item(s). $SHAREABLE_COUNT$ items are sharable, $NONSHAREABLE_COUNT$ are not.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -795,6 +802,12 @@
|
||||
"length": {
|
||||
"message": "Länge"
|
||||
},
|
||||
"numWords": {
|
||||
"message": "Anzahl der Wörter"
|
||||
},
|
||||
"wordSeparator": {
|
||||
"message": "Trennzeichen"
|
||||
},
|
||||
"passwordHistory": {
|
||||
"message": "Kennwort-Historie"
|
||||
},
|
||||
@@ -852,6 +865,39 @@
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Neues Master-Passwort bestätigen"
|
||||
},
|
||||
"encKeySettings": {
|
||||
"message": "Verschlüsselungsschlüssel-Einstellungen"
|
||||
},
|
||||
"kdfAlgorithm": {
|
||||
"message": "KDF-Algorithmus"
|
||||
},
|
||||
"kdfIterations": {
|
||||
"message": "KDF-Iterationen"
|
||||
},
|
||||
"kdfIterationsDesc": {
|
||||
"message": "Eine höhere Anzahl von KDF-Iterationen hilft dabei, dein Master-Passwort besser vor Brute-Force-Angriffen zu schützen. Wir empfehlen einen Wert von $VALUE$ oder mehr.",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1",
|
||||
"example": "100,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kdfIterationsWarning": {
|
||||
"message": "Wenn du die Anzahl der KDF-Iterationen zu hoch setzt, kann es sein, dass das Einloggen in Bitwarden (und Entsperren) auf langsameren Geräten länger dauert. Wir empfehlen, dass du den Wert um $INCREMENT$ Schrittweise anhebest und es auf allen Geräten testest.",
|
||||
"placeholders": {
|
||||
"increment": {
|
||||
"content": "$1",
|
||||
"example": "50,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"changeKdf": {
|
||||
"message": "KDF ändern"
|
||||
},
|
||||
"encKeySettingsChanged": {
|
||||
"message": "Verschlüsselungsschlüssel-Einstellungen wurden geändert"
|
||||
},
|
||||
"dangerZone": {
|
||||
"message": "Gefahrenzone"
|
||||
},
|
||||
@@ -873,9 +919,15 @@
|
||||
"purgeVault": {
|
||||
"message": "Tresor leeren"
|
||||
},
|
||||
"purgedOrganizationVault": {
|
||||
"message": "Gelöschter Organisations-Tresor."
|
||||
},
|
||||
"purgeVaultDesc": {
|
||||
"message": "Gehen Sie wie folgt vor, um alle Einträge und Ordner in Ihrem Tresor zu löschen. Einträge, die zu einer Organisation gehören, die Sie mit anderen teilen, werden nicht gelöscht."
|
||||
},
|
||||
"purgeOrgVaultDesc": {
|
||||
"message": "Bitte bestätige, dass du alle Inhalte dieses Tresores löschen möchtest."
|
||||
},
|
||||
"purgeVaultWarning": {
|
||||
"message": "Die Leerung des Tresor ist permanent. Sie kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
@@ -1042,6 +1094,12 @@
|
||||
"premiumRequiredDesc": {
|
||||
"message": "Für diese Funktion ist eine Premium-Mitgliedschaft notwendig."
|
||||
},
|
||||
"youHavePremiumAccess": {
|
||||
"message": "Du hast Zugriff auf Premium-Funktionen"
|
||||
},
|
||||
"alreadyPremiumFromOrg": {
|
||||
"message": "Du hast bereits Zugriff auf Premiumfunktionen weil du Mitglied einer Organisation bist."
|
||||
},
|
||||
"manage": {
|
||||
"message": "Verwalten"
|
||||
},
|
||||
@@ -1097,7 +1155,7 @@
|
||||
"message": "Einen neuen YubiKey zu Ihrem Konto hinzufügen"
|
||||
},
|
||||
"twoFactorYubikeyPlugIn": {
|
||||
"message": "Stecken Sie den YubiKey (NEO oder 4er Serie) in den USB-Anschluss Ihres Computers."
|
||||
"message": "Stecke den YubiKey in einen USB-Anschluss deines Computers."
|
||||
},
|
||||
"twoFactorYubikeySelectKey": {
|
||||
"message": "Selektieren Sie unten das erste YubiKey-Eingabefeld."
|
||||
@@ -1126,6 +1184,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"u2fkeyX": {
|
||||
"message": "U2F Schlüssel $INDEX$",
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nfcSupport": {
|
||||
"message": "NFC-Unterstützung"
|
||||
},
|
||||
@@ -1168,12 +1235,27 @@
|
||||
"twoFactorU2fAdd": {
|
||||
"message": "Fügen Sie Ihrem Konto einen FIDO U2F-Sicherheitsschlüssel hinzu"
|
||||
},
|
||||
"twoFactorU2fPlugIn": {
|
||||
"message": "Stecken Sie den Sicherheitsschlüssel in USB-Port Ihres Computers."
|
||||
"removeU2fConfirmation": {
|
||||
"message": "Bist du sicher das du diesen Sicherheitsschlüssel entfernen möchtest?"
|
||||
},
|
||||
"readKey": {
|
||||
"message": "Schlüssel erfassen"
|
||||
},
|
||||
"keyCompromised": {
|
||||
"message": "Dieser Schlüssel ist kompromitiert."
|
||||
},
|
||||
"twoFactorU2fGiveName": {
|
||||
"message": "Gib dem Sicherheitsschlüssel einen passenden Namen um ihn zu erkennen."
|
||||
},
|
||||
"twoFactorU2fPlugInReadKey": {
|
||||
"message": "Stecke den Sicherheitsschlüssel in deinen Computer und drücke \"Schlüssel erfassen\"."
|
||||
},
|
||||
"twoFactorU2fTouchButton": {
|
||||
"message": "Wenn der Sicherheitsschlüssel eine Taste hat, drücken Sie die."
|
||||
},
|
||||
"twoFactorU2fSaveForm": {
|
||||
"message": "Formular speichern."
|
||||
},
|
||||
"twoFactorU2fWarning": {
|
||||
"message": "Aufgrund von Plattformbeschränkungen kann FIDO U2F nicht mit allen Bitwarden-Anwendungen verwendet werden. Sie sollten einen anderen Zwei-Faktor-Authentifizierungsanbieter aktivieren, damit Sie auf Ihr Konto zugreifen können, wenn FIDO U2F nicht verwendet werden kann. Unterstützte Plattformen sind:"
|
||||
},
|
||||
@@ -1183,11 +1265,11 @@
|
||||
"twoFactorU2fWaiting": {
|
||||
"message": "Es wird darauf gewartet, dass Sie die Taste Ihres Sicherheitsschlüssels betätigen"
|
||||
},
|
||||
"twoFactorU2fClickEnable": {
|
||||
"message": "Klicken Sie unten auf \"Aktivieren\", um diesen Sicherheitsschlüssel für die Zwei-Faktor-Anmeldung zu verwenden."
|
||||
"twoFactorU2fClickSave": {
|
||||
"message": "Drücke \"Speichern\" um den Sicherheitsschlüssel für die Zwei-Schritte Anmeldung zu aktivieren."
|
||||
},
|
||||
"twoFactorU2fProblemReading": {
|
||||
"message": "Es gab ein Problem beim Lesen des Sicherheitsschlüssels."
|
||||
"twoFactorU2fProblemReadingTryAgain": {
|
||||
"message": "Es gab ein Problem beim lesen des Sicherheitsschlüssels, bitte erneut versuchen."
|
||||
},
|
||||
"twoFactorRecoveryYourCode": {
|
||||
"message": "Ihr Wiederherstellungsschlüssel für die Zwei-Faktor-Anmeldung in Bitwarden"
|
||||
@@ -1301,14 +1383,30 @@
|
||||
"addons": {
|
||||
"message": "Erweiterungen"
|
||||
},
|
||||
"premiumAccess": {
|
||||
"message": "Premium Access"
|
||||
},
|
||||
"premiumAccessDesc": {
|
||||
"message": "You can add premium access to all members of your organization for $PRICE$ \/$INTERVAL$.",
|
||||
"placeholders": {
|
||||
"price": {
|
||||
"content": "$1",
|
||||
"example": "$3.33"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$2",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalStorageGb": {
|
||||
"message": "Zusätzlicher Speicher (GB)"
|
||||
},
|
||||
"additionalStorageGbDesc": {
|
||||
"message": "# zusätzliche GB"
|
||||
},
|
||||
"additionalStorageDesc": {
|
||||
"message": "Ihr Abo beinhaltet einen $SIZE$ großen verschlüsselten Datenspeicher. Sie können zusätzlichen Speicher für $PRICE$ pro GB \/ Jahr dazubuchen.",
|
||||
"additionalStorageIntervalDesc": {
|
||||
"message": "Dein Abo beinhaltet $SIZE$ verschlüsselten Datenspeicher. Du kannst zusätzlichen Speicher für $PRICE$ pro GB\/$INTERVAL$ hinzufügen.",
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"content": "$1",
|
||||
@@ -1317,6 +1415,10 @@
|
||||
"price": {
|
||||
"content": "$2",
|
||||
"example": "$4.00"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$3",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1661,6 +1763,9 @@
|
||||
"onPremHostingOptional": {
|
||||
"message": "Hosting auf eigenem Server (optional)"
|
||||
},
|
||||
"usersGetPremium": {
|
||||
"message": "Mitglieder erhalten Zugriff auf Premium-Funktionen"
|
||||
},
|
||||
"controlAccessWithGroups": {
|
||||
"message": "Zugriffskontrolle durch Gruppen"
|
||||
},
|
||||
@@ -1821,13 +1926,19 @@
|
||||
"message": "Administrator"
|
||||
},
|
||||
"adminDesc": {
|
||||
"message": " Administratoren können auf alle Einträge, Sammlungen und Benutzer in Ihrer Organisation zugreifen und diese verwalten."
|
||||
"message": "Administratoren können auf alle Elemente, Sammlungen und Benutzer in der Organisation zugreifen und diese verwalten."
|
||||
},
|
||||
"user": {
|
||||
"message": "Benutzer"
|
||||
},
|
||||
"userDesc": {
|
||||
"message": "Ein normaler Benutzer mit Zugriff auf die Sammlungen Ihrer Organisation."
|
||||
"message": "Ein normaler Benutzer mit Zugriff auf die ihm zugewiesenen Sammlungen der Organisation."
|
||||
},
|
||||
"manager": {
|
||||
"message": "Manager"
|
||||
},
|
||||
"managerDesc": {
|
||||
"message": "Manager können auf Ihnen zugewiesene Sammlungen in der Organisation zugreifen und diese verwalten."
|
||||
},
|
||||
"all": {
|
||||
"message": "Alle"
|
||||
@@ -1868,8 +1979,8 @@
|
||||
"changedPassword": {
|
||||
"message": "Benutzerpasswort geändert."
|
||||
},
|
||||
"enabled2fa": {
|
||||
"message": "Zwei-Faktor-Anmeldung aktiviert."
|
||||
"enabledUpdated2fa": {
|
||||
"message": "Zwei-Faktor-Anmeldung aktiviert\/aktualisiert."
|
||||
},
|
||||
"disabled2fa": {
|
||||
"message": "Zwei-Faktor-Anmeldung deaktiviert."
|
||||
@@ -2090,6 +2201,9 @@
|
||||
"confirm": {
|
||||
"message": "Bestätigen"
|
||||
},
|
||||
"confirmUser": {
|
||||
"message": "Confirm User"
|
||||
},
|
||||
"hasBeenConfirmed": {
|
||||
"message": "$USER$ wurde bestätigt.",
|
||||
"placeholders": {
|
||||
@@ -2100,7 +2214,7 @@
|
||||
}
|
||||
},
|
||||
"confirmUsers": {
|
||||
"message": "Bestätigen Sie die Benutzer"
|
||||
"message": "Benutzer bestätigen"
|
||||
},
|
||||
"usersNeedConfirmed": {
|
||||
"message": "Sie haben Nutzer, die ihre Einladung angenommen haben, aber noch bestätigt werden müssen. Benutzer haben erst Zugriff auf die Organisation, wenn sie bestätigt wurden."
|
||||
@@ -2402,5 +2516,69 @@
|
||||
},
|
||||
"licenseIsExpired": {
|
||||
"message": "Lizenz ist abgelaufen."
|
||||
},
|
||||
"updatedUsers": {
|
||||
"message": "Aktualisierte Benutzer"
|
||||
},
|
||||
"selected": {
|
||||
"message": "Ausgewählt"
|
||||
},
|
||||
"ownership": {
|
||||
"message": "Besitzer"
|
||||
},
|
||||
"whoOwnsThisItem": {
|
||||
"message": "Wem gehört dieser Eintrag?"
|
||||
},
|
||||
"strong": {
|
||||
"message": "Strong",
|
||||
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"good": {
|
||||
"message": "Good",
|
||||
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weak": {
|
||||
"message": "Weak",
|
||||
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weakMasterPassword": {
|
||||
"message": "Weak Master Password"
|
||||
},
|
||||
"weakMasterPasswordDesc": {
|
||||
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
|
||||
},
|
||||
"rotateAccountEncKey": {
|
||||
"message": "Also rotate my account's encryption key"
|
||||
},
|
||||
"rotateEncKeyTitle": {
|
||||
"message": "Rotate Encryption Key"
|
||||
},
|
||||
"rotateEncKeyConfirmation": {
|
||||
"message": "Are you sure you want to rotate your account's encryption key?"
|
||||
},
|
||||
"attachmentsNeedFix": {
|
||||
"message": "This item has old file attachments that need to be fixed."
|
||||
},
|
||||
"attachmentFixDesc": {
|
||||
"message": "This is an old file attachment the needs to be fixed. Click to learn more."
|
||||
},
|
||||
"fix": {
|
||||
"message": "Fix",
|
||||
"description": "This is a verb. ex. 'Fix The Car'"
|
||||
},
|
||||
"oldAttachmentsNeedFixDesc": {
|
||||
"message": "There are old file attachments in your vault that need to be fixed before you can rotate your account's encryption key."
|
||||
},
|
||||
"yourAccountsFingerprint": {
|
||||
"message": "Your account's fingerprint phrase",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"fingerprintEnsureIntegrityVerify": {
|
||||
"message": "To ensure the integrity of your encryption keys, please verify the user's fingerprint phrase before continuing.",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"dontAskFingerprintAgain": {
|
||||
"message": "Don't ask to verify fingerprint phrase again",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Password"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Passphrase"
|
||||
},
|
||||
"notes": {
|
||||
"message": "Notes"
|
||||
},
|
||||
@@ -204,6 +207,10 @@
|
||||
"toggleVisibility": {
|
||||
"message": "Toggle Visibility"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Toggle Collapse",
|
||||
"description": "Toggling an expand/collapse state."
|
||||
},
|
||||
"generatePassword": {
|
||||
"message": "Generate Password"
|
||||
},
|
||||
@@ -683,7 +690,7 @@
|
||||
"message": "YubiKey OTP Security Key"
|
||||
},
|
||||
"yubiKeyDesc": {
|
||||
"message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices."
|
||||
"message": "Use a YubiKey to access your account. Works with YubiKey 4 series, 5 series, and NEO devices."
|
||||
},
|
||||
"duoDesc": {
|
||||
"message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.",
|
||||
@@ -741,8 +748,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shareSelectedItemsDesc": {
|
||||
"message": "You have selected $COUNT$ item(s). $SHAREABLE_COUNT$ items are sharable, $NONSHAREABLE_COUNT$ are not. Items with attachments must be shared individually.",
|
||||
"shareSelectedItemsCountDesc": {
|
||||
"message": "You have selected $COUNT$ item(s). $SHAREABLE_COUNT$ items are sharable, $NONSHAREABLE_COUNT$ are not.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -798,6 +805,12 @@
|
||||
"length": {
|
||||
"message": "Length"
|
||||
},
|
||||
"numWords": {
|
||||
"message": "Number of Words"
|
||||
},
|
||||
"wordSeparator": {
|
||||
"message": "Word Separator"
|
||||
},
|
||||
"passwordHistory": {
|
||||
"message": "Password History"
|
||||
},
|
||||
@@ -855,6 +868,39 @@
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Confirm New Master Password"
|
||||
},
|
||||
"encKeySettings": {
|
||||
"message": "Encryption Key Settings"
|
||||
},
|
||||
"kdfAlgorithm": {
|
||||
"message": "KDF Algorithm"
|
||||
},
|
||||
"kdfIterations": {
|
||||
"message": "KDF Iterations"
|
||||
},
|
||||
"kdfIterationsDesc": {
|
||||
"message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker. We recommend a value of $VALUE$ or more.",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1",
|
||||
"example": "100,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kdfIterationsWarning": {
|
||||
"message": "Setting your KDF iterations too high could result in poor performance when logging into (and unlocking) Bitwarden on devices with slower CPUs. We recommend that you increase the value in increments of $INCREMENT$ and then test all of your devices.",
|
||||
"placeholders": {
|
||||
"increment": {
|
||||
"content": "$1",
|
||||
"example": "50,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"changeKdf": {
|
||||
"message": "Change KDF"
|
||||
},
|
||||
"encKeySettingsChanged": {
|
||||
"message": "Encryption Key Settings Changed"
|
||||
},
|
||||
"dangerZone": {
|
||||
"message": "Danger Zone"
|
||||
},
|
||||
@@ -876,9 +922,15 @@
|
||||
"purgeVault": {
|
||||
"message": "Purge Vault"
|
||||
},
|
||||
"purgedOrganizationVault": {
|
||||
"message": "Purged organization vault."
|
||||
},
|
||||
"purgeVaultDesc": {
|
||||
"message": "Proceed below to delete all items and folders in your vault. Items that belong to an organization that you share with will not be deleted."
|
||||
},
|
||||
"purgeOrgVaultDesc": {
|
||||
"message": "Proceed below to delete all items in the organization's vault."
|
||||
},
|
||||
"purgeVaultWarning": {
|
||||
"message": "Purging your vault is permanent. It cannot be undone."
|
||||
},
|
||||
@@ -1045,6 +1097,12 @@
|
||||
"premiumRequiredDesc": {
|
||||
"message": "A premium membership is required to use this feature."
|
||||
},
|
||||
"youHavePremiumAccess": {
|
||||
"message": "You have premium access"
|
||||
},
|
||||
"alreadyPremiumFromOrg": {
|
||||
"message": "You already have access to premium features because of an organization you are a member of."
|
||||
},
|
||||
"manage": {
|
||||
"message": "Manage"
|
||||
},
|
||||
@@ -1100,7 +1158,7 @@
|
||||
"message": "Add a new YubiKey to your account"
|
||||
},
|
||||
"twoFactorYubikeyPlugIn": {
|
||||
"message": "Plug the YubiKey (NEO or 4 series) into your computer's USB port."
|
||||
"message": "Plug the YubiKey into your computer's USB port."
|
||||
},
|
||||
"twoFactorYubikeySelectKey": {
|
||||
"message": "Select the first empty YubiKey input field below."
|
||||
@@ -1129,6 +1187,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"u2fkeyX": {
|
||||
"message": "U2F Key $INDEX$",
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nfcSupport": {
|
||||
"message": "NFC Support"
|
||||
},
|
||||
@@ -1171,12 +1238,27 @@
|
||||
"twoFactorU2fAdd": {
|
||||
"message": "Add a FIDO U2F security key to your account"
|
||||
},
|
||||
"twoFactorU2fPlugIn": {
|
||||
"message": "Plug the security key into your computer's USB port."
|
||||
"removeU2fConfirmation": {
|
||||
"message": "Are you sure you want to remove this security key?"
|
||||
},
|
||||
"readKey": {
|
||||
"message": "Read Key"
|
||||
},
|
||||
"keyCompromised": {
|
||||
"message": "Key is compromised."
|
||||
},
|
||||
"twoFactorU2fGiveName": {
|
||||
"message": "Give the security key a friendly name to identify it."
|
||||
},
|
||||
"twoFactorU2fPlugInReadKey": {
|
||||
"message": "Plug the security key into your computer's USB port and click the \"Read Key\" button."
|
||||
},
|
||||
"twoFactorU2fTouchButton": {
|
||||
"message": "If the security key has a button, touch it."
|
||||
},
|
||||
"twoFactorU2fSaveForm": {
|
||||
"message": "Save the form."
|
||||
},
|
||||
"twoFactorU2fWarning": {
|
||||
"message": "Due to platform limitations, FIDO U2F cannot be used on all Bitwarden applications. You should enable another two-step login provider so that you can access your account when FIDO U2F cannot be used. Supported platforms:"
|
||||
},
|
||||
@@ -1186,11 +1268,11 @@
|
||||
"twoFactorU2fWaiting": {
|
||||
"message": "Waiting for you to touch the button on your security key"
|
||||
},
|
||||
"twoFactorU2fClickEnable": {
|
||||
"message": "Click the \"Enable\" button below to enable this security key for two-step login."
|
||||
"twoFactorU2fClickSave": {
|
||||
"message": "Click the \"Save\" button below to enable this security key for two-step login."
|
||||
},
|
||||
"twoFactorU2fProblemReading": {
|
||||
"message": "There was a problem reading the security key."
|
||||
"twoFactorU2fProblemReadingTryAgain": {
|
||||
"message": "There was a problem reading the security key. Try again."
|
||||
},
|
||||
"twoFactorRecoveryYourCode": {
|
||||
"message": "Your Bitwarden two-step login recovery code"
|
||||
@@ -1304,14 +1386,30 @@
|
||||
"addons": {
|
||||
"message": "Addons"
|
||||
},
|
||||
"premiumAccess": {
|
||||
"message": "Premium Access"
|
||||
},
|
||||
"premiumAccessDesc": {
|
||||
"message": "You can add premium access to all members of your organization for $PRICE$ /$INTERVAL$.",
|
||||
"placeholders": {
|
||||
"price": {
|
||||
"content": "$1",
|
||||
"example": "$3.33"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$2",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalStorageGb": {
|
||||
"message": "Additional Storage (GB)"
|
||||
},
|
||||
"additionalStorageGbDesc": {
|
||||
"message": "# of additional GB"
|
||||
},
|
||||
"additionalStorageDesc": {
|
||||
"message": "Your plan comes with $SIZE$ of encrypted file storage. You can add additional storage for $PRICE$ per GB /year.",
|
||||
"additionalStorageIntervalDesc": {
|
||||
"message": "Your plan comes with $SIZE$ of encrypted file storage. You can add additional storage for $PRICE$ per GB /$INTERVAL$.",
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"content": "$1",
|
||||
@@ -1320,6 +1418,10 @@
|
||||
"price": {
|
||||
"content": "$2",
|
||||
"example": "$4.00"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$3",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1664,6 +1766,9 @@
|
||||
"onPremHostingOptional": {
|
||||
"message": "On-premise hosting (optional)"
|
||||
},
|
||||
"usersGetPremium": {
|
||||
"message": "Users get access to premium membership features"
|
||||
},
|
||||
"controlAccessWithGroups": {
|
||||
"message": "Control user access with groups"
|
||||
},
|
||||
@@ -1824,13 +1929,19 @@
|
||||
"message": "Admin"
|
||||
},
|
||||
"adminDesc": {
|
||||
"message": " Admins can access and manage all items, collections and users in your organization."
|
||||
"message": "Admins can access and manage all items, collections and users in your organization."
|
||||
},
|
||||
"user": {
|
||||
"message": "User"
|
||||
},
|
||||
"userDesc": {
|
||||
"message": "A regular user with access to your organization's collections."
|
||||
"message": "A regular user with access to assigned collections in your organization."
|
||||
},
|
||||
"manager": {
|
||||
"message": "Manager"
|
||||
},
|
||||
"managerDesc": {
|
||||
"message": "Managers can access and manage assigned collections in your organization."
|
||||
},
|
||||
"all": {
|
||||
"message": "All"
|
||||
@@ -1871,8 +1982,8 @@
|
||||
"changedPassword": {
|
||||
"message": "Changed account password."
|
||||
},
|
||||
"enabled2fa": {
|
||||
"message": "Enabled two-step login."
|
||||
"enabledUpdated2fa": {
|
||||
"message": "Enabled/updated two-step login."
|
||||
},
|
||||
"disabled2fa": {
|
||||
"message": "Disabled two-step login."
|
||||
@@ -2093,6 +2204,9 @@
|
||||
"confirm": {
|
||||
"message": "Confirm"
|
||||
},
|
||||
"confirmUser": {
|
||||
"message": "Confirm User"
|
||||
},
|
||||
"hasBeenConfirmed": {
|
||||
"message": "$USER$ has been confirmed.",
|
||||
"placeholders": {
|
||||
@@ -2405,5 +2519,69 @@
|
||||
},
|
||||
"licenseIsExpired": {
|
||||
"message": "License is expired."
|
||||
},
|
||||
"updatedUsers": {
|
||||
"message": "Updated users"
|
||||
},
|
||||
"selected": {
|
||||
"message": "Selected"
|
||||
},
|
||||
"ownership": {
|
||||
"message": "Ownership"
|
||||
},
|
||||
"whoOwnsThisItem": {
|
||||
"message": "Who owns this item?"
|
||||
},
|
||||
"strong": {
|
||||
"message": "Strong",
|
||||
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"good": {
|
||||
"message": "Good",
|
||||
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weak": {
|
||||
"message": "Weak",
|
||||
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weakMasterPassword": {
|
||||
"message": "Weak Master Password"
|
||||
},
|
||||
"weakMasterPasswordDesc": {
|
||||
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
|
||||
},
|
||||
"rotateAccountEncKey": {
|
||||
"message": "Also rotate my account's encryption key"
|
||||
},
|
||||
"rotateEncKeyTitle": {
|
||||
"message": "Rotate Encryption Key"
|
||||
},
|
||||
"rotateEncKeyConfirmation": {
|
||||
"message": "Are you sure you want to rotate your account's encryption key?"
|
||||
},
|
||||
"attachmentsNeedFix": {
|
||||
"message": "This item has old file attachments that need to be fixed."
|
||||
},
|
||||
"attachmentFixDesc": {
|
||||
"message": "This is an old file attachment the needs to be fixed. Click to learn more."
|
||||
},
|
||||
"fix": {
|
||||
"message": "Fix",
|
||||
"description": "This is a verb. ex. 'Fix The Car'"
|
||||
},
|
||||
"oldAttachmentsNeedFixDesc": {
|
||||
"message": "There are old file attachments in your vault that need to be fixed before you can rotate your account's encryption key."
|
||||
},
|
||||
"yourAccountsFingerprint": {
|
||||
"message": "Your account's fingerprint phrase",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"fingerprintEnsureIntegrityVerify": {
|
||||
"message": "To ensure the integrity of your encryption keys, please verify the user's fingerprint phrase before continuing.",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"dontAskFingerprintAgain": {
|
||||
"message": "Don't ask to verify fingerprint phrase again",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Contraseña"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Frase de contraseña"
|
||||
},
|
||||
"notes": {
|
||||
"message": "Notas"
|
||||
},
|
||||
@@ -204,6 +207,10 @@
|
||||
"toggleVisibility": {
|
||||
"message": "Alternar visibilidad"
|
||||
},
|
||||
"toggleCollapse": {
|
||||
"message": "Colapsar\/Expandir",
|
||||
"description": "Toggling an expand\/collapse state."
|
||||
},
|
||||
"generatePassword": {
|
||||
"message": "Generar contraseña"
|
||||
},
|
||||
@@ -738,8 +745,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shareSelectedItemsDesc": {
|
||||
"message": "Has seleccionado $COUNT$ elementos. $SHAREABLE_COUNT$ son compartibles, $NONSHAREABLE_COUNT$ no lo son. Los elementos que contengan adjuntos deben ser compartidos de forma individual.",
|
||||
"shareSelectedItemsCountDesc": {
|
||||
"message": "Ha seleccionado $COUNT$ elemento(s). $SHAREABLE_COUNT$ elementos son compartibles, $NONSHAREABLE_COUNT$ no.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -795,6 +802,12 @@
|
||||
"length": {
|
||||
"message": "Longitud"
|
||||
},
|
||||
"numWords": {
|
||||
"message": "Número de palabras"
|
||||
},
|
||||
"wordSeparator": {
|
||||
"message": "Separador de palabras"
|
||||
},
|
||||
"passwordHistory": {
|
||||
"message": "Historial de contraseñas"
|
||||
},
|
||||
@@ -852,6 +865,39 @@
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Confirma la nueva contraseña maestra"
|
||||
},
|
||||
"encKeySettings": {
|
||||
"message": "Configuración de clave de cifrado"
|
||||
},
|
||||
"kdfAlgorithm": {
|
||||
"message": "Algoritmo KDF"
|
||||
},
|
||||
"kdfIterations": {
|
||||
"message": "Iteraciones de KDF"
|
||||
},
|
||||
"kdfIterationsDesc": {
|
||||
"message": "Mientras más iteraciones KDF, mejor la protección a su contraseña maestra de ser descubierta por un ataque de fuerza bruta. Recomendamos un valor de $VALUE$ o más.",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1",
|
||||
"example": "100,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kdfIterationsWarning": {
|
||||
"message": "Establecer las iteraciones KDF con un valor muy alto, podría resultar en un rendimiento pobre al ingresar, y\/o desbloquear Bitwarden en dispositivos con CPUs lentos. Recomendamos que aumente el valor en incrementos de $INCREMENT$ y luego pruebe en todos sus dispositivos.",
|
||||
"placeholders": {
|
||||
"increment": {
|
||||
"content": "$1",
|
||||
"example": "50,000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"changeKdf": {
|
||||
"message": "Modificar KDF"
|
||||
},
|
||||
"encKeySettingsChanged": {
|
||||
"message": "Se cambió la configuración de clave de cifrado"
|
||||
},
|
||||
"dangerZone": {
|
||||
"message": "Zona peligrosa"
|
||||
},
|
||||
@@ -873,9 +919,15 @@
|
||||
"purgeVault": {
|
||||
"message": "Purgar caja fuerte"
|
||||
},
|
||||
"purgedOrganizationVault": {
|
||||
"message": "Caja fuerte de organización purgada."
|
||||
},
|
||||
"purgeVaultDesc": {
|
||||
"message": "Proceder eliminará todos los elementos y carpetas de tu caja fuerte. Los elementos que petenezcan a una organización con la que compartes contenido, no serán eliminados."
|
||||
},
|
||||
"purgeOrgVaultDesc": {
|
||||
"message": "Proceda a continuación para eliminar todos los elementos de la caja fuerte de la organización."
|
||||
},
|
||||
"purgeVaultWarning": {
|
||||
"message": "Purgar tu caja fuerte es permanente. No se puede deshacer."
|
||||
},
|
||||
@@ -1042,6 +1094,12 @@
|
||||
"premiumRequiredDesc": {
|
||||
"message": "Se quiere membrasía Premium para poder utilizar esta característica."
|
||||
},
|
||||
"youHavePremiumAccess": {
|
||||
"message": "Tienes acceso premium"
|
||||
},
|
||||
"alreadyPremiumFromOrg": {
|
||||
"message": "Ya tienes acceso a las características premium, debido a una organización de la que eres miembro."
|
||||
},
|
||||
"manage": {
|
||||
"message": "Gestionar"
|
||||
},
|
||||
@@ -1100,7 +1158,7 @@
|
||||
"message": "Conecta la YubiKey (NEO o serie 4) al puerto USB de tu ordenador."
|
||||
},
|
||||
"twoFactorYubikeySelectKey": {
|
||||
"message": "Select in the first empty YubiKey input field below."
|
||||
"message": "Elije el primer campo de entrada vacío de YubiKey de abajo."
|
||||
},
|
||||
"twoFactorYubikeyTouchButton": {
|
||||
"message": "Toca el botón del YubiKey."
|
||||
@@ -1126,6 +1184,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"u2fkeyX": {
|
||||
"message": "Llave U2F $INDEX$",
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nfcSupport": {
|
||||
"message": "Soporte NFC"
|
||||
},
|
||||
@@ -1142,7 +1209,7 @@
|
||||
"message": "Deshabilitar todas las llaves"
|
||||
},
|
||||
"twoFactorDuoDesc": {
|
||||
"message": "Ingrese la información de la aplicación Bitwarden desde su panel de administración Duo."
|
||||
"message": "Introduce la información de la aplicación Bitwarden de tu panel de administración Duo."
|
||||
},
|
||||
"twoFactorDuoIntegrationKey": {
|
||||
"message": "Clave de integración"
|
||||
@@ -1157,46 +1224,61 @@
|
||||
"message": "Sigue estos pasos para configurar la autenticación en dos pasos con una aplicación autenticadora:"
|
||||
},
|
||||
"twoFactorEmailEnterEmail": {
|
||||
"message": "Ingrese el correo electrónico donde desea recibir los códigos de verificación"
|
||||
"message": "Introduce el correo electrónico donde deseas recibir los códigos de verificación"
|
||||
},
|
||||
"twoFactorEmailEnterCode": {
|
||||
"message": "Ingrese el código de verificación de 6 dígitos enviado a su correo electrónico"
|
||||
"message": "Introduce el código de verificación de 6 dígitos enviado a tu correo electrónico"
|
||||
},
|
||||
"sendEmail": {
|
||||
"message": "Enviar correo electrónico"
|
||||
},
|
||||
"twoFactorU2fAdd": {
|
||||
"message": "Agregar una clave de seguridad U2F FIDO a su cuenta"
|
||||
"message": "Agregar una clave de seguridad U2F FIDO a tu cuenta"
|
||||
},
|
||||
"twoFactorU2fPlugIn": {
|
||||
"message": "Conecte la llave de seguridad al puerto USB de su ordenador."
|
||||
"removeU2fConfirmation": {
|
||||
"message": "¿Estás seguro de que quieres eliminar esta clave de seguridad?"
|
||||
},
|
||||
"readKey": {
|
||||
"message": "Leer llave"
|
||||
},
|
||||
"keyCompromised": {
|
||||
"message": "La clave está comprometida."
|
||||
},
|
||||
"twoFactorU2fGiveName": {
|
||||
"message": "Asigna un nombre descriptivo a la llave de seguridad."
|
||||
},
|
||||
"twoFactorU2fPlugInReadKey": {
|
||||
"message": "Conecta la llave de seguridad al puerto USB de tu ordenador y haz clic en el botón \"Leer llave\"."
|
||||
},
|
||||
"twoFactorU2fTouchButton": {
|
||||
"message": "Si la clave de seguridad tiene un botón, tócalo."
|
||||
},
|
||||
"twoFactorU2fSaveForm": {
|
||||
"message": "Guardar el formulario."
|
||||
},
|
||||
"twoFactorU2fWarning": {
|
||||
"message": "Debido a las limitaciones de la plataforma, FIDO U2F no puede utilizarse en todas las aplicaciones de Bitwarden. Debe habilitar otro proveedor de inicio de sesión de dos pasos para que puede acceder a su cuenta cuando FIDO U2F no pueda utilizarse. Plataformas soportadas:"
|
||||
"message": "Debido a limitaciones de la plataforma, FIDO U2F no puede ser usado en todas las aplicaciones de Bitwarden. Deberías habilitar otro proveedor de inicio de sesión de dos pasos, para que puedas acceder a tu cuenta cuando FIDO U2F no pueda ser utilizado. Plataformas soportadas:"
|
||||
},
|
||||
"twoFactorU2fSupportWeb": {
|
||||
"message": "Bóveda web y extensiones de navegador en un escritorio\/portátil con un navegador compatible con U2F (Chrome, Opera, Vivaldi, o Firefox con FIDO U2F activado)."
|
||||
"message": "Caja fuerte web y extensiones de navegador en un escritorio\/portátil con un navegador compatible con U2F (Chrome, Opera, Vivaldi, o Firefox con FIDO U2F activado)."
|
||||
},
|
||||
"twoFactorU2fWaiting": {
|
||||
"message": "Esperando que toque el botón de su clave de seguridad"
|
||||
"message": "Esperando a que toques el botón de tu llave de seguridad"
|
||||
},
|
||||
"twoFactorU2fClickEnable": {
|
||||
"message": "Haga clic en el botón \"Habilitar\" a continuación para habilitar esta clave de seguridad para el inicio de sesión en dos pasos."
|
||||
"twoFactorU2fClickSave": {
|
||||
"message": "Haz clic en el botón \"guardar\" para habilitar esta llave de seguridad para el inicio de sesión de dos pasos."
|
||||
},
|
||||
"twoFactorU2fProblemReading": {
|
||||
"message": "Hubo un problema leyendo la clave de seguridad."
|
||||
"twoFactorU2fProblemReadingTryAgain": {
|
||||
"message": "Hubo un problema al leer la llave de seguridad. Inténtalo de nuevo."
|
||||
},
|
||||
"twoFactorRecoveryYourCode": {
|
||||
"message": "Su código de recuperación de inicio de sesión de dos pasos Bitwarden"
|
||||
"message": "Tu código de recuperación de inicio de sesión de dos pasos de Bitwarden"
|
||||
},
|
||||
"twoFactorRecoveryNoCode": {
|
||||
"message": "Aún no ha habilitado ningún proveedor de inicio de sesión de dos pasos. Después de haber habilitado un proveedor de inicio de sesión de dos pasos, puede volver aquí para ver el código de recuperación."
|
||||
"message": "Aún no has habilitado ningún proveedor de inicio de sesión en dos pasos. Después de haber habilitado un proveedor de inicio de sesión en dos pasos, puedes volver aquí para ver el código de recuperación."
|
||||
},
|
||||
"printCode": {
|
||||
"message": "Imprimir Código",
|
||||
"message": "Imprimir código",
|
||||
"description": "Print 2FA recovery code"
|
||||
},
|
||||
"reports": {
|
||||
@@ -1206,16 +1288,16 @@
|
||||
"message": "Informe de violación de datos"
|
||||
},
|
||||
"breachDesc": {
|
||||
"message": "Una \"violación\" es un incidente en el que los delincuentes informáticos han accedido ilegalmente a los datos de un sitio y los han hecho públicos. Revise los tipos de datos que fueron comprometidos (direcciones de correo electrónico, contraseñas, tarjetas de crédito, etc.) y tome las medidas apropiadas, como cambiar las contraseñas."
|
||||
"message": "Una \"filtración\" es un incidente en el que los delincuentes informáticos han accedido ilegalmente a los datos de un sitio y los han hecho públicos. Revisa los tipos de datos que fueron comprometidos (direcciones de correo electrónico, contraseñas, tarjetas de crédito, etc.) y toma las medidas apropiadas, como cambiar las contraseñas."
|
||||
},
|
||||
"breachCheckUsernameEmail": {
|
||||
"message": "Verifique cualquier nombre de usuario o dirección de correo electrónico que use."
|
||||
"message": "Verifica cualquier nombre de usuario o dirección de correo electrónico que utilices."
|
||||
},
|
||||
"checkBreaches": {
|
||||
"message": "Comprobar las violaciones"
|
||||
"message": "Comprobar filtraciones"
|
||||
},
|
||||
"breachUsernameNotFound": {
|
||||
"message": "$USERNAME$ no se encontró en ninguna violación de datos conocida.",
|
||||
"message": "$USERNAME$ no se encontró en ninguna filtración de datos conocida.",
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"content": "$1",
|
||||
@@ -1228,7 +1310,7 @@
|
||||
"description": "ex. Good News, No Breached Accounts Found!"
|
||||
},
|
||||
"breachUsernameFound": {
|
||||
"message": "$USERNAME$ fue encontrado $COUNT$ diferentes violaciones de datos en línea.",
|
||||
"message": "$USERNAME$ fue encontrado en $COUNT$ filtración\/es de datos en línea.",
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"content": "$1",
|
||||
@@ -1253,10 +1335,10 @@
|
||||
"message": "Usuarios afectados"
|
||||
},
|
||||
"breachOccurred": {
|
||||
"message": "Violación ocurrida"
|
||||
"message": "Se ha producido una filtración"
|
||||
},
|
||||
"breachReported": {
|
||||
"message": "Infracción notificada"
|
||||
"message": "Filtración reportada"
|
||||
},
|
||||
"reportError": {
|
||||
"message": "Se ha producido un error al intentar cargar el informe. Vuelve a intentarlo"
|
||||
@@ -1265,14 +1347,14 @@
|
||||
"message": "Facturación y licencias"
|
||||
},
|
||||
"goPremium": {
|
||||
"message": "Hazte premium",
|
||||
"message": "Hazte Premium",
|
||||
"description": "Another way of saying \"Get a premium membership\""
|
||||
},
|
||||
"premiumUpdated": {
|
||||
"message": "Has actualizado a premium."
|
||||
},
|
||||
"premiumUpgradeUnlockFeatures": {
|
||||
"message": "Actualice su cuenta a una membresía Premium y desbloquear algunas características adicionales."
|
||||
"message": "Actualice su cuenta a una membresía premium y desbloquee estupendas características adicionales."
|
||||
},
|
||||
"premiumSignUpStorage": {
|
||||
"message": "1 GB de almacenamiento de archivos cifrados."
|
||||
@@ -1287,7 +1369,7 @@
|
||||
"message": "Atención prioritaria al cliente."
|
||||
},
|
||||
"premiumSignUpFuture": {
|
||||
"message": "Todas las funciones Premium futuras. ¡próximamente!"
|
||||
"message": "Acceso a nuevas características premium en el futuro. ¡Hay más en camino!"
|
||||
},
|
||||
"premiumPrice": {
|
||||
"message": "Todo por sólo $PRICE$ \/año!",
|
||||
@@ -1301,14 +1383,30 @@
|
||||
"addons": {
|
||||
"message": "Complementos"
|
||||
},
|
||||
"premiumAccess": {
|
||||
"message": "Acceso Premium"
|
||||
},
|
||||
"premiumAccessDesc": {
|
||||
"message": "Puede Agregar acceso premium a todos los miembros de su organización por $PRICE$ \/$INTERVAL$.",
|
||||
"placeholders": {
|
||||
"price": {
|
||||
"content": "$1",
|
||||
"example": "$3.33"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$2",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalStorageGb": {
|
||||
"message": "Almacenamiento adicional (GB)"
|
||||
},
|
||||
"additionalStorageGbDesc": {
|
||||
"message": "# de GB adicional"
|
||||
},
|
||||
"additionalStorageDesc": {
|
||||
"message": "Su plan viene con $SIZE$ de almacenamiento de archivos cifrados. Puede agregar almacenamiento adicional por $PRICE$ por GB\/año.",
|
||||
"additionalStorageIntervalDesc": {
|
||||
"message": "Su plan viene con $SIZE$ de almacenamiento de archivos cifrados. Puede agregar almacenamiento adicional por $PRICE$ por GB\/$INTERVAL$.",
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"content": "$1",
|
||||
@@ -1317,6 +1415,10 @@
|
||||
"price": {
|
||||
"content": "$2",
|
||||
"example": "$4.00"
|
||||
},
|
||||
"interval": {
|
||||
"content": "$3",
|
||||
"example": "'month' or 'year'"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1661,6 +1763,9 @@
|
||||
"onPremHostingOptional": {
|
||||
"message": "Alojamiento propio (opcional)"
|
||||
},
|
||||
"usersGetPremium": {
|
||||
"message": "Los usuarios obtienen acceso a las características de una membresía premium"
|
||||
},
|
||||
"controlAccessWithGroups": {
|
||||
"message": "Control el acceso de usuarios con grupos"
|
||||
},
|
||||
@@ -1821,7 +1926,7 @@
|
||||
"message": "Administrador"
|
||||
},
|
||||
"adminDesc": {
|
||||
"message": " Admins can access and manage all items, collections and users in your organization."
|
||||
"message": "Los administradores pueden acceder y gestionar todos los elementos, colecciones y usuarios de la organización."
|
||||
},
|
||||
"user": {
|
||||
"message": "Usuario"
|
||||
@@ -1829,6 +1934,12 @@
|
||||
"userDesc": {
|
||||
"message": "Un usuario regular con acceso a las colecciones de su organización."
|
||||
},
|
||||
"manager": {
|
||||
"message": "Gestor"
|
||||
},
|
||||
"managerDesc": {
|
||||
"message": "Los gestores pueden acceder y gestionar colecciones asignadas en tu organización."
|
||||
},
|
||||
"all": {
|
||||
"message": "Todo"
|
||||
},
|
||||
@@ -1868,8 +1979,8 @@
|
||||
"changedPassword": {
|
||||
"message": "Contraseña de la cuenta cambiada."
|
||||
},
|
||||
"enabled2fa": {
|
||||
"message": "Autenticación en dos pasos habilitada."
|
||||
"enabledUpdated2fa": {
|
||||
"message": "Autenticación en dos pasos habilitado\/actualizado."
|
||||
},
|
||||
"disabled2fa": {
|
||||
"message": "Autenticación en dos pasos deshabilitada."
|
||||
@@ -2090,6 +2201,9 @@
|
||||
"confirm": {
|
||||
"message": "Confirmar"
|
||||
},
|
||||
"confirmUser": {
|
||||
"message": "Confirmar usuario"
|
||||
},
|
||||
"hasBeenConfirmed": {
|
||||
"message": "El usuario $USER$ ha sido confirmado.",
|
||||
"placeholders": {
|
||||
@@ -2181,7 +2295,7 @@
|
||||
"message": "Eliminar organización"
|
||||
},
|
||||
"deleteOrganizationDesc": {
|
||||
"message": "Proceed below to delete this organization and all associated data. Individual user accounts will remain, though they will not be associated to this organization anymore. "
|
||||
"message": "Continua el proceso para eliminar esta organización y todos los datos asociados a ella. Las cuentas de usuario individuales se mantendrán, aunque no estarán asociadas con la organización a partir de ahora."
|
||||
},
|
||||
"deleteOrganizationWarning": {
|
||||
"message": "Eliminar la organización es permanente. No se puede deshacer."
|
||||
@@ -2402,5 +2516,69 @@
|
||||
},
|
||||
"licenseIsExpired": {
|
||||
"message": "Licencia expirada."
|
||||
},
|
||||
"updatedUsers": {
|
||||
"message": "Usuarios actualizados"
|
||||
},
|
||||
"selected": {
|
||||
"message": "Seleccionado"
|
||||
},
|
||||
"ownership": {
|
||||
"message": "Propiedad"
|
||||
},
|
||||
"whoOwnsThisItem": {
|
||||
"message": "¿Quién posee este elemento?"
|
||||
},
|
||||
"strong": {
|
||||
"message": "Fuerte",
|
||||
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"good": {
|
||||
"message": "Bueno",
|
||||
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weak": {
|
||||
"message": "Débil",
|
||||
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weakMasterPassword": {
|
||||
"message": "Contraseña maestra débil"
|
||||
},
|
||||
"weakMasterPasswordDesc": {
|
||||
"message": "La contraseña maestra que ha elegido es débil. Debe usar una contraseña maestra fuerte (o una frase de contraseña) para proteger adecuadamente su cuenta de Bitwarden. ¿Está seguro de que desea utilizar esta contraseña maestra?"
|
||||
},
|
||||
"rotateAccountEncKey": {
|
||||
"message": "También rotar la clave de encriptación de mi cuenta"
|
||||
},
|
||||
"rotateEncKeyTitle": {
|
||||
"message": "Rotar clave de encriptación"
|
||||
},
|
||||
"rotateEncKeyConfirmation": {
|
||||
"message": "¿Está seguro de que desea rotar la clave de encriptación de su cuenta?"
|
||||
},
|
||||
"attachmentsNeedFix": {
|
||||
"message": "Este elemento tiene archivos adjuntos antiguos que deben ser corregidos."
|
||||
},
|
||||
"attachmentFixDesc": {
|
||||
"message": "Este es un archivo adjunto antiguo que necesita ser corregido. Haga clic para obtener más información."
|
||||
},
|
||||
"fix": {
|
||||
"message": "Arreglar",
|
||||
"description": "This is a verb. ex. 'Fix The Car'"
|
||||
},
|
||||
"oldAttachmentsNeedFixDesc": {
|
||||
"message": "Hay archivos adjuntos antiguos en la bóveda que necesitan ser corregidos antes de poder rotar la clave de encriptación de su cuenta."
|
||||
},
|
||||
"yourAccountsFingerprint": {
|
||||
"message": "Frase de la huella digital de su cuenta",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"fingerprintEnsureIntegrityVerify": {
|
||||
"message": "Para asegurar la integridad de sus claves de encriptación, por favor verifique la frase de la huella digital del usuario antes de continuar.",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"dontAskFingerprintAgain": {
|
||||
"message": "No pida verificar la frase de la huella dactilar de nuevo",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user