mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e95a8565c | ||
|
|
e83d0f2a9d | ||
|
|
eaebbcf6c8 | ||
|
|
7df5ed9b35 | ||
|
|
6b66f14319 | ||
|
|
2db1684b3c | ||
|
|
4625b44703 | ||
|
|
0356ecc17b | ||
|
|
1e7c27fba1 | ||
|
|
03f575f66f | ||
|
|
82b36c1b70 | ||
|
|
6878ab51fb | ||
|
|
8662033979 | ||
|
|
ef61652fba | ||
|
|
933a66b24c | ||
|
|
e2c6a5f8cd | ||
|
|
a818e7dd40 | ||
|
|
759dc647e5 | ||
|
|
37cf46d581 | ||
|
|
407032114e | ||
|
|
94aece134c | ||
|
|
7532bf9825 | ||
|
|
0f4f541b11 | ||
|
|
07a3d38bef | ||
|
|
e9273ff79a | ||
|
|
1aa708aed4 | ||
|
|
ebe5a6030e | ||
|
|
f6946085d8 | ||
|
|
beebe7c98b | ||
|
|
a51331d6b2 | ||
|
|
b7b970e654 | ||
|
|
d823e8522c | ||
|
|
6bc5ac46b7 | ||
|
|
1193a93f86 | ||
|
|
4cd052e009 | ||
|
|
949f61f1a4 | ||
|
|
2145c3f88c | ||
|
|
bb71d5dc0a | ||
|
|
41856ff6af | ||
|
|
a1388ddab7 | ||
|
|
b2d13f586d | ||
|
|
9f0cd586ee | ||
|
|
ce67497d3a | ||
|
|
0dc26e589a | ||
|
|
e14a676eea | ||
|
|
11cf89493d | ||
|
|
5be121ec71 | ||
|
|
95e58b5e69 | ||
|
|
506fd22280 | ||
|
|
d79b12dedc | ||
|
|
599cd7299c | ||
|
|
18d26b79af | ||
|
|
1f81b81a58 | ||
|
|
cc5e420484 | ||
|
|
b4eaa48765 | ||
|
|
76354741be | ||
|
|
1b466609f0 | ||
|
|
7e11b8bb5a | ||
|
|
b251e1f73c | ||
|
|
fa11382c08 | ||
|
|
e17a49acd5 | ||
|
|
bc71ffa6f2 | ||
|
|
95dc3c92c5 | ||
|
|
2135accaf4 | ||
|
|
429c38fc66 | ||
|
|
56e92b1695 | ||
|
|
b2685d455b | ||
|
|
abfd1fa254 | ||
|
|
24a5717e27 | ||
|
|
9d9503b00e | ||
|
|
7b0579ccf3 | ||
|
|
df84dff54f | ||
|
|
367c09f7e6 | ||
|
|
46967dc126 | ||
|
|
e0ede7ba74 | ||
|
|
1fe7554818 | ||
|
|
eff3332fef | ||
|
|
caea4775b3 | ||
|
|
5f04950358 | ||
|
|
c46af91240 | ||
|
|
f5034effd2 | ||
|
|
20408347fb | ||
|
|
49d5bfd3e7 | ||
|
|
e99d1a74fd | ||
|
|
43d1cede98 | ||
|
|
091fc93645 | ||
|
|
dfe2771ba7 | ||
|
|
d3664321fd | ||
|
|
2e01ff7826 | ||
|
|
59d5a7439d | ||
|
|
6e3edd75eb | ||
|
|
78992444bf | ||
|
|
f1dea8fb1a | ||
|
|
04e5ab0d01 | ||
|
|
22a1cef498 | ||
|
|
98eaeddbfd | ||
|
|
00e4df2dd3 | ||
|
|
cfb4133152 | ||
|
|
42361d17b5 | ||
|
|
02ee95506c |
@@ -1,4 +1,4 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch.
|
||||
# How to Contribute
|
||||
|
||||
Contributions of all kinds are welcome!
|
||||
|
||||
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
|
||||
|
||||
Here is how you can get involved:
|
||||
|
||||
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
|
||||
|
||||
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
|
||||
|
||||
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
||||
|
||||
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
||||
|
||||
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
|
||||
|
||||
* **Translate:** See the localization (l10n) section below
|
||||
|
||||
## Contributor Agreement
|
||||
|
||||
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
* use `npm run lint` and fix any linting suggestions before submitting a pull request
|
||||
* commit any pull requests against the `master` branch
|
||||
* include a link to your Community Forums post
|
||||
|
||||
# Localization (l10n)
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ files:
|
||||
zh-CN: zh_CN
|
||||
zh-TW: zh_TW
|
||||
en-GB: en_GB
|
||||
en-IN: en_IN
|
||||
|
||||
2
jslib
2
jslib
Submodule jslib updated: 57ace40845...abb54f0073
2621
package-lock.json
generated
2621
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bitwarden-web",
|
||||
"version": "2.15.1",
|
||||
"version": "2.17.1",
|
||||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/bitwarden/web",
|
||||
"scripts": {
|
||||
@@ -28,10 +28,11 @@
|
||||
"lint:fix": "tslint src/**/*.ts --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^7.2.11",
|
||||
"@ngtools/webpack": "^7.2.2",
|
||||
"@types/jquery": "^3.3.6",
|
||||
"@angular/compiler-cli": "^9.1.12",
|
||||
"@ngtools/webpack": "^9.1.12",
|
||||
"@types/jquery": "^3.5.1",
|
||||
"@types/lunr": "^2.3.3",
|
||||
"@types/node": "^10.17.28",
|
||||
"@types/node-forge": "^0.7.5",
|
||||
"@types/papaparse": "^4.5.3",
|
||||
"@types/webcrypto": "^0.0.28",
|
||||
@@ -43,45 +44,44 @@
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"del": "^3.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"gh-pages": "^1.2.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-google-webfonts": "^2.0.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-loader": "^5.3.3",
|
||||
"tslint": "^5.12.1",
|
||||
"ts-loader": "^7.0.5",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-loader": "^3.5.4",
|
||||
"typescript": "3.2.4",
|
||||
"typescript": "3.8.3",
|
||||
"webpack": "^4.29.0",
|
||||
"webpack-cli": "^3.2.1",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "7.2.1",
|
||||
"@angular/cdk": "7.2.1",
|
||||
"@angular/common": "7.2.1",
|
||||
"@angular/compiler": "7.2.1",
|
||||
"@angular/core": "7.2.1",
|
||||
"@angular/forms": "7.2.1",
|
||||
"@angular/platform-browser": "7.2.1",
|
||||
"@angular/platform-browser-dynamic": "7.2.1",
|
||||
"@angular/router": "7.2.1",
|
||||
"@angular/upgrade": "7.2.1",
|
||||
"@angular/animations": "9.1.12",
|
||||
"@angular/cdk": "9.2.4",
|
||||
"@angular/common": "9.1.12",
|
||||
"@angular/compiler": "9.1.12",
|
||||
"@angular/core": "9.1.12",
|
||||
"@angular/forms": "9.1.12",
|
||||
"@angular/platform-browser": "9.1.12",
|
||||
"@angular/platform-browser-dynamic": "9.1.12",
|
||||
"@angular/router": "9.1.12",
|
||||
"@microsoft/signalr": "3.1.0",
|
||||
"@microsoft/signalr-protocol-msgpack": "3.1.0",
|
||||
"angular2-toaster": "6.1.0",
|
||||
"angulartics2": "6.3.0",
|
||||
"angular2-toaster": "8.0.0",
|
||||
"angulartics2": "9.1.0",
|
||||
"big-integer": "1.6.36",
|
||||
"bootstrap": "4.3.1",
|
||||
"braintree-web-drop-in": "1.13.0",
|
||||
"core-js": "2.6.2",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.4.1",
|
||||
"lunr": "2.3.3",
|
||||
@@ -90,12 +90,13 @@
|
||||
"papaparse": "4.6.0",
|
||||
"popper.js": "1.14.4",
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "6.3.3",
|
||||
"rxjs": "6.6.2",
|
||||
"sweetalert2": "9.8.1",
|
||||
"tslib": "^2.0.1",
|
||||
"web-animations-js": "2.3.1",
|
||||
"webcrypto-shim": "0.1.4",
|
||||
"whatwg-fetch": "3.0.0",
|
||||
"zone.js": "0.8.28",
|
||||
"zone.js": "0.10.3",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<form (ngSubmit)="submit()" class="container" ngNativeValidate>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="text-center mb-4">
|
||||
@@ -25,9 +25,11 @@
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<i class="fa fa-unlock-alt" aria-hidden="true"></i>
|
||||
{{'unlock' | i18n}}
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||
<span>
|
||||
<i class="fa fa-unlock-alt" aria-hidden="true"></i> {{'unlock' | i18n}}
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
|
||||
{{'logOut' | i18n}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -25,9 +26,9 @@ export class LockComponent extends BaseLockComponent {
|
||||
userService: UserService, cryptoService: CryptoService,
|
||||
storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
|
||||
environmentService: EnvironmentService, private routerService: RouterService,
|
||||
stateService: StateService) {
|
||||
stateService: StateService, apiService: ApiService) {
|
||||
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
|
||||
storageService, vaultTimeoutService, environmentService, stateService);
|
||||
storageService, vaultTimeoutService, environmentService, stateService, apiService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
|
||||
<i class="fa fa-bank" aria-hidden="true"></i> {{'enterpriseSingleSignOn' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
} from '@angular/router';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
@@ -20,8 +23,13 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, private route: ActivatedRoute,
|
||||
storageService: StorageService, stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
|
||||
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
|
||||
super(authService, router,
|
||||
platformUtilsService, i18nService,
|
||||
stateService, environmentService,
|
||||
passwordGenerationService, cryptoFunctionService,
|
||||
storageService);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
</cite>
|
||||
</figcaption>
|
||||
<blockquote>
|
||||
"Bitwarden has become a popular choice among open-source software advocates. After using it for a
|
||||
few months, I can see why." - February 2020
|
||||
"Bitwarden has become a popular choice among open-source software advocates. After using
|
||||
it for a few months, I can see why." - February 2020
|
||||
</blockquote>
|
||||
</figure>
|
||||
</div>
|
||||
@@ -44,14 +44,15 @@
|
||||
<p class="lead text-center mb-4" *ngIf="!layout">{{'createAccount' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info" icon="fa-thumb-tack"
|
||||
*ngIf="showCreateOrgMessage">
|
||||
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info"
|
||||
icon="fa-thumb-tack" *ngIf="showCreateOrgMessage">
|
||||
{{'createOrganizationCreatePersonalAccount' | i18n}}
|
||||
</app-callout>
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email"
|
||||
required [appAutofocus]="email === ''" inputmode="email" appInputVerbatim="false">
|
||||
required [appAutofocus]="email === ''" inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -120,6 +121,19 @@
|
||||
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
|
||||
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="showTerms">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="acceptPolicies"
|
||||
[(ngModel)]="acceptPolicies" name="AcceptPolicies">
|
||||
<label class="form-check-label small text-muted" for="acceptPolicies">
|
||||
{{'acceptPolicies' | i18n}}<br>
|
||||
<a href="https://bitwarden.com/terms/" target="_blank"
|
||||
rel="noopener">{{'termsOfService' | i18n}}</a>,
|
||||
<a href="https://bitwarden.com/privacy/" target="_blank"
|
||||
rel="noopener">{{'privacyPolicy' | i18n}}</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex mb-2">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit"
|
||||
@@ -132,13 +146,6 @@
|
||||
{{'cancel' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted" *ngIf="showTerms">
|
||||
{{'submitAgreePolicies' | i18n}}
|
||||
<a href="https://bitwarden.com/terms/" target="_blank"
|
||||
rel="noopener">{{'termsOfService' | i18n}}</a>,
|
||||
<a href="https://bitwarden.com/privacy/" target="_blank"
|
||||
rel="noopener">{{'privacyPolicy' | i18n}}</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordP
|
||||
import { Policy } from 'jslib/models/domain/policy';
|
||||
|
||||
import { PolicyData } from 'jslib/models/data/policyData';
|
||||
import { ReferenceEventRequest } from 'jslib/models/request/referenceEventRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
@@ -26,7 +27,6 @@ import { PolicyData } from 'jslib/models/data/policyData';
|
||||
})
|
||||
export class RegisterComponent extends BaseRegisterComponent {
|
||||
showCreateOrgMessage = false;
|
||||
showTerms = true;
|
||||
layout = '';
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
|
||||
@@ -39,7 +39,6 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService) {
|
||||
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
|
||||
passwordGenerationService);
|
||||
this.showTerms = !platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
getPasswordScoreAlertDisplay() {
|
||||
@@ -64,6 +63,7 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
|
||||
async ngOnInit() {
|
||||
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
|
||||
this.referenceData = new ReferenceEventRequest();
|
||||
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
@@ -71,19 +71,20 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
this.stateService.save('loginRedirect', { route: '/settings/premium' });
|
||||
} else if (qParams.org != null) {
|
||||
this.showCreateOrgMessage = true;
|
||||
this.referenceData.flow = qParams.org;
|
||||
this.stateService.save('loginRedirect',
|
||||
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
|
||||
}
|
||||
if (qParams.layout != null) {
|
||||
this.layout = qParams.layout;
|
||||
this.layout = this.referenceData.layout = qParams.layout;
|
||||
}
|
||||
if (qParams.reference != null) {
|
||||
this.referenceId = qParams.reference;
|
||||
this.referenceData.id = qParams.reference;
|
||||
} else {
|
||||
this.referenceId = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
|
||||
this.referenceData.id = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
|
||||
}
|
||||
if (this.referenceId === '') {
|
||||
this.referenceId = null;
|
||||
if (this.referenceData.id === '') {
|
||||
this.referenceData.id = null;
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
|
||||
85
src/app/accounts/set-password.component.html
Normal file
85
src/app/accounts/set-password.component.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{'setMasterPassword' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body text-center" *ngIf="syncLoading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
{{'loading' | i18n}}
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!syncLoading">
|
||||
<app-callout type="info">{{'ssoCompleteRegistration' | i18n}}</app-callout>
|
||||
<div class="form-group">
|
||||
<app-callout type="info" *ngIf="enforcedPolicyOptions">
|
||||
{{'masterPasswordPolicyInEffect' | i18n}}
|
||||
<ul class="mb-0">
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{'policyInEffectUppercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{'policyInEffectLowercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{'policyInEffectNumbers' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<div class="w-100">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPasswordHash" class="text-monospace form-control mb-1"
|
||||
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
|
||||
appInputVerbatim>
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true">
|
||||
</app-password-strength>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
<div class="progress-bar invisible"></div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPasswordRetype" class="text-monospace form-control"
|
||||
[(ngModel)]="masterPasswordRetype" required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword(true)">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hint">{{'masterPassHint' | i18n}}</label>
|
||||
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
|
||||
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
|
||||
{{'logOut' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
34
src/app/accounts/set-password.component.ts
Normal file
34
src/app/accounts/set-password.component.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
SetPasswordComponent as BaseSetPasswordComponent,
|
||||
} from 'jslib/angular/components/set-password.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-set-password',
|
||||
templateUrl: 'set-password.component.html',
|
||||
})
|
||||
export class SetPasswordComponent extends BaseSetPasswordComponent {
|
||||
constructor(apiService: ApiService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, messagingService: MessagingService,
|
||||
userService: UserService, passwordGenerationService: PasswordGenerationService,
|
||||
platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router,
|
||||
syncService: SyncService, route: ActivatedRoute) {
|
||||
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
|
||||
platformUtilsService, policyService, router, apiService, syncService, route);
|
||||
}
|
||||
}
|
||||
33
src/app/accounts/sso.component.html
Normal file
33
src/app/accounts/sso.component.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<form #form (ngSubmit)="submit()" class="container" [appApiAction]="initiateSsoFormPromise" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<img src="../../images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden">
|
||||
<div class="card d-block mt-4">
|
||||
<div class="card-body" *ngIf="loggingIn">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
{{'loading' | i18n}}
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!loggingIn">
|
||||
<p>{{'ssoLogInWithOrgIdentifier' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="identifier">{{'organizationIdentifier' | i18n}}</label>
|
||||
<input id="identifier" class="form-control" type="text" name="Identifier"
|
||||
[(ngModel)]="identifier" required appAutofocus>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||
<span>
|
||||
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{'cancel' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
61
src/app/accounts/sso.component.ts
Normal file
61
src/app/accounts/sso.component.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
|
||||
|
||||
const IdentifierStorageKey = 'ssoOrgIdentifier';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sso',
|
||||
templateUrl: 'sso.component.html',
|
||||
})
|
||||
export class SsoComponent extends BaseSsoComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, route: ActivatedRoute,
|
||||
storageService: StorageService, stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService, apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
passwordGenerationService: PasswordGenerationService) {
|
||||
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
|
||||
apiService, cryptoFunctionService, passwordGenerationService);
|
||||
this.redirectUri = window.location.origin + '/sso-connector.html';
|
||||
this.clientId = 'web';
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
super.ngOnInit();
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
this.identifier = qParams.identifier;
|
||||
} else {
|
||||
const storedIdentifier = await this.storageService.get<string>(IdentifierStorageKey);
|
||||
if (storedIdentifier != null) {
|
||||
this.identifier = storedIdentifier;
|
||||
}
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.storageService.save(IdentifierStorageKey, this.identifier);
|
||||
if (this.clientId === 'browser') {
|
||||
document.cookie = `ssoHandOffMessage=${this.i18nService.t('ssoHandOff')};SameSite=strict`
|
||||
}
|
||||
super.submit();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { TwoFactorOptionsComponent } from './two-factor-options.component';
|
||||
|
||||
@@ -28,15 +31,15 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/comp
|
||||
templateUrl: 'two-factor.component.html',
|
||||
})
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, stateService: StateService,
|
||||
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
storageService: StorageService) {
|
||||
storageService: StorageService, route: ActivatedRoute) {
|
||||
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
|
||||
stateService, storageService);
|
||||
stateService, storageService, route);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
@@ -66,7 +69,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
|
||||
await this.stateService.remove('loginRedirect');
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
this.router.navigate([this.successRoute], {
|
||||
queryParams: {
|
||||
identifier: this.identifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import { LoginComponent } from './accounts/login.component';
|
||||
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
|
||||
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
|
||||
import { RegisterComponent } from './accounts/register.component';
|
||||
import { SetPasswordComponent } from './accounts/set-password.component';
|
||||
import { SsoComponent } from './accounts/sso.component';
|
||||
import { TwoFactorComponent } from './accounts/two-factor.component';
|
||||
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
|
||||
import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.component';
|
||||
@@ -55,6 +57,9 @@ import {
|
||||
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
|
||||
|
||||
import { AccessComponent } from './send/access.component';
|
||||
import { SendComponent } from './send/send.component';
|
||||
|
||||
import { AccountComponent } from './settings/account.component';
|
||||
import { CreateOrganizationComponent } from './settings/create-organization.component';
|
||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||
@@ -99,6 +104,15 @@ const routes: Routes = [
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'createAccount' },
|
||||
},
|
||||
{
|
||||
path: 'sso', component: SsoComponent,
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'enterpriseSingleSignOn' },
|
||||
},
|
||||
{
|
||||
path: 'set-password', component: SetPasswordComponent,
|
||||
data: { titleId: 'setMasterPassword' },
|
||||
},
|
||||
{
|
||||
path: 'hint', component: HintComponent,
|
||||
canActivate: [UnauthGuardService],
|
||||
@@ -130,6 +144,11 @@ const routes: Routes = [
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'deleteAccount' },
|
||||
},
|
||||
{
|
||||
path: 'send/:sendId/:key',
|
||||
component: AccessComponent,
|
||||
data: { title: 'Bitwarden Send' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -138,6 +157,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuardService],
|
||||
children: [
|
||||
{ path: 'vault', component: VaultComponent, data: { titleId: 'myVault' } },
|
||||
// { path: 'sends', component: SendComponent, data: { title: 'Send' } },
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as jq from 'jquery';
|
||||
import Swal from 'sweetalert2/src/sweetalert2.js';
|
||||
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
||||
|
||||
import {
|
||||
BodyOutputType,
|
||||
@@ -203,6 +203,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
|
||||
this.i18nService.t('loginExpired'));
|
||||
}
|
||||
|
||||
Swal.close();
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ import { LoginComponent } from './accounts/login.component';
|
||||
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
|
||||
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
|
||||
import { RegisterComponent } from './accounts/register.component';
|
||||
import { SetPasswordComponent } from './accounts/set-password.component';
|
||||
import { SsoComponent } from './accounts/sso.component';
|
||||
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
|
||||
import { TwoFactorComponent } from './accounts/two-factor.component';
|
||||
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
|
||||
@@ -58,13 +60,11 @@ import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/m
|
||||
|
||||
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
|
||||
import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component';
|
||||
import { ApiKeyComponent as OrgApiKeyComponent } from './organizations/settings/api-key.component';
|
||||
import { ChangePlanComponent } from './organizations/settings/change-plan.component';
|
||||
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
|
||||
import { DownloadLicenseComponent } from './organizations/settings/download-license.component';
|
||||
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
|
||||
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
|
||||
import { RotateApiKeyComponent as OrgRotateApiKeyComponent } from './organizations/settings/rotate-api-key.component';
|
||||
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
|
||||
import {
|
||||
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
|
||||
@@ -96,10 +96,15 @@ import { CollectionsComponent as OrgCollectionsComponent } from './organizations
|
||||
import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vault/groupings.component';
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
|
||||
|
||||
import { AccessComponent } from './send/access.component';
|
||||
import { AddEditComponent as SendAddEditComponent } from './send/add-edit.component';
|
||||
import { SendComponent } from './send/send.component';
|
||||
|
||||
import { AccountComponent } from './settings/account.component';
|
||||
import { AddCreditComponent } from './settings/add-credit.component';
|
||||
import { AdjustPaymentComponent } from './settings/adjust-payment.component';
|
||||
import { AdjustStorageComponent } from './settings/adjust-storage.component';
|
||||
import { ApiKeyComponent } from './settings/api-key.component';
|
||||
import { ChangeEmailComponent } from './settings/change-email.component';
|
||||
import { ChangeKdfComponent } from './settings/change-kdf.component';
|
||||
import { ChangePasswordComponent } from './settings/change-password.component';
|
||||
@@ -107,6 +112,7 @@ import { CreateOrganizationComponent } from './settings/create-organization.comp
|
||||
import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.component';
|
||||
import { DeleteAccountComponent } from './settings/delete-account.component';
|
||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||
import { LinkSsoComponent } from './settings/link-sso.component';
|
||||
import { OptionsComponent } from './settings/options.component';
|
||||
import { OrganizationPlansComponent } from './settings/organization-plans.component';
|
||||
import { OrganizationsComponent } from './settings/organizations.component';
|
||||
@@ -144,6 +150,7 @@ import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.comp
|
||||
|
||||
import { AddEditComponent } from './vault/add-edit.component';
|
||||
import { AttachmentsComponent } from './vault/attachments.component';
|
||||
import { BulkActionsComponent } from './vault/bulk-actions.component';
|
||||
import { BulkDeleteComponent } from './vault/bulk-delete.component';
|
||||
import { BulkMoveComponent } from './vault/bulk-move.component';
|
||||
import { BulkRestoreComponent } from './vault/bulk-restore.component';
|
||||
@@ -175,7 +182,10 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
|
||||
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
||||
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
|
||||
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import {
|
||||
registerLocaleData,
|
||||
DatePipe,
|
||||
} from '@angular/common';
|
||||
import localeCa from '@angular/common/locales/ca';
|
||||
import localeCs from '@angular/common/locales/cs';
|
||||
import localeDa from '@angular/common/locales/da';
|
||||
@@ -189,6 +199,8 @@ import localeHe from '@angular/common/locales/he';
|
||||
import localeIt from '@angular/common/locales/it';
|
||||
import localeJa from '@angular/common/locales/ja';
|
||||
import localeKo from '@angular/common/locales/ko';
|
||||
import localeLv from '@angular/common/locales/lv';
|
||||
import localeMl from '@angular/common/locales/ml';
|
||||
import localeNb from '@angular/common/locales/nb';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
@@ -214,6 +226,8 @@ registerLocaleData(localeHe, 'he');
|
||||
registerLocaleData(localeIt, 'it');
|
||||
registerLocaleData(localeJa, 'ja');
|
||||
registerLocaleData(localeKo, 'ko');
|
||||
registerLocaleData(localeLv, 'lv');
|
||||
registerLocaleData(localeMl, 'ml');
|
||||
registerLocaleData(localeNb, 'nb');
|
||||
registerLocaleData(localeNl, 'nl');
|
||||
registerLocaleData(localePl, 'pl');
|
||||
@@ -233,7 +247,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
FormsModule,
|
||||
AppRoutingModule,
|
||||
ServicesModule,
|
||||
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
|
||||
Angulartics2Module.forRoot({
|
||||
pageTracking: {
|
||||
clearQueryParams: true,
|
||||
},
|
||||
@@ -244,14 +258,17 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
],
|
||||
declarations: [
|
||||
A11yTitleDirective,
|
||||
AccessComponent,
|
||||
AcceptOrganizationComponent,
|
||||
AccountComponent,
|
||||
SetPasswordComponent,
|
||||
AddCreditComponent,
|
||||
AddEditComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustSeatsComponent,
|
||||
AdjustStorageComponent,
|
||||
ApiActionDirective,
|
||||
ApiKeyComponent,
|
||||
AppComponent,
|
||||
AttachmentsComponent,
|
||||
AutofocusDirective,
|
||||
@@ -259,6 +276,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
BlurClickDirective,
|
||||
BoxRowDirective,
|
||||
BreachReportComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
BulkMoveComponent,
|
||||
BulkRestoreComponent,
|
||||
@@ -290,6 +308,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
ImportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
InputVerbatimDirective,
|
||||
LinkSsoComponent,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
ModalComponent,
|
||||
@@ -297,7 +316,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OptionsComponent,
|
||||
OrgAccountComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrganizationBillingComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrganizationSubscriptionComponent,
|
||||
@@ -321,7 +339,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OrgPolicyEditComponent,
|
||||
OrgPoliciesComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgSettingComponent,
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
@@ -347,8 +364,11 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
SearchCiphersPipe,
|
||||
SearchPipe,
|
||||
SelectCopyDirective,
|
||||
SendAddEditComponent,
|
||||
SendComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
SsoComponent,
|
||||
StopClickDirective,
|
||||
StopPropDirective,
|
||||
TaxInfoComponent,
|
||||
@@ -378,7 +398,9 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
],
|
||||
entryComponents: [
|
||||
AddEditComponent,
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
BulkMoveComponent,
|
||||
BulkRestoreComponent,
|
||||
@@ -390,7 +412,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
FolderAddEditComponent,
|
||||
ModalComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCollectionAddEditComponent,
|
||||
OrgCollectionsComponent,
|
||||
@@ -398,12 +419,12 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OrgEntityUsersComponent,
|
||||
OrgGroupAddEditComponent,
|
||||
OrgPolicyEditComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PurgeVaultComponent,
|
||||
SendAddEditComponent,
|
||||
ShareComponent,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
TwoFactorDuoComponent,
|
||||
@@ -414,7 +435,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
TwoFactorYubiKeyComponent,
|
||||
UpdateKeyComponent,
|
||||
],
|
||||
providers: [],
|
||||
providers: [DatePipe],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule { }
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/vault">{{'myVault' | i18n}}</a>
|
||||
</li>
|
||||
<!--
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/sends">Send</a>
|
||||
</li>
|
||||
-->
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/tools">{{'tools' | i18n}}</a>
|
||||
</li>
|
||||
@@ -39,7 +44,7 @@
|
||||
<i class="fa fa-fw fa-question-circle" aria-hidden="true"></i>
|
||||
{{'getHelp' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="https://bitwarden.com#download" target="_blank" rel="noopener">
|
||||
<a class="dropdown-item" href="https://bitwarden.com/download/" target="_blank" rel="noopener">
|
||||
<i class="fa fa-fw fa-download" aria-hidden="true"></i>
|
||||
{{'getApps' | i18n}}
|
||||
</a>
|
||||
|
||||
@@ -1,45 +1,56 @@
|
||||
<app-navbar></app-navbar>
|
||||
<div class="org-nav" *ngIf="organization">
|
||||
<div class="container d-flex flex-column">
|
||||
<div class="my-auto d-flex align-items-center pl-1">
|
||||
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
|
||||
<div class="org-name ml-3">
|
||||
<span>{{organization.name}}</span>
|
||||
<small class="text-muted">{{'organization' | i18n}}</small>
|
||||
</div>
|
||||
<div class="ml-auto card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
|
||||
<div class="card-body py-2">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'organizationIsDisabled' | i18n}}
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="my-auto d-flex align-items-center pl-1">
|
||||
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
|
||||
<div class="org-name ml-3">
|
||||
<span>{{organization.name}}</span>
|
||||
<small class="text-muted">{{'organization' | i18n}}</small>
|
||||
</div>
|
||||
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
|
||||
<div class="card-body py-2">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'organizationIsDisabled' | i18n}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="organization.isManager">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i>
|
||||
{{'vault' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="manage" routerLinkActive="active">
|
||||
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||
{{'manage' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isAdmin">
|
||||
<a class="nav-link" routerLink="tools" routerLinkActive="active">
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||
{{'tools' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isOwner">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ml-auto d-flex align-items-center">
|
||||
<button class="btn btn-primary" (click)="goToEnterprisePortal()" #enterpriseBtn
|
||||
[appApiAction]="enterpriseTokenPromise" *ngIf="organization.useBusinessPortal">
|
||||
<i class="fa fa-bank fa-fw" [hidden]="enterpriseBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!enterpriseBtn.loading" title="{{'loading' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
{{'businessPortal' | i18n}} →
|
||||
</button>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="organization.isManager">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i>
|
||||
{{'vault' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="manage" routerLinkActive="active">
|
||||
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||
{{'manage' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isAdmin">
|
||||
<a class="nav-link" routerLink="tools" routerLinkActive="active">
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||
{{'tools' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isOwner">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -4,11 +4,14 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
@@ -21,20 +24,21 @@ const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
|
||||
enterpriseTokenPromise: Promise<any>;
|
||||
private organizationId: string;
|
||||
private enterpriseUrl: string;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private broadcasterService: BroadcasterService, private environmentService: EnvironmentService,
|
||||
private ngZone: NgZone) { }
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone,
|
||||
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.enterpriseUrl = 'https://enterprise.bitwarden.com';
|
||||
this.enterpriseUrl = 'https://portal.bitwarden.com';
|
||||
if (this.environmentService.enterpriseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.enterpriseUrl;
|
||||
} else if (this.environmentService.baseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.baseUrl + '/enterprise';
|
||||
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
|
||||
}
|
||||
|
||||
document.body.classList.remove('layout_frontend');
|
||||
@@ -42,7 +46,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
@@ -61,4 +64,20 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
async load() {
|
||||
this.organization = await this.userService.getOrganization(this.organizationId);
|
||||
}
|
||||
|
||||
async goToEnterprisePortal() {
|
||||
if (this.enterpriseTokenPromise != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
|
||||
const token = await this.enterpriseTokenPromise;
|
||||
if (token != null) {
|
||||
const userId = await this.userService.getUserId();
|
||||
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
|
||||
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id);
|
||||
}
|
||||
} catch { }
|
||||
this.enterpriseTokenPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<div class="form-group">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required
|
||||
appAutofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="externalId">{{'externalId' | i18n}}</label>
|
||||
|
||||
@@ -35,8 +35,8 @@ import { EntityUsersComponent } from './entity-users.component';
|
||||
templateUrl: 'collections.component.html',
|
||||
})
|
||||
export class CollectionsComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
@@ -85,7 +85,7 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.collections.length <= this.pageSize) {
|
||||
if (!this.collections || this.collections.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedCollections.length;
|
||||
@@ -185,7 +185,7 @@ export class CollectionsComponent implements OnInit {
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.collections.length > this.pageSize;
|
||||
return !searching && this.collections && this.collections.length > this.pageSize;
|
||||
}
|
||||
|
||||
private removeCollection(collection: CollectionView) {
|
||||
|
||||
@@ -32,8 +32,8 @@ import { GroupAddEditComponent } from './group-add-edit.component';
|
||||
templateUrl: 'groups.component.html',
|
||||
})
|
||||
export class GroupsComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
@@ -81,7 +81,7 @@ export class GroupsComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.groups.length <= this.pageSize) {
|
||||
if (!this.groups || this.groups.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedGroups.length;
|
||||
@@ -179,7 +179,7 @@ export class GroupsComponent implements OnInit {
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.groups.length > this.pageSize;
|
||||
return !searching && this.groups && this.groups.length > this.pageSize;
|
||||
}
|
||||
|
||||
private removeGroup(group: GroupResponse) {
|
||||
|
||||
@@ -43,10 +43,10 @@ import { UserGroupsComponent } from './user-groups.component';
|
||||
templateUrl: 'people.component.html',
|
||||
})
|
||||
export class PeopleComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('groupsTemplate', { read: ViewContainerRef }) groupsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild('confirmTemplate', { read: ViewContainerRef }) confirmModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
@@ -129,7 +129,7 @@ export class PeopleComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.users.length <= this.pageSize) {
|
||||
if (!this.users || this.users.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedUsers.length;
|
||||
@@ -331,7 +331,7 @@ export class PeopleComponent implements OnInit {
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.users.length > this.pageSize;
|
||||
return !searching && this.users && this.users.length > this.pageSize;
|
||||
}
|
||||
|
||||
private async doConfirmation(user: OrganizationUserUserDetailsResponse) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<app-callout [type]="'warning'">
|
||||
<p>{{'webPoliciesDeprecationWarning' | i18n}}</p>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
(click)="goToEnterprisePortal()">{{'businessPortal' | i18n}}</button>
|
||||
</app-callout>
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{'policies' | i18n}}</h1>
|
||||
</div>
|
||||
@@ -8,7 +13,7 @@
|
||||
<table class="table table-hover table-list" *ngIf="!loading">
|
||||
<tbody>
|
||||
<tr *ngFor="let p of policies">
|
||||
<td>
|
||||
<td *ngIf="p.display">
|
||||
<a href="#" appStopClick (click)="edit(p)">{{p.name}}</a>
|
||||
<span class="badge badge-success" *ngIf="p.enabled">{{'enabled' | i18n}}</span>
|
||||
<small class="text-muted d-block">{{p.description}}</small>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { PolicyType } from 'jslib/enums/policyType';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
@@ -28,41 +29,25 @@ import { PolicyEditComponent } from './policy-edit.component';
|
||||
templateUrl: 'policies.component.html',
|
||||
})
|
||||
export class PoliciesComponent implements OnInit {
|
||||
@ViewChild('editTemplate', { read: ViewContainerRef }) editModalRef: ViewContainerRef;
|
||||
@ViewChild('editTemplate', { read: ViewContainerRef, static: true }) editModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
policies: any[];
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
enterpriseTokenPromise: Promise<any>;
|
||||
private enterpriseUrl: string;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
private orgPolicies: PolicyResponse[];
|
||||
private policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService, private userService: UserService,
|
||||
private router: Router) {
|
||||
this.policies = [
|
||||
{
|
||||
name: i18nService.t('twoStepLogin'),
|
||||
description: i18nService.t('twoStepLoginPolicyDesc'),
|
||||
type: PolicyType.TwoFactorAuthentication,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
name: i18nService.t('masterPass'),
|
||||
description: i18nService.t('masterPassPolicyDesc'),
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
name: i18nService.t('passwordGenerator'),
|
||||
description: i18nService.t('passwordGeneratorPolicyDesc'),
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
private router: Router, private environmentService: EnvironmentService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
@@ -72,8 +57,53 @@ export class PoliciesComponent implements OnInit {
|
||||
this.router.navigate(['/organizations', this.organizationId]);
|
||||
return;
|
||||
}
|
||||
this.policies = [
|
||||
{
|
||||
name: this.i18nService.t('twoStepLogin'),
|
||||
description: this.i18nService.t('twoStepLoginPolicyDesc'),
|
||||
type: PolicyType.TwoFactorAuthentication,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('masterPass'),
|
||||
description: this.i18nService.t('masterPassPolicyDesc'),
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('passwordGenerator'),
|
||||
description: this.i18nService.t('passwordGeneratorPolicyDesc'),
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('singleOrg'),
|
||||
description: this.i18nService.t('singleOrgDesc'),
|
||||
type: PolicyType.SingleOrg,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('requireSso'),
|
||||
description: this.i18nService.t('requireSsoPolicyDesc'),
|
||||
type: PolicyType.RequireSso,
|
||||
enabled: false,
|
||||
display: organization.useSso,
|
||||
},
|
||||
];
|
||||
await this.load();
|
||||
});
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
this.enterpriseUrl = 'https://portal.bitwarden.com';
|
||||
if (this.environmentService.enterpriseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.enterpriseUrl;
|
||||
} else if (this.environmentService.baseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
@@ -102,6 +132,7 @@ export class PoliciesComponent implements OnInit {
|
||||
childComponent.description = p.description;
|
||||
childComponent.type = p.type;
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.policiesEnabledMap = this.policiesEnabledMap;
|
||||
childComponent.onSavedPolicy.subscribe(() => {
|
||||
this.modal.close();
|
||||
this.load();
|
||||
@@ -111,4 +142,22 @@ export class PoliciesComponent implements OnInit {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
async goToEnterprisePortal() {
|
||||
if (this.enterpriseTokenPromise != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
|
||||
const token = await this.enterpriseTokenPromise;
|
||||
if (token != null) {
|
||||
const userId = await this.userService.getUserId();
|
||||
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
|
||||
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organizationId);
|
||||
}
|
||||
} catch { }
|
||||
this.enterpriseTokenPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,18 @@
|
||||
title="{{'warning' | i18n}}" icon="fa-warning">
|
||||
{{'twoStepLoginPolicyWarning' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning" *ngIf="type === policyType.SingleOrg" title="{{'warning' | i18n}}"
|
||||
icon="fa-warning">
|
||||
{{'singleOrgPolicyWarning' | i18n}}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="type === policyType.RequireSso">
|
||||
<app-callout type="tip" title="{{'prerequisite' | i18n}}">
|
||||
{{'requireSsoPolicyReq' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning">
|
||||
{{'requireSsoExemption' | i18n}}
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
|
||||
|
||||
@@ -27,6 +27,7 @@ export class PolicyEditComponent implements OnInit {
|
||||
@Input() description: string;
|
||||
@Input() type: PolicyType;
|
||||
@Input() organizationId: string;
|
||||
@Input() policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
@Output() onSavedPolicy = new EventEmitter();
|
||||
|
||||
policyType = PolicyType;
|
||||
@@ -127,45 +128,66 @@ export class PolicyEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const request = new PolicyRequest();
|
||||
request.enabled = this.enabled;
|
||||
request.type = this.type;
|
||||
request.data = null;
|
||||
switch (this.type) {
|
||||
case PolicyType.PasswordGenerator:
|
||||
request.data = {
|
||||
defaultType: this.passGenDefaultType,
|
||||
minLength: this.passGenMinLength || null,
|
||||
useUpper: this.passGenUseUpper,
|
||||
useLower: this.passGenUseLower,
|
||||
useNumbers: this.passGenUseNumbers,
|
||||
useSpecial: this.passGenUseSpecial,
|
||||
minNumbers: this.passGenMinNumbers || null,
|
||||
minSpecial: this.passGenMinSpecial || null,
|
||||
minNumberWords: this.passGenMinNumberWords || null,
|
||||
capitalize: this.passGenCapitalize,
|
||||
includeNumber: this.passGenIncludeNumber,
|
||||
};
|
||||
break;
|
||||
case PolicyType.MasterPassword:
|
||||
request.data = {
|
||||
minComplexity: this.masterPassMinComplexity || null,
|
||||
minLength: this.masterPassMinLength || null,
|
||||
requireUpper: this.masterPassRequireUpper,
|
||||
requireLower: this.masterPassRequireLower,
|
||||
requireNumbers: this.masterPassRequireNumbers,
|
||||
requireSpecial: this.masterPassRequireSpecial,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (this.preValidate()) {
|
||||
const request = new PolicyRequest();
|
||||
request.enabled = this.enabled;
|
||||
request.type = this.type;
|
||||
request.data = null;
|
||||
switch (this.type) {
|
||||
case PolicyType.PasswordGenerator:
|
||||
request.data = {
|
||||
defaultType: this.passGenDefaultType,
|
||||
minLength: this.passGenMinLength || null,
|
||||
useUpper: this.passGenUseUpper,
|
||||
useLower: this.passGenUseLower,
|
||||
useNumbers: this.passGenUseNumbers,
|
||||
useSpecial: this.passGenUseSpecial,
|
||||
minNumbers: this.passGenMinNumbers || null,
|
||||
minSpecial: this.passGenMinSpecial || null,
|
||||
minNumberWords: this.passGenMinNumberWords || null,
|
||||
capitalize: this.passGenCapitalize,
|
||||
includeNumber: this.passGenIncludeNumber,
|
||||
};
|
||||
break;
|
||||
case PolicyType.MasterPassword:
|
||||
request.data = {
|
||||
minComplexity: this.masterPassMinComplexity || null,
|
||||
minLength: this.masterPassMinLength || null,
|
||||
requireUpper: this.masterPassRequireUpper,
|
||||
requireLower: this.masterPassRequireLower,
|
||||
requireNumbers: this.masterPassRequireNumbers,
|
||||
requireSpecial: this.masterPassRequireSpecial,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
try {
|
||||
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Edited Policy' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
|
||||
this.onSavedPolicy.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private preValidate(): boolean {
|
||||
switch (this.type) {
|
||||
case PolicyType.RequireSso:
|
||||
if (!this.enabled) { // Don't need prevalidation checks if submitting to disable
|
||||
return true;
|
||||
}
|
||||
// Have SingleOrg policy enabled?
|
||||
if (!(this.policiesEnabledMap.has(PolicyType.SingleOrg)
|
||||
&& this.policiesEnabledMap.get(PolicyType.SingleOrg))) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('requireSsoPolicyReqError'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Edited Policy' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
|
||||
this.onSavedPolicy.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
<p>{{'inviteUserDesc' | i18n}}</p>
|
||||
<div class="form-group mb-4">
|
||||
<label for="emails">{{'email' | i18n}}</label>
|
||||
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required>
|
||||
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
|
||||
appAutoFocus>
|
||||
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -10,17 +10,23 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{'organizationName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name">
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name"
|
||||
[disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
|
||||
[(ngModel)]="org.billingEmail">
|
||||
[(ngModel)]="org.billingEmail" [disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName"
|
||||
[(ngModel)]="org.businessName">
|
||||
[(ngModel)]="org.businessName" [disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="identifier">{{'identifier' | i18n}}</label>
|
||||
<input id="identifier" class="form-control" type="text" name="Identifier"
|
||||
[(ngModel)]="org.identifier">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
|
||||
@@ -11,29 +11,30 @@ import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
|
||||
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
import { ApiKeyComponent } from '../../settings/api-key.component';
|
||||
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
|
||||
import { TaxInfoComponent } from '../../settings/tax-info.component';
|
||||
import { ApiKeyComponent } from './api-key.component';
|
||||
import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
import { RotateApiKeyComponent } from './rotate-api-key.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-account',
|
||||
templateUrl: 'account.component.html',
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('apiKeyTemplate', { read: ViewContainerRef, static: true }) apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
|
||||
|
||||
selfHosted = false;
|
||||
loading = true;
|
||||
canUseApi = false;
|
||||
org: OrganizationResponse;
|
||||
@@ -46,9 +47,11 @@ export class AccountComponent {
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private route: ActivatedRoute, private syncService: SyncService) { }
|
||||
private route: ActivatedRoute, private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
try {
|
||||
@@ -65,6 +68,7 @@ export class AccountComponent {
|
||||
request.name = this.org.name;
|
||||
request.businessName = this.org.businessName;
|
||||
request.billingEmail = this.org.billingEmail;
|
||||
request.identifier = this.org.identifier;
|
||||
this.formPromise = this.apiService.putOrganization(this.organizationId, request).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
@@ -120,7 +124,14 @@ export class AccountComponent {
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.apiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.apiKeyModalRef);
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.keyType = 'organization';
|
||||
childComponent.entityId = this.organizationId;
|
||||
childComponent.postKey = this.apiService.postOrganizationApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api.organization';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'apiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'apiKeyDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
@@ -134,8 +145,16 @@ export class AccountComponent {
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.rotateApiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<RotateApiKeyComponent>(RotateApiKeyComponent, this.rotateApiKeyModalRef);
|
||||
childComponent.organizationId = this.organizationId;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateApiKeyModalRef);
|
||||
childComponent.keyType = 'organization';
|
||||
childComponent.isRotation = true;
|
||||
childComponent.entityId = this.organizationId;
|
||||
childComponent.postKey = this.apiService.postOrganizationRotateApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api.organization';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'apiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AdjustSeatsComponent {
|
||||
@Output() onAdjusted = new EventEmitter<number>();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
|
||||
seatAdjustment = 0;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
aria-hidden="true">×</span></button>
|
||||
<h2 class="card-body-header">{{'changeBillingPlan' | i18n}}</h2>
|
||||
<p class="mb-0">{{'changeBillingPlanUpgrade' | i18n}}</p>
|
||||
<app-organization-plans [showFree]="false" [showCancel]="true" plan="families" [organizationId]="organizationId"
|
||||
(onCanceled)="cancel()">
|
||||
<app-organization-plans [showFree]="false" [showCancel]="true" [plan]="defaultUpgradePlan"
|
||||
[product]="defaultUpgradeProduct" [organizationId]="organizationId" (onCanceled)="cancel()">
|
||||
</app-organization-plans>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { ProductType } from 'jslib/enums/productType';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-plan',
|
||||
templateUrl: 'change-plan.component.html',
|
||||
@@ -18,6 +21,8 @@ export class ChangePlanComponent {
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
defaultUpgradePlan: PlanType = PlanType.FamiliesAnnually;
|
||||
defaultUpgradeProduct: ProductType = ProductType.Families;
|
||||
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</app-callout>
|
||||
<dl *ngIf="selfHosted">
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<dd>{{sub.plan.name}}</dd>
|
||||
<dt>{{'expiration' | i18n}}</dt>
|
||||
<dd *ngIf="sub.expiration">
|
||||
{{sub.expiration | date:'mediumDate'}}
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="col-4">
|
||||
<dl>
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<dd>{{sub.plan.name}}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{'status' | i18n}}</dt>
|
||||
<dd>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
|
||||
@@ -38,10 +37,10 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
|
||||
constructor(private tokenService: TokenService, private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private messagingService: MessagingService, private route: ActivatedRoute) {
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private messagingService: MessagingService,
|
||||
private route: ActivatedRoute) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
@@ -192,34 +191,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
|
||||
get billingInterval() {
|
||||
const monthly = this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.TeamsMonthly;
|
||||
const monthly = !this.sub.plan.isAnnual;
|
||||
return monthly ? 'month' : 'year';
|
||||
}
|
||||
|
||||
get storageGbPrice() {
|
||||
return this.billingInterval === 'month' ? 0.5 : 4;
|
||||
return this.sub.plan.additionalStoragePricePerGb;
|
||||
}
|
||||
|
||||
get seatPrice() {
|
||||
switch (this.sub.planType) {
|
||||
case PlanType.EnterpriseMonthly:
|
||||
return 4;
|
||||
case PlanType.EnterpriseAnnually:
|
||||
return 36;
|
||||
case PlanType.TeamsMonthly:
|
||||
return 2.5;
|
||||
case PlanType.TeamsAnnually:
|
||||
return 24;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return this.sub.plan.seatPrice;
|
||||
}
|
||||
|
||||
get canAdjustSeats() {
|
||||
return this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually ||
|
||||
this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually;
|
||||
return this.sub.plan.hasAdditionalSeatsOption;
|
||||
}
|
||||
|
||||
get canDownloadLicense() {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="rotateKeyTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="rotateKeyTitle">{{'rotateApiKey' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'apiKeyRotateDesc' | i18n}}</p>
|
||||
<ng-container *ngIf="!clientSecret">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</ng-container>
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{'apiKeyWarning' | i18n}}</app-callout>
|
||||
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
|
||||
*ngIf="clientSecret">
|
||||
<p class="mb-1">
|
||||
<strong>client_id:</strong><br>
|
||||
<code>{{clientId}}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>client_secret:</strong><br>
|
||||
<code>{{clientSecret}}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>scope:</strong><br>
|
||||
<code>{{scope}}</code>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong>grant_type:</strong><br>
|
||||
<code>client_credentials</code>
|
||||
</p>
|
||||
</app-callout>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"
|
||||
*ngIf="!clientSecret">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'rotateApiKey' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
|
||||
|
||||
import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rotate-api-key',
|
||||
templateUrl: 'rotate-api-key.component.html',
|
||||
})
|
||||
export class RotateApiKeyComponent {
|
||||
organizationId: string;
|
||||
|
||||
masterPassword: string;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private router: Router) { }
|
||||
|
||||
async submit() {
|
||||
if (this.masterPassword == null || this.masterPassword === '') {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new PasswordVerificationRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
try {
|
||||
this.formPromise = this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
|
||||
const response = await this.formPromise;
|
||||
this.clientSecret = response.apiKey;
|
||||
this.clientId = 'organization.' + this.organizationId;
|
||||
this.scope = 'api.organization';
|
||||
this.analytics.eventTrack.next({ action: 'Rotated Organization API Key' });
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
@@ -82,10 +82,6 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
await this.resetPaging();
|
||||
}
|
||||
|
||||
checkCipher(c: CipherView) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
events(c: CipherView) {
|
||||
this.onEventsClicked.emit(c);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,15 @@
|
||||
</ng-container>
|
||||
</small>
|
||||
</h1>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()"
|
||||
*ngIf="!deleted">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [modal]="modal" [deleted]="deleted"
|
||||
[organization]="organization">
|
||||
</app-vault-bulk-actions>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()"
|
||||
*ngIf="!deleted">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
||||
|
||||
@@ -41,19 +41,19 @@ const BroadcasterSubscriptionId = 'OrgVaultComponent';
|
||||
templateUrl: 'vault.component.html',
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
|
||||
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
|
||||
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
|
||||
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
||||
@ViewChild('attachments', { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('collections', { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||
|
||||
organization: Organization;
|
||||
collectionId: string = null;
|
||||
type: CipherType = null;
|
||||
deleted: boolean = false;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
modal: ModalComponent = null;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
||||
|
||||
59
src/app/send/access.component.html
Normal file
59
src/app/send/access.component.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<form #form (ngSubmit)="load()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">Bitwarden Send</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body" *ngIf="loading" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!loading && passwordRequired">
|
||||
<p>{{'sendProtectedPassword' | i18n}}</p>
|
||||
<p>{{'sendProtectedPasswordDontKnow' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="password">{{'password' | i18n}}</label>
|
||||
<input id="password" type="password" name="Password" class="text-monospace form-control"
|
||||
[(ngModel)]="password" required appInputVerbatim appAutofocus>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||
<span>
|
||||
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!loading && !passwordRequired && send">
|
||||
<p class="text-center"><b>{{send.name}}</b></p>
|
||||
<hr>
|
||||
<!-- Text -->
|
||||
<ng-container *ngIf="send.type === sendType.Text">
|
||||
<app-callout *ngIf="send.text.hidden" type="tip">{{'sendHiddenByDefault' | i18n}}</app-callout>
|
||||
<div class="form-group">
|
||||
<textarea id="text" rows="8" name="Text" [(ngModel)]="sendText" class="form-control"
|
||||
readonly (click)="selectText()"></textarea>
|
||||
</div>
|
||||
<button class="btn btn-block btn-link" type="button" (click)="toggleText()"
|
||||
*ngIf="send.text.hidden">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showText, 'fa-eye-slash': showText}"></i>
|
||||
{{'toggleVisibility' | i18n}}
|
||||
</button>
|
||||
<button class="btn btn-block btn-link" type="button" (click)="copyText()">
|
||||
<i class="fa fa-copy" aria-hidden="true"></i> {{'copyValue' | i18n}}
|
||||
</button>
|
||||
</ng-container>
|
||||
<!-- File -->
|
||||
<ng-container *ngIf="send.type === sendType.File">
|
||||
<p>{{send.file.fileName}}</p>
|
||||
<button class="btn btn-primary btn-block" type="button" (click)="download()">
|
||||
<i class="fa fa-download" aria-hidden="true"></i>
|
||||
{{'downloadFile' | i18n}} ({{send.file.sizeName}})</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
139
src/app/send/access.component.ts
Normal file
139
src/app/send/access.component.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
import { SendAccess } from 'jslib/models/domain/sendAccess';
|
||||
|
||||
import { SendAccessView } from 'jslib/models/view/sendAccessView';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
import { SendAccessRequest } from 'jslib/models/request/sendAccessRequest';
|
||||
import { ErrorResponse } from 'jslib/models/response/errorResponse';
|
||||
|
||||
import { SendAccessResponse } from 'jslib/models/response/sendAccessResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-send-access',
|
||||
templateUrl: 'access.component.html',
|
||||
})
|
||||
export class AccessComponent implements OnInit {
|
||||
send: SendAccessView;
|
||||
sendType = SendType;
|
||||
downloading = false;
|
||||
loading = true;
|
||||
passwordRequired = false;
|
||||
formPromise: Promise<SendAccessResponse>;
|
||||
password: string;
|
||||
showText = false;
|
||||
|
||||
private id: string;
|
||||
private key: string;
|
||||
private decKey: SymmetricCryptoKey;
|
||||
|
||||
constructor(private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService,
|
||||
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||
private route: ActivatedRoute, private cryptoService: CryptoService) {
|
||||
}
|
||||
|
||||
get sendText() {
|
||||
if (this.send == null || this.send.text == null) {
|
||||
return null;
|
||||
}
|
||||
return this.showText ? this.send.text.text : this.send.text.maskedText;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.params.subscribe(async (params) => {
|
||||
this.id = params.sendId;
|
||||
this.key = params.key;
|
||||
if (this.key == null || this.id == null) {
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async download() {
|
||||
if (this.send == null || this.decKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.downloading = true;
|
||||
const response = await fetch(new Request(this.send.file.url, { cache: 'no-store' }));
|
||||
if (response.status !== 200) {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
|
||||
this.downloading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const buf = await response.arrayBuffer();
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, this.decKey);
|
||||
this.platformUtilsService.saveFile(window, decBuf, null, this.send.file.fileName);
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
|
||||
}
|
||||
|
||||
this.downloading = false;
|
||||
}
|
||||
|
||||
selectText() {
|
||||
(document.getElementById('text') as HTMLInputElement).select();
|
||||
}
|
||||
|
||||
copyText() {
|
||||
this.platformUtilsService.copyToClipboard(this.send.text.text);
|
||||
this.platformUtilsService.showToast('success', null,
|
||||
this.i18nService.t('valueCopied', this.i18nService.t('sendTypeText')));
|
||||
}
|
||||
|
||||
toggleText() {
|
||||
this.showText = !this.showText;
|
||||
}
|
||||
|
||||
async load() {
|
||||
const keyArray = Utils.fromUrlB64ToArray(this.key);
|
||||
const accessRequest = new SendAccessRequest();
|
||||
if (this.password != null) {
|
||||
const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password, keyArray, 'sha256', 100000);
|
||||
accessRequest.password = Utils.fromBufferToB64(passwordHash);
|
||||
}
|
||||
try {
|
||||
let sendResponse: SendAccessResponse = null;
|
||||
if (this.loading) {
|
||||
sendResponse = await this.apiService.postSendAccess(this.id, accessRequest);
|
||||
} else {
|
||||
this.formPromise = this.apiService.postSendAccess(this.id, accessRequest);
|
||||
sendResponse = await this.formPromise;
|
||||
}
|
||||
this.passwordRequired = false;
|
||||
const sendAccess = new SendAccess(sendResponse);
|
||||
this.decKey = await this.cryptoService.makeSendKey(keyArray);
|
||||
this.send = await sendAccess.decrypt(this.decKey);
|
||||
this.showText = this.send.text != null ? !this.send.text.hidden : true;
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse) {
|
||||
if (e.statusCode === 401) {
|
||||
this.passwordRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
129
src/app/send/add-edit.component.html
Normal file
129
src/app/send/add-edit.component.html
Normal file
@@ -0,0 +1,129 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="sendAddEditTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
|
||||
autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="sendAddEditTitle">{{title}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="send">
|
||||
<div class="row" *ngIf="!editMode">
|
||||
<div class="col-6 form-group">
|
||||
<label for="type">{{'whatTypeOfSend' | i18n}}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="send.type" class="form-control" appAutofocus>
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="send.name" required>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text -->
|
||||
<ng-container *ngIf="send.type === sendType.Text">
|
||||
<div class="form-group">
|
||||
<label for="text">{{'sendTypeText' | i18n}}</label>
|
||||
<textarea id="text" name="Text.Text" rows="6" [(ngModel)]="send.text.text"
|
||||
class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" [(ngModel)]="send.text.hidden"
|
||||
id="text-hidden" name="Text.Hidden">
|
||||
<label class="form-check-label" for="text-hidden">{{'cfTypeHidden' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- File -->
|
||||
<ng-container *ngIf="send.type === sendType.File">
|
||||
<div class="form-group">
|
||||
<div *ngIf="editMode">
|
||||
<strong class="d-block">{{'file' | i18n}}</strong>
|
||||
{{send.file.fileName}} ({{send.file.sizeName}})
|
||||
</div>
|
||||
<div *ngIf="!editMode">
|
||||
<label for="file">{{'file' | i18n}}</label>
|
||||
<input type="file" id="file" class="form-control-file" name="file" required>
|
||||
<small class="form-text text-muted">{{'maxFileSize' | i18n}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3 class="mt-4">{{'options' | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="deletionDate">{{'deletionDate' | i18n}}</label>
|
||||
<input id="deletionDate" class="form-control" type="datetime-local" name="DeletionDate"
|
||||
[(ngModel)]="deletionDate" required>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<div class="d-flex">
|
||||
<label for="expirationDate">{{'expirationDate' | i18n}}</label>
|
||||
<a href="#" appStopClick (click)="clearExpiration()" class="ml-auto">
|
||||
{{'clear' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<input id="expirationDate" class="form-control" type="datetime-local" name="ExpirationDate"
|
||||
[(ngModel)]="expirationDate">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
|
||||
<input id="maxAccessCount" class="form-control" type="number" name="MaxAccessCount"
|
||||
[(ngModel)]="send.maxAccessCount">
|
||||
</div>
|
||||
<div class="col-6 form-group" *ngIf="editMode">
|
||||
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
|
||||
<input id="accessCount" class="form-control" type="number" name="AccessCount" readonly
|
||||
[(ngModel)]="send.accessCount">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="password" *ngIf="!hasPassword">{{'password' | i18n}}</label>
|
||||
<label for="password" *ngIf="hasPassword">{{'newPassword' | i18n}}</label>
|
||||
<input id="password" class="form-control" type="password" name="Password"
|
||||
[(ngModel)]="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes">{{'notes' | i18n}}</label>
|
||||
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="send.notes" class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" [(ngModel)]="send.disabled" id="disabled"
|
||||
name="Disabled">
|
||||
<label class="form-check-label" for="disabled">{{'disabled' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="link">
|
||||
<label for="link">{{'sendLink' | i18n}}</label>
|
||||
<input type="text" readonly id="link" name="Link" [(ngModel)]="link" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
<div class="ml-auto" *ngIf="send">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
192
src/app/send/add-edit.component.ts
Normal file
192
src/app/send/add-edit.component.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
import {
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
|
||||
import { SendView } from 'jslib/models/view/sendView';
|
||||
import { SendFileView } from 'jslib/models/view/sendFileView';
|
||||
import { SendTextView } from 'jslib/models/view/sendTextView';
|
||||
|
||||
import { Send } from 'jslib/models/domain/send';
|
||||
|
||||
import { SendData } from 'jslib/models/data/sendData';
|
||||
|
||||
@Component({
|
||||
selector: 'app-send-add-edit',
|
||||
templateUrl: 'add-edit.component.html',
|
||||
})
|
||||
export class AddEditComponent {
|
||||
@Input() sendId: string;
|
||||
@Input() type: SendType;
|
||||
|
||||
@Output() onSavedSend = new EventEmitter<SendView>();
|
||||
@Output() onDeletedSend = new EventEmitter<SendView>();
|
||||
@Output() onCancelled = new EventEmitter<SendView>();
|
||||
|
||||
editMode: boolean = false;
|
||||
send: SendView;
|
||||
link: string;
|
||||
title: string;
|
||||
deletionDate: string;
|
||||
expirationDate: string;
|
||||
hasPassword: boolean;
|
||||
password: string;
|
||||
formPromise: Promise<any>;
|
||||
deletePromise: Promise<any>;
|
||||
sendType = SendType;
|
||||
typeOptions: any[];
|
||||
|
||||
constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
|
||||
private apiService: ApiService, private environmentService: EnvironmentService,
|
||||
private datePipe: DatePipe, private sendService: SendService) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t('sendTypeFile'), value: SendType.File },
|
||||
{ name: i18nService.t('sendTypeText'), value: SendType.Text },
|
||||
];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.editMode = this.sendId != null;
|
||||
if (this.editMode) {
|
||||
this.editMode = true;
|
||||
this.title = this.i18nService.t('editSend');
|
||||
} else {
|
||||
this.title = this.i18nService.t('createSend');
|
||||
}
|
||||
|
||||
if (this.send == null) {
|
||||
if (this.editMode) {
|
||||
const send = await this.loadSend();
|
||||
this.send = await send.decrypt();
|
||||
} else {
|
||||
this.send = new SendView();
|
||||
this.send.type = this.type == null ? SendType.File : this.type;
|
||||
this.send.file = new SendFileView();
|
||||
this.send.text = new SendTextView();
|
||||
this.send.deletionDate = new Date();
|
||||
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
|
||||
}
|
||||
}
|
||||
|
||||
this.hasPassword = this.send.password != null && this.send.password.trim() !== '';
|
||||
|
||||
// Parse dates
|
||||
this.deletionDate = this.send.deletionDate == null ? null :
|
||||
this.datePipe.transform(this.send.deletionDate, 'yyyy-MM-ddTHH:mm');
|
||||
this.expirationDate = this.send.expirationDate == null ? null :
|
||||
this.datePipe.transform(this.send.expirationDate, 'yyyy-MM-ddTHH:mm');
|
||||
|
||||
if (this.editMode) {
|
||||
let webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
if (webVaultUrl == null) {
|
||||
webVaultUrl = 'https://vault.bitwarden.com';
|
||||
}
|
||||
this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key;
|
||||
}
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
if (this.send.name == null || this.send.name === '') {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nameRequired'));
|
||||
return false;
|
||||
}
|
||||
|
||||
let file: File = null;
|
||||
if (this.send.type === SendType.File && !this.editMode) {
|
||||
const fileEl = document.getElementById('file') as HTMLInputElement;
|
||||
const files = fileEl.files;
|
||||
if (files == null || files.length === 0) {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('selectFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
file = files[0];
|
||||
if (file.size > 104857600) { // 100 MB
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('maxFileSize'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const encSend = await this.encryptSend(file);
|
||||
try {
|
||||
this.formPromise = this.sendService.saveWithServer(encSend);
|
||||
await this.formPromise;
|
||||
this.send.id = encSend[0].id;
|
||||
this.platformUtilsService.showToast('success', null,
|
||||
this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'));
|
||||
this.onSavedSend.emit(this.send);
|
||||
return true;
|
||||
} catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
clearExpiration() {
|
||||
this.expirationDate = null;
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
if (this.deletePromise != null) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('deleteSendConfirmation'),
|
||||
this.i18nService.t('deleteSend'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromise = this.apiService.deleteSend(this.send.id);
|
||||
await this.deletePromise;
|
||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
|
||||
await this.load();
|
||||
this.onDeletedSend.emit(this.send);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
protected async loadSend(): Promise<Send> {
|
||||
const response = await this.apiService.getSend(this.sendId);
|
||||
const data = new SendData(response);
|
||||
return new Send(data);
|
||||
}
|
||||
|
||||
protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> {
|
||||
const sendData = await this.sendService.encrypt(this.send, file, this.password, null);
|
||||
|
||||
// Parse dates
|
||||
try {
|
||||
sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
|
||||
} catch {
|
||||
sendData[0].deletionDate = null;
|
||||
}
|
||||
try {
|
||||
sendData[0].expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate);
|
||||
} catch {
|
||||
sendData[0].expirationDate = null;
|
||||
}
|
||||
|
||||
return sendData;
|
||||
}
|
||||
}
|
||||
113
src/app/send/send.component.html
Normal file
113
src/app/send/send.component.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3 groupings">
|
||||
<div class="card vault-filters">
|
||||
<div class="card-header d-flex">
|
||||
{{'filters' | i18n}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="search" placeholder="{{searchPlaceholder || ('searchSends' | i18n)}}" id="search"
|
||||
class="form-control" [(ngModel)]="searchText" (input)="searchTextChanged()" autocomplete="off"
|
||||
appAutofocus>
|
||||
<ul class="fa-ul card-ul">
|
||||
<li [ngClass]="{active: selectedAll}">
|
||||
<a href="#" appStopClick (click)="selectAll()">
|
||||
<i class="fa-li fa fa-fw fa-th"></i>{{'allSends' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{'types' | i18n}}</h3>
|
||||
<ul class="fa-ul card-ul">
|
||||
<li [ngClass]="{active: selectedType === sendType.Text}">
|
||||
<a href="#" appStopClick (click)="selectType(sendType.Text)">
|
||||
<i class="fa-li fa fa-fw fa-file-text-o"></i>{{'sendTypeText' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li [ngClass]="{active: selectedType === sendType.File}">
|
||||
<a href="#" appStopClick (click)="selectType(sendType.File)">
|
||||
<i class="fa-li fa fa-fw fa-file-o"></i>{{'sendTypeFile' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="page-header d-flex">
|
||||
<h1>
|
||||
Send
|
||||
<small #actionSpinner [appApiAction]="actionPromise">
|
||||
<ng-container *ngIf="actionSpinner.loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addSend()">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'createSend' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!--Listing Table-->
|
||||
<table class="table table-hover table-list" *ngIf="filteredSends && filteredSends.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let s of filteredSends">
|
||||
<td class="table-list-icon">
|
||||
<div class="icon" aria-hidden="true">
|
||||
<i class="fa fa-fw fa-lg fa-file-o" *ngIf="s.type == sendType.File"></i>
|
||||
<i class="fa fa-fw fa-lg fa-file-text-o" *ngIf="s.type == sendType.Text"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick appStopProp (click)="editSend(s)">{{s.name}}</a>
|
||||
<ng-container *ngIf="s.password">
|
||||
<i class="fa fa-key" appStopProp title="{{'password' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'password' | i18n}}</span>
|
||||
</ng-container>
|
||||
<br>
|
||||
<small appStopProp>{{s.deletionDate | date:'medium'}}</small>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="copy(s)">
|
||||
<i class="fa fa-fw fa-copy" aria-hidden="true"></i>
|
||||
{{'copySendLink' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="removePassword(s)"
|
||||
*ngIf="s.password">
|
||||
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||
{{'removePassword' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(s)">
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{'delete' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="no-items" *ngIf="filteredSends && !filteredSends.length">
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="loaded">
|
||||
<p>{{'noSendsInList' | i18n}}</p>
|
||||
<button (click)="addSend()" class="btn btn-outline-primary">
|
||||
<i class="fa fa-plus fa-fw"></i>{{'createSend' | i18n}}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #sendAddEdit></ng-template>
|
||||
207
src/app/send/send.component.ts
Normal file
207
src/app/send/send.component.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
|
||||
import { SendView } from 'jslib/models/view/sendView';
|
||||
|
||||
import { AddEditComponent } from './add-edit.component';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-send',
|
||||
templateUrl: 'send.component.html',
|
||||
})
|
||||
export class SendComponent implements OnInit {
|
||||
@ViewChild('sendAddEdit', { read: ViewContainerRef, static: true }) sendAddEditModalRef: ViewContainerRef;
|
||||
|
||||
sendType = SendType;
|
||||
loaded = false;
|
||||
loading = true;
|
||||
refreshing = false;
|
||||
expired: boolean = false;
|
||||
type: SendType = null;
|
||||
sends: SendView[] = [];
|
||||
filteredSends: SendView[] = [];
|
||||
searchText: string;
|
||||
selectedType: SendType;
|
||||
selectedAll: boolean;
|
||||
searchPlaceholder: string;
|
||||
filter: (cipher: SendView) => boolean;
|
||||
searchPending = false;
|
||||
|
||||
modal: ModalComponent = null;
|
||||
actionPromise: any;
|
||||
|
||||
private searchTimeout: any;
|
||||
|
||||
constructor(private apiService: ApiService, private sendService: SendService,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
async load(filter: (send: SendView) => boolean = null) {
|
||||
this.loading = true;
|
||||
const sends = await this.sendService.getAllDecrypted();
|
||||
this.sends = sends;
|
||||
this.selectAll();
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async reload(filter: (send: SendView) => boolean = null) {
|
||||
this.loaded = false;
|
||||
this.sends = [];
|
||||
await this.load(filter);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
this.refreshing = true;
|
||||
await this.reload(this.filter);
|
||||
} finally {
|
||||
this.refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async applyFilter(filter: (send: SendView) => boolean = null) {
|
||||
this.filter = filter;
|
||||
await this.search(null);
|
||||
}
|
||||
|
||||
async search(timeout: number = null) {
|
||||
this.searchPending = false;
|
||||
if (this.searchTimeout != null) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
if (timeout == null) {
|
||||
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||
return;
|
||||
}
|
||||
this.searchPending = true;
|
||||
this.searchTimeout = setTimeout(async () => {
|
||||
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||
this.searchPending = false;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
addSend() {
|
||||
const component = this.editSend(null);
|
||||
component.type = this.type;
|
||||
}
|
||||
|
||||
editSend(send: SendView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.sendAddEditModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<AddEditComponent>(
|
||||
AddEditComponent, this.sendAddEditModalRef);
|
||||
|
||||
childComponent.sendId = send == null ? null : send.id;
|
||||
childComponent.onSavedSend.subscribe(async (s: SendView) => {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
childComponent.onDeletedSend.subscribe(async (s: SendView) => {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
async removePassword(s: SendView): Promise<boolean> {
|
||||
if (this.actionPromise != null || s.password == null) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'),
|
||||
this.i18nService.t('removePassword'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.apiService.putSendRemovePassword(s.id);
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword'));
|
||||
await this.load();
|
||||
} catch { }
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async delete(s: SendView): Promise<boolean> {
|
||||
if (this.actionPromise != null) {
|
||||
return false;
|
||||
}
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('deleteSendConfirmation'),
|
||||
this.i18nService.t('deleteSend'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.apiService.deleteSend(s.id);
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
|
||||
await this.load();
|
||||
} catch { }
|
||||
this.actionPromise = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
copy(s: SendView) {
|
||||
let webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
if (webVaultUrl == null) {
|
||||
webVaultUrl = 'https://vault.bitwarden.com';
|
||||
}
|
||||
const link = webVaultUrl + '/#/send/' + s.accessId + '/' + s.urlB64Key;
|
||||
this.platformUtilsService.copyToClipboard(link);
|
||||
this.platformUtilsService.showToast('success', null,
|
||||
this.i18nService.t('valueCopied', this.i18nService.t('sendLink')));
|
||||
}
|
||||
|
||||
searchTextChanged() {
|
||||
this.search(200);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
this.clearSelections();
|
||||
this.selectedAll = true;
|
||||
this.applyFilter(null);
|
||||
}
|
||||
|
||||
selectType(type: SendType) {
|
||||
this.clearSelections();
|
||||
this.selectedType = type;
|
||||
this.applyFilter((s) => s.type === type);
|
||||
}
|
||||
|
||||
clearSelections() {
|
||||
this.selectedAll = false;
|
||||
this.selectedType = null;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import { NotificationsService } from 'jslib/services/notifications.service';
|
||||
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
|
||||
import { PolicyService } from 'jslib/services/policy.service';
|
||||
import { SearchService } from 'jslib/services/search.service';
|
||||
import { SendService } from 'jslib/services/send.service';
|
||||
import { SettingsService } from 'jslib/services/settings.service';
|
||||
import { StateService } from 'jslib/services/state.service';
|
||||
import { SyncService } from 'jslib/services/sync.service';
|
||||
@@ -74,6 +75,7 @@ import {
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService as PolicyServiceAbstraction } from 'jslib/abstractions/policy.service';
|
||||
import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.service';
|
||||
import { SendService as SendServiceAbstraction } from 'jslib/abstractions/send.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';
|
||||
@@ -93,7 +95,7 @@ const secureStorageService: StorageServiceAbstraction = new MemoryStorageService
|
||||
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new WebCryptoFunctionService(window,
|
||||
platformUtilsService);
|
||||
const cryptoService = new CryptoService(storageService,
|
||||
platformUtilsService.isDev() ? storageService : secureStorageService, cryptoFunctionService);
|
||||
platformUtilsService.isDev() ? storageService : secureStorageService, cryptoFunctionService, platformUtilsService);
|
||||
const tokenService = new TokenService(storageService);
|
||||
const appIdService = new AppIdService(storageService);
|
||||
const apiService = new ApiService(tokenService, platformUtilsService,
|
||||
@@ -106,19 +108,21 @@ const cipherService = new CipherService(cryptoService, userService, settingsServ
|
||||
const folderService = new FolderService(cryptoService, userService, apiService, storageService,
|
||||
i18nService, cipherService);
|
||||
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
|
||||
searchService = new SearchService(cipherService, platformUtilsService);
|
||||
searchService = new SearchService(cipherService);
|
||||
const policyService = new PolicyService(userService, storageService);
|
||||
const sendService = new SendService(cryptoService, userService, apiService, storageService,
|
||||
i18nService, cryptoFunctionService);
|
||||
const vaultTimeoutService = new VaultTimeoutService(cipherService, folderService, collectionService,
|
||||
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, tokenService,
|
||||
null, async () => messagingService.send('logout', { expired: false }));
|
||||
const syncService = new SyncService(userService, apiService, settingsService,
|
||||
folderService, cipherService, cryptoService, collectionService, storageService, messagingService, policyService,
|
||||
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
|
||||
sendService, async (expired: boolean) => messagingService.send('logout', { expired: expired }));
|
||||
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, policyService);
|
||||
const totpService = new TotpService(storageService, cryptoFunctionService);
|
||||
const containerService = new ContainerService(cryptoService);
|
||||
const authService = new AuthService(cryptoService, apiService,
|
||||
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService);
|
||||
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService);
|
||||
const exportService = new ExportService(folderService, cipherService, apiService);
|
||||
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService);
|
||||
const notificationsService = new NotificationsService(userService, syncService, appIdService,
|
||||
@@ -140,8 +144,8 @@ export function initFactory(): Function {
|
||||
} else {
|
||||
environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
|
||||
'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
|
||||
environmentService.enterpriseUrl = isDev ? 'http://localhost:61840' :
|
||||
'https://enterprise.bitwarden.com'; // window.location.origin + '/enterprise';
|
||||
environmentService.enterpriseUrl = isDev ? 'http://localhost:52313' :
|
||||
'https://portal.bitwarden.com'; // window.location.origin + '/portal';
|
||||
}
|
||||
apiService.setUrls({
|
||||
base: isDev ? null : window.location.origin,
|
||||
@@ -218,6 +222,7 @@ export function initFactory(): Function {
|
||||
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
|
||||
{ provide: EventLoggingServiceAbstraction, useValue: eventLoggingService },
|
||||
{ provide: PolicyServiceAbstraction, useValue: policyService },
|
||||
{ provide: SendServiceAbstraction, useValue: sendService },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initFactory,
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
<h1>{{'encKeySettings' | i18n}}</h1>
|
||||
</div>
|
||||
<app-change-kdf></app-change-kdf>
|
||||
<div class="secondary-header border-0 mb-0">
|
||||
<h1>{{'apiKey' | i18n}}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{'userApiKeyDesc' | i18n}}
|
||||
</p>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="viewUserApiKey()">{{'viewApiKey' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="rotateUserApiKey()">{{'rotateApiKey' | i18n}}</button>
|
||||
<div class="secondary-header text-danger border-0 mb-0">
|
||||
<h1>{{'dangerZone' | i18n}}</h1>
|
||||
</div>
|
||||
@@ -30,3 +38,5 @@
|
||||
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
||||
<ng-template #purgeVaultTemplate></ng-template>
|
||||
<ng-template #deleteAccountTemplate></ng-template>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
|
||||
@@ -6,22 +6,29 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
import { ApiKeyComponent } from './api-key.component';
|
||||
import { DeauthorizeSessionsComponent } from './deauthorize-sessions.component';
|
||||
import { DeleteAccountComponent } from './delete-account.component';
|
||||
import { PurgeVaultComponent } from './purge-vault.component';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: 'account.component.html',
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef }) deauthModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeVaultTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('deleteAccountTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef, static: true }) deauthModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeVaultTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('deleteAccountTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('viewUserApiKeyTemplate', { read: ViewContainerRef, static: true }) viewUserApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('rotateUserApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateUserApiKeyModalRef: ViewContainerRef;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver, private apiService: ApiService,
|
||||
private userService: UserService) { }
|
||||
|
||||
deauthorizeSessions() {
|
||||
if (this.modal != null) {
|
||||
@@ -64,4 +71,49 @@ export class AccountComponent {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
async viewUserApiKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.viewUserApiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.viewUserApiKeyModalRef);
|
||||
childComponent.keyType = 'user';
|
||||
childComponent.entityId = await this.userService.getUserId();
|
||||
childComponent.postKey = this.apiService.postUserApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'userApiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'userApiKeyDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
async rotateUserApiKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.rotateUserApiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateUserApiKeyModalRef);
|
||||
childComponent.keyType = 'user';
|
||||
childComponent.isRotation = true;
|
||||
childComponent.entityId = await this.userService.getUserId();
|
||||
childComponent.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'userApiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AddCreditComponent implements OnInit {
|
||||
@Output() onAdded = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild('ppButtonForm', { read: ElementRef }) ppButtonFormRef: ElementRef;
|
||||
@ViewChild('ppButtonForm', { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
|
||||
|
||||
paymentMethodType = PaymentMethodType;
|
||||
ppButtonFormAction = WebConstants.paypal.buttonActionProduction;
|
||||
|
||||
@@ -24,8 +24,8 @@ import { TaxInfoComponent } from './tax-info.component';
|
||||
templateUrl: 'adjust-payment.component.html',
|
||||
})
|
||||
export class AdjustPaymentComponent {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
|
||||
|
||||
@Input() currentType?: PaymentMethodType;
|
||||
@Input() organizationId: string;
|
||||
|
||||
@@ -35,7 +35,7 @@ export class AdjustStorageComponent {
|
||||
@Output() onAdjusted = new EventEmitter<number>();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
|
||||
storageAdjustment = 0;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="apiKeyTitle">{{'apiKey' | i18n}}</h2>
|
||||
<h2 class="modal-title" id="apiKeyTitle">{{apiKeyTitle | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'apiKeyDesc' | i18n}}</p>
|
||||
<p>{{apiKeyDescription | i18n}}</p>
|
||||
<ng-container *ngIf="!clientSecret">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</ng-container>
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{'apiKeyWarning' | i18n}}</app-callout>
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{apiKeyWarning | i18n}}</app-callout>
|
||||
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
|
||||
*ngIf="clientSecret">
|
||||
<p class="mb-1">
|
||||
@@ -31,7 +31,7 @@
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong>grant_type:</strong><br>
|
||||
<code>client_credentials</code>
|
||||
<code>{{grantType}}</code>
|
||||
</p>
|
||||
</app-callout>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"
|
||||
*ngIf="!clientSecret">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'viewApiKey' | i18n}}</span>
|
||||
<span>{{(isRotation ? 'rotateApiKey' : 'viewApiKey') | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
@@ -17,17 +15,23 @@ import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
|
||||
templateUrl: 'api-key.component.html',
|
||||
})
|
||||
export class ApiKeyComponent {
|
||||
organizationId: string;
|
||||
keyType: string;
|
||||
isRotation: boolean;
|
||||
postKey: (entityId: string, request: PasswordVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
entityId: string;
|
||||
scope: string;
|
||||
grantType: string;
|
||||
apiKeyTitle: string;
|
||||
apiKeyWarning: string;
|
||||
apiKeyDescription: string;
|
||||
|
||||
masterPassword: string;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private router: Router) { }
|
||||
constructor(private i18nService: I18nService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private cryptoService: CryptoService) { }
|
||||
|
||||
async submit() {
|
||||
if (this.masterPassword == null || this.masterPassword === '') {
|
||||
@@ -39,12 +43,11 @@ export class ApiKeyComponent {
|
||||
const request = new PasswordVerificationRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
try {
|
||||
this.formPromise = this.apiService.postOrganizationApiKey(this.organizationId, request);
|
||||
this.formPromise = this.postKey(this.entityId, request);
|
||||
const response = await this.formPromise;
|
||||
this.clientSecret = response.apiKey;
|
||||
this.clientId = 'organization.' + this.organizationId;
|
||||
this.scope = 'api.organization';
|
||||
this.analytics.eventTrack.next({ action: 'Viewed Organization API Key' });
|
||||
this.clientId = `${this.keyType}.${this.entityId}`;
|
||||
this.analytics.eventTrack.next({ action: `Viewed ${this.keyType} API Key` });
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
@@ -28,18 +28,18 @@
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="newMasterPassword">{{'newMasterPass' | i18n}}</label>
|
||||
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
|
||||
[(ngModel)]="newMasterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
|
||||
<label for="masterPassword">{{'newMasterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
|
||||
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true"></app-password-strength>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="confirmNewMasterPassword">{{'confirmNewMasterPass' | i18n}}</label>
|
||||
<input id="confirmNewMasterPassword" type="password" name="ConfirmNewMasterPasswordHash"
|
||||
class="form-control" [(ngModel)]="confirmNewMasterPassword" required appInputVerbatim
|
||||
<label for="masterPasswordRetype">{{'confirmNewMasterPass' | i18n}}</label>
|
||||
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
|
||||
class="form-control" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
@@ -18,8 +12,11 @@ import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
ChangePasswordComponent as BaseChangePasswordComponent,
|
||||
} from 'jslib/angular/components/change-password.component';
|
||||
|
||||
import { CipherString } from 'jslib/models/domain/cipherString';
|
||||
import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordPolicyOptions';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
|
||||
import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest';
|
||||
@@ -31,136 +28,18 @@ import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
|
||||
selector: 'app-change-password',
|
||||
templateUrl: 'change-password.component.html',
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
currentMasterPassword: string;
|
||||
newMasterPassword: string;
|
||||
confirmNewMasterPassword: string;
|
||||
formPromise: Promise<any>;
|
||||
masterPasswordScore: number;
|
||||
export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
rotateEncKey = false;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
currentMasterPassword: string;
|
||||
|
||||
private masterPasswordStrengthTimeout: any;
|
||||
private email: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private messagingService: MessagingService,
|
||||
private userService: UserService, private passwordGenerationService: PasswordGenerationService,
|
||||
private platformUtilsService: PlatformUtilsService, private folderService: FolderService,
|
||||
private cipherService: CipherService, private syncService: SyncService,
|
||||
private policyService: PolicyService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await this.userService.getEmail();
|
||||
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
|
||||
}
|
||||
|
||||
getPasswordScoreAlertDisplay() {
|
||||
if (this.enforcedPolicyOptions == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let str: string;
|
||||
switch (this.enforcedPolicyOptions.minComplexity) {
|
||||
case 4:
|
||||
str = this.i18nService.t('strong');
|
||||
break;
|
||||
case 3:
|
||||
str = this.i18nService.t('good');
|
||||
break;
|
||||
default:
|
||||
str = this.i18nService.t('weak');
|
||||
break;
|
||||
}
|
||||
return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')';
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const hasEncKey = await this.cryptoService.hasEncKey();
|
||||
if (!hasEncKey) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('updateKey'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === '' ||
|
||||
this.newMasterPassword == null || this.newMasterPassword === '') {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassRequired'));
|
||||
return;
|
||||
}
|
||||
if (this.newMasterPassword.length < 8) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassLength'));
|
||||
return;
|
||||
}
|
||||
if (this.newMasterPassword !== this.confirmNewMasterPassword) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassDoesntMatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
|
||||
this.getPasswordStrengthUserInput());
|
||||
|
||||
if (this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
strengthResult.score,
|
||||
this.newMasterPassword,
|
||||
this.enforcedPolicyOptions)) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (strengthResult != null && strengthResult.score < 3) {
|
||||
const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'),
|
||||
this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'),
|
||||
'warning');
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.rotateEncKey) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
|
||||
const email = await this.userService.getEmail();
|
||||
const kdf = await this.userService.getKdf();
|
||||
const kdfIterations = await this.userService.getKdfIterations();
|
||||
const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email.trim().toLowerCase(),
|
||||
kdf, kdfIterations);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.newMasterPassword, newKey);
|
||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
try {
|
||||
if (this.rotateEncKey) {
|
||||
this.formPromise = this.apiService.postPassword(request).then(() => {
|
||||
return this.updateKey(newKey, request.newMasterPasswordHash);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Changed Password' });
|
||||
this.toasterService.popAsync('success', this.i18nService.t('masterPasswordChanged'),
|
||||
this.i18nService.t('logBackIn'));
|
||||
this.messagingService.send('logout');
|
||||
} catch { }
|
||||
}
|
||||
|
||||
updatePasswordStrength() {
|
||||
if (this.masterPasswordStrengthTimeout != null) {
|
||||
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||
}
|
||||
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
|
||||
this.getPasswordStrengthUserInput());
|
||||
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
}, 300);
|
||||
constructor(i18nService: I18nService,
|
||||
cryptoService: CryptoService, messagingService: MessagingService,
|
||||
userService: UserService, passwordGenerationService: PasswordGenerationService,
|
||||
platformUtilsService: PlatformUtilsService, policyService: PolicyService,
|
||||
private folderService: FolderService, private cipherService: CipherService,
|
||||
private syncService: SyncService, private apiService: ApiService, ) {
|
||||
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
|
||||
platformUtilsService, policyService);
|
||||
}
|
||||
|
||||
async rotateEncKeyClicked() {
|
||||
@@ -198,13 +77,54 @@ export class ChangePasswordComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private getPasswordStrengthUserInput() {
|
||||
let userInput: string[] = [];
|
||||
const atPosition = this.email.indexOf('@');
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/));
|
||||
async submit() {
|
||||
const hasEncKey = await this.cryptoService.hasEncKey();
|
||||
if (!hasEncKey) {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('updateKey'));
|
||||
return;
|
||||
}
|
||||
|
||||
await super.submit();
|
||||
}
|
||||
|
||||
async setupSubmitActions() {
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === '') {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassRequired'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.rotateEncKey) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
return super.setupSubmitActions();
|
||||
}
|
||||
|
||||
async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey,
|
||||
newEncKey: [SymmetricCryptoKey, CipherString]) {
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
|
||||
request.newMasterPasswordHash = newMasterPasswordHash;
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
|
||||
try {
|
||||
if (this.rotateEncKey) {
|
||||
this.formPromise = this.apiService.postPassword(request).then(() => {
|
||||
return this.updateKey(newKey, request.newMasterPasswordHash);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
|
||||
this.platformUtilsService.showToast('success', this.i18nService.t('masterPasswordChanged'),
|
||||
this.i18nService.t('logBackIn'));
|
||||
this.messagingService.send('logout');
|
||||
} catch {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
|
||||
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { OrganizationPlansComponent } from './organization-plans.component';
|
||||
templateUrl: 'create-organization.component.html',
|
||||
})
|
||||
export class CreateOrganizationComponent implements OnInit {
|
||||
@ViewChild(OrganizationPlansComponent) orgPlansComponent: OrganizationPlansComponent;
|
||||
@ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
|
||||
|
||||
constructor(private route: ActivatedRoute) { }
|
||||
|
||||
|
||||
4
src/app/settings/link-sso.component.html
Normal file
4
src/app/settings/link-sso.component.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="submit(returnUri, true)">
|
||||
<i class="fa fa-fw fa-link" aria-hidden="true"></i>
|
||||
{{'linkSso' | i18n}}
|
||||
</a>
|
||||
48
src/app/settings/link-sso.component.ts
Normal file
48
src/app/settings/link-sso.component.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
Component,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { SsoComponent } from 'jslib/angular/components/sso.component';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
@Component({
|
||||
selector: 'app-link-sso',
|
||||
templateUrl: 'link-sso.component.html',
|
||||
})
|
||||
export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
|
||||
@Input() organization: Organization;
|
||||
returnUri: string = '/settings/organizations'
|
||||
|
||||
constructor(platformUtilsService: PlatformUtilsService, i18nService: I18nService,
|
||||
apiService: ApiService, authService: AuthService,
|
||||
router: Router, route: ActivatedRoute,
|
||||
cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService,
|
||||
storageService: StorageService, stateService: StateService) {
|
||||
super(authService, router,
|
||||
i18nService, route,
|
||||
storageService, stateService,
|
||||
platformUtilsService, apiService,
|
||||
cryptoFunctionService, passwordGenerationService);
|
||||
|
||||
this.returnUri = '/settings/organizations';
|
||||
this.redirectUri = window.location.origin + '/sso-connector.html';
|
||||
this.clientId = 'web';
|
||||
}
|
||||
|
||||
async ngAfterContentInit() {
|
||||
this.identifier = this.organization.identifier;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
<ng-container *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="createOrganization && selfHosted">
|
||||
<p>{{'uploadLicenseFileOrg' | i18n}}</p>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
@@ -13,7 +17,8 @@
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
|
||||
*ngIf="!loading && !selfHosted && this.plans">
|
||||
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
||||
<div class="row" *ngIf="createOrganization">
|
||||
<div class="form-group col-6">
|
||||
@@ -38,69 +43,61 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
|
||||
<div class="form-check form-check-block" *ngIf="!ownedBusiness && showFree">
|
||||
<input class="form-check-input" type="radio" name="PlanType" id="planFree" value="free" [(ngModel)]="plan"
|
||||
(change)="changedPlan()">
|
||||
<label class="form-check-label" for="planFree">
|
||||
{{'planNameFree' | i18n}}
|
||||
<small class="mb-1">{{'planDescFree' | i18n : '1'}}</small>
|
||||
<small>• {{'limitedUsers' | i18n : '2'}}</small>
|
||||
<small>• {{'limitedCollections' | i18n : '2'}}</small>
|
||||
<span>{{'freeForever' | i18n}}</span>
|
||||
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="product" id="product{{selectableProduct.product}}"
|
||||
[value]="selectableProduct.product" [(ngModel)]="product" (change)="changedProduct()">
|
||||
<label class="form-check-label" for="product{{selectableProduct.product}}">
|
||||
{{ selectableProduct.nameLocalizationKey | i18n}}
|
||||
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n : '1'}}</small>
|
||||
<ng-container *ngIf="selectableProduct.product === productTypes.Enterprise; else fullFeatureList">
|
||||
<small>• {{'includeAllTeamsFeatures' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasSelfHost">• {{'onPremHostingOptional' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasSso">• {{'includeSsoAuthentication' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasPolicies">• {{'includeEnterprisePolicies' | i18n}}</small>
|
||||
</ng-container>
|
||||
<ng-template #fullFeatureList>
|
||||
<small *ngIf="selectableProduct.product == productTypes.Free">•
|
||||
{{'limitedUsers' | i18n : selectableProduct.maxUsers }}</small>
|
||||
<small *ngIf="selectableProduct.product != productTypes.Free && selectableProduct.maxUsers">•
|
||||
{{'addShareLimitedUsers' | i18n : selectableProduct.maxUsers}}</small>
|
||||
<small *ngIf="!selectableProduct.maxUsers">•
|
||||
{{'addShareUnlimitedUsers' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.maxCollections">•
|
||||
{{'limitedCollections' | i18n : selectableProduct.maxCollections }}</small>
|
||||
<small *ngIf="selectableProduct.maxAdditionalSeats">•
|
||||
{{'addShareLimitedUsers' | i18n : selectableProduct.maxAdditionalSeats }}</small>
|
||||
<small *ngIf="!selectableProduct.maxCollections">• {{'createUnlimitedCollections' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.baseStorageGb">•
|
||||
{{'gbEncryptedFileStorage' | i18n : selectableProduct.baseStorageGb + 'GB'}}</small>
|
||||
<small *ngIf="selectableProduct.hasGroups">• {{'controlAccessWithGroups' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasApi">• {{'trackAuditLogs' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasDirectory">• {{'syncUsersFromDirectory' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasSelfHost">• {{'onPremHostingOptional' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.usersGetPremium">• {{'usersGetPremium' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.product != productTypes.Free">•
|
||||
{{'priorityCustomerSupport' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.trialPeriodDays">•
|
||||
{{'xDayFreeTrial' | i18n : selectableProduct.trialPeriodDays }}
|
||||
</small>
|
||||
</ng-template>
|
||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
||||
<ng-container *ngIf="selectableProduct.basePrice">
|
||||
{{selectableProduct.basePrice / 12 | currency:'$'}} /{{'month' | i18n}},
|
||||
{{'includesXUsers' | i18n : selectableProduct.baseSeats}}
|
||||
<ng-container *ngIf="selectableProduct.hasAdditionalSeatsOption">
|
||||
{{('additionalUsers' | i18n).toLowerCase()}}
|
||||
{{selectableProduct.seatPrice / 12 | currency:'$'}} /{{'month' | i18n}}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</span>
|
||||
<span *ngIf="!selectableProduct.basePrice && selectableProduct.hasAdditionalSeatsOption">
|
||||
{{'costPerUser' | i18n : (selectableProduct.seatPrice / 12 | currency:'$')}} /{{'month' | i18n}}
|
||||
</span>
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{'freeForever' | i18n}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
|
||||
<input class="form-check-input" type="radio" name="PlanType" id="planFamilies" value="families"
|
||||
[(ngModel)]="plan" (change)="changedPlan()">
|
||||
<label class="form-check-label" for="planFamilies">
|
||||
{{'planNameFamilies' | i18n}}
|
||||
<small class="mb-1">{{'planDescFamilies' | i18n}}</small>
|
||||
<small>• {{'addShareLimitedUsers' | i18n : '5'}}</small>
|
||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||
<small>• {{'onPremHostingOptional' | i18n}}</small>
|
||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||
<span>{{1 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="PlanType" id="planTeams" value="teams" [(ngModel)]="plan"
|
||||
(change)="changedPlan()">
|
||||
<label class="form-check-label" for="planTeams">
|
||||
{{'planNameTeams' | i18n}}
|
||||
<small class="mb-1">{{'planDescTeams' | i18n}}</small>
|
||||
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
|
||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||
<span>{{5 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}},
|
||||
{{('additionalUsers' | i18n).toLowerCase()}}
|
||||
{{2 | currency:'$'}} /{{'month' | i18n}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="PlanType" id="planEnterprise" value="enterprise"
|
||||
[(ngModel)]="plan" (change)="changedPlan()">
|
||||
<label class="form-check-label" for="planEnterprise">
|
||||
{{'planNameEnterprise' | i18n}}
|
||||
<small class="mb-1">{{'planDescEnterprise' | i18n}}</small>
|
||||
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
|
||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||
<small>• {{'controlAccessWithGroups' | i18n}}</small>
|
||||
<small>• {{'trackAuditLogs' | i18n}}</small>
|
||||
<small>• {{'syncUsersFromDirectory' | i18n}}</small>
|
||||
<small>• {{'onPremHostingOptional' | i18n}}</small>
|
||||
<small>• {{'usersGetPremium' | i18n}}</small>
|
||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||
<span>{{'costPerUser' | i18n : (3 | currency:'$')}} /{{'month' | i18n}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<ng-container *ngIf="!plans[plan].noPayment">
|
||||
<ng-container *ngIf="!plans[plan].noAdditionalSeats && !plans[plan].baseSeats">
|
||||
<div *ngIf="product !== productTypes.Free">
|
||||
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
|
||||
<h2 class="mt-5">{{'users' | i18n}}</h2>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
@@ -113,13 +110,13 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="mt-5">{{'addons' | i18n}}</h2>
|
||||
<div class="row" *ngIf="!plans[plan].noAdditionalSeats && plans[plan].baseSeats">
|
||||
<div class="row" *ngIf="selectedPlan.hasAdditionalSeatsOption && selectedPlan.baseSeats">
|
||||
<div class="form-group col-6">
|
||||
<label for="additionalSeats">{{'additionalUserSeats' | i18n}}</label>
|
||||
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
|
||||
[(ngModel)]="additionalSeats" min="0" max="100000" placeholder="{{'userSeatsDesc' | i18n}}">
|
||||
<small
|
||||
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : plans[plan].baseSeats : (plans[plan].seatPrice | currency:'$')}}</small>
|
||||
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : selectedPlan.baseSeats : (seatPriceMonthly(selectedPlan) | currency:'$')}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -129,11 +126,11 @@
|
||||
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
|
||||
placeholder="{{'additionalStorageGbDesc' | i18n}}">
|
||||
<small
|
||||
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGb.price | currency:'$') : ('month' | i18n)}}</small>
|
||||
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (additionalStoragePriceMonthly(selectedPlan) | currency:'$') : ('month' | i18n)}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
|
||||
<div class="form-group col-6" *ngIf="selectedPlan.hasPremiumAccessOption">
|
||||
<div class="form-check">
|
||||
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon"
|
||||
[(ngModel)]="premiumAccessAddon">
|
||||
@@ -144,62 +141,77 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
|
||||
<div class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalAnnually" value="year"
|
||||
[(ngModel)]="interval">
|
||||
<label class="form-check-label" for="intervalAnnually">
|
||||
{{'annually' | i18n}}
|
||||
<small *ngIf="plans[plan].annualBasePrice">
|
||||
{{'basePrice' | i18n}}: {{plans[plan].basePrice | currency:'$'}} ×12 {{'monthAbbr' | i18n}} =
|
||||
{{baseTotal(true) | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="!plans[plan].noAdditionalSeats">
|
||||
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{plans[plan].seatPrice | currency:'$'}} ×12
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(true)
|
||||
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
|
||||
<input class="form-check-input" type="radio" name="BillingInterval" id="interval{{selectablePlan.type}}"
|
||||
[value]="selectablePlan.type" [(ngModel)]="plan">
|
||||
<label class="form-check-label" for="interval{{selectablePlan.type}}">
|
||||
<ng-container *ngIf="selectablePlan.isAnnual">
|
||||
{{'annually' | i18n}}
|
||||
<small *ngIf="selectablePlan.basePrice">
|
||||
{{'basePrice' | i18n}}: {{ selectablePlan.basePrice / 12 | currency:'$'}} × 12
|
||||
{{'monthAbbr' | i18n}}
|
||||
=
|
||||
{{selectablePlan.basePrice | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
|
||||
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{selectablePlan.seatPrice / 12 | currency:'$'}} × 12
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
|
||||
| currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
<small>
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{storageGb.price | currency:'$'}} ×12 {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{3.33 | currency:'$'}} ×12 {{'monthAbbr' | i18n}} = {{40 | currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-block" *ngIf="plans[plan].monthlySeatPrice">
|
||||
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalMonthly" value="month"
|
||||
[(ngModel)]="interval">
|
||||
<label class="form-check-label" for="intervalMonthly">
|
||||
{{'monthly' | i18n}}
|
||||
<small *ngIf="plans[plan].monthlyBasePrice">
|
||||
{{'basePrice' | i18n}}: {{baseTotal(false) | currency:'$'}} /{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="!plans[plan].noAdditionalSeats">
|
||||
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{plans[plan].monthlySeatPrice | currency:'$'}} =
|
||||
{{seatTotal(false) | currency:'$'}} /{{'month'
|
||||
| i18n}}
|
||||
</small>
|
||||
<small>
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{selectablePlan.additionalStoragePricePerGb / 12 | currency:'$'}} × 12 {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{selectablePlan.premiumAccessOptionCost / 12 | currency:'$'}} × 12 {{'monthAbbr' | i18n}}
|
||||
=
|
||||
{{40 | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
||||
{{'monthly' | i18n}}
|
||||
<small *ngIf="selectablePlan.basePrice">
|
||||
{{'basePrice' | i18n}}: {{selectablePlan.basePrice | currency:'$'}} {{'monthAbbr' | i18n}}
|
||||
=
|
||||
{{selectablePlan.basePrice | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
|
||||
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{selectablePlan.seatPrice | currency:'$'}}
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
|
||||
| currency:'$'}} /{{'month' | i18n}}
|
||||
</small>
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{selectablePlan.additionalStoragePricePerGb | currency:'$'}} {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{selectablePlan.premiumAccessOptionCost | currency:'$'}} {{'monthAbbr' | i18n}} =
|
||||
{{40 | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
</ng-container>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<div class="text-lg">
|
||||
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
|
||||
<strong>{{'total' | i18n}}:</strong> {{subtotal | currency:'USD $'}} /{{selectedPlanInterval | i18n}}
|
||||
</div>
|
||||
<ng-container *ngIf="createOrganization">
|
||||
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
|
||||
<small
|
||||
class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (selectedPlanInterval | i18n) }}</small>
|
||||
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
|
||||
<app-payment [hideCredit]="true"></app-payment>
|
||||
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||
@@ -209,8 +221,11 @@
|
||||
</ng-container>
|
||||
<small class="text-muted font-italic mt-2 d-block" *ngIf="!createOrganization">
|
||||
{{'paymentCharged' | i18n : (interval | i18n) }}</small>
|
||||
</ng-container>
|
||||
<div [ngClass]="{'mt-4': !createOrganization || plans[plan].noPayment}">
|
||||
</div>
|
||||
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
|
||||
<app-callout [type]="'error'">{{'singleOrgBlockCreateMessage' | i18n}}</app-callout>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
@@ -16,91 +17,196 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { PaymentComponent } from './payment.component';
|
||||
import { TaxInfoComponent } from './tax-info.component';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { PolicyType } from 'jslib/enums/policyType';
|
||||
import { ProductType } from 'jslib/enums/productType';
|
||||
|
||||
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
|
||||
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
|
||||
import { PlanResponse } from 'jslib/models/response/planResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-organization-plans',
|
||||
templateUrl: 'organization-plans.component.html',
|
||||
})
|
||||
export class OrganizationPlansComponent {
|
||||
export class OrganizationPlansComponent implements OnInit {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
|
||||
|
||||
@Input() organizationId: string;
|
||||
@Input() showFree = true;
|
||||
@Input() showCancel = false;
|
||||
@Input() plan = 'free';
|
||||
@Input() product: ProductType = ProductType.Free;
|
||||
@Input() plan: PlanType = PlanType.Free;
|
||||
@Output() onSuccess = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
selfHosted = false;
|
||||
ownedBusiness = false;
|
||||
premiumAccessAddon = false;
|
||||
storageGbPriceMonthly = 0.33;
|
||||
additionalStorage = 0;
|
||||
additionalSeats = 0;
|
||||
interval = 'year';
|
||||
loading: boolean = true;
|
||||
selfHosted: boolean = false;
|
||||
ownedBusiness: boolean = false;
|
||||
premiumAccessAddon: boolean = false;
|
||||
additionalStorage: number = 0;
|
||||
additionalSeats: number = 0;
|
||||
name: string;
|
||||
billingEmail: string;
|
||||
businessName: string;
|
||||
|
||||
storageGb: any = {
|
||||
price: 0.33,
|
||||
monthlyPrice: 0.50,
|
||||
yearlyPrice: 4,
|
||||
};
|
||||
|
||||
plans: any = {
|
||||
free: {
|
||||
basePrice: 0,
|
||||
noAdditionalSeats: true,
|
||||
noPayment: true,
|
||||
},
|
||||
families: {
|
||||
basePrice: 1,
|
||||
annualBasePrice: 12,
|
||||
baseSeats: 5,
|
||||
noAdditionalSeats: true,
|
||||
annualPlanType: PlanType.FamiliesAnnually,
|
||||
canBuyPremiumAccessAddon: true,
|
||||
},
|
||||
teams: {
|
||||
basePrice: 5,
|
||||
annualBasePrice: 60,
|
||||
monthlyBasePrice: 8,
|
||||
baseSeats: 5,
|
||||
seatPrice: 2,
|
||||
annualSeatPrice: 24,
|
||||
monthlySeatPrice: 2.5,
|
||||
monthPlanType: PlanType.TeamsMonthly,
|
||||
annualPlanType: PlanType.TeamsAnnually,
|
||||
},
|
||||
enterprise: {
|
||||
seatPrice: 3,
|
||||
annualSeatPrice: 36,
|
||||
monthlySeatPrice: 4,
|
||||
monthPlanType: PlanType.EnterpriseMonthly,
|
||||
annualPlanType: PlanType.EnterpriseAnnually,
|
||||
},
|
||||
};
|
||||
|
||||
productTypes = ProductType;
|
||||
formPromise: Promise<any>;
|
||||
singleOrgPolicyBlock: boolean = false;
|
||||
|
||||
plans: PlanResponse[];
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
|
||||
private router: Router, private syncService: SyncService) {
|
||||
private router: Router, private syncService: SyncService,
|
||||
private policyService: PolicyService) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.selfHosted) {
|
||||
const plans = await this.apiService.getPlans();
|
||||
this.plans = plans.data;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
get createOrganization() {
|
||||
return this.organizationId == null;
|
||||
}
|
||||
|
||||
get selectedPlan() {
|
||||
return this.plans.find((plan) => plan.type === this.plan);
|
||||
}
|
||||
|
||||
get selectedPlanInterval() {
|
||||
return this.selectedPlan.isAnnual
|
||||
? 'year'
|
||||
: 'month';
|
||||
}
|
||||
|
||||
get selectableProducts() {
|
||||
let validPlans = this.plans.filter((plan) => plan.type !== PlanType.Custom);
|
||||
|
||||
if (this.ownedBusiness) {
|
||||
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
|
||||
}
|
||||
|
||||
if (!this.showFree) {
|
||||
validPlans = validPlans.filter((plan) => plan.product !== ProductType.Free);
|
||||
}
|
||||
|
||||
validPlans = validPlans
|
||||
.filter((plan) => !plan.legacyYear
|
||||
&& !plan.disabled
|
||||
&& (plan.isAnnual || plan.product === this.productTypes.Free));
|
||||
|
||||
return validPlans;
|
||||
}
|
||||
|
||||
get selectablePlans() {
|
||||
return this.plans.filter((plan) => !plan.legacyYear && !plan.disabled && plan.product === this.product);
|
||||
}
|
||||
|
||||
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
|
||||
if (!selectedPlan.isAnnual) {
|
||||
return selectedPlan.additionalStoragePricePerGb;
|
||||
}
|
||||
return selectedPlan.additionalStoragePricePerGb / 12;
|
||||
}
|
||||
|
||||
seatPriceMonthly(selectedPlan: PlanResponse) {
|
||||
if (!selectedPlan.isAnnual) {
|
||||
return selectedPlan.seatPrice;
|
||||
}
|
||||
return selectedPlan.seatPrice / 12;
|
||||
}
|
||||
|
||||
additionalStorageTotal(plan: PlanResponse): number {
|
||||
if (!plan.hasAdditionalStorageOption) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return plan.additionalStoragePricePerGb * Math.abs(this.additionalStorage || 0);
|
||||
}
|
||||
|
||||
seatTotal(plan: PlanResponse): number {
|
||||
if (!plan.hasAdditionalSeatsOption) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return plan.seatPrice * Math.abs(this.additionalSeats || 0);
|
||||
}
|
||||
|
||||
get subtotal() {
|
||||
let subTotal = this.selectedPlan.basePrice;
|
||||
if (this.selectedPlan.hasAdditionalSeatsOption && this.additionalSeats) {
|
||||
subTotal += this.seatTotal(this.selectedPlan);
|
||||
}
|
||||
if (this.selectedPlan.hasAdditionalStorageOption && this.additionalStorage) {
|
||||
subTotal += this.additionalStorageTotal(this.selectedPlan);
|
||||
}
|
||||
if (this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon) {
|
||||
subTotal += this.selectedPlan.premiumAccessOptionPrice;
|
||||
}
|
||||
return subTotal;
|
||||
}
|
||||
|
||||
changedProduct() {
|
||||
this.plan = this.selectablePlans[0].type;
|
||||
if (!this.selectedPlan.hasPremiumAccessOption) {
|
||||
this.premiumAccessAddon = false;
|
||||
}
|
||||
if (!this.selectedPlan.hasAdditionalStorageOption) {
|
||||
this.additionalStorage = 0;
|
||||
}
|
||||
if (!this.selectedPlan.hasAdditionalSeatsOption) {
|
||||
this.additionalSeats = 0;
|
||||
} else if (!this.additionalSeats && !this.selectedPlan.baseSeats &&
|
||||
this.selectedPlan.hasAdditionalSeatsOption) {
|
||||
this.additionalSeats = 1;
|
||||
}
|
||||
}
|
||||
|
||||
changedOwnedBusiness() {
|
||||
if (!this.ownedBusiness || this.selectedPlan.canBeUsedByBusiness) {
|
||||
return;
|
||||
}
|
||||
this.plan = PlanType.TeamsMonthly;
|
||||
}
|
||||
|
||||
changedCountry() {
|
||||
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
|
||||
// Bank Account payments are only available for US customers
|
||||
if (this.paymentComponent.hideBank &&
|
||||
this.paymentComponent.method === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.singleOrgPolicyBlock) {
|
||||
return;
|
||||
} else {
|
||||
const policies = await this.policyService.getAll(PolicyType.SingleOrg);
|
||||
this.singleOrgPolicyBlock = policies.some(policy => policy.enabled);
|
||||
if (this.singleOrgPolicyBlock) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let files: FileList = null;
|
||||
if (this.createOrganization && this.selfHosted) {
|
||||
const fileEl = document.getElementById('file') as HTMLInputElement;
|
||||
@@ -117,7 +223,7 @@ export class OrganizationPlansComponent {
|
||||
let orgId: string = null;
|
||||
if (this.createOrganization) {
|
||||
let tokenResult: [string, PaymentMethodType] = null;
|
||||
if (!this.selfHosted && this.plan !== 'free') {
|
||||
if (!this.selfHosted && this.plan !== PlanType.Free) {
|
||||
tokenResult = await this.paymentComponent.createPaymentToken();
|
||||
}
|
||||
const shareKey = await this.cryptoService.makeShareKey();
|
||||
@@ -140,7 +246,7 @@ export class OrganizationPlansComponent {
|
||||
request.name = this.name;
|
||||
request.billingEmail = this.billingEmail;
|
||||
|
||||
if (this.plan === 'free') {
|
||||
if (this.selectedPlan.type === PlanType.Free) {
|
||||
request.planType = PlanType.Free;
|
||||
} else {
|
||||
request.paymentToken = tokenResult[0];
|
||||
@@ -148,13 +254,9 @@ export class OrganizationPlansComponent {
|
||||
request.businessName = this.ownedBusiness ? this.businessName : null;
|
||||
request.additionalSeats = this.additionalSeats;
|
||||
request.additionalStorageGb = this.additionalStorage;
|
||||
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
||||
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
|
||||
this.premiumAccessAddon;
|
||||
if (this.interval === 'month') {
|
||||
request.planType = this.plans[this.plan].monthPlanType;
|
||||
} else {
|
||||
request.planType = this.plans[this.plan].annualPlanType;
|
||||
}
|
||||
request.planType = this.selectedPlan.type;
|
||||
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
|
||||
request.billingAddressCountry = this.taxComponent.taxInfo.country;
|
||||
if (this.taxComponent.taxInfo.includeTaxId) {
|
||||
@@ -173,13 +275,9 @@ export class OrganizationPlansComponent {
|
||||
request.businessName = this.ownedBusiness ? this.businessName : null;
|
||||
request.additionalSeats = this.additionalSeats;
|
||||
request.additionalStorageGb = this.additionalStorage;
|
||||
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
||||
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
|
||||
this.premiumAccessAddon;
|
||||
if (this.interval === 'month') {
|
||||
request.planType = this.plans[this.plan].monthPlanType;
|
||||
} else {
|
||||
request.planType = this.plans[this.plan].annualPlanType;
|
||||
}
|
||||
request.planType = this.selectedPlan.type;
|
||||
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
|
||||
if (!result.success && result.paymentIntentClientSecret != null) {
|
||||
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
|
||||
@@ -208,88 +306,4 @@ export class OrganizationPlansComponent {
|
||||
} catch { }
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
|
||||
changedPlan() {
|
||||
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
|
||||
this.premiumAccessAddon = false;
|
||||
}
|
||||
|
||||
if (this.plans[this.plan].monthPlanType == null) {
|
||||
this.interval = 'year';
|
||||
}
|
||||
|
||||
if (this.plans[this.plan].noAdditionalSeats) {
|
||||
this.additionalSeats = 0;
|
||||
} else if (!this.additionalSeats && !this.plans[this.plan].baseSeats &&
|
||||
!this.plans[this.plan].noAdditionalSeats) {
|
||||
this.additionalSeats = 1;
|
||||
}
|
||||
}
|
||||
|
||||
changedOwnedBusiness() {
|
||||
if (!this.ownedBusiness || this.plan === 'teams' || this.plan === 'enterprise') {
|
||||
return;
|
||||
}
|
||||
this.plan = 'teams';
|
||||
}
|
||||
|
||||
additionalStorageTotal(annual: boolean): number {
|
||||
if (annual) {
|
||||
return Math.abs(this.additionalStorage || 0) * this.storageGb.yearlyPrice;
|
||||
} else {
|
||||
return Math.abs(this.additionalStorage || 0) * this.storageGb.monthlyPrice;
|
||||
}
|
||||
}
|
||||
|
||||
seatTotal(annual: boolean): number {
|
||||
if (this.plans[this.plan].noAdditionalSeats) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (annual) {
|
||||
return this.plans[this.plan].annualSeatPrice * Math.abs(this.additionalSeats || 0);
|
||||
} else {
|
||||
return this.plans[this.plan].monthlySeatPrice * Math.abs(this.additionalSeats || 0);
|
||||
}
|
||||
}
|
||||
|
||||
baseTotal(annual: boolean): number {
|
||||
if (annual) {
|
||||
return Math.abs(this.plans[this.plan].annualBasePrice || 0);
|
||||
} else {
|
||||
return Math.abs(this.plans[this.plan].monthlyBasePrice || 0);
|
||||
}
|
||||
}
|
||||
|
||||
premiumAccessTotal(annual: boolean): number {
|
||||
if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
|
||||
if (annual) {
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
changedCountry() {
|
||||
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
|
||||
// Bank Account payments are only available for US customers
|
||||
if (this.paymentComponent.hideBank &&
|
||||
this.paymentComponent.method === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
}
|
||||
|
||||
get total(): number {
|
||||
const annual = this.interval === 'year';
|
||||
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
|
||||
this.premiumAccessTotal(annual);
|
||||
}
|
||||
|
||||
get createOrganization() {
|
||||
return this.organizationId == null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,17 @@
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<ng-container *ngIf="o.useSso && o.identifier">
|
||||
<a *ngIf="o.ssoBound; else linkSso" class="dropdown-item" href="#" appStopClick
|
||||
(click)="unlinkSso(o)">
|
||||
<i class="fa fa-fw fa-chain-broken" aria-hidden="true"></i>
|
||||
{{'unlinkSso' | i18n}}
|
||||
</a>
|
||||
<ng-template #linkSso>
|
||||
<app-link-sso [organization]="o">
|
||||
</app-link-sso>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="leave(o)">
|
||||
<i class="fa fa-fw fa-sign-out" aria-hidden="true"></i>
|
||||
{{'leave' | i18n}}
|
||||
|
||||
@@ -35,6 +35,7 @@ export class OrganizationsComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.vault) {
|
||||
await this.syncService.fullSync(true);
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
@@ -46,6 +47,25 @@ export class OrganizationsComponent implements OnInit {
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async unlinkSso(org: Organization) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
'Are you sure you want to unlink SSO for this organization?', org.name,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.apiService.deleteSsoUser(org.id).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Unlinked SSO' });
|
||||
this.toasterService.popAsync('success', null, 'Unlinked SSO');
|
||||
await this.load();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async leave(org: Organization) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('leaveOrganizationConfirmation'), org.name,
|
||||
|
||||
@@ -39,13 +39,15 @@
|
||||
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="stripe-card-cvc-element" class="d-flex">
|
||||
{{'securityCode' | i18n}}
|
||||
<div class="d-flex">
|
||||
<label for="stripe-card-cvc-element">
|
||||
{{'securityCode' | i18n}}
|
||||
</label>
|
||||
<a href="https://www.cvvnumber.com/cvv.html" tabindex="-1" target="_blank" rel="noopener noreferrer"
|
||||
class="ml-auto" appA11yTitle="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
<option value="SE">Sweden</option>
|
||||
<option value="CH">Switzerland</option>
|
||||
<option value="SY">Syrian Arab Republic</option>
|
||||
<option value="TW">Taiwan, Province of China</option>
|
||||
<option value="TW">Taiwan</option>
|
||||
<option value="TJ">Tajikistan</option>
|
||||
<option value="TZ">Tanzania, United Republic of</option>
|
||||
<option value="TH">Thailand</option>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Directive,
|
||||
EventEmitter,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
@@ -13,6 +14,7 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||
import { TwoFactorProviderRequest } from 'jslib/models/request/twoFactorProviderRequest';
|
||||
|
||||
@Directive()
|
||||
export abstract class TwoFactorBaseComponent {
|
||||
@Output() onUpdated = new EventEmitter<boolean>();
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
|
||||
templateUrl: 'two-factor-setup.component.html',
|
||||
})
|
||||
export class TwoFactorSetupComponent implements OnInit {
|
||||
@ViewChild('recoveryTemplate', { read: ViewContainerRef }) recoveryModalRef: ViewContainerRef;
|
||||
@ViewChild('authenticatorTemplate', { read: ViewContainerRef }) authenticatorModalRef: ViewContainerRef;
|
||||
@ViewChild('yubikeyTemplate', { read: ViewContainerRef }) yubikeyModalRef: ViewContainerRef;
|
||||
@ViewChild('u2fTemplate', { read: ViewContainerRef }) u2fModalRef: ViewContainerRef;
|
||||
@ViewChild('duoTemplate', { read: ViewContainerRef }) duoModalRef: ViewContainerRef;
|
||||
@ViewChild('emailTemplate', { read: ViewContainerRef }) emailModalRef: ViewContainerRef;
|
||||
@ViewChild('recoveryTemplate', { read: ViewContainerRef, static: true }) recoveryModalRef: ViewContainerRef;
|
||||
@ViewChild('authenticatorTemplate', { read: ViewContainerRef, static: true }) authenticatorModalRef: ViewContainerRef;
|
||||
@ViewChild('yubikeyTemplate', { read: ViewContainerRef, static: true }) yubikeyModalRef: ViewContainerRef;
|
||||
@ViewChild('u2fTemplate', { read: ViewContainerRef, static: true }) u2fModalRef: ViewContainerRef;
|
||||
@ViewChild('duoTemplate', { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
|
||||
@ViewChild('emailTemplate', { read: ViewContainerRef, static: true }) emailModalRef: ViewContainerRef;
|
||||
|
||||
organizationId: string;
|
||||
providers: any[] = [];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ComponentFactoryResolver,
|
||||
Directive,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
@@ -15,8 +16,9 @@ import { AddEditComponent } from '../vault/add-edit.component';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Directive()
|
||||
export class CipherReportComponent {
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
|
||||
|
||||
loading = false;
|
||||
hasLoaded = false;
|
||||
|
||||
@@ -41,9 +41,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const exposedPasswordCiphers: CipherView[] = [];
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '' || c.isDeleted) {
|
||||
return;
|
||||
}
|
||||
const promise = this.auditService.passwordLeaked(c.login.password).then((exposedCount) => {
|
||||
|
||||
@@ -233,6 +233,9 @@
|
||||
Once syncing of your data is complete, the download icon in the top right corner will turn pink. Click
|
||||
the download icon and save the CSV file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'yoticsv'">
|
||||
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the CSV file.
|
||||
</ng-container>
|
||||
</app-callout>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
|
||||
@@ -42,10 +42,11 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
||||
if (this.services.size > 0) {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const inactive2faCiphers: CipherView[] = [];
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
const docs = new Map<string, string>();
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris) {
|
||||
if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris ||
|
||||
c.isDeleted) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < c.login.uris.length; i++) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="ml-auto">
|
||||
<button class="btn btn-link" appA11yTitle="{{'copyPassword' | i18n}}"
|
||||
(click)="copy(h.password)">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { PasswordGeneratorHistoryComponent } from './password-generator-history.
|
||||
templateUrl: 'password-generator.component.html',
|
||||
})
|
||||
export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
|
||||
@ViewChild('historyTemplate', { read: ViewContainerRef }) historyModalRef: ViewContainerRef;
|
||||
@ViewChild('historyTemplate', { read: ViewContainerRef, static: true }) historyModalRef: ViewContainerRef;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
||||
const ciphersWithPasswords: CipherView[] = [];
|
||||
this.passwordUseMap = new Map<string, number>();
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '' || c.isDeleted) {
|
||||
return;
|
||||
}
|
||||
ciphersWithPasswords.push(c);
|
||||
|
||||
@@ -33,7 +33,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const unsecuredCiphers = allCiphers.filter((c) => {
|
||||
if (c.type !== CipherType.Login || !c.login.hasUris) {
|
||||
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
return c.login.uris.some((u) => u.uri != null && u.uri.indexOf('http://') === 0);
|
||||
|
||||
@@ -40,7 +40,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const weakPasswordCiphers: CipherView[] = [];
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '' || c.isDeleted) {
|
||||
return;
|
||||
}
|
||||
const hasUsername = c.login.username != null && c.login.username.trim() !== '';
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<div class="row" *ngIf="!editMode">
|
||||
<div class="col-6 form-group">
|
||||
<label for="type">{{'whatTypeOfItem' | i18n}}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control"
|
||||
[disabled]="cipher.isDeleted" appAutofocus>
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyUsername' | i18n}}"
|
||||
(click)="copy(cipher.login.username, 'username', 'Username')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,7 +84,7 @@
|
||||
appA11yTitle="{{'copyPassword' | i18n}}"
|
||||
(click)="copy(cipher.login.password, 'password', 'Password')" tabindex="-1"
|
||||
[disabled]="!cipher.viewPassword">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,7 +126,7 @@
|
||||
<button type="button" class="btn btn-link"
|
||||
appA11yTitle="{{'copyVerificationCode' | i18n}}"
|
||||
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')">
|
||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyUri' | i18n}}" (click)="copy(u.uri, 'uri', 'URI')"
|
||||
tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,7 +209,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyNumber' | i18n}}"
|
||||
(click)="copy(cipher.card.number, 'number', 'Number')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,7 +246,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'securityCode' | i18n}}"
|
||||
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -395,7 +395,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyValue' | i18n}}"
|
||||
(click)="copy(f.value, 'value', 'Field')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,7 +416,7 @@
|
||||
appA11yTitle="{{'copyValue' | i18n}}"
|
||||
(click)="copy(f.value, 'value', f.type === fieldType.Hidden ? 'H_Field' : 'Field')"
|
||||
tabindex="-1" [disabled]="!cipher.viewPassword && !f.newField">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
38
src/app/vault/bulk-actions.component.html
Normal file
38
src/app/vault/bulk-actions.component.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<div class="dropdown mr-2" appListDropdown>
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||
<button class="dropdown-item" appStopClick (click)="bulkMove()" *ngIf="!deleted && !organization">
|
||||
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
|
||||
{{'moveSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item" appStopClick (click)="bulkShare()" *ngIf="!deleted && !organization">
|
||||
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
||||
{{'shareSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item" (click)="bulkRestore()" *ngIf="deleted && !organization">
|
||||
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||
{{'restoreSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item text-danger" (click)="bulkDelete()">
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
|
||||
{{'selectAll' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
|
||||
{{'unselectAll' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #bulkDeleteTemplate></ng-template>
|
||||
<ng-template #bulkRestoreTemplate></ng-template>
|
||||
<ng-template #bulkMoveTemplate></ng-template>
|
||||
<ng-template #bulkShareTemplate></ng-template>
|
||||
154
src/app/vault/bulk-actions.component.ts
Normal file
154
src/app/vault/bulk-actions.component.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
Input,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
|
||||
import { BulkDeleteComponent } from './bulk-delete.component';
|
||||
import { BulkMoveComponent } from './bulk-move.component';
|
||||
import { BulkRestoreComponent } from './bulk-restore.component';
|
||||
import { BulkShareComponent } from './bulk-share.component';
|
||||
import { CiphersComponent } from './ciphers.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-bulk-actions',
|
||||
templateUrl: 'bulk-actions.component.html',
|
||||
})
|
||||
export class BulkActionsComponent {
|
||||
@Input() ciphersComponent: CiphersComponent;
|
||||
@Input() modal: ModalComponent;
|
||||
@Input() deleted: boolean;
|
||||
@Input() organization: Organization;
|
||||
|
||||
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef, static: true }) bulkDeleteModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef, static: true }) bulkRestoreModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef, static: true }) bulkMoveModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkShareTemplate', { read: ViewContainerRef, static: true }) bulkShareModalRef: ViewContainerRef;
|
||||
|
||||
constructor(private toasterService: ToasterService,
|
||||
private i18nService: I18nService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver) { }
|
||||
|
||||
bulkDelete() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkDeleteModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkDeleteComponent>(BulkDeleteComponent, this.bulkDeleteModalRef);
|
||||
|
||||
childComponent.permanent = this.deleted;
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.organization = this.organization;
|
||||
childComponent.onDeleted.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkRestore() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkRestoreModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkRestoreComponent>(BulkRestoreComponent, this.bulkRestoreModalRef);
|
||||
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onRestored.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkShare() {
|
||||
const selectedCiphers = this.ciphersComponent.getSelected();
|
||||
if (selectedCiphers.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkShareModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkShareComponent>(BulkShareComponent, this.bulkShareModalRef);
|
||||
|
||||
childComponent.ciphers = selectedCiphers;
|
||||
childComponent.onShared.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkMove() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkMoveModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkMoveComponent>(BulkMoveComponent, this.bulkMoveModalRef);
|
||||
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onMoved.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
this.ciphersComponent.selectAll(select);
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,16 @@ import {
|
||||
Input,
|
||||
Output,
|
||||
} 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 { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
import { CipherBulkDeleteRequest } from 'jslib/models/request/cipherBulkDeleteRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-bulk-delete',
|
||||
templateUrl: 'bulk-delete.component.html',
|
||||
@@ -18,20 +21,44 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
export class BulkDeleteComponent {
|
||||
@Input() cipherIds: string[] = [];
|
||||
@Input() permanent: boolean = false;
|
||||
@Input() organization: Organization;
|
||||
@Output() onDeleted = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private analytics: Angulartics2, private cipherService: CipherService,
|
||||
private toasterService: ToasterService, private i18nService: I18nService) { }
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private apiService: ApiService) { }
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.permanent ? this.cipherService.deleteManyWithServer(this.cipherIds) :
|
||||
this.cipherService.softDeleteManyWithServer(this.cipherIds);
|
||||
if (!this.organization || !this.organization.isAdmin) {
|
||||
await this.deleteCiphers();
|
||||
} else {
|
||||
await this.deleteCiphersAdmin();
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
|
||||
this.onDeleted.emit();
|
||||
this.analytics.eventTrack.next({ action: 'Bulk Deleted Items' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t(this.permanent ? 'permanentlyDeletedItems'
|
||||
: 'deletedItems'));
|
||||
}
|
||||
|
||||
private async deleteCiphers() {
|
||||
if (this.permanent) {
|
||||
this.formPromise = await this.cipherService.deleteManyWithServer(this.cipherIds);
|
||||
} else {
|
||||
this.formPromise = await this.cipherService.softDeleteManyWithServer(this.cipherIds);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteCiphersAdmin() {
|
||||
const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id);
|
||||
if (this.permanent) {
|
||||
this.formPromise = await this.apiService.deleteManyCiphersAdmin(deleteRequest);
|
||||
} else {
|
||||
this.formPromise = await this.apiService.putDeleteManyCiphersAdmin(deleteRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of filteredCiphers">
|
||||
<td (click)="checkCipher(c)" class="table-list-checkbox" *ngIf="!organization">
|
||||
<td (click)="checkCipher(c)" class="table-list-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" appStopProp>
|
||||
</td>
|
||||
<td (click)="checkCipher(c)" class="table-list-icon">
|
||||
@@ -37,9 +37,14 @@
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<ng-container *ngIf="c.type === cipherType.Login && !c.isDeleted">
|
||||
<a class="dropdown-item" href="#" appStopClick
|
||||
(click)="copy(c, c.login.username, 'username', 'username')">
|
||||
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
|
||||
{{'copyUsername' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick
|
||||
(click)="copy(c, c.login.password, 'password', 'password')" *ngIf="c.viewPassword">
|
||||
<i class="fa fa-fw fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
|
||||
{{'copyPassword' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick *ngIf="c.login.canLaunch"
|
||||
@@ -55,12 +60,11 @@
|
||||
<a class="dropdown-item" href="#" appStopClick
|
||||
*ngIf="((!organization && !c.organizationId) || organization) && !c.isDeleted"
|
||||
(click)="clone(c)">
|
||||
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
|
||||
<i class="fa fa-fw fa-files-o" aria-hidden="true"></i>
|
||||
{{'clone' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick
|
||||
*ngIf="!organization && !c.organizationId && !c.isDeleted"
|
||||
(click)="share(c)">
|
||||
<a class="dropdown-item" href="#" appStopClick
|
||||
*ngIf="!organization && !c.organizationId && !c.isDeleted" (click)="share(c)">
|
||||
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
||||
{{'share' | i18n}}
|
||||
</a>
|
||||
|
||||
@@ -50,36 +50,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
||||
this.selectAll(false);
|
||||
}
|
||||
|
||||
checkCipher(c: CipherView, select?: boolean) {
|
||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||
}
|
||||
|
||||
launch(uri: string) {
|
||||
this.platformUtilsService.eventTrack('Launched Login URI');
|
||||
this.platformUtilsService.launchUri(uri);
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
if (select) {
|
||||
this.selectAll(false);
|
||||
}
|
||||
const selectCount = select && this.ciphers.length > MaxCheckedCount ? MaxCheckedCount : this.ciphers.length;
|
||||
for (let i = 0; i < selectCount; i++) {
|
||||
this.checkCipher(this.ciphers[i], select);
|
||||
}
|
||||
}
|
||||
|
||||
getSelected(): CipherView[] {
|
||||
if (this.ciphers == null) {
|
||||
return [];
|
||||
}
|
||||
return this.ciphers.filter((c) => !!(c as any).checked);
|
||||
}
|
||||
|
||||
getSelectedIds(): string[] {
|
||||
return this.getSelected().map((c) => c.id);
|
||||
}
|
||||
|
||||
attachments(c: CipherView) {
|
||||
this.onAttachmentsClicked.emit(c);
|
||||
}
|
||||
@@ -159,6 +134,33 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
if (select) {
|
||||
this.selectAll(false);
|
||||
}
|
||||
const selectCount = select && this.ciphers.length > MaxCheckedCount
|
||||
? MaxCheckedCount
|
||||
: this.ciphers.length;
|
||||
for (let i = 0; i < selectCount; i++) {
|
||||
this.checkCipher(this.ciphers[i], select);
|
||||
}
|
||||
}
|
||||
|
||||
checkCipher(c: CipherView, select?: boolean) {
|
||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||
}
|
||||
|
||||
getSelected(): CipherView[] {
|
||||
if (this.ciphers == null) {
|
||||
return [];
|
||||
}
|
||||
return this.ciphers.filter((c) => !!(c as any).checked);
|
||||
}
|
||||
|
||||
getSelectedIds(): string[] {
|
||||
return this.getSelected().map((c) => c.id);
|
||||
}
|
||||
|
||||
protected deleteCipher(id: string, permanent: boolean) {
|
||||
return permanent ? this.cipherService.deleteWithServer(id) : this.cipherService.softDeleteWithServer(id);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="folder.name" required>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="folder.name" required
|
||||
appAutofocus>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -21,40 +21,8 @@
|
||||
</small>
|
||||
</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="dropdown mr-2" appListDropdown>
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
||||
id="bulkActionsButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="bulkMove()" *ngIf="!deleted">
|
||||
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
|
||||
{{'moveSelected' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="bulkShare()" *ngIf="!deleted">
|
||||
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
||||
{{'shareSelected' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" (click)="bulkRestore()" *ngIf="deleted">
|
||||
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||
{{'restoreSelected' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" (click)="bulkDelete()">
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="selectAll(true)">
|
||||
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
|
||||
{{'selectAll' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="selectAll(false)">
|
||||
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
|
||||
{{'unselectAll' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [modal]="modal" [deleted]="deleted">
|
||||
</app-vault-bulk-actions>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addCipher()" *ngIf="!deleted">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
@@ -122,8 +90,4 @@
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #share></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #bulkDeleteTemplate></ng-template>
|
||||
<ng-template #bulkRestoreTemplate></ng-template>
|
||||
<ng-template #bulkMoveTemplate></ng-template>
|
||||
<ng-template #bulkShareTemplate></ng-template>
|
||||
<ng-template #updateKeyTemplate></ng-template>
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
@@ -25,10 +23,6 @@ import { OrganizationsComponent } from '../settings/organizations.component';
|
||||
import { UpdateKeyComponent } from '../settings/update-key.component';
|
||||
import { AddEditComponent } from './add-edit.component';
|
||||
import { AttachmentsComponent } from './attachments.component';
|
||||
import { BulkDeleteComponent } from './bulk-delete.component';
|
||||
import { BulkMoveComponent } from './bulk-move.component';
|
||||
import { BulkRestoreComponent } from './bulk-restore.component';
|
||||
import { BulkShareComponent } from './bulk-share.component';
|
||||
import { CiphersComponent } from './ciphers.component';
|
||||
import { CollectionsComponent } from './collections.component';
|
||||
import { FolderAddEditComponent } from './folder-add-edit.component';
|
||||
@@ -52,19 +46,15 @@ const BroadcasterSubscriptionId = 'VaultComponent';
|
||||
templateUrl: 'vault.component.html',
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
|
||||
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
|
||||
@ViewChild(OrganizationsComponent) organizationsComponent: OrganizationsComponent;
|
||||
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild('folderAddEdit', { read: ViewContainerRef }) folderAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('share', { read: ViewContainerRef }) shareModalRef: ViewContainerRef;
|
||||
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef }) bulkDeleteModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef }) bulkRestoreModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef }) bulkMoveModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkShareTemplate', { read: ViewContainerRef }) bulkShareModalRef: ViewContainerRef;
|
||||
@ViewChild('updateKeyTemplate', { read: ViewContainerRef }) updateKeyModalRef: ViewContainerRef;
|
||||
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
|
||||
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
||||
@ViewChild(OrganizationsComponent, { static: true }) organizationsComponent: OrganizationsComponent;
|
||||
@ViewChild('attachments', { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild('folderAddEdit', { read: ViewContainerRef, static: true }) folderAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('share', { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
|
||||
@ViewChild('collections', { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild('updateKeyTemplate', { read: ViewContainerRef, static: true }) updateKeyModalRef: ViewContainerRef;
|
||||
|
||||
favorites: boolean = false;
|
||||
type: CipherType = null;
|
||||
@@ -76,15 +66,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
showPremiumCallout = false;
|
||||
deleted: boolean = false;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
modal: ModalComponent = null;
|
||||
|
||||
constructor(private syncService: SyncService, private route: ActivatedRoute,
|
||||
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
||||
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 broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
private platformUtilsService: PlatformUtilsService, private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
||||
@@ -391,119 +381,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
component.cloneMode = true;
|
||||
}
|
||||
|
||||
bulkDelete() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkDeleteModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkDeleteComponent>(BulkDeleteComponent, this.bulkDeleteModalRef);
|
||||
|
||||
childComponent.permanent = this.deleted;
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onDeleted.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkRestore() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkRestoreModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkRestoreComponent>(BulkRestoreComponent, this.bulkRestoreModalRef);
|
||||
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onRestored.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkShare() {
|
||||
const selectedCiphers = this.ciphersComponent.getSelected();
|
||||
if (selectedCiphers.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkShareModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkShareComponent>(BulkShareComponent, this.bulkShareModalRef);
|
||||
|
||||
childComponent.ciphers = selectedCiphers;
|
||||
childComponent.onShared.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkMove() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkMoveModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkMoveComponent>(BulkMoveComponent, this.bulkMoveModalRef);
|
||||
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onMoved.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
this.ciphersComponent.selectAll(select);
|
||||
}
|
||||
|
||||
updateKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
|
||||
31
src/connectors/sso.html
Normal file
31
src/connectors/sso.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=1010">
|
||||
<meta name="theme-color" content="#175DDC">
|
||||
|
||||
<title>Bitwarden</title>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="images/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="images/icons/favicon-16x16.png">
|
||||
<link rel="mask-icon" href="images/icons/safari-pinned-tab.svg" color="#175DDC">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
|
||||
<body class="layout_frontend">
|
||||
<div class="mt-5 d-flex justify-content-center">
|
||||
<div>
|
||||
<img src="../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
|
||||
<div id="content">
|
||||
<p class="text-center">
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="Loading" aria-hidden="true"></i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
src/connectors/sso.scss
Normal file
1
src/connectors/sso.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import "../scss/styles.scss";
|
||||
55
src/connectors/sso.ts
Normal file
55
src/connectors/sso.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// tslint:disable-next-line
|
||||
require('./sso.scss');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const code = getQsParam('code');
|
||||
const state = getQsParam('state');
|
||||
|
||||
if (state != null && state.includes(':clientId=browser')) {
|
||||
initiateBrowserSso(code, state);
|
||||
} else {
|
||||
window.location.href = window.location.origin + '/#/sso?code=' + code + '&state=' + state;
|
||||
// Match any characters between "_returnUri='" and the next "'"
|
||||
const returnUri = extractFromRegex(state, '(?<=_returnUri=\')(.*)(?=\')');
|
||||
if (returnUri) {
|
||||
window.location.href = window.location.origin + `/#${returnUri}`;
|
||||
} else {
|
||||
window.location.href = window.location.origin + '/#/sso?code=' + code + '&state=' + state;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getQsParam(name: string) {
|
||||
const url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, '\\$&');
|
||||
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
|
||||
const results = regex.exec(url);
|
||||
|
||||
if (!results) {
|
||||
return null;
|
||||
}
|
||||
if (!results[2]) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
function initiateBrowserSso(code: string, state: string) {
|
||||
window.postMessage({ command: 'authResult', code: code, state: state }, '*');
|
||||
let handOffMessage = ('; ' + document.cookie).split('; ssoHandOffMessage=').pop().split(';').shift();
|
||||
document.cookie = 'ssoHandOffMessage=;SameSite=strict;max-age=0'
|
||||
document.getElementById('content').innerHTML =
|
||||
`<p>${handOffMessage}</p>`;
|
||||
}
|
||||
|
||||
function extractFromRegex(s: string, regexString: string) {
|
||||
const regex = new RegExp(regexString);
|
||||
const results = regex.exec(s);
|
||||
|
||||
if (!results) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return results[0];
|
||||
}
|
||||
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 34 KiB |
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Пароль"
|
||||
},
|
||||
"newPassword": {
|
||||
"message": "Новы пароль"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Парольная фраза"
|
||||
},
|
||||
@@ -527,22 +530,22 @@
|
||||
"message": "What should we call you?"
|
||||
},
|
||||
"masterPass": {
|
||||
"message": "Майстар-пароль"
|
||||
"message": "Асноўны пароль"
|
||||
},
|
||||
"masterPassDesc": {
|
||||
"message": "Майстар-пароль — ключ да вашага бяспечнага сховішча. Ён вельмі важны, таму не забывайце яго. Аднавіць майстар-пароль немагчыма."
|
||||
"message": "Асноўны пароль — ключ да вашага бяспечнага сховішча. Ён вельмі важны, таму не забывайце яго. Аднавіць асноўны пароль немагчыма."
|
||||
},
|
||||
"masterPassHintDesc": {
|
||||
"message": "Падказка да майстра-пароля можа дапамагчы вам яго ўспомніць."
|
||||
"message": "Падказка да асноўнага пароля можа дапамагчы вам яго ўспомніць."
|
||||
},
|
||||
"reTypeMasterPass": {
|
||||
"message": "Увядзіце майстар-пароль паўторна"
|
||||
"message": "Увядзіце асноўны пароль паўторна"
|
||||
},
|
||||
"masterPassHint": {
|
||||
"message": "Падказка да майстра-пароля (неабавязкова)"
|
||||
"message": "Падказка да асноўнага пароля (неабавязкова)"
|
||||
},
|
||||
"masterPassHintLabel": {
|
||||
"message": "Падказка да майстра-пароля"
|
||||
"message": "Падказка да асноўнага пароля"
|
||||
},
|
||||
"settings": {
|
||||
"message": "Налады"
|
||||
@@ -551,10 +554,10 @@
|
||||
"message": "Падказка да пароля"
|
||||
},
|
||||
"enterEmailToGetHint": {
|
||||
"message": "Увядзіце адрас электроннай пошты ўліковага запісу для атрымання падказкі для пароля."
|
||||
"message": "Увядзіце адрас электроннай пошты ўліковага запісу для атрымання падказкі для асноўнага пароля."
|
||||
},
|
||||
"getMasterPasswordHint": {
|
||||
"message": "Атрымаць падказку для майстра-пароля"
|
||||
"message": "Атрымаць падказку для асноўнага пароля"
|
||||
},
|
||||
"emailRequired": {
|
||||
"message": "Патрабуецца адрас электроннай пошты."
|
||||
@@ -563,19 +566,19 @@
|
||||
"message": "Памылковы адрас электроннай пошты."
|
||||
},
|
||||
"masterPassRequired": {
|
||||
"message": "Патрабуецца майстар-пароль."
|
||||
"message": "Патрабуецца асноўны пароль."
|
||||
},
|
||||
"masterPassLength": {
|
||||
"message": "Майстар-пароль павінен быць даўжынёй не менш за 8 сімвалаў."
|
||||
"message": "Асноўны пароль павінен быць даўжынёй не менш за 8 сімвалаў."
|
||||
},
|
||||
"masterPassDoesntMatch": {
|
||||
"message": "Майстры-паролі не супадаюць."
|
||||
"message": "Асноўныя паролі не супадаюць."
|
||||
},
|
||||
"newAccountCreated": {
|
||||
"message": "Ваш уліковы запіс створаны! Вы можаце ўвайсці."
|
||||
},
|
||||
"masterPassSent": {
|
||||
"message": "Мы адправілі вам на электронную пошту падказку для майстра-пароля."
|
||||
"message": "Мы адправілі вам на электронную пошту падказку для асноўнага пароля."
|
||||
},
|
||||
"unexpectedError": {
|
||||
"message": "Адбылася нечаканая памылка."
|
||||
@@ -584,7 +587,7 @@
|
||||
"message": "Адрас эл. пошты"
|
||||
},
|
||||
"yourVaultIsLocked": {
|
||||
"message": "Ваша сховішча заблакіравана. Каб працягнуць, увядзіце майстар-пароль."
|
||||
"message": "Ваша сховішча заблакіравана. Каб працягнуць, увядзіце асноўны пароль."
|
||||
},
|
||||
"unlock": {
|
||||
"message": "Разблакіраваць"
|
||||
@@ -603,7 +606,7 @@
|
||||
}
|
||||
},
|
||||
"invalidMasterPassword": {
|
||||
"message": "Памылковы майстар-пароль"
|
||||
"message": "Памылковы асноўны пароль"
|
||||
},
|
||||
"lockNow": {
|
||||
"message": "Заблакіраваць"
|
||||
@@ -791,7 +794,7 @@
|
||||
"message": "Экспартуемы файл утрымлівае даныя вашага сховішча ў незашыфраваным фармаце. Яго не варта захоўваць ці адпраўляць па небяспечным каналам (напрыклад, па электроннай пошце). Выдаліце яго адразу пасля выкарыстання."
|
||||
},
|
||||
"exportMasterPassword": {
|
||||
"message": "Увядзіце ваш майстар-пароль для экспарту даных са сховішча."
|
||||
"message": "Увядзіце ваш асноўны пароль для экспарту даных са сховішча."
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Экспарт сховішча"
|
||||
@@ -881,19 +884,19 @@
|
||||
"message": "Please log back in. If you are using other Bitwarden applications log out and back in to those as well."
|
||||
},
|
||||
"changeMasterPassword": {
|
||||
"message": "Змяніць майстар-пароль"
|
||||
"message": "Змяніць асноўны пароль"
|
||||
},
|
||||
"masterPasswordChanged": {
|
||||
"message": "Майстар-пароль зменены"
|
||||
"message": "Асноўны пароль зменены"
|
||||
},
|
||||
"currentMasterPass": {
|
||||
"message": "Current Master Password"
|
||||
"message": "Бягучы асноўны пароль"
|
||||
},
|
||||
"newMasterPass": {
|
||||
"message": "New Master Password"
|
||||
"message": "Новы асноўны пароль"
|
||||
},
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Confirm New Master Password"
|
||||
"message": "Пацвердзіць новы асноўны пароль"
|
||||
},
|
||||
"encKeySettings": {
|
||||
"message": "Encryption Key Settings"
|
||||
@@ -980,13 +983,13 @@
|
||||
"message": "Your account has been closed and all associated data has been deleted."
|
||||
},
|
||||
"myAccount": {
|
||||
"message": "My Account"
|
||||
"message": "Мой уліковы запіс"
|
||||
},
|
||||
"tools": {
|
||||
"message": "Tools"
|
||||
"message": "Інструменты"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Import Data"
|
||||
"message": "Імпарт даных"
|
||||
},
|
||||
"importSuccess": {
|
||||
"message": "Data has been successfully imported into your vault."
|
||||
@@ -1971,7 +1974,7 @@
|
||||
"message": "Add and share with unlimited users"
|
||||
},
|
||||
"createUnlimitedCollections": {
|
||||
"message": "Create unlimited collections"
|
||||
"message": "Create unlimited Collections"
|
||||
},
|
||||
"gbEncryptedFileStorage": {
|
||||
"message": "$SIZE$ encrypted file storage",
|
||||
@@ -1986,13 +1989,13 @@
|
||||
"message": "On-premise hosting (optional)"
|
||||
},
|
||||
"usersGetPremium": {
|
||||
"message": "Users get access to premium membership features"
|
||||
"message": "Users get access to Premium Features"
|
||||
},
|
||||
"controlAccessWithGroups": {
|
||||
"message": "Control user access with groups"
|
||||
"message": "Control user access with Groups"
|
||||
},
|
||||
"syncUsersFromDirectory": {
|
||||
"message": "Sync your users and groups from a directory"
|
||||
"message": "Sync your users and Groups from a directory"
|
||||
},
|
||||
"trackAuditLogs": {
|
||||
"message": "Track user actions with audit logs"
|
||||
@@ -2810,9 +2813,11 @@
|
||||
"nothingSelected": {
|
||||
"message": "You have not selected anything."
|
||||
},
|
||||
"submitAgreePolicies": {
|
||||
"message": "By clicking the \"Submit\" button, you agree to the following policies:",
|
||||
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
|
||||
"acceptPolicies": {
|
||||
"message": "By checking this box you agree to the following:"
|
||||
},
|
||||
"acceptPoliciesError": {
|
||||
"message": "Terms of Service and Privacy Policy have not been acknowledged."
|
||||
},
|
||||
"termsOfService": {
|
||||
"message": "Terms of Service"
|
||||
@@ -2893,10 +2898,10 @@
|
||||
"description": "ex. A very weak password. Scale: Very Weak -> Weak -> Good -> Strong"
|
||||
},
|
||||
"weakMasterPassword": {
|
||||
"message": "Weak Master Password"
|
||||
"message": "Слабы асноўны пароль"
|
||||
},
|
||||
"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?"
|
||||
"message": "Асноўны пароль, выбраны вамі, з'яўляецца слабым. Для належнай абароны ўліковага запісу Bitwarden, вы павінны выкарыстоўваць моцны асноўны пароль (або парольную фразу). Вы ўпэўнены, што хочаце выкарыстоўваць гэты асноўны пароль?"
|
||||
},
|
||||
"rotateAccountEncKey": {
|
||||
"message": "Also rotate my account's encryption key"
|
||||
@@ -2948,6 +2953,12 @@
|
||||
"apiKeyWarning": {
|
||||
"message": "Your API key has full access to the organization. It should be kept secret."
|
||||
},
|
||||
"userApiKeyDesc": {
|
||||
"message": "Your API key can be used to authenticate in the Bitwarden CLI."
|
||||
},
|
||||
"userApiKeyWarning": {
|
||||
"message": "Your API key is an alternative authentication mechanism. It should be kept secret."
|
||||
},
|
||||
"oauth2ClientCredentials": {
|
||||
"message": "OAuth 2.0 Client Credentials",
|
||||
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
|
||||
@@ -2980,13 +2991,13 @@
|
||||
"message": "Clone"
|
||||
},
|
||||
"masterPassPolicyDesc": {
|
||||
"message": "Set minimum requirements for master password strength."
|
||||
"message": "Задайце мінімальныя патрабаванні да надзейнасці асноўнага пароля."
|
||||
},
|
||||
"twoStepLoginPolicyDesc": {
|
||||
"message": "Require users to set up two-step login on their personal accounts."
|
||||
},
|
||||
"twoStepLoginPolicyWarning": {
|
||||
"message": "Organization members who do not have two-step login enabled for their personal account will be removed from the organization and will receive an email notifying them about the change."
|
||||
"message": "Organization members who are not Owners or Administrators and do not have two-step login enabled for their personal account will be removed from the organization and will receive an email notifying them about the change."
|
||||
},
|
||||
"twoStepLoginPolicyUserWarning": {
|
||||
"message": "You are a member of an organization that requires two-step login to be enabled on your user account. If you disable all two-step login providers you will be automatically removed from these organizations."
|
||||
@@ -2998,7 +3009,7 @@
|
||||
"message": "One or more organization policies are affecting your generator settings."
|
||||
},
|
||||
"masterPasswordPolicyInEffect": {
|
||||
"message": "One or more organization policies require your master password to meet the following requirements:"
|
||||
"message": "Згодна з адной або некалькімі палітыкамі арганізацыі неабходна, каб ваш асноўны пароль адказваў наступным патрабаванням:"
|
||||
},
|
||||
"policyInEffectMinComplexity": {
|
||||
"message": "Minimum complexity score of $SCORE$",
|
||||
@@ -3037,7 +3048,7 @@
|
||||
}
|
||||
},
|
||||
"masterPasswordPolicyRequirementsNotMet": {
|
||||
"message": "Your new master password does not meet the policy requirements."
|
||||
"message": "Ваш новы асноўны пароль не адпавядае патрабаванням палітыкі арганізацыі."
|
||||
},
|
||||
"minimumNumberOfWords": {
|
||||
"message": "Minimum Number of Words"
|
||||
@@ -3163,5 +3174,180 @@
|
||||
},
|
||||
"taxInfoUpdated": {
|
||||
"message": "Tax information updated."
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Задаць асноўны пароль"
|
||||
},
|
||||
"ssoCompleteRegistration": {
|
||||
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
|
||||
},
|
||||
"identifier": {
|
||||
"message": "Identifier"
|
||||
},
|
||||
"organizationIdentifier": {
|
||||
"message": "Organization Identifier"
|
||||
},
|
||||
"ssoLogInWithOrgIdentifier": {
|
||||
"message": "Log in using your organization's single sign-on portal. Please enter your organization's identifier to begin."
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Enterprise Single Sign-On"
|
||||
},
|
||||
"ssoHandOff": {
|
||||
"message": "You may now close this tab and continue in the extension."
|
||||
},
|
||||
"businessPortal": {
|
||||
"message": "Business Portal",
|
||||
"description": "The web portal used by business organizations for configuring certain features."
|
||||
},
|
||||
"includeAllTeamsFeatures": {
|
||||
"message": "All Teams features, plus:"
|
||||
},
|
||||
"includeSsoAuthentication": {
|
||||
"message": "SSO Authentication via SAML2.0 and OpenID Connect"
|
||||
},
|
||||
"includeEnterprisePolicies": {
|
||||
"message": "Enterprise Policies"
|
||||
},
|
||||
"ssoValidationFailed": {
|
||||
"message": "SSO Validation Failed"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Organization Identifier is required."
|
||||
},
|
||||
"unlinkSso": {
|
||||
"message": "Unlink SSO"
|
||||
},
|
||||
"linkSso": {
|
||||
"message": "Link SSO"
|
||||
},
|
||||
"webPoliciesDeprecationWarning": {
|
||||
"message": "Policy configuration has been moved, and this page will soon be deprecated. Please click below to use the Business Portal policies page instead."
|
||||
},
|
||||
"singleOrg": {
|
||||
"message": "Single Organization"
|
||||
},
|
||||
"singleOrgDesc": {
|
||||
"message": "Restrict users from being able to join any other organizations."
|
||||
},
|
||||
"singleOrgBlockCreateMessage": {
|
||||
"message": "Your current organization has a policy that does not allow you to join more than one organization. Please contact your organization admins or sign up from a different Bitwarden account."
|
||||
},
|
||||
"singleOrgPolicyWarning": {
|
||||
"message": "Organization members who are not Owners or Administrators and are already a member of another organization will be removed from your organization."
|
||||
},
|
||||
"requireSso": {
|
||||
"message": "Single Sign-On Authentication"
|
||||
},
|
||||
"requireSsoPolicyDesc": {
|
||||
"message": "Require users to log in with the Enterprise Single Sign-On method."
|
||||
},
|
||||
"prerequisite": {
|
||||
"message": "Prerequisite"
|
||||
},
|
||||
"requireSsoPolicyReq": {
|
||||
"message": "The Single Organization enterprise policy must be enabled before activating this policy."
|
||||
},
|
||||
"requireSsoPolicyReqError": {
|
||||
"message": "Single Organization policy not enabled."
|
||||
},
|
||||
"requireSsoExemption": {
|
||||
"message": "Organization Owners and Administrators are exempt from this policy's enforcement."
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "File"
|
||||
},
|
||||
"sendTypeText": {
|
||||
"message": "Text"
|
||||
},
|
||||
"createSend": {
|
||||
"message": "Create New Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editSend": {
|
||||
"message": "Edit Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createdSend": {
|
||||
"message": "Created Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editedSend": {
|
||||
"message": "Edited Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletedSend": {
|
||||
"message": "Deleted Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSend": {
|
||||
"message": "Delete Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSendConfirmation": {
|
||||
"message": "Are you sure you want to delete this Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"whatTypeOfSend": {
|
||||
"message": "What type of Send is this?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletionDate": {
|
||||
"message": "Deletion Date"
|
||||
},
|
||||
"expirationDate": {
|
||||
"message": "Expiration Date"
|
||||
},
|
||||
"maxAccessCount": {
|
||||
"message": "Maximum Access Count"
|
||||
},
|
||||
"currentAccessCount": {
|
||||
"message": "Current Access Count"
|
||||
},
|
||||
"disabled": {
|
||||
"message": "Disabled"
|
||||
},
|
||||
"sendLink": {
|
||||
"message": "Send Link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"copySendLink": {
|
||||
"message": "Copy Send Link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"removePassword": {
|
||||
"message": "Remove Password"
|
||||
},
|
||||
"removedPassword": {
|
||||
"message": "Removed Password"
|
||||
},
|
||||
"removePasswordConfirmation": {
|
||||
"message": "Are you sure you want to remove the password?"
|
||||
},
|
||||
"allSends": {
|
||||
"message": "All Sends"
|
||||
},
|
||||
"searchSends": {
|
||||
"message": "Search Sends",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPassword": {
|
||||
"message": "This Send is protected with a password. Please type the password below to continue.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPasswordDontKnow": {
|
||||
"message": "Don't know the password? Ask the Sender for the password needed to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendHiddenByDefault": {
|
||||
"message": "This send is hidden by default. You can toggle its visibility using the button below.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"downloadFile": {
|
||||
"message": "Download File"
|
||||
},
|
||||
"noSendsInList": {
|
||||
"message": "There are no Sends to list.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Парола"
|
||||
},
|
||||
"newPassword": {
|
||||
"message": "Нова парола"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Парола-фраза"
|
||||
},
|
||||
@@ -2810,9 +2813,11 @@
|
||||
"nothingSelected": {
|
||||
"message": "Не сте избрали нищо."
|
||||
},
|
||||
"submitAgreePolicies": {
|
||||
"message": "С натискането на бутона „Подаване“ се съгласявате със следните политики:",
|
||||
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
|
||||
"acceptPolicies": {
|
||||
"message": "Чрез тази отметка вие се съгласявате със следните:"
|
||||
},
|
||||
"acceptPoliciesError": {
|
||||
"message": "Условията за използване и политиката за поверителност не бяха приети."
|
||||
},
|
||||
"termsOfService": {
|
||||
"message": "Общи условия"
|
||||
@@ -2948,6 +2953,12 @@
|
||||
"apiKeyWarning": {
|
||||
"message": "Ключът за API дава пълен достъп до организацията. Трябва да го пазите в тайна."
|
||||
},
|
||||
"userApiKeyDesc": {
|
||||
"message": "Your API key can be used to authenticate in the Bitwarden CLI."
|
||||
},
|
||||
"userApiKeyWarning": {
|
||||
"message": "Вашият API ключ е алтернативен механизъм автентикация. Той трябва да се пази в тайна."
|
||||
},
|
||||
"oauth2ClientCredentials": {
|
||||
"message": "Идентификация за клиент за OAuth 2.0",
|
||||
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
|
||||
@@ -3001,7 +3012,7 @@
|
||||
"message": "Поне една политика на организация има следните изисквания към главната ви парола:"
|
||||
},
|
||||
"policyInEffectMinComplexity": {
|
||||
"message": "Минимална сила от $SCORE$",
|
||||
"message": "Минимална сложност от $SCORE$",
|
||||
"placeholders": {
|
||||
"score": {
|
||||
"content": "$1",
|
||||
@@ -3163,5 +3174,180 @@
|
||||
},
|
||||
"taxInfoUpdated": {
|
||||
"message": "Данъчната информация е обновена."
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Задаване на главна парола"
|
||||
},
|
||||
"ssoCompleteRegistration": {
|
||||
"message": "За да завършите настройките за еднократна идентификация, трябва да зададете главна парола за трезора."
|
||||
},
|
||||
"identifier": {
|
||||
"message": "Идентификатор"
|
||||
},
|
||||
"organizationIdentifier": {
|
||||
"message": "Идентификатор на организация"
|
||||
},
|
||||
"ssoLogInWithOrgIdentifier": {
|
||||
"message": "Вписване чрез портала на организацията ви за еднократна идентификация. За да продължите, въведете идентификатора на организацията."
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Еднократна идентификация (SSO)"
|
||||
},
|
||||
"ssoHandOff": {
|
||||
"message": "You may now close this tab and continue in the extension."
|
||||
},
|
||||
"businessPortal": {
|
||||
"message": "Бизнес портал",
|
||||
"description": "The web portal used by business organizations for configuring certain features."
|
||||
},
|
||||
"includeAllTeamsFeatures": {
|
||||
"message": "Всички възможности в екип плюс:"
|
||||
},
|
||||
"includeSsoAuthentication": {
|
||||
"message": "Еднократна идентификация чрез SAML2.0 и OpenID Connect"
|
||||
},
|
||||
"includeEnterprisePolicies": {
|
||||
"message": "Политики на бизнеса"
|
||||
},
|
||||
"ssoValidationFailed": {
|
||||
"message": "Неуспешна еднократна идентификация"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Идентификаторът на организация е задължителен."
|
||||
},
|
||||
"unlinkSso": {
|
||||
"message": "Прекъсване на еднократна идентификация"
|
||||
},
|
||||
"linkSso": {
|
||||
"message": "Свързване на еднократна идентификация"
|
||||
},
|
||||
"webPoliciesDeprecationWarning": {
|
||||
"message": "Policy configuration has been moved, and this page will soon be deprecated. Please click below to use the Business Portal policies page instead."
|
||||
},
|
||||
"singleOrg": {
|
||||
"message": "Една организация"
|
||||
},
|
||||
"singleOrgDesc": {
|
||||
"message": "Restrict users from being able to join any other organizations."
|
||||
},
|
||||
"singleOrgBlockCreateMessage": {
|
||||
"message": "Вашата настояща организация има политика, която не позволява да участвате в повече от една организация. Моля свържете се с администратора на организацията или се впишете с друг Bitwarden потребител."
|
||||
},
|
||||
"singleOrgPolicyWarning": {
|
||||
"message": "Organization members who are not Owners or Administrators and are already a member of another organization will be removed from your organization."
|
||||
},
|
||||
"requireSso": {
|
||||
"message": "Single Sign-On Authentication"
|
||||
},
|
||||
"requireSsoPolicyDesc": {
|
||||
"message": "Require users to log in with the Enterprise Single Sign-On method."
|
||||
},
|
||||
"prerequisite": {
|
||||
"message": "Предпоставкa"
|
||||
},
|
||||
"requireSsoPolicyReq": {
|
||||
"message": "The Single Organization enterprise policy must be enabled before activating this policy."
|
||||
},
|
||||
"requireSsoPolicyReqError": {
|
||||
"message": "Single Organization policy not enabled."
|
||||
},
|
||||
"requireSsoExemption": {
|
||||
"message": "Organization Owners and Administrators are exempt from this policy's enforcement."
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "Файл"
|
||||
},
|
||||
"sendTypeText": {
|
||||
"message": "Текст"
|
||||
},
|
||||
"createSend": {
|
||||
"message": "Create New Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editSend": {
|
||||
"message": "Edit Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createdSend": {
|
||||
"message": "Created Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editedSend": {
|
||||
"message": "Edited Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletedSend": {
|
||||
"message": "Deleted Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSend": {
|
||||
"message": "Delete Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSendConfirmation": {
|
||||
"message": "Are you sure you want to delete this Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"whatTypeOfSend": {
|
||||
"message": "What type of Send is this?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletionDate": {
|
||||
"message": "Deletion Date"
|
||||
},
|
||||
"expirationDate": {
|
||||
"message": "Срок на валидност"
|
||||
},
|
||||
"maxAccessCount": {
|
||||
"message": "Maximum Access Count"
|
||||
},
|
||||
"currentAccessCount": {
|
||||
"message": "Current Access Count"
|
||||
},
|
||||
"disabled": {
|
||||
"message": "Disabled"
|
||||
},
|
||||
"sendLink": {
|
||||
"message": "Send Link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"copySendLink": {
|
||||
"message": "Copy Send Link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"removePassword": {
|
||||
"message": "Премахване на парола"
|
||||
},
|
||||
"removedPassword": {
|
||||
"message": "Removed Password"
|
||||
},
|
||||
"removePasswordConfirmation": {
|
||||
"message": "Are you sure you want to remove the password?"
|
||||
},
|
||||
"allSends": {
|
||||
"message": "All Sends"
|
||||
},
|
||||
"searchSends": {
|
||||
"message": "Search Sends",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPassword": {
|
||||
"message": "This Send is protected with a password. Please type the password below to continue.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPasswordDontKnow": {
|
||||
"message": "Don't know the password? Ask the Sender for the password needed to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendHiddenByDefault": {
|
||||
"message": "This send is hidden by default. You can toggle its visibility using the button below.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"downloadFile": {
|
||||
"message": "Изтеглете файл"
|
||||
},
|
||||
"noSendsInList": {
|
||||
"message": "There are no Sends to list.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Contrasenya"
|
||||
},
|
||||
"newPassword": {
|
||||
"message": "Nova contrasenya"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Frase de pas"
|
||||
},
|
||||
@@ -2810,9 +2813,11 @@
|
||||
"nothingSelected": {
|
||||
"message": "No heu seleccionat res."
|
||||
},
|
||||
"submitAgreePolicies": {
|
||||
"message": "Fent clic al botó \"Envia\", accepteu les polítiques següents:",
|
||||
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
|
||||
"acceptPolicies": {
|
||||
"message": "Si activeu aquesta casella, indiqueu que esteu d’acord amb el següent:"
|
||||
},
|
||||
"acceptPoliciesError": {
|
||||
"message": "No s’han reconegut les condicions del servei i la declaració de privadesa."
|
||||
},
|
||||
"termsOfService": {
|
||||
"message": "Condicions del servei"
|
||||
@@ -2948,6 +2953,12 @@
|
||||
"apiKeyWarning": {
|
||||
"message": "La clau de l'API té accés total a l'organització. S'ha de mantenir en secret."
|
||||
},
|
||||
"userApiKeyDesc": {
|
||||
"message": "La vostra clau API es pot utilitzar per autenticar-se al CLI de Bitwarden."
|
||||
},
|
||||
"userApiKeyWarning": {
|
||||
"message": "La vostra clau API és un mecanisme d'autenticació alternatiu. S’ha de mantenir en secret."
|
||||
},
|
||||
"oauth2ClientCredentials": {
|
||||
"message": "Credencials de client OAuth 2.0",
|
||||
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
|
||||
@@ -3163,5 +3174,180 @@
|
||||
},
|
||||
"taxInfoUpdated": {
|
||||
"message": "Informació fiscal actualitzada."
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Estableix la contrasenya mestra"
|
||||
},
|
||||
"ssoCompleteRegistration": {
|
||||
"message": "Per completar la sessió amb SSO, configureu una contrasenya mestra per accedir i protegir la vostra caixa forta."
|
||||
},
|
||||
"identifier": {
|
||||
"message": "Identificador"
|
||||
},
|
||||
"organizationIdentifier": {
|
||||
"message": "Identificador d’organització"
|
||||
},
|
||||
"ssoLogInWithOrgIdentifier": {
|
||||
"message": "Inicieu la sessió ràpidament mitjançant el portal d'inici de sessió únic de la vostra organització. Introduïu l'identificador de la vostra organització per començar."
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Inici de sessió únic d'empresa"
|
||||
},
|
||||
"ssoHandOff": {
|
||||
"message": "You may now close this tab and continue in the extension."
|
||||
},
|
||||
"businessPortal": {
|
||||
"message": "Portal empresarial",
|
||||
"description": "The web portal used by business organizations for configuring certain features."
|
||||
},
|
||||
"includeAllTeamsFeatures": {
|
||||
"message": "Característiques de tots els equips, a més de:"
|
||||
},
|
||||
"includeSsoAuthentication": {
|
||||
"message": "Autenticació SSO mitjançant SAML2.0 i OpenID Connect"
|
||||
},
|
||||
"includeEnterprisePolicies": {
|
||||
"message": "Polítiques empresarials"
|
||||
},
|
||||
"ssoValidationFailed": {
|
||||
"message": "La validació SSO ha fallat"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Cal identificador d’organització."
|
||||
},
|
||||
"unlinkSso": {
|
||||
"message": "Desenllaça SSO"
|
||||
},
|
||||
"linkSso": {
|
||||
"message": "Enllaça SSO"
|
||||
},
|
||||
"webPoliciesDeprecationWarning": {
|
||||
"message": "La configuració de la política s'ha desplaçat i aquesta pàgina quedarà obsoleta aviat. Feu clic a continuació per utilitzar la pàgina de polítiques del portal empresarial."
|
||||
},
|
||||
"singleOrg": {
|
||||
"message": "Organització única"
|
||||
},
|
||||
"singleOrgDesc": {
|
||||
"message": "Restringeix els usuaris perquè no puguen unir-se a qualsevol altra organització."
|
||||
},
|
||||
"singleOrgBlockCreateMessage": {
|
||||
"message": "La vostra organització actual té una política que no us permet unir-vos a més d'una organització. Poseu-vos en contacte amb els administradors de la vostra organització o registreu-vos des d’un altre compte de Bitwarden."
|
||||
},
|
||||
"singleOrgPolicyWarning": {
|
||||
"message": "Els membres que no siguen propietaris ni administradors i que ja siguen membres d'una altra organització se suprimiran de la vostra organització."
|
||||
},
|
||||
"requireSso": {
|
||||
"message": "Autenticació d'inici de sessió únic"
|
||||
},
|
||||
"requireSsoPolicyDesc": {
|
||||
"message": "Sol·liciteu als usuaris que inicien la sessió amb el mètode d’inici de sessió únic de l’empresa."
|
||||
},
|
||||
"prerequisite": {
|
||||
"message": "Requisit previ"
|
||||
},
|
||||
"requireSsoPolicyReq": {
|
||||
"message": "La política empresarial d'una organització única s'ha d'activar abans d'activar aquesta política."
|
||||
},
|
||||
"requireSsoPolicyReqError": {
|
||||
"message": "La política d'una única organització no està habilitada."
|
||||
},
|
||||
"requireSsoExemption": {
|
||||
"message": "Els propietaris i administradors d’organitzacions estan exempts de fer complir aquesta política."
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "Fitxer"
|
||||
},
|
||||
"sendTypeText": {
|
||||
"message": "Text"
|
||||
},
|
||||
"createSend": {
|
||||
"message": "Crea un nou Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editSend": {
|
||||
"message": "Edita Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createdSend": {
|
||||
"message": "Send creat",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editedSend": {
|
||||
"message": "Send editat",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletedSend": {
|
||||
"message": "Send suprimit",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSend": {
|
||||
"message": "Suprimeix el Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSendConfirmation": {
|
||||
"message": "Esteu segur que voleu suprimir aquest Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"whatTypeOfSend": {
|
||||
"message": "Quin tipus de Send és aquest?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletionDate": {
|
||||
"message": "Data de supressió"
|
||||
},
|
||||
"expirationDate": {
|
||||
"message": "Data de venciment"
|
||||
},
|
||||
"maxAccessCount": {
|
||||
"message": "Recompte màxim d'accés"
|
||||
},
|
||||
"currentAccessCount": {
|
||||
"message": "Recompte d’accés actual"
|
||||
},
|
||||
"disabled": {
|
||||
"message": "Deshabilitat"
|
||||
},
|
||||
"sendLink": {
|
||||
"message": "Enllaç Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"copySendLink": {
|
||||
"message": "Copia l'enllaç Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"removePassword": {
|
||||
"message": "Suprimeix la contrasenya"
|
||||
},
|
||||
"removedPassword": {
|
||||
"message": "Contrasenya suprimida"
|
||||
},
|
||||
"removePasswordConfirmation": {
|
||||
"message": "Esteu segur que voleu suprimir la contrasenya?"
|
||||
},
|
||||
"allSends": {
|
||||
"message": "Tots els Send"
|
||||
},
|
||||
"searchSends": {
|
||||
"message": "Cerca Sends",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPassword": {
|
||||
"message": "Aquest Send està protegit amb una contrasenya. Escriviu la contrasenya següent per continuar.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPasswordDontKnow": {
|
||||
"message": "No sabeu la contrasenya? Demaneu al remitent la contrasenya necessària per accedir a aquest Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendHiddenByDefault": {
|
||||
"message": "Aquest Send està ocult per defecte. Podeu canviar la seua visibilitat mitjançant el botó següent.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"downloadFile": {
|
||||
"message": "Baixa el fitxer"
|
||||
},
|
||||
"noSendsInList": {
|
||||
"message": "No hi ha cap Send a llistar.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Heslo"
|
||||
},
|
||||
"newPassword": {
|
||||
"message": "Nové heslo"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Heslová fráze"
|
||||
},
|
||||
@@ -461,10 +464,10 @@
|
||||
"message": "Opravdu chcete tuto položku smazat?"
|
||||
},
|
||||
"deletedItem": {
|
||||
"message": "Položka byla smazána."
|
||||
"message": "Položka byla smazána"
|
||||
},
|
||||
"deletedItems": {
|
||||
"message": "Položky byly smazány."
|
||||
"message": "Položky byly smazány"
|
||||
},
|
||||
"movedItems": {
|
||||
"message": "Položky byly přesunuty"
|
||||
@@ -1045,11 +1048,11 @@
|
||||
"message": "Použije profilový obrázek načtený z gravatar.com."
|
||||
},
|
||||
"enableFullWidth": {
|
||||
"message": "Enable Full Width Layout",
|
||||
"message": "Zapnout rozvržení na celou šířku stránky",
|
||||
"description": "Allows scaling the web vault UI's width"
|
||||
},
|
||||
"enableFullWidthDesc": {
|
||||
"message": "Allow the web vault to expand the full width of the browser window."
|
||||
"message": "Povolit webovému trezoru roztáhnout se na celou šířku okna."
|
||||
},
|
||||
"default": {
|
||||
"message": "Výchozí"
|
||||
@@ -2654,7 +2657,7 @@
|
||||
"description": "A billing plan/package. For example: families, teams, enterprise, etc."
|
||||
},
|
||||
"changeBillingPlanUpgrade": {
|
||||
"message": "Upgrade your account to another plan be providing the information below. Please ensure that you have an active payment method added to the account.",
|
||||
"message": "Povyšte svůj účet na jiný plán zadáním údajů níže. Ujistěte se prosím, že máte k účtu přidaný platný způsob platby.",
|
||||
"description": "A billing plan/package. For example: families, teams, enterprise, etc."
|
||||
},
|
||||
"changeBillingPlanDesc": {
|
||||
@@ -2810,9 +2813,11 @@
|
||||
"nothingSelected": {
|
||||
"message": "Nevybrali jste žádné položky."
|
||||
},
|
||||
"submitAgreePolicies": {
|
||||
"message": "Klepnutím na tlačítko \"Odeslat\" souhlasíte s následující podmínkami:",
|
||||
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
|
||||
"acceptPolicies": {
|
||||
"message": "Zaškrtnutím tohoto políčka souhlasím s následujícím:"
|
||||
},
|
||||
"acceptPoliciesError": {
|
||||
"message": "Terms of Service and Privacy Policy have not been acknowledged."
|
||||
},
|
||||
"termsOfService": {
|
||||
"message": "Podmínky služby"
|
||||
@@ -2948,6 +2953,12 @@
|
||||
"apiKeyWarning": {
|
||||
"message": "Váš API klíč má plný přístup k organizaci. Měl by být uchován v tajnosti."
|
||||
},
|
||||
"userApiKeyDesc": {
|
||||
"message": "Your API key can be used to authenticate in the Bitwarden CLI."
|
||||
},
|
||||
"userApiKeyWarning": {
|
||||
"message": "Your API key is an alternative authentication mechanism. It should be kept secret."
|
||||
},
|
||||
"oauth2ClientCredentials": {
|
||||
"message": "OAuth 2.0 klientské údaje",
|
||||
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
|
||||
@@ -3150,18 +3161,193 @@
|
||||
"message": "Potvrzení akce při vypršení časového limitu"
|
||||
},
|
||||
"hidePasswords": {
|
||||
"message": "Hide Passwords"
|
||||
"message": "Skrýt hesla"
|
||||
},
|
||||
"countryPostalCodeRequiredDesc": {
|
||||
"message": "We require this information for calculating sales tax and financial reporting only."
|
||||
"message": "Tyto informace potřebujeme pouze pro výpočet daně a pro finanční přehledy."
|
||||
},
|
||||
"includeVAT": {
|
||||
"message": "Include VAT/GST Information (optional)"
|
||||
"message": "Zahrnout údaje o DPH (volitelné)"
|
||||
},
|
||||
"taxIdNumber": {
|
||||
"message": "VAT/GST Tax ID"
|
||||
"message": "DIČ"
|
||||
},
|
||||
"taxInfoUpdated": {
|
||||
"message": "Tax information updated."
|
||||
"message": "Údaje pro DPH aktualizovány."
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Nastavit hlavní heslo"
|
||||
},
|
||||
"ssoCompleteRegistration": {
|
||||
"message": "Chcete-li dokončit přihlášení pomocí SSO, nastavte prosím hlavní heslo pro přístup a ochranu vašeho trezoru."
|
||||
},
|
||||
"identifier": {
|
||||
"message": "Identifikátor"
|
||||
},
|
||||
"organizationIdentifier": {
|
||||
"message": "Identifikátor organizace"
|
||||
},
|
||||
"ssoLogInWithOrgIdentifier": {
|
||||
"message": "Přihlaste se pomocí přihlašovacího portálu vaší organizace. Chcete-li začít, zadejte prosím identifikátor vaší organizace."
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Jednotné podnikové přihlášení"
|
||||
},
|
||||
"ssoHandOff": {
|
||||
"message": "You may now close this tab and continue in the extension."
|
||||
},
|
||||
"businessPortal": {
|
||||
"message": "Podnikový portál",
|
||||
"description": "The web portal used by business organizations for configuring certain features."
|
||||
},
|
||||
"includeAllTeamsFeatures": {
|
||||
"message": "Všechny funkce Týmů, navíc:"
|
||||
},
|
||||
"includeSsoAuthentication": {
|
||||
"message": "Podnikové přihlášení prostřednictvím SAML2.0 a OpenID Connect"
|
||||
},
|
||||
"includeEnterprisePolicies": {
|
||||
"message": "Podnikové politiky"
|
||||
},
|
||||
"ssoValidationFailed": {
|
||||
"message": "Ověření pomocí SSO selhalo"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Je vyžadován identifikátor organizace."
|
||||
},
|
||||
"unlinkSso": {
|
||||
"message": "Odebrat podnikové přihlášení"
|
||||
},
|
||||
"linkSso": {
|
||||
"message": "Propojit s podnikovým přihlášením"
|
||||
},
|
||||
"webPoliciesDeprecationWarning": {
|
||||
"message": "Policy configuration has been moved, and this page will soon be deprecated. Please click below to use the Business Portal policies page instead."
|
||||
},
|
||||
"singleOrg": {
|
||||
"message": "Single Organization"
|
||||
},
|
||||
"singleOrgDesc": {
|
||||
"message": "Restrict users from being able to join any other organizations."
|
||||
},
|
||||
"singleOrgBlockCreateMessage": {
|
||||
"message": "Your current organization has a policy that does not allow you to join more than one organization. Please contact your organization admins or sign up from a different Bitwarden account."
|
||||
},
|
||||
"singleOrgPolicyWarning": {
|
||||
"message": "Organization members who are not Owners or Administrators and are already a member of another organization will be removed from your organization."
|
||||
},
|
||||
"requireSso": {
|
||||
"message": "Single Sign-On Authentication"
|
||||
},
|
||||
"requireSsoPolicyDesc": {
|
||||
"message": "Require users to log in with the Enterprise Single Sign-On method."
|
||||
},
|
||||
"prerequisite": {
|
||||
"message": "Prerequisite"
|
||||
},
|
||||
"requireSsoPolicyReq": {
|
||||
"message": "The Single Organization enterprise policy must be enabled before activating this policy."
|
||||
},
|
||||
"requireSsoPolicyReqError": {
|
||||
"message": "Single Organization policy not enabled."
|
||||
},
|
||||
"requireSsoExemption": {
|
||||
"message": "Organization Owners and Administrators are exempt from this policy's enforcement."
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "Soubor"
|
||||
},
|
||||
"sendTypeText": {
|
||||
"message": "Text"
|
||||
},
|
||||
"createSend": {
|
||||
"message": "Create New Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editSend": {
|
||||
"message": "Edit Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createdSend": {
|
||||
"message": "Created Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editedSend": {
|
||||
"message": "Edited Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletedSend": {
|
||||
"message": "Deleted Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSend": {
|
||||
"message": "Delete Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSendConfirmation": {
|
||||
"message": "Are you sure you want to delete this Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"whatTypeOfSend": {
|
||||
"message": "What type of Send is this?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletionDate": {
|
||||
"message": "Datum odstranění"
|
||||
},
|
||||
"expirationDate": {
|
||||
"message": "Datum expirace"
|
||||
},
|
||||
"maxAccessCount": {
|
||||
"message": "Maximum Access Count"
|
||||
},
|
||||
"currentAccessCount": {
|
||||
"message": "Current Access Count"
|
||||
},
|
||||
"disabled": {
|
||||
"message": "Disabled"
|
||||
},
|
||||
"sendLink": {
|
||||
"message": "Odeslat odkaz",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"copySendLink": {
|
||||
"message": "Copy Send Link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"removePassword": {
|
||||
"message": "Odstranit heslo"
|
||||
},
|
||||
"removedPassword": {
|
||||
"message": "Removed Password"
|
||||
},
|
||||
"removePasswordConfirmation": {
|
||||
"message": "Are you sure you want to remove the password?"
|
||||
},
|
||||
"allSends": {
|
||||
"message": "All Sends"
|
||||
},
|
||||
"searchSends": {
|
||||
"message": "Search Sends",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPassword": {
|
||||
"message": "This Send is protected with a password. Please type the password below to continue.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPasswordDontKnow": {
|
||||
"message": "Don't know the password? Ask the Sender for the password needed to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendHiddenByDefault": {
|
||||
"message": "This send is hidden by default. You can toggle its visibility using the button below.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"downloadFile": {
|
||||
"message": "Stáhnout soubor"
|
||||
},
|
||||
"noSendsInList": {
|
||||
"message": "There are no Sends to list.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Adgangskode"
|
||||
},
|
||||
"newPassword": {
|
||||
"message": "Ny adgangskode"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Adgangssætning"
|
||||
},
|
||||
@@ -1986,13 +1989,13 @@
|
||||
"message": "Lokal-hosting (valgfri)"
|
||||
},
|
||||
"usersGetPremium": {
|
||||
"message": "Brugere får adgang til premium-medlemskabsfunktioner"
|
||||
"message": "Brugere får adgang til premium-funktioner"
|
||||
},
|
||||
"controlAccessWithGroups": {
|
||||
"message": "Kontroller brugeradgang med grupper"
|
||||
},
|
||||
"syncUsersFromDirectory": {
|
||||
"message": "Synkroniser dine brugere og grupper fra et kartotek"
|
||||
"message": "Synkronisér dine brugere og grupper fra et kartotek"
|
||||
},
|
||||
"trackAuditLogs": {
|
||||
"message": "Spor brugerhandlinger med revisionslogfiler"
|
||||
@@ -2810,9 +2813,11 @@
|
||||
"nothingSelected": {
|
||||
"message": "Du har ikke valgt noget."
|
||||
},
|
||||
"submitAgreePolicies": {
|
||||
"message": "Ved at klikke på \"Indsend\"-knappen accepterer du følgende politikker:",
|
||||
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
|
||||
"acceptPolicies": {
|
||||
"message": "Ved at markere dette felt accepterer du følgende:"
|
||||
},
|
||||
"acceptPoliciesError": {
|
||||
"message": "Servicevilkår og fortrolighedspolitik er ikke blevet bekræftet."
|
||||
},
|
||||
"termsOfService": {
|
||||
"message": "Servicevilkår"
|
||||
@@ -2948,6 +2953,12 @@
|
||||
"apiKeyWarning": {
|
||||
"message": "Din API-nøgle har fuld adgang til organisationen. Den skal holdes hemmelig."
|
||||
},
|
||||
"userApiKeyDesc": {
|
||||
"message": "Din API-nøgle kan bruges til godkendelse i Bitwarden-CLI."
|
||||
},
|
||||
"userApiKeyWarning": {
|
||||
"message": "Din API-nøgle er en alternativ godkendelsesmekanisme. Den bør holdes hemmelig."
|
||||
},
|
||||
"oauth2ClientCredentials": {
|
||||
"message": "OAuth 2.0 legitimationsoplysninger",
|
||||
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
|
||||
@@ -2986,7 +2997,7 @@
|
||||
"message": "Kræv at brugerne konfigurerer to-trins-login på deres personlige konti."
|
||||
},
|
||||
"twoStepLoginPolicyWarning": {
|
||||
"message": "Organisationsmedlemmer, der ikke har to-trins login aktiveret på deres personlige konto, fjernes fra organisationen og vil modtage en e-mail, der giver dem besked om ændringen."
|
||||
"message": "Organisationsmedlemmer, der ikke er ejere eller administratorer og ikke har to-trins login aktiveret på deres personlige konto, fjernes fra organisationen og modtager en e-mail med besked om ændringen."
|
||||
},
|
||||
"twoStepLoginPolicyUserWarning": {
|
||||
"message": "Du er medlem af en organisation, der kræver at to-trins login er aktiveret på din brugerkonto. Hvis du deaktiverer alle to-trins login-udbydere, fjernes du automatisk fra disse organisationer."
|
||||
@@ -3163,5 +3174,180 @@
|
||||
},
|
||||
"taxInfoUpdated": {
|
||||
"message": "Skatteoplysninger opdateret."
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Indstil hovedadgangskode"
|
||||
},
|
||||
"ssoCompleteRegistration": {
|
||||
"message": "For at fuldføre indlogning vha. SSO skal en hovedadgangskode opsættes for at tilgå og beskytte din boks."
|
||||
},
|
||||
"identifier": {
|
||||
"message": "Identifikator"
|
||||
},
|
||||
"organizationIdentifier": {
|
||||
"message": "Organisationsidentifikator"
|
||||
},
|
||||
"ssoLogInWithOrgIdentifier": {
|
||||
"message": "Log ind vha. din organisations single sign-on portal. Angiv din organisations identifikator for at begynde."
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Virksomheds Single Sign On"
|
||||
},
|
||||
"ssoHandOff": {
|
||||
"message": "You may now close this tab and continue in the extension."
|
||||
},
|
||||
"businessPortal": {
|
||||
"message": "Virksomhedssportal",
|
||||
"description": "The web portal used by business organizations for configuring certain features."
|
||||
},
|
||||
"includeAllTeamsFeatures": {
|
||||
"message": "Alle teamfunktioner, plus:"
|
||||
},
|
||||
"includeSsoAuthentication": {
|
||||
"message": "SSO godkendelse via SAML2.0 og OpenID Connect"
|
||||
},
|
||||
"includeEnterprisePolicies": {
|
||||
"message": "Virksomhedspolitikker"
|
||||
},
|
||||
"ssoValidationFailed": {
|
||||
"message": "SSO validering mislykkedes"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Organisationsidentifikator er påkrævet."
|
||||
},
|
||||
"unlinkSso": {
|
||||
"message": "Fjern SSO tilknytning"
|
||||
},
|
||||
"linkSso": {
|
||||
"message": "Tilknyt SSO"
|
||||
},
|
||||
"webPoliciesDeprecationWarning": {
|
||||
"message": "Politikkonfiguration er flyttet, og denne side vil snart blive udfaset. Klik nedenfor for at bruge siden Politikker i forretningsportalen i stedet."
|
||||
},
|
||||
"singleOrg": {
|
||||
"message": "Enkel organisation"
|
||||
},
|
||||
"singleOrgDesc": {
|
||||
"message": "Begræns brugere fra at kunne deltage i andre organisationer."
|
||||
},
|
||||
"singleOrgBlockCreateMessage": {
|
||||
"message": "Din nuværende organisation har en politik, der ikke tillader dig at deltage i mere end en organisation. Kontakt din organisations administratorer, eller tilmeld dig fra en anden Bitwarden-konto."
|
||||
},
|
||||
"singleOrgPolicyWarning": {
|
||||
"message": "Organisationsmedlemmer, der ikke er ejere eller administratorer og allerede er medlem af en anden organisation, fjernes fra din organisation."
|
||||
},
|
||||
"requireSso": {
|
||||
"message": "Single Sign-On autentificering"
|
||||
},
|
||||
"requireSsoPolicyDesc": {
|
||||
"message": "Kræv at brugerne logger ind med Virksomheds Single Sign-On-metoden."
|
||||
},
|
||||
"prerequisite": {
|
||||
"message": "Forudsætning"
|
||||
},
|
||||
"requireSsoPolicyReq": {
|
||||
"message": "Enkel organisations virksomhedspolitikken skal aktiveres, før denne politik aktiveres."
|
||||
},
|
||||
"requireSsoPolicyReqError": {
|
||||
"message": "Enkelt organisationspolitik er ikke aktiveret."
|
||||
},
|
||||
"requireSsoExemption": {
|
||||
"message": "Organisationsejere og administratorer er undtaget fra denne politik."
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "Fil"
|
||||
},
|
||||
"sendTypeText": {
|
||||
"message": "Tekst"
|
||||
},
|
||||
"createSend": {
|
||||
"message": "Opret ny Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editSend": {
|
||||
"message": "Redigér Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createdSend": {
|
||||
"message": "Send oprettet",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editedSend": {
|
||||
"message": "Send opdateret",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletedSend": {
|
||||
"message": "Send slettet",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSend": {
|
||||
"message": "Slet Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSendConfirmation": {
|
||||
"message": "Er du sikker på, at du vil slette denne Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"whatTypeOfSend": {
|
||||
"message": "Hvilken type Send er denne?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletionDate": {
|
||||
"message": "Sletningsdato"
|
||||
},
|
||||
"expirationDate": {
|
||||
"message": "Udløbsdato"
|
||||
},
|
||||
"maxAccessCount": {
|
||||
"message": "Maksimal antal tilgange"
|
||||
},
|
||||
"currentAccessCount": {
|
||||
"message": "Aktuelt antal tilgange"
|
||||
},
|
||||
"disabled": {
|
||||
"message": "Deaktiveret"
|
||||
},
|
||||
"sendLink": {
|
||||
"message": "Send link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"copySendLink": {
|
||||
"message": "Kopiér Send link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"removePassword": {
|
||||
"message": "Fjern adgangskode"
|
||||
},
|
||||
"removedPassword": {
|
||||
"message": "Adgangskode fjernet"
|
||||
},
|
||||
"removePasswordConfirmation": {
|
||||
"message": "Er du sikker på, at du vil fjerne adgangskoden?"
|
||||
},
|
||||
"allSends": {
|
||||
"message": "Alle Send"
|
||||
},
|
||||
"searchSends": {
|
||||
"message": "Søg Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPassword": {
|
||||
"message": "Denne Send er beskyttet med en adgangskode. Indtast adgangskoden nedenfor for at fortsætte.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPasswordDontKnow": {
|
||||
"message": "Kender du ikke adgangskoden? Bed afsenderen om den nødvendige adgangskode for at få adgang til denne Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendHiddenByDefault": {
|
||||
"message": "Denne Send er som standard skjult. Du kan skifte synlighed ved hjælp af knappen nedenfor.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"downloadFile": {
|
||||
"message": "Download fil"
|
||||
},
|
||||
"noSendsInList": {
|
||||
"message": "Der er ingen Send at vise.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"password": {
|
||||
"message": "Passwort"
|
||||
},
|
||||
"newPassword": {
|
||||
"message": "Neues Passwort"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Passphrase"
|
||||
},
|
||||
@@ -831,7 +834,7 @@
|
||||
"message": "Worttrennzeichen"
|
||||
},
|
||||
"capitalize": {
|
||||
"message": "Wortanfänge großschreiben",
|
||||
"message": "Großschreiben",
|
||||
"description": "Make the first letter of a work uppercase."
|
||||
},
|
||||
"includeNumber": {
|
||||
@@ -1049,7 +1052,7 @@
|
||||
"description": "Allows scaling the web vault UI's width"
|
||||
},
|
||||
"enableFullWidthDesc": {
|
||||
"message": "Dem Web-Tresor erlauben die volle Breite des Browserfenster zu benutzen."
|
||||
"message": "Dem Web-Tresor erlauben, die volle Breite des Browserfensters zu benutzen."
|
||||
},
|
||||
"default": {
|
||||
"message": "Standard"
|
||||
@@ -2643,7 +2646,7 @@
|
||||
"message": "Informationen zur Steuer"
|
||||
},
|
||||
"taxInformationDesc": {
|
||||
"message": "Bitte kontaktieren Sie unseren Support um Ihre Steuerinformationen Ihrer Rechnung abzufragen (oder zu aktualisieren)."
|
||||
"message": "Für Kunden innerhalb der USA ist die Postleitzahl erforderlich, um die Umsatzsteuer-Anforderungen zu erfüllen, für andere Länder können Sie optional eine Steuernummer (VAT/GST) und/oder eine Adresse angeben, die auf Ihren Rechnungen erscheint."
|
||||
},
|
||||
"billingPlan": {
|
||||
"message": "Abo",
|
||||
@@ -2810,9 +2813,11 @@
|
||||
"nothingSelected": {
|
||||
"message": "Sie haben keine Auswahl getroffen."
|
||||
},
|
||||
"submitAgreePolicies": {
|
||||
"message": "Durch Klicken des Absenden-Buttons stimmen Sie den folgenden Bedingungen zu:",
|
||||
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
|
||||
"acceptPolicies": {
|
||||
"message": "Durch Anwählen dieses Kästchens erklären Sie sich mit folgendem einverstanden:"
|
||||
},
|
||||
"acceptPoliciesError": {
|
||||
"message": "Die Nutzungsbedingungen und Datenschutzerklärung wurden nicht akzeptiert."
|
||||
},
|
||||
"termsOfService": {
|
||||
"message": "Allgemeine Geschäftsbedingungen"
|
||||
@@ -2948,6 +2953,12 @@
|
||||
"apiKeyWarning": {
|
||||
"message": "Ihr API-Schlüssel hat vollen Zugriff auf die Organisation. Er sollte geheim gehalten werden."
|
||||
},
|
||||
"userApiKeyDesc": {
|
||||
"message": "Ihr API-Schlüssel kann zur Authentifizierung im Bitwarden CLI verwendet werden."
|
||||
},
|
||||
"userApiKeyWarning": {
|
||||
"message": "Ihr API-Schlüssel ist ein alternativer Authentifizierungsmechanismus. Er sollte geheim gehalten werden."
|
||||
},
|
||||
"oauth2ClientCredentials": {
|
||||
"message": "OAuth 2.0 Client Anmeldeinformationen",
|
||||
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
|
||||
@@ -3153,15 +3164,190 @@
|
||||
"message": "Passwörter verstecken"
|
||||
},
|
||||
"countryPostalCodeRequiredDesc": {
|
||||
"message": "We require this information for calculating sales tax and financial reporting only."
|
||||
"message": "Wir benötigen diese Informationen nur zur Berechnung der Umsatzsteuer und Finanzberichterstattung."
|
||||
},
|
||||
"includeVAT": {
|
||||
"message": "Include VAT/GST Information (optional)"
|
||||
"message": "MwSt./GST-Informationen einschließen (optional)"
|
||||
},
|
||||
"taxIdNumber": {
|
||||
"message": "Umsatzsteuernummer"
|
||||
},
|
||||
"taxInfoUpdated": {
|
||||
"message": "Steuerinformationen aktualisiert."
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Masterpasswort festlegen"
|
||||
},
|
||||
"ssoCompleteRegistration": {
|
||||
"message": "Bitte legen Sie ein Masterpasswort für den Schutz Ihres Tresors fest, um die Anmeldung über SSO abzuschließen."
|
||||
},
|
||||
"identifier": {
|
||||
"message": "Kennung"
|
||||
},
|
||||
"organizationIdentifier": {
|
||||
"message": "Organisationskennung"
|
||||
},
|
||||
"ssoLogInWithOrgIdentifier": {
|
||||
"message": "Über den Single Sign-on Ihrer Organisation anmelden. Bitte geben Sie Ihre Organisationskennung an, um zu beginnen."
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Enterprise Single Sign-On"
|
||||
},
|
||||
"ssoHandOff": {
|
||||
"message": "You may now close this tab and continue in the extension."
|
||||
},
|
||||
"businessPortal": {
|
||||
"message": "Unternehmensportal",
|
||||
"description": "The web portal used by business organizations for configuring certain features."
|
||||
},
|
||||
"includeAllTeamsFeatures": {
|
||||
"message": "Alle Teams Funktionen, zusätzlich:"
|
||||
},
|
||||
"includeSsoAuthentication": {
|
||||
"message": "SSO Authentifikation über SAML2.0 und OpenID Connect"
|
||||
},
|
||||
"includeEnterprisePolicies": {
|
||||
"message": "Unternehmensrichtlinien"
|
||||
},
|
||||
"ssoValidationFailed": {
|
||||
"message": "SSO Validierung fehlgeschlagen"
|
||||
},
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Unternehmenskennung ist erforderlich."
|
||||
},
|
||||
"unlinkSso": {
|
||||
"message": "SSO aufheben"
|
||||
},
|
||||
"linkSso": {
|
||||
"message": "SSO verknüpfen"
|
||||
},
|
||||
"webPoliciesDeprecationWarning": {
|
||||
"message": "Die Richtlinien-Konfiguration wurde verschoben und diese Seite wird bald nicht mehr aktualisiert. Benutzen Sie stattdessen über den Link die Richtlinien-Seite des Unternehmensportal."
|
||||
},
|
||||
"singleOrg": {
|
||||
"message": "Einzelne Organisation"
|
||||
},
|
||||
"singleOrgDesc": {
|
||||
"message": "Benutzern verbieten, anderen Organisationen beizutreten."
|
||||
},
|
||||
"singleOrgBlockCreateMessage": {
|
||||
"message": "Ihre aktuelle Organisation hat eine Richtlinie, die es Ihnen nicht erlaubt, mehr als einer Organisation beizutreten. Bitte kontaktieren Sie die Administratoren Ihrer Organisation oder melden Sie sich mit einem anderen Bitwarden-Konto an."
|
||||
},
|
||||
"singleOrgPolicyWarning": {
|
||||
"message": "Organisationsmitglieder, die nicht Eigentümer oder Administratoren sind und bereits Mitglied einer anderen Organisation sind, werden aus Ihrer Organisation entfernt."
|
||||
},
|
||||
"requireSso": {
|
||||
"message": "Single Sign-On Authentifizierung"
|
||||
},
|
||||
"requireSsoPolicyDesc": {
|
||||
"message": "Benutzer müssen sich per Enterprise Single Sign-On anmelden."
|
||||
},
|
||||
"prerequisite": {
|
||||
"message": "Voraussetzung"
|
||||
},
|
||||
"requireSsoPolicyReq": {
|
||||
"message": "Die Unternehmensrichtlinie für eine einzelne Organisation muss aktiviert sein, bevor diese Richtlinie aktiviert werden kann."
|
||||
},
|
||||
"requireSsoPolicyReqError": {
|
||||
"message": "Richtlinie für eine einzelne Organisation nicht aktiviert."
|
||||
},
|
||||
"requireSsoExemption": {
|
||||
"message": "Organisationseigentümer und Administratoren sind von der Durchsetzung dieser Richtlinie ausgenommen."
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "Datei"
|
||||
},
|
||||
"sendTypeText": {
|
||||
"message": "Text"
|
||||
},
|
||||
"createSend": {
|
||||
"message": "Neues Send erstellen",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editSend": {
|
||||
"message": "Send bearbeiten",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createdSend": {
|
||||
"message": "Send erstellt",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editedSend": {
|
||||
"message": "Bearbeitetes Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletedSend": {
|
||||
"message": "Gelöschtes Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSend": {
|
||||
"message": "Send löschen",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSendConfirmation": {
|
||||
"message": "Sind Sie sicher, dass Sie dieses Send löschen möchten?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"whatTypeOfSend": {
|
||||
"message": "Welche Art von Send ist das?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletionDate": {
|
||||
"message": "Löschdatum"
|
||||
},
|
||||
"expirationDate": {
|
||||
"message": "Ablaufdatum"
|
||||
},
|
||||
"maxAccessCount": {
|
||||
"message": "Maximale Zugriffsanzahl"
|
||||
},
|
||||
"currentAccessCount": {
|
||||
"message": "Aktuelle Zugriffsanzahl"
|
||||
},
|
||||
"disabled": {
|
||||
"message": "Deaktiviert"
|
||||
},
|
||||
"sendLink": {
|
||||
"message": "Send Link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"copySendLink": {
|
||||
"message": "Send Link kopieren",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"removePassword": {
|
||||
"message": "Passwort entfernen"
|
||||
},
|
||||
"removedPassword": {
|
||||
"message": "Passwort entfernt"
|
||||
},
|
||||
"removePasswordConfirmation": {
|
||||
"message": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?"
|
||||
},
|
||||
"allSends": {
|
||||
"message": "Alle Sends"
|
||||
},
|
||||
"searchSends": {
|
||||
"message": "Sends suchen",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPassword": {
|
||||
"message": "Dieses Send ist mit einem Passwort geschützt. Bitte geben Sie unten das Passwort ein, um fortzufahren.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendProtectedPasswordDontKnow": {
|
||||
"message": "Kennen Sie das Passwort nicht? Fragen Sie den Absender nach dem benötigten Passwort, um auf dieses Send zuzugreifen.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendHiddenByDefault": {
|
||||
"message": "Dieses Send ist standardmäßig ausgeblendet. Sie können die Sichtbarkeit mit dem Button unten umschalten.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"downloadFile": {
|
||||
"message": "Datei herunterladen"
|
||||
},
|
||||
"noSendsInList": {
|
||||
"message": "Es gibt keine Sends aufzulisten.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user