1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-28 14:13:22 +00:00

Apply Prettier (#1347)

This commit is contained in:
Oscar Hinton
2021-12-17 15:57:11 +01:00
committed by GitHub
parent 2b0a9d995e
commit 56477eb39c
414 changed files with 33390 additions and 26857 deletions

View File

@@ -1,45 +1,53 @@
<div class="page-header">
<h1>{{'myAccount' | i18n}}</h1>
<h1>{{ "myAccount" | i18n }}</h1>
</div>
<app-profile></app-profile>
<ng-container *ngIf="showChangeEmail">
<div class="secondary-header">
<h1>{{'changeEmail' | i18n}}</h1>
</div>
<app-change-email></app-change-email>
<div class="secondary-header">
<h1>{{ "changeEmail" | i18n }}</h1>
</div>
<app-change-email></app-change-email>
</ng-container>
<ng-container *ngIf="showChangePassword">
<div class="secondary-header">
<h1>{{'changeMasterPassword' | i18n}}</h1>
</div>
<app-change-password></app-change-password>
<div class="secondary-header">
<h1>{{ "changeMasterPassword" | i18n }}</h1>
</div>
<app-change-password></app-change-password>
</ng-container>
<ng-container *ngIf="showChangeKdf">
<div class="secondary-header">
<h1>{{'encKeySettings' | i18n}}</h1>
</div>
<app-change-kdf></app-change-kdf>
<div class="secondary-header">
<h1>{{ "encKeySettings" | i18n }}</h1>
</div>
<app-change-kdf></app-change-kdf>
</ng-container>
<div class="secondary-header border-0 mb-0">
<h1>{{'apiKey' | i18n}}</h1>
<h1>{{ "apiKey" | i18n }}</h1>
</div>
<p>
{{'userApiKeyDesc' | i18n}}
{{ "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>
<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>
<h1>{{ "dangerZone" | i18n }}</h1>
</div>
<div class="card border-danger">
<div class="card-body">
<p>{{'dangerZoneDesc' | i18n}}</p>
<button type="button" class="btn btn-outline-danger"
(click)="deauthorizeSessions()">{{'deauthorizeSessions' | i18n}}</button>
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">{{'purgeVault' | i18n}}</button>
<button type="button" class="btn btn-outline-danger"
(click)="deleteAccount()">{{'deleteAccount' | i18n}}</button>
</div>
<div class="card-body">
<p>{{ "dangerZoneDesc" | i18n }}</p>
<button type="button" class="btn btn-outline-danger" (click)="deauthorizeSessions()">
{{ "deauthorizeSessions" | i18n }}
</button>
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">
{{ "purgeVault" | i18n }}
</button>
<button type="button" class="btn btn-outline-danger" (click)="deleteAccount()">
{{ "deleteAccount" | i18n }}
</button>
</div>
</div>
<ng-template #deauthorizeSessionsTemplate></ng-template>
<ng-template #purgeVaultTemplate></ng-template>

View File

@@ -1,81 +1,88 @@
import {
Component,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
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 { 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-common/abstractions/api.service';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { ModalService } from 'jslib-angular/services/modal.service';
import { ModalService } from "jslib-angular/services/modal.service";
@Component({
selector: 'app-account',
templateUrl: 'account.component.html',
selector: "app-account",
templateUrl: "account.component.html",
})
export class AccountComponent {
@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;
@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;
showChangePassword = true;
showChangeKdf = true;
showChangeEmail = true;
showChangePassword = true;
showChangeKdf = true;
showChangeEmail = true;
constructor(private modalService: ModalService, private apiService: ApiService,
private keyConnectorService: KeyConnectorService, private stateService: StateService) { }
constructor(
private modalService: ModalService,
private apiService: ApiService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService
) {}
async ngOnInit() {
this.showChangeEmail = this.showChangeKdf = this.showChangePassword =
!await this.keyConnectorService.getUsesKeyConnector();
}
async ngOnInit() {
this.showChangeEmail =
this.showChangeKdf =
this.showChangePassword =
!(await this.keyConnectorService.getUsesKeyConnector());
}
async deauthorizeSessions() {
await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef);
}
async deauthorizeSessions() {
await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef);
}
async purgeVault() {
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef);
}
async purgeVault() {
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef);
}
async deleteAccount() {
await this.modalService.openViewRef(DeleteAccountComponent, this.deleteModalRef);
}
async deleteAccount() {
await this.modalService.openViewRef(DeleteAccountComponent, this.deleteModalRef);
}
async viewUserApiKey() {
const entityId = await this.stateService.getUserId();
await this.modalService.openViewRef(ApiKeyComponent, this.viewUserApiKeyModalRef, comp => {
comp.keyType = 'user';
comp.entityId = entityId;
comp.postKey = this.apiService.postUserApiKey.bind(this.apiService);
comp.scope = 'api';
comp.grantType = 'client_credentials';
comp.apiKeyTitle = 'apiKey';
comp.apiKeyWarning = 'userApiKeyWarning';
comp.apiKeyDescription = 'userApiKeyDesc';
});
}
async viewUserApiKey() {
const entityId = await this.stateService.getUserId();
await this.modalService.openViewRef(ApiKeyComponent, this.viewUserApiKeyModalRef, (comp) => {
comp.keyType = "user";
comp.entityId = entityId;
comp.postKey = this.apiService.postUserApiKey.bind(this.apiService);
comp.scope = "api";
comp.grantType = "client_credentials";
comp.apiKeyTitle = "apiKey";
comp.apiKeyWarning = "userApiKeyWarning";
comp.apiKeyDescription = "userApiKeyDesc";
});
}
async rotateUserApiKey() {
const entityId = await this.stateService.getUserId();
await this.modalService.openViewRef(ApiKeyComponent, this.rotateUserApiKeyModalRef, comp => {
comp.keyType = 'user';
comp.isRotation = true;
comp.entityId = entityId;
comp.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
comp.scope = 'api';
comp.grantType = 'client_credentials';
comp.apiKeyTitle = 'apiKey';
comp.apiKeyWarning = 'userApiKeyWarning';
comp.apiKeyDescription = 'apiKeyRotateDesc';
});
}
async rotateUserApiKey() {
const entityId = await this.stateService.getUserId();
await this.modalService.openViewRef(ApiKeyComponent, this.rotateUserApiKeyModalRef, (comp) => {
comp.keyType = "user";
comp.isRotation = true;
comp.entityId = entityId;
comp.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
comp.scope = "api";
comp.grantType = "client_credentials";
comp.apiKeyTitle = "apiKey";
comp.apiKeyWarning = "userApiKeyWarning";
comp.apiKeyDescription = "apiKeyRotateDesc";
});
}
}

View File

@@ -1,58 +1,80 @@
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{'addCredit' | i18n}}</h3>
<div class="mb-4 text-lg" *ngIf="showOptions">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="Method" id="credit-method-paypal"
[value]="paymentMethodType.PayPal" [(ngModel)]="method">
<label class="form-check-label" for="credit-method-paypal">
<i class="fa fa-fw fa-paypal" aria-hidden="true"></i> PayPal</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="Method" id="credit-method-bitcoin"
[value]="paymentMethodType.BitPay" [(ngModel)]="method">
<label class="form-check-label" for="credit-method-bitcoin">
<i class="fa fa-fw fa-bitcoin" aria-hidden="true"></i> Bitcoin</label>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-4">
<label for="creditAmount">{{'amount' | i18n}}</label>
<div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">$USD</span></div>
<input id="creditAmount" class="form-control" type="text" name="CreditAmount"
[(ngModel)]="creditAmount" (blur)="formatAmount()" required>
</div>
</div>
</div>
<small class="form-text text-muted">{{'creditDelayed' | i18n}}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading || ppLoading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{'cancel' | i18n}}
</button>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
<h3 class="card-body-header">{{ "addCredit" | i18n }}</h3>
<div class="mb-4 text-lg" *ngIf="showOptions">
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="Method"
id="credit-method-paypal"
[value]="paymentMethodType.PayPal"
[(ngModel)]="method"
/>
<label class="form-check-label" for="credit-method-paypal">
<i class="fa fa-fw fa-paypal" aria-hidden="true"></i> PayPal</label
>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="Method"
id="credit-method-bitcoin"
[value]="paymentMethodType.BitPay"
[(ngModel)]="method"
/>
<label class="form-check-label" for="credit-method-bitcoin">
<i class="fa fa-fw fa-bitcoin" aria-hidden="true"></i> Bitcoin</label
>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-4">
<label for="creditAmount">{{ "amount" | i18n }}</label>
<div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">$USD</span></div>
<input
id="creditAmount"
class="form-control"
type="text"
name="CreditAmount"
[(ngModel)]="creditAmount"
(blur)="formatAmount()"
required
/>
</div>
</div>
</div>
<small class="form-text text-muted">{{ "creditDelayed" | i18n }}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading || ppLoading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
</div>
</form>
<form #ppButtonForm action="{{ppButtonFormAction}}" method="post" target="_top">
<input type="hidden" name="cmd" value="_xclick">
<input type="hidden" name="business" value="{{ppButtonBusinessId}}">
<input type="hidden" name="button_subtype" value="services">
<input type="hidden" name="no_note" value="1">
<input type="hidden" name="no_shipping" value="1">
<input type="hidden" name="rm" value="1">
<input type="hidden" name="return" value="{{returnUrl}}">
<input type="hidden" name="cancel_return" value="{{returnUrl}}">
<input type="hidden" name="currency_code" value="USD">
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png">
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted">
<input type="hidden" name="amount" value="{{creditAmount}}">
<input type="hidden" name="custom" value="{{ppButtonCustomField}}">
<input type="hidden" name="item_name" value="Bitwarden Account Credit">
<input type="hidden" name="item_number" value="{{subject}}">
<form #ppButtonForm action="{{ ppButtonFormAction }}" method="post" target="_top">
<input type="hidden" name="cmd" value="_xclick" />
<input type="hidden" name="business" value="{{ ppButtonBusinessId }}" />
<input type="hidden" name="button_subtype" value="services" />
<input type="hidden" name="no_note" value="1" />
<input type="hidden" name="no_shipping" value="1" />
<input type="hidden" name="rm" value="1" />
<input type="hidden" name="return" value="{{ returnUrl }}" />
<input type="hidden" name="cancel_return" value="{{ returnUrl }}" />
<input type="hidden" name="currency_code" value="USD" />
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
<input type="hidden" name="amount" value="{{ creditAmount }}" />
<input type="hidden" name="custom" value="{{ ppButtonCustomField }}" />
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
<input type="hidden" name="item_number" value="{{ subject }}" />
</form>

View File

@@ -1,146 +1,151 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { PayPalConfig } from 'jslib-common/abstractions/environment.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { PayPalConfig } from "jslib-common/abstractions/environment.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
import { PaymentMethodType } from "jslib-common/enums/paymentMethodType";
import { BitPayInvoiceRequest } from 'jslib-common/models/request/bitPayInvoiceRequest';
import { BitPayInvoiceRequest } from "jslib-common/models/request/bitPayInvoiceRequest";
@Component({
selector: 'app-add-credit',
templateUrl: 'add-credit.component.html',
selector: "app-add-credit",
templateUrl: "add-credit.component.html",
})
export class AddCreditComponent implements OnInit {
@Input() creditAmount: string;
@Input() showOptions = true;
@Input() method = PaymentMethodType.PayPal;
@Input() organizationId: string;
@Output() onAdded = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@Input() creditAmount: string;
@Input() showOptions = true;
@Input() method = PaymentMethodType.PayPal;
@Input() organizationId: string;
@Output() onAdded = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@ViewChild('ppButtonForm', { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
@ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
paymentMethodType = PaymentMethodType;
ppButtonFormAction: string;
ppButtonBusinessId: string;
ppButtonCustomField: string;
ppLoading = false;
subject: string;
returnUrl: string;
formPromise: Promise<any>;
paymentMethodType = PaymentMethodType;
ppButtonFormAction: string;
ppButtonBusinessId: string;
ppButtonCustomField: string;
ppLoading = false;
subject: string;
returnUrl: string;
formPromise: Promise<any>;
private userId: string;
private name: string;
private email: string;
private userId: string;
private name: string;
private email: string;
constructor(private stateService: StateService, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private organizationService: OrganizationService,
private logService: LogService) {
const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
this.ppButtonFormAction = payPalConfig.buttonAction;
this.ppButtonBusinessId = payPalConfig.businessId;
constructor(
private stateService: StateService,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private logService: LogService
) {
const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
this.ppButtonFormAction = payPalConfig.buttonAction;
this.ppButtonBusinessId = payPalConfig.businessId;
}
async ngOnInit() {
if (this.organizationId != null) {
if (this.creditAmount == null) {
this.creditAmount = "20.00";
}
this.ppButtonCustomField = "organization_id:" + this.organizationId;
const org = await this.organizationService.get(this.organizationId);
if (org != null) {
this.subject = org.name;
this.name = org.name;
}
} else {
if (this.creditAmount == null) {
this.creditAmount = "10.00";
}
this.userId = await this.stateService.getUserId();
this.subject = await this.stateService.getEmail();
this.email = this.subject;
this.ppButtonCustomField = "user_id:" + this.userId;
}
this.ppButtonCustomField += ",account_credit:1";
this.returnUrl = window.location.href;
}
async submit() {
if (this.creditAmount == null || this.creditAmount === "") {
return;
}
async ngOnInit() {
if (this.organizationId != null) {
if (this.creditAmount == null) {
this.creditAmount = '20.00';
}
this.ppButtonCustomField = 'organization_id:' + this.organizationId;
const org = await this.organizationService.get(this.organizationId);
if (org != null) {
this.subject = org.name;
this.name = org.name;
}
} else {
if (this.creditAmount == null) {
this.creditAmount = '10.00';
}
this.userId = await this.stateService.getUserId();
this.subject = await this.stateService.getEmail();
this.email = this.subject;
this.ppButtonCustomField = 'user_id:' + this.userId;
}
this.ppButtonCustomField += ',account_credit:1';
this.returnUrl = window.location.href;
if (this.method === PaymentMethodType.PayPal) {
this.ppButtonFormRef.nativeElement.submit();
this.ppLoading = true;
return;
}
async submit() {
if (this.creditAmount == null || this.creditAmount === '') {
return;
}
if (this.method === PaymentMethodType.PayPal) {
this.ppButtonFormRef.nativeElement.submit();
this.ppLoading = true;
return;
}
if (this.method === PaymentMethodType.BitPay) {
try {
const req = new BitPayInvoiceRequest();
req.email = this.email;
req.name = this.name;
req.credit = true;
req.amount = this.creditAmountNumber;
req.organizationId = this.organizationId;
req.userId = this.userId;
req.returnUrl = this.returnUrl;
this.formPromise = this.apiService.postBitPayInvoice(req);
const bitPayUrl: string = await this.formPromise;
this.platformUtilsService.launchUri(bitPayUrl);
} catch (e) {
this.logService.error(e);
}
return;
}
try {
this.onAdded.emit();
} catch (e) {
this.logService.error(e);
}
if (this.method === PaymentMethodType.BitPay) {
try {
const req = new BitPayInvoiceRequest();
req.email = this.email;
req.name = this.name;
req.credit = true;
req.amount = this.creditAmountNumber;
req.organizationId = this.organizationId;
req.userId = this.userId;
req.returnUrl = this.returnUrl;
this.formPromise = this.apiService.postBitPayInvoice(req);
const bitPayUrl: string = await this.formPromise;
this.platformUtilsService.launchUri(bitPayUrl);
} catch (e) {
this.logService.error(e);
}
return;
}
cancel() {
this.onCanceled.emit();
try {
this.onAdded.emit();
} catch (e) {
this.logService.error(e);
}
}
formatAmount() {
try {
if (this.creditAmount != null && this.creditAmount !== '') {
const floatAmount = Math.abs(parseFloat(this.creditAmount));
if (floatAmount > 0) {
this.creditAmount = parseFloat((Math.round(floatAmount * 100) / 100).toString())
.toFixed(2).toString();
return;
}
}
} catch (e) {
this.logService.error(e);
cancel() {
this.onCanceled.emit();
}
formatAmount() {
try {
if (this.creditAmount != null && this.creditAmount !== "") {
const floatAmount = Math.abs(parseFloat(this.creditAmount));
if (floatAmount > 0) {
this.creditAmount = parseFloat((Math.round(floatAmount * 100) / 100).toString())
.toFixed(2)
.toString();
return;
}
this.creditAmount = '';
}
} catch (e) {
this.logService.error(e);
}
this.creditAmount = "";
}
get creditAmountNumber(): number {
if (this.creditAmount != null && this.creditAmount !== '') {
try {
return parseFloat(this.creditAmount);
} catch (e) {
this.logService.error(e);
}
}
return null;
get creditAmountNumber(): number {
if (this.creditAmount != null && this.creditAmount !== "") {
try {
return parseFloat(this.creditAmount);
} catch (e) {
this.logService.error(e);
}
}
return null;
}
}

View File

@@ -1,16 +1,19 @@
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}</h3>
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{'cancel' | i18n}}
</button>
</div>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
<h3 class="card-body-header">
{{ (currentType != null ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
</h3>
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
</div>
</form>

View File

@@ -1,83 +1,85 @@
import {
Component,
EventEmitter,
Input,
Output,
ViewChild,
} from '@angular/core';
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PaymentRequest } from 'jslib-common/models/request/paymentRequest';
import { PaymentRequest } from "jslib-common/models/request/paymentRequest";
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
import { PaymentMethodType } from "jslib-common/enums/paymentMethodType";
import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
import { PaymentComponent } from "./payment.component";
import { TaxInfoComponent } from "./tax-info.component";
@Component({
selector: 'app-adjust-payment',
templateUrl: 'adjust-payment.component.html',
selector: "app-adjust-payment",
templateUrl: "adjust-payment.component.html",
})
export class AdjustPaymentComponent {
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
@Input() currentType?: PaymentMethodType;
@Input() organizationId: string;
@Output() onAdjusted = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@Input() currentType?: PaymentMethodType;
@Input() organizationId: string;
@Output() onAdjusted = new EventEmitter();
@Output() onCanceled = new EventEmitter();
paymentMethodType = PaymentMethodType;
formPromise: Promise<any>;
paymentMethodType = PaymentMethodType;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async submit() {
try {
const request = new PaymentRequest();
this.formPromise = this.paymentComponent.createPaymentToken().then(result => {
request.paymentToken = result[0];
request.paymentMethodType = result[1];
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
request.country = this.taxInfoComponent.taxInfo.country;
if (this.organizationId == null) {
return this.apiService.postAccountPayment(request);
} else {
request.taxId = this.taxInfoComponent.taxInfo.taxId;
request.state = this.taxInfoComponent.taxInfo.state;
request.line1 = this.taxInfoComponent.taxInfo.line1;
request.line2 = this.taxInfoComponent.taxInfo.line2;
request.city = this.taxInfoComponent.taxInfo.city;
request.state = this.taxInfoComponent.taxInfo.state;
return this.apiService.postOrganizationPayment(this.organizationId, request);
}
});
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('updatedPaymentMethod'));
this.onAdjusted.emit();
} catch (e) {
this.logService.error(e);
}
}
cancel() {
this.onCanceled.emit();
}
changeCountry() {
if (this.taxInfoComponent.taxInfo.country === 'US') {
this.paymentComponent.hideBank = !this.organizationId;
async submit() {
try {
const request = new PaymentRequest();
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
request.paymentToken = result[0];
request.paymentMethodType = result[1];
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
request.country = this.taxInfoComponent.taxInfo.country;
if (this.organizationId == null) {
return this.apiService.postAccountPayment(request);
} else {
this.paymentComponent.hideBank = true;
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
request.taxId = this.taxInfoComponent.taxInfo.taxId;
request.state = this.taxInfoComponent.taxInfo.state;
request.line1 = this.taxInfoComponent.taxInfo.line1;
request.line2 = this.taxInfoComponent.taxInfo.line2;
request.city = this.taxInfoComponent.taxInfo.city;
request.state = this.taxInfoComponent.taxInfo.state;
return this.apiService.postOrganizationPayment(this.organizationId, request);
}
});
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("updatedPaymentMethod")
);
this.onAdjusted.emit();
} catch (e) {
this.logService.error(e);
}
}
cancel() {
this.onCanceled.emit();
}
changeCountry() {
if (this.taxInfoComponent.taxInfo.country === "US") {
this.paymentComponent.hideBank = !this.organizationId;
} else {
this.paymentComponent.hideBank = true;
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
}
}

View File

@@ -1,30 +1,43 @@
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{(add ? 'addStorage' : 'removeStorage') | i18n}}</h3>
<div class="row">
<div class="form-group col-6">
<label for="storageAdjustment">{{(add ? 'gbStorageAdd' : 'gbStorageRemove') | i18n}}</label>
<input id="storageAdjustment" class="form-control" type="number" name="StroageGbAdjustment"
[(ngModel)]="storageAdjustment" min="0" max="99" step="1" required>
</div>
</div>
<div *ngIf="add" class="mb-3">
<strong>{{'total' | i18n}}:</strong> {{storageAdjustment || 0}} GB &times; {{storageGbPrice | currency:'$'}}
= {{adjustedStorageTotal
| currency:'$'}} /{{interval | i18n}}
</div>
<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>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{'cancel' | i18n}}
</button>
<small class="d-block text-muted mt-3">
{{(add ? 'storageAddNote' : 'storageRemoveNote') | i18n}}
</small>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
<h3 class="card-body-header">{{ (add ? "addStorage" : "removeStorage") | i18n }}</h3>
<div class="row">
<div class="form-group col-6">
<label for="storageAdjustment">{{
(add ? "gbStorageAdd" : "gbStorageRemove") | i18n
}}</label>
<input
id="storageAdjustment"
class="form-control"
type="number"
name="StroageGbAdjustment"
[(ngModel)]="storageAdjustment"
min="0"
max="99"
step="1"
required
/>
</div>
</div>
<div *ngIf="add" class="mb-3">
<strong>{{ "total" | i18n }}:</strong> {{ storageAdjustment || 0 }} GB &times;
{{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{
interval | i18n
}}
</div>
<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>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
<small class="d-block text-muted mt-3">
{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}
</small>
</div>
</form>
<app-payment [showMethods]="false"></app-payment>

View File

@@ -1,94 +1,103 @@
import {
Component,
EventEmitter,
Input,
Output,
ViewChild,
} from '@angular/core';
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ActivatedRoute, Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StorageRequest } from 'jslib-common/models/request/storageRequest';
import { StorageRequest } from "jslib-common/models/request/storageRequest";
import { PaymentResponse } from 'jslib-common/models/response/paymentResponse';
import { PaymentResponse } from "jslib-common/models/response/paymentResponse";
import { PaymentComponent } from './payment.component';
import { PaymentComponent } from "./payment.component";
@Component({
selector: 'app-adjust-storage',
templateUrl: 'adjust-storage.component.html',
selector: "app-adjust-storage",
templateUrl: "adjust-storage.component.html",
})
export class AdjustStorageComponent {
@Input() storageGbPrice = 0;
@Input() add = true;
@Input() organizationId: string;
@Input() interval = 'year';
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
@Input() storageGbPrice = 0;
@Input() add = true;
@Input() organizationId: string;
@Input() interval = "year";
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
storageAdjustment = 0;
formPromise: Promise<any>;
storageAdjustment = 0;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private router: Router,
private activatedRoute: ActivatedRoute, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private router: Router,
private activatedRoute: ActivatedRoute,
private logService: LogService
) {}
async submit() {
try {
const request = new StorageRequest();
request.storageGbAdjustment = this.storageAdjustment;
if (!this.add) {
request.storageGbAdjustment *= -1;
}
async submit() {
try {
const request = new StorageRequest();
request.storageGbAdjustment = this.storageAdjustment;
if (!this.add) {
request.storageGbAdjustment *= -1;
}
let paymentFailed = false;
const action = async () => {
let response: Promise<PaymentResponse>;
if (this.organizationId == null) {
response = this.formPromise = this.apiService.postAccountStorage(request);
} else {
response = this.formPromise = this.apiService.postOrganizationStorage(this.organizationId, request);
}
const result = await response;
if (result != null && result.paymentIntentClientSecret != null) {
try {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
} catch {
paymentFailed = true;
}
}
};
this.formPromise = action();
await this.formPromise;
this.onAdjusted.emit(this.storageAdjustment);
if (paymentFailed) {
this.platformUtilsService.showToast('warning', null,
this.i18nService.t('couldNotChargeCardPayInvoice'), { timeout: 10000 });
this.router.navigate(['../billing'], { relativeTo: this.activatedRoute });
} else {
this.platformUtilsService.showToast('success', null,
this.i18nService.t('adjustedStorage', request.storageGbAdjustment.toString()));
}
} catch (e) {
this.logService.error(e);
let paymentFailed = false;
const action = async () => {
let response: Promise<PaymentResponse>;
if (this.organizationId == null) {
response = this.formPromise = this.apiService.postAccountStorage(request);
} else {
response = this.formPromise = this.apiService.postOrganizationStorage(
this.organizationId,
request
);
}
const result = await response;
if (result != null && result.paymentIntentClientSecret != null) {
try {
await this.paymentComponent.handleStripeCardPayment(
result.paymentIntentClientSecret,
null
);
} catch {
paymentFailed = true;
}
}
};
this.formPromise = action();
await this.formPromise;
this.onAdjusted.emit(this.storageAdjustment);
if (paymentFailed) {
this.platformUtilsService.showToast(
"warning",
null,
this.i18nService.t("couldNotChargeCardPayInvoice"),
{ timeout: 10000 }
);
this.router.navigate(["../billing"], { relativeTo: this.activatedRoute });
} else {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString())
);
}
} catch (e) {
this.logService.error(e);
}
}
cancel() {
this.onCanceled.emit();
}
cancel() {
this.onCanceled.emit();
}
get adjustedStorageTotal(): number {
return this.storageGbPrice * this.storageAdjustment;
}
get adjustedStorageTotal(): number {
return this.storageGbPrice * this.storageAdjustment;
}
}

View File

@@ -1,46 +1,72 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="apiKeyTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="apiKeyTitle">{{apiKeyTitle | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{apiKeyDescription | i18n}}</p>
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret" *ngIf="!clientSecret">
</app-verify-master-password>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="apiKeyTitle">{{ apiKeyTitle | i18n }}</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ apiKeyDescription | i18n }}</p>
<app-verify-master-password
[(ngModel)]="masterPassword"
ngDefaultControl
name="secret"
*ngIf="!clientSecret"
>
</app-verify-master-password>
<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>{{grantType}}</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>{{(isRotation ? 'rotateApiKey' : 'viewApiKey') | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
<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>{{ grantType }}</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>{{ (isRotation ? "rotateApiKey" : "viewApiKey") | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,45 +1,49 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { LogService } from 'jslib-common/abstractions/log.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { LogService } from "jslib-common/abstractions/log.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest';
import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
import { ApiKeyResponse } from 'jslib-common/models/response/apiKeyResponse';
import { ApiKeyResponse } from "jslib-common/models/response/apiKeyResponse";
import { Verification } from 'jslib-common/types/verification';
import { Verification } from "jslib-common/types/verification";
@Component({
selector: 'app-api-key',
templateUrl: 'api-key.component.html',
selector: "app-api-key",
templateUrl: "api-key.component.html",
})
export class ApiKeyComponent {
keyType: string;
isRotation: boolean;
postKey: (entityId: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
entityId: string;
scope: string;
grantType: string;
apiKeyTitle: string;
apiKeyWarning: string;
apiKeyDescription: string;
keyType: string;
isRotation: boolean;
postKey: (entityId: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
entityId: string;
scope: string;
grantType: string;
apiKeyTitle: string;
apiKeyWarning: string;
apiKeyDescription: string;
masterPassword: Verification;
formPromise: Promise<ApiKeyResponse>;
clientId: string;
clientSecret: string;
masterPassword: Verification;
formPromise: Promise<ApiKeyResponse>;
clientId: string;
clientSecret: string;
constructor(private userVerificationService: UserVerificationService, private logService: LogService) { }
constructor(
private userVerificationService: UserVerificationService,
private logService: LogService
) {}
async submit() {
try {
this.formPromise = this.userVerificationService.buildRequest(this.masterPassword)
.then(request => this.postKey(this.entityId, request));
const response = await this.formPromise;
this.clientSecret = response.apiKey;
this.clientId = `${this.keyType}.${this.entityId}`;
} catch (e) {
this.logService.error(e);
}
async submit() {
try {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword)
.then((request) => this.postKey(this.entityId, request));
const response = await this.formPromise;
this.clientSecret = response.apiKey;
this.clientId = `${this.keyType}.${this.entityId}`;
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,41 +1,65 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<app-callout type="warning" *ngIf="showTwoFactorEmailWarning">
{{'changeEmailTwoFactorWarning' | i18n}}
</app-callout>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="masterPassword" required [readonly]="tokenSent" appInputVerbatim>
</div>
<div class="form-group">
<label for="newEmail">{{'newEmail' | i18n}}</label>
<input id="newEmail" class="form-control" type="text" name="NewEmail" [(ngModel)]="newEmail" required
[readonly]="tokenSent" inputmode="email" appInputVerbatim="false">
</div>
</div>
<app-callout type="warning" *ngIf="showTwoFactorEmailWarning">
{{ "changeEmailTwoFactorWarning" | i18n }}
</app-callout>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[(ngModel)]="masterPassword"
required
[readonly]="tokenSent"
appInputVerbatim
/>
</div>
<div class="form-group">
<label for="newEmail">{{ "newEmail" | i18n }}</label>
<input
id="newEmail"
class="form-control"
type="text"
name="NewEmail"
[(ngModel)]="newEmail"
required
[readonly]="tokenSent"
inputmode="email"
appInputVerbatim="false"
/>
</div>
</div>
<ng-container *ngIf="tokenSent">
<hr>
<p>{{'changeEmailDesc' | i18n : newEmail}}</p>
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="token">{{'code' | i18n}}</label>
<input id="token" class="form-control" type="text" name="Token" [(ngModel)]="token" required
appInputVerbatim>
</div>
</div>
</div>
<ng-container *ngIf="tokenSent">
<hr />
<p>{{ "changeEmailDesc" | i18n: newEmail }}</p>
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="token">{{ "code" | i18n }}</label>
<input
id="token"
class="form-control"
type="text"
name="Token"
[(ngModel)]="token"
required
appInputVerbatim
/>
</div>
</ng-container>
<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 *ngIf="!tokenSent">{{'continue' | i18n}}</span>
<span *ngIf="tokenSent">{{'changeEmail' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" *ngIf="tokenSent" (click)="reset()">
{{'cancel' | i18n}}
</button>
</div>
</div>
</ng-container>
<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 *ngIf="!tokenSent">{{ "continue" | i18n }}</span>
<span *ngIf="tokenSent">{{ "changeEmail" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" *ngIf="tokenSent" (click)="reset()">
{{ "cancel" | i18n }}
</button>
</form>

View File

@@ -1,95 +1,104 @@
import {
Component,
OnInit,
} from '@angular/core';
import { Component, OnInit } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { EmailRequest } from 'jslib-common/models/request/emailRequest';
import { EmailTokenRequest } from 'jslib-common/models/request/emailTokenRequest';
import { EmailRequest } from "jslib-common/models/request/emailRequest";
import { EmailTokenRequest } from "jslib-common/models/request/emailTokenRequest";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
@Component({
selector: 'app-change-email',
templateUrl: 'change-email.component.html',
selector: "app-change-email",
templateUrl: "change-email.component.html",
})
export class ChangeEmailComponent implements OnInit {
masterPassword: string;
newEmail: string;
token: string;
tokenSent = false;
showTwoFactorEmailWarning = false;
masterPassword: string;
newEmail: string;
token: string;
tokenSent = false;
showTwoFactorEmailWarning = false;
formPromise: Promise<any>;
formPromise: Promise<any>;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private logService: LogService,
private stateService: StateService,
) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private logService: LogService,
private stateService: StateService
) {}
async ngOnInit() {
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
this.showTwoFactorEmailWarning = twoFactorProviders.data.some(p => p.type === TwoFactorProviderType.Email &&
p.enabled);
async ngOnInit() {
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
this.showTwoFactorEmailWarning = twoFactorProviders.data.some(
(p) => p.type === TwoFactorProviderType.Email && p.enabled
);
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
return;
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('updateKey'));
return;
}
this.newEmail = this.newEmail.trim().toLowerCase();
if (!this.tokenSent) {
const request = new EmailTokenRequest();
request.newEmail = this.newEmail;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postEmailToken(request);
await this.formPromise;
this.tokenSent = true;
} catch (e) {
this.logService.error(e);
}
} else {
const request = new EmailRequest();
request.token = this.token;
request.newEmail = this.newEmail;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
const kdf = await this.stateService.getKdfType();
const kdfIterations = await this.stateService.getKdfIterations();
const newKey = await this.cryptoService.makeKey(this.masterPassword, this.newEmail, kdf, kdfIterations);
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey);
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
this.formPromise = this.apiService.postEmail(request);
await this.formPromise;
this.reset();
this.platformUtilsService.showToast('success', this.i18nService.t('emailChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');
} catch (e) {
this.logService.error(e);
}
}
this.newEmail = this.newEmail.trim().toLowerCase();
if (!this.tokenSent) {
const request = new EmailTokenRequest();
request.newEmail = this.newEmail;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postEmailToken(request);
await this.formPromise;
this.tokenSent = true;
} catch (e) {
this.logService.error(e);
}
} else {
const request = new EmailRequest();
request.token = this.token;
request.newEmail = this.newEmail;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
const kdf = await this.stateService.getKdfType();
const kdfIterations = await this.stateService.getKdfIterations();
const newKey = await this.cryptoService.makeKey(
this.masterPassword,
this.newEmail,
kdf,
kdfIterations
);
request.newMasterPasswordHash = await this.cryptoService.hashPassword(
this.masterPassword,
newKey
);
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
this.formPromise = this.apiService.postEmail(request);
await this.formPromise;
this.reset();
this.platformUtilsService.showToast(
"success",
this.i18nService.t("emailChanged"),
this.i18nService.t("logBackIn")
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
}
}
}
reset() {
this.token = this.newEmail = this.masterPassword = null;
this.tokenSent = false;
}
reset() {
this.token = this.newEmail = this.masterPassword = null;
this.tokenSent = false;
}
}

View File

@@ -1,49 +1,75 @@
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="kdfMasterPassword">{{'masterPass' | i18n}}</label>
<input id="kdfMasterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="masterPassword" required appInputVerbatim>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="kdfMasterPassword">{{ "masterPass" | i18n }}</label>
<input
id="kdfMasterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group mb-0">
<label for="kdf">{{'kdfAlgorithm' | i18n}}</label>
<a class="ml-auto" href="https://en.wikipedia.org/wiki/Key_derivation_function" target="_blank"
rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
<select id="kdf" name="Kdf" [(ngModel)]="kdf" class="form-control" required>
<option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
<div class="col-6">
<div class="form-group mb-0">
<label for="kdfIterations">{{'kdfIterations' | i18n}}</label>
<a class="ml-auto" href="https://bitwarden.com/help/article/what-encryption-is-used/#pbkdf2" target="_blank" rel="noopener"
appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
<input id="kdfIterations" type="number" min="5000" max="2000000" name="KdfIterations"
class="form-control" [(ngModel)]="kdfIterations" required>
</div>
</div>
<div class="col-12">
<div class="form-group">
<div class="small form-text text-muted">
<p>{{'kdfIterationsDesc' | i18n : (100000 | number)}}</p>
<strong>{{'warning' | i18n}}</strong>: {{'kdfIterationsWarning' | i18n : (50000 | number)}}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group mb-0">
<label for="kdf">{{ "kdfAlgorithm" | i18n }}</label>
<a
class="ml-auto"
href="https://en.wikipedia.org/wiki/Key_derivation_function"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
<select id="kdf" name="Kdf" [(ngModel)]="kdf" class="form-control" required>
<option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
<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>{{'changeKdf' | i18n}}</span>
</button>
<div class="col-6">
<div class="form-group mb-0">
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
<a
class="ml-auto"
href="https://bitwarden.com/help/article/what-encryption-is-used/#pbkdf2"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
<input
id="kdfIterations"
type="number"
min="5000"
max="2000000"
name="KdfIterations"
class="form-control"
[(ngModel)]="kdfIterations"
required
/>
</div>
</div>
<div class="col-12">
<div class="form-group">
<div class="small form-text text-muted">
<p>{{ "kdfIterationsDesc" | i18n: (100000 | number) }}</p>
<strong>{{ "warning" | i18n }}</strong
>: {{ "kdfIterationsWarning" | i18n: (50000 | number) }}
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "changeKdf" | i18n }}</span>
</button>
</form>

View File

@@ -1,74 +1,80 @@
import {
Component,
OnInit,
} from '@angular/core';
import { Component, OnInit } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { KdfRequest } from 'jslib-common/models/request/kdfRequest';
import { KdfRequest } from "jslib-common/models/request/kdfRequest";
import { KdfType } from 'jslib-common/enums/kdfType';
import { KdfType } from "jslib-common/enums/kdfType";
@Component({
selector: 'app-change-kdf',
templateUrl: 'change-kdf.component.html',
selector: "app-change-kdf",
templateUrl: "change-kdf.component.html",
})
export class ChangeKdfComponent implements OnInit {
masterPassword: string;
kdfIterations: number;
kdf = KdfType.PBKDF2_SHA256;
kdfOptions: any[] = [];
formPromise: Promise<any>;
masterPassword: string;
kdfIterations: number;
kdf = KdfType.PBKDF2_SHA256;
kdfOptions: any[] = [];
formPromise: Promise<any>;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private logService: LogService,
private stateService: StateService,
) {
this.kdfOptions = [
{ name: 'PBKDF2 SHA-256', value: KdfType.PBKDF2_SHA256 },
];
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private logService: LogService,
private stateService: StateService
) {
this.kdfOptions = [{ name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 }];
}
async ngOnInit() {
this.kdf = await this.stateService.getKdfType();
this.kdfIterations = await this.stateService.getKdfIterations();
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
return;
}
async ngOnInit() {
this.kdf = await this.stateService.getKdfType();
this.kdfIterations = await this.stateService.getKdfIterations();
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('updateKey'));
return;
}
const request = new KdfRequest();
request.kdf = this.kdf;
request.kdfIterations = this.kdfIterations;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
const email = await this.stateService.getEmail();
const newKey = await this.cryptoService.makeKey(this.masterPassword, email, this.kdf, this.kdfIterations);
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey);
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
this.formPromise = this.apiService.postAccountKdf(request);
await this.formPromise;
this.platformUtilsService.showToast('success', this.i18nService.t('encKeySettingsChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');
} catch (e) {
this.logService.error(e);
}
const request = new KdfRequest();
request.kdf = this.kdf;
request.kdfIterations = this.kdfIterations;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
const email = await this.stateService.getEmail();
const newKey = await this.cryptoService.makeKey(
this.masterPassword,
email,
this.kdf,
this.kdfIterations
);
request.newMasterPasswordHash = await this.cryptoService.hashPassword(
this.masterPassword,
newKey
);
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
this.formPromise = this.apiService.postAccountKdf(request);
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("encKeySettingsChanged"),
this.i18nService.t("logBackIn")
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,50 +1,90 @@
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
<app-callout type="info" [enforcedPolicyOptions]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="currentMasterPassword">{{'currentMasterPass' | i18n}}</label>
<input id="currentMasterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="currentMasterPassword" required appInputVerbatim>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="currentMasterPassword">{{ "currentMasterPass" | i18n }}</label>
<input
id="currentMasterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[(ngModel)]="currentMasterPassword"
required
appInputVerbatim
/>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="newMasterPassword">{{'newMasterPass' | i18n}}</label>
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control 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="masterPasswordRetype">{{'confirmNewMasterPass' | i18n}}</label>
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype" class="form-control"
[(ngModel)]="masterPasswordRetype" required appInputVerbatim autocomplete="new-password">
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="newMasterPassword">{{ "newMasterPass" | i18n }}</label>
<input
id="newMasterPassword"
type="password"
name="NewMasterPasswordHash"
class="form-control 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="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="rotateEncKey" name="RotateEncKey"
[(ngModel)]="rotateEncKey" (change)="rotateEncKeyClicked()">
<label class="form-check-label" for="rotateEncKey">
{{'rotateAccountEncKey' | i18n}}
</label>
<a href="https://bitwarden.com/help/article/account-encryption-key/#rotate-your-encryption-key"
target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="col-6">
<div class="form-group">
<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>
<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>{{'changeMasterPassword' | i18n}}</span>
</button>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="rotateEncKey"
name="RotateEncKey"
[(ngModel)]="rotateEncKey"
(change)="rotateEncKeyClicked()"
/>
<label class="form-check-label" for="rotateEncKey">
{{ "rotateAccountEncKey" | i18n }}
</label>
<a
href="https://bitwarden.com/help/article/account-encryption-key/#rotate-your-encryption-key"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "changeMasterPassword" | i18n }}</span>
</button>
</form>

View File

@@ -1,233 +1,271 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { SendService } from 'jslib-common/abstractions/send.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import {
ChangePasswordComponent as BaseChangePasswordComponent,
} from 'jslib-angular/components/change-password.component';
import { ChangePasswordComponent as BaseChangePasswordComponent } from "jslib-angular/components/change-password.component";
import { EmergencyAccessStatusType } from 'jslib-common/enums/emergencyAccessStatusType';
import { Utils } from 'jslib-common/misc/utils';
import { EmergencyAccessStatusType } from "jslib-common/enums/emergencyAccessStatusType";
import { Utils } from "jslib-common/misc/utils";
import { EncString } from 'jslib-common/models/domain/encString';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { EncString } from "jslib-common/models/domain/encString";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { CipherWithIdRequest } from 'jslib-common/models/request/cipherWithIdRequest';
import { EmergencyAccessUpdateRequest } from 'jslib-common/models/request/emergencyAccessUpdateRequest';
import { FolderWithIdRequest } from 'jslib-common/models/request/folderWithIdRequest';
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest';
import { PasswordRequest } from 'jslib-common/models/request/passwordRequest';
import { SendWithIdRequest } from 'jslib-common/models/request/sendWithIdRequest';
import { UpdateKeyRequest } from 'jslib-common/models/request/updateKeyRequest';
import { CipherWithIdRequest } from "jslib-common/models/request/cipherWithIdRequest";
import { EmergencyAccessUpdateRequest } from "jslib-common/models/request/emergencyAccessUpdateRequest";
import { FolderWithIdRequest } from "jslib-common/models/request/folderWithIdRequest";
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
import { PasswordRequest } from "jslib-common/models/request/passwordRequest";
import { SendWithIdRequest } from "jslib-common/models/request/sendWithIdRequest";
import { UpdateKeyRequest } from "jslib-common/models/request/updateKeyRequest";
@Component({
selector: 'app-change-password',
templateUrl: 'change-password.component.html',
selector: "app-change-password",
templateUrl: "change-password.component.html",
})
export class ChangePasswordComponent extends BaseChangePasswordComponent {
rotateEncKey = false;
currentMasterPassword: string;
rotateEncKey = false;
currentMasterPassword: string;
constructor(i18nService: I18nService,
cryptoService: CryptoService, messagingService: MessagingService,
stateService: StateService, passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService,
private folderService: FolderService, private cipherService: CipherService,
private syncService: SyncService, private apiService: ApiService,
private sendService: SendService, private organizationService: OrganizationService) {
super(i18nService, cryptoService, messagingService, passwordGenerationService,
platformUtilsService, policyService, stateService);
}
constructor(
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
stateService: StateService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
private folderService: FolderService,
private cipherService: CipherService,
private syncService: SyncService,
private apiService: ApiService,
private sendService: SendService,
private organizationService: OrganizationService
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyService,
stateService
);
}
async rotateEncKeyClicked() {
if (this.rotateEncKey) {
const ciphers = await this.cipherService.getAllDecrypted();
let hasOldAttachments = false;
if (ciphers != null) {
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) {
hasOldAttachments = true;
break;
}
}
}
if (hasOldAttachments) {
const learnMore = await this.platformUtilsService.showDialog(
this.i18nService.t('oldAttachmentsNeedFixDesc'), null,
this.i18nService.t('learnMore'), this.i18nService.t('close'), 'warning');
if (learnMore) {
this.platformUtilsService.launchUri(
'https://help.bitwarden.com/article/attachments/#fixing-old-attachments');
}
this.rotateEncKey = false;
return;
}
const result = await this.platformUtilsService.showDialog(
this.i18nService.t('updateEncryptionKeyWarning') + ' ' +
this.i18nService.t('updateEncryptionKeyExportWarning') + ' ' +
this.i18nService.t('rotateEncKeyConfirmation'), this.i18nService.t('rotateEncKeyTitle'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!result) {
this.rotateEncKey = false;
}
}
}
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, EncString]) {
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'));
}
}
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {
const encKey = await this.cryptoService.makeEncKey(key);
const privateKey = await this.cryptoService.getPrivateKey();
let encPrivateKey: EncString = null;
if (privateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(privateKey, encKey[0]);
}
const request = new UpdateKeyRequest();
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
request.key = encKey[1].encryptedString;
request.masterPasswordHash = masterPasswordHash;
const folders = await this.folderService.getAllDecrypted();
for (let i = 0; i < folders.length; i++) {
if (folders[i].id == null) {
continue;
}
const folder = await this.folderService.encrypt(folders[i], encKey[0]);
request.folders.push(new FolderWithIdRequest(folder));
}
const ciphers = await this.cipherService.getAllDecrypted();
async rotateEncKeyClicked() {
if (this.rotateEncKey) {
const ciphers = await this.cipherService.getAllDecrypted();
let hasOldAttachments = false;
if (ciphers != null) {
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId != null) {
continue;
}
const cipher = await this.cipherService.encrypt(ciphers[i], encKey[0]);
request.ciphers.push(new CipherWithIdRequest(cipher));
if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) {
hasOldAttachments = true;
break;
}
}
}
const sends = await this.sendService.getAll();
await Promise.all(sends.map(async send => {
const cryptoKey = await this.cryptoService.decryptToBytes(send.key, null);
send.key = await this.cryptoService.encrypt(cryptoKey, encKey[0]) ?? send.key;
request.sends.push(new SendWithIdRequest(send));
}));
if (hasOldAttachments) {
const learnMore = await this.platformUtilsService.showDialog(
this.i18nService.t("oldAttachmentsNeedFixDesc"),
null,
this.i18nService.t("learnMore"),
this.i18nService.t("close"),
"warning"
);
if (learnMore) {
this.platformUtilsService.launchUri(
"https://help.bitwarden.com/article/attachments/#fixing-old-attachments"
);
}
this.rotateEncKey = false;
return;
}
await this.apiService.postAccountKey(request);
const result = await this.platformUtilsService.showDialog(
this.i18nService.t("updateEncryptionKeyWarning") +
" " +
this.i18nService.t("updateEncryptionKeyExportWarning") +
" " +
this.i18nService.t("rotateEncKeyConfirmation"),
this.i18nService.t("rotateEncKeyTitle"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!result) {
this.rotateEncKey = false;
}
}
}
await this.updateEmergencyAccesses(encKey[0]);
await this.updateAllResetPasswordKeys(encKey[0]);
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
return;
}
private async updateEmergencyAccesses(encKey: SymmetricCryptoKey) {
const emergencyAccess = await this.apiService.getEmergencyAccessTrusted();
const allowedStatuses = [
EmergencyAccessStatusType.Confirmed,
EmergencyAccessStatusType.RecoveryInitiated,
EmergencyAccessStatusType.RecoveryApproved,
];
await super.submit();
}
const filteredAccesses = emergencyAccess.data.filter(d => allowedStatuses.includes(d.status));
for (const details of filteredAccesses) {
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
const updateRequest = new EmergencyAccessUpdateRequest();
updateRequest.type = details.type;
updateRequest.waitTimeDays = details.waitTimeDays;
updateRequest.keyEncrypted = encryptedKey.encryptedString;
await this.apiService.putEmergencyAccess(details.id, updateRequest);
}
async setupSubmitActions() {
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPassRequired")
);
return false;
}
private async updateAllResetPasswordKeys(encKey: SymmetricCryptoKey) {
const orgs = await this.organizationService.getAll();
for (const org of orgs) {
// If not already enrolled, skip
if (!org.resetPasswordEnrolled) {
continue;
}
// Retrieve public key
const response = await this.apiService.getOrganizationKeys(org.id);
const publicKey = Utils.fromB64ToArray(response?.publicKey);
// Re-enroll - encrpyt user's encKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
// Create/Execute request
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = encryptedKey.encryptedString;
await this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request);
}
if (this.rotateEncKey) {
await this.syncService.fullSync(true);
}
return super.setupSubmitActions();
}
async performSubmitActions(
newMasterPasswordHash: string,
newKey: SymmetricCryptoKey,
newEncKey: [SymmetricCryptoKey, EncString]
) {
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"));
}
}
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {
const encKey = await this.cryptoService.makeEncKey(key);
const privateKey = await this.cryptoService.getPrivateKey();
let encPrivateKey: EncString = null;
if (privateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(privateKey, encKey[0]);
}
const request = new UpdateKeyRequest();
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
request.key = encKey[1].encryptedString;
request.masterPasswordHash = masterPasswordHash;
const folders = await this.folderService.getAllDecrypted();
for (let i = 0; i < folders.length; i++) {
if (folders[i].id == null) {
continue;
}
const folder = await this.folderService.encrypt(folders[i], encKey[0]);
request.folders.push(new FolderWithIdRequest(folder));
}
const ciphers = await this.cipherService.getAllDecrypted();
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId != null) {
continue;
}
const cipher = await this.cipherService.encrypt(ciphers[i], encKey[0]);
request.ciphers.push(new CipherWithIdRequest(cipher));
}
const sends = await this.sendService.getAll();
await Promise.all(
sends.map(async (send) => {
const cryptoKey = await this.cryptoService.decryptToBytes(send.key, null);
send.key = (await this.cryptoService.encrypt(cryptoKey, encKey[0])) ?? send.key;
request.sends.push(new SendWithIdRequest(send));
})
);
await this.apiService.postAccountKey(request);
await this.updateEmergencyAccesses(encKey[0]);
await this.updateAllResetPasswordKeys(encKey[0]);
}
private async updateEmergencyAccesses(encKey: SymmetricCryptoKey) {
const emergencyAccess = await this.apiService.getEmergencyAccessTrusted();
const allowedStatuses = [
EmergencyAccessStatusType.Confirmed,
EmergencyAccessStatusType.RecoveryInitiated,
EmergencyAccessStatusType.RecoveryApproved,
];
const filteredAccesses = emergencyAccess.data.filter((d) => allowedStatuses.includes(d.status));
for (const details of filteredAccesses) {
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
const updateRequest = new EmergencyAccessUpdateRequest();
updateRequest.type = details.type;
updateRequest.waitTimeDays = details.waitTimeDays;
updateRequest.keyEncrypted = encryptedKey.encryptedString;
await this.apiService.putEmergencyAccess(details.id, updateRequest);
}
}
private async updateAllResetPasswordKeys(encKey: SymmetricCryptoKey) {
const orgs = await this.organizationService.getAll();
for (const org of orgs) {
// If not already enrolled, skip
if (!org.resetPasswordEnrolled) {
continue;
}
// Retrieve public key
const response = await this.apiService.getOrganizationKeys(org.id);
const publicKey = Utils.fromB64ToArray(response?.publicKey);
// Re-enroll - encrpyt user's encKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
// Create/Execute request
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = encryptedKey.encryptedString;
await this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request);
}
}
}

View File

@@ -1,5 +1,5 @@
<div class="page-header">
<h1>{{'newOrganization' | i18n}}</h1>
<h1>{{ "newOrganization" | i18n }}</h1>
</div>
<p>{{'newOrganizationDesc' | i18n}}</p>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>

View File

@@ -1,38 +1,35 @@
import {
Component,
OnInit,
ViewChild,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from 'rxjs/operators';
import { first } from "rxjs/operators";
import { PlanType } from 'jslib-common/enums/planType';
import { ProductType } from 'jslib-common/enums/productType';
import { PlanType } from "jslib-common/enums/planType";
import { ProductType } from "jslib-common/enums/productType";
import { OrganizationPlansComponent } from './organization-plans.component';
import { OrganizationPlansComponent } from "./organization-plans.component";
@Component({
selector: 'app-create-organization',
templateUrl: 'create-organization.component.html',
selector: "app-create-organization",
templateUrl: "create-organization.component.html",
})
export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
@ViewChild(OrganizationPlansComponent, { static: true })
orgPlansComponent: OrganizationPlansComponent;
constructor(private route: ActivatedRoute) { }
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async qParams => {
if (qParams.plan === 'families') {
this.orgPlansComponent.plan = PlanType.FamiliesAnnually;
this.orgPlansComponent.product = ProductType.Families;
} else if (qParams.plan === 'teams') {
this.orgPlansComponent.plan = PlanType.TeamsAnnually;
this.orgPlansComponent.product = ProductType.Teams;
} else if (qParams.plan === 'enterprise') {
this.orgPlansComponent.plan = PlanType.EnterpriseAnnually;
this.orgPlansComponent.product = ProductType.Enterprise;
}
});
}
ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.plan === "families") {
this.orgPlansComponent.plan = PlanType.FamiliesAnnually;
this.orgPlansComponent.product = ProductType.Families;
} else if (qParams.plan === "teams") {
this.orgPlansComponent.plan = PlanType.TeamsAnnually;
this.orgPlansComponent.product = ProductType.Teams;
} else if (qParams.plan === "enterprise") {
this.orgPlansComponent.plan = PlanType.EnterpriseAnnually;
this.orgPlansComponent.product = ProductType.Enterprise;
}
});
}
}

View File

@@ -1,25 +1,38 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deAuthTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="deAuthTitle">{{'deauthorizeSessions' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'deauthorizeSessionsDesc' | i18n}}</p>
<app-callout type="warning">{{'deauthorizeSessionsWarning' | i18n}}</app-callout>
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-verify-master-password>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'deauthorizeSessions' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="deAuthTitle">{{ "deauthorizeSessions" | i18n }}</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "deauthorizeSessionsDesc" | i18n }}</p>
<app-callout type="warning">{{ "deauthorizeSessionsWarning" | i18n }}</app-callout>
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-verify-master-password>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "deauthorizeSessions" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,36 +1,45 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { Verification } from 'jslib-common/types/verification';
import { Verification } from "jslib-common/types/verification";
@Component({
selector: 'app-deauthorize-sessions',
templateUrl: 'deauthorize-sessions.component.html',
selector: "app-deauthorize-sessions",
templateUrl: "deauthorize-sessions.component.html",
})
export class DeauthorizeSessionsComponent {
masterPassword: Verification;
formPromise: Promise<any>;
masterPassword: Verification;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private userVerificationService: UserVerificationService,
private messagingService: MessagingService, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private userVerificationService: UserVerificationService,
private messagingService: MessagingService,
private logService: LogService
) {}
async submit() {
try {
this.formPromise = this.userVerificationService.buildRequest(this.masterPassword)
.then(request => this.apiService.postSecurityStamp(request));
await this.formPromise;
this.platformUtilsService.showToast('success', this.i18nService.t('sessionsDeauthorized'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');
} catch (e) {
this.logService.error(e);
}
async submit() {
try {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword)
.then((request) => this.apiService.postSecurityStamp(request));
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("sessionsDeauthorized"),
this.i18nService.t("logBackIn")
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,25 +1,38 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deleteAccountTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="deleteAccountTitle">{{'deleteAccount' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'deleteAccountDesc' | i18n}}</p>
<app-callout type="warning">{{'deleteAccountWarning' | i18n}}</app-callout>
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-verify-master-password>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'deleteAccount' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="deleteAccountTitle">{{ "deleteAccount" | i18n }}</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "deleteAccountDesc" | i18n }}</p>
<app-callout type="warning">{{ "deleteAccountWarning" | i18n }}</app-callout>
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-verify-master-password>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "deleteAccount" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,36 +1,45 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { Verification } from 'jslib-common/types/verification';
import { Verification } from "jslib-common/types/verification";
@Component({
selector: 'app-delete-account',
templateUrl: 'delete-account.component.html',
selector: "app-delete-account",
templateUrl: "delete-account.component.html",
})
export class DeleteAccountComponent {
masterPassword: Verification;
formPromise: Promise<any>;
masterPassword: Verification;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private userVerificationService: UserVerificationService,
private messagingService: MessagingService, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private userVerificationService: UserVerificationService,
private messagingService: MessagingService,
private logService: LogService
) {}
async submit() {
try {
this.formPromise = this.userVerificationService.buildRequest(this.masterPassword)
.then(request => this.apiService.deleteAccount(request));
await this.formPromise;
this.platformUtilsService.showToast('success', this.i18nService.t('accountDeleted'),
this.i18nService.t('accountDeletedDesc'));
this.messagingService.send('logout');
} catch (e) {
this.logService.error(e);
}
async submit() {
try {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword)
.then((request) => this.apiService.deleteAccount(request));
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("accountDeleted"),
this.i18nService.t("accountDeletedDesc")
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,72 +1,108 @@
<div class="page-header">
<h1>{{'domainRules' | i18n}}</h1>
<h1>{{ "domainRules" | i18n }}</h1>
</div>
<p>{{'domainRulesDesc' | i18n}}</p>
<p>{{ "domainRulesDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<h2>{{'customEqDomains' | i18n}}</h2>
<p *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>
</p>
<ng-container *ngIf="!loading">
<div class="form-group d-flex" *ngFor="let d of custom; let i = index; trackBy: indexTrackBy">
<div class="flex-fill">
<label for="customDomain_{{i}}" class="sr-only">{{'customDomainX' | i18n : (i + 1)}}</label>
<textarea class="form-control" name="CustomDomain[{{i}}]" id="customDomain_{{i}}"
[(ngModel)]="custom[i]" placeholder="{{'ex' | i18n}} google.com, gmail.com" required></textarea>
</div>
<button type="button" class="btn btn-link text-danger ml-2" (click)="remove(i)"
appA11yTitle="{{'remove' | i18n}}">
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
<h2>{{ "customEqDomains" | i18n }}</h2>
<p *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>
</p>
<ng-container *ngIf="!loading">
<div class="form-group d-flex" *ngFor="let d of custom; let i = index; trackBy: indexTrackBy">
<div class="flex-fill">
<label for="customDomain_{{ i }}" class="sr-only">{{
"customDomainX" | i18n: i + 1
}}</label>
<textarea
class="form-control"
name="CustomDomain[{{ i }}]"
id="customDomain_{{ i }}"
[(ngModel)]="custom[i]"
placeholder="{{ 'ex' | i18n }} google.com, gmail.com"
required
></textarea>
</div>
<button
type="button"
class="btn btn-link text-danger ml-2"
(click)="remove(i)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
</button>
</div>
<button type="button" (click)="add()" class="btn btn-outline-secondary btn-sm mb-2">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i> {{ "newCustomDomain" | i18n }}
</button>
<small class="text-muted d-block mb-3">{{ "newCustomDomainDesc" | i18n }}</small>
</ng-container>
<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>
<h2 class="spaced-header">{{ "globalEqDomains" | i18n }}</h2>
<p *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>
</p>
<table class="table table-hover table-list" *ngIf="!loading && global.length > 0">
<tbody>
<tr *ngFor="let d of global">
<td [ngClass]="{ 'table-list-strike': d.excluded }">{{ d.domains }}</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
</div>
<button type="button" (click)="add()" class="btn btn-outline-secondary btn-sm mb-2">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i> {{'newCustomDomain' | i18n}}
</button>
<small class="text-muted d-block mb-3">{{'newCustomDomainDesc' | i18n}}</small>
</ng-container>
<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>
<h2 class="spaced-header">{{'globalEqDomains' | i18n}}</h2>
<p *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>
</p>
<table class="table table-hover table-list" *ngIf="!loading && global.length > 0">
<tbody>
<tr *ngFor="let d of global">
<td [ngClass]="{'table-list-strike': d.excluded}">{{d.domains}}</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" appStopClick (click)="toggleExcluded(d)"
*ngIf="!d.excluded">
<i class="fa fa-fw fa-close" aria-hidden="true"></i>
{{'exclude' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="toggleExcluded(d)"
*ngIf="d.excluded">
<i class="fa fa-fw fa-plus" aria-hidden="true"></i>
{{'include' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="customize(d)">
<i class="fa fa-fw fa-scissors" aria-hidden="true"></i>
{{'customize' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<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>
<div class="dropdown-menu dropdown-menu-right">
<a
class="dropdown-item"
href="#"
appStopClick
(click)="toggleExcluded(d)"
*ngIf="!d.excluded"
>
<i class="fa fa-fw fa-close" aria-hidden="true"></i>
{{ "exclude" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="toggleExcluded(d)"
*ngIf="d.excluded"
>
<i class="fa fa-fw fa-plus" aria-hidden="true"></i>
{{ "include" | i18n }}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="customize(d)">
<i class="fa fa-fw fa-scissors" aria-hidden="true"></i>
{{ "customize" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<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>
</form>

View File

@@ -1,85 +1,88 @@
import {
Component,
OnInit,
} from '@angular/core';
import { Component, OnInit } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UpdateDomainsRequest } from 'jslib-common/models/request/updateDomainsRequest';
import { UpdateDomainsRequest } from "jslib-common/models/request/updateDomainsRequest";
@Component({
selector: 'app-domain-rules',
templateUrl: 'domain-rules.component.html',
selector: "app-domain-rules",
templateUrl: "domain-rules.component.html",
})
export class DomainRulesComponent implements OnInit {
loading = true;
custom: string[] = [];
global: any[] = [];
formPromise: Promise<any>;
loading = true;
custom: string[] = [];
global: any[] = [];
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
const response = await this.apiService.getSettingsDomains();
this.loading = false;
if (response.equivalentDomains != null) {
this.custom = response.equivalentDomains.map(d => d.join(', '));
}
if (response.globalEquivalentDomains != null) {
this.global = response.globalEquivalentDomains.map(d => {
return {
domains: d.domains.join(', '),
excluded: d.excluded,
key: d.type,
};
});
}
async ngOnInit() {
const response = await this.apiService.getSettingsDomains();
this.loading = false;
if (response.equivalentDomains != null) {
this.custom = response.equivalentDomains.map((d) => d.join(", "));
}
if (response.globalEquivalentDomains != null) {
this.global = response.globalEquivalentDomains.map((d) => {
return {
domains: d.domains.join(", "),
excluded: d.excluded,
key: d.type,
};
});
}
}
toggleExcluded(globalDomain: any) {
globalDomain.excluded = !globalDomain.excluded;
}
customize(globalDomain: any) {
globalDomain.excluded = true;
this.custom.push(globalDomain.domains);
}
remove(index: number) {
this.custom.splice(index, 1);
}
add() {
this.custom.push("");
}
async submit() {
const request = new UpdateDomainsRequest();
request.excludedGlobalEquivalentDomains = this.global
.filter((d) => d.excluded)
.map((d) => d.key);
if (request.excludedGlobalEquivalentDomains.length === 0) {
request.excludedGlobalEquivalentDomains = null;
}
request.equivalentDomains = this.custom
.filter((d) => d != null && d.trim() !== "")
.map((d) => d.split(",").map((d2) => d2.trim()));
if (request.equivalentDomains.length === 0) {
request.equivalentDomains = null;
}
toggleExcluded(globalDomain: any) {
globalDomain.excluded = !globalDomain.excluded;
try {
this.formPromise = this.apiService.putSettingsDomains(request);
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainsUpdated"));
} catch (e) {
this.logService.error(e);
}
}
customize(globalDomain: any) {
globalDomain.excluded = true;
this.custom.push(globalDomain.domains);
}
remove(index: number) {
this.custom.splice(index, 1);
}
add() {
this.custom.push('');
}
async submit() {
const request = new UpdateDomainsRequest();
request.excludedGlobalEquivalentDomains = this.global.filter(d => d.excluded)
.map(d => d.key);
if (request.excludedGlobalEquivalentDomains.length === 0) {
request.excludedGlobalEquivalentDomains = null;
}
request.equivalentDomains = this.custom.filter(d => d != null && d.trim() !== '')
.map(d => d.split(',').map(d2 => d2.trim()));
if (request.equivalentDomains.length === 0) {
request.equivalentDomains = null;
}
try {
this.formPromise = this.apiService.putSettingsDomains(request);
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('domainsUpdated'));
} catch (e) {
this.logService.error(e);
}
}
indexTrackBy(index: number, obj: any): any {
return index;
}
indexTrackBy(index: number, obj: any): any {
return index;
}
}

View File

@@ -1,77 +1,146 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
<span class="badge badge-primary" *ngIf="readOnly">{{'premium' | i18n}}</span>
{{title}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *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>
</div>
<div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode">
<p>{{'inviteEmergencyContactDesc' | i18n}}</p>
<div class="form-group mb-4">
<label for="email">{{'email' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required>
</div>
</ng-container>
<h3>
{{'userAccess' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/emergency-access/#user-access">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</h3>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="emergencyTypeView"
[value]="emergencyAccessType.View" [(ngModel)]="type">
<label class="form-check-label" for="emergencyTypeView">
{{'view' | i18n}}
<small>{{'viewDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="emergencyTypeTakeover"
[value]="emergencyAccessType.Takeover" [(ngModel)]="type" [disabled]="readOnly">
<label class="form-check-label" for="emergencyTypeTakeover">
{{'takeover' | i18n}}
<small>{{'takeoverDesc' | i18n}}</small>
</label>
</div>
<div class="form-group col-6 mt-4">
<label for="waitTime">{{'waitTime' | i18n}}</label>
<select id="waitTime" name="waitTime" [(ngModel)]="waitTime" class="form-control" [disabled]="readOnly">
<option *ngFor="let o of waitTimes" [ngValue]="o.value">{{o.name}}</option>
</select>
<small class="text-muted">{{'waitTimeDesc' | i18n}}</small>
</div>
</div>
<div class="modal-footer">
<button #submitBtn type="submit" class="btn btn-primary"
[disabled]="loading || submitBtn.loading || readOnly">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"
*ngIf="loading || submitBtn.loading"></i>
<span *ngIf="!loading && !submitBtn.loading">{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
<div class="ml-auto">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading">
<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 class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
<span class="badge badge-primary" *ngIf="readOnly">{{ "premium" | i18n }}</span>
{{ title }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *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>
</div>
<div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode">
<p>{{ "inviteEmergencyContactDesc" | i18n }}</p>
<div class="form-group mb-4">
<label for="email">{{ "email" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
/>
</div>
</ng-container>
<h3>
{{ "userAccess" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/article/emergency-access/#user-access"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</h3>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="userType"
id="emergencyTypeView"
[value]="emergencyAccessType.View"
[(ngModel)]="type"
/>
<label class="form-check-label" for="emergencyTypeView">
{{ "view" | i18n }}
<small>{{ "viewDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="userType"
id="emergencyTypeTakeover"
[value]="emergencyAccessType.Takeover"
[(ngModel)]="type"
[disabled]="readOnly"
/>
<label class="form-check-label" for="emergencyTypeTakeover">
{{ "takeover" | i18n }}
<small>{{ "takeoverDesc" | i18n }}</small>
</label>
</div>
<div class="form-group col-6 mt-4">
<label for="waitTime">{{ "waitTime" | i18n }}</label>
<select
id="waitTime"
name="waitTime"
[(ngModel)]="waitTime"
class="form-control"
[disabled]="readOnly"
>
<option *ngFor="let o of waitTimes" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="text-muted">{{ "waitTimeDesc" | i18n }}</small>
</div>
</div>
<div class="modal-footer">
<button
#submitBtn
type="submit"
class="btn btn-primary"
[disabled]="loading || submitBtn.loading || readOnly"
>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="loading || submitBtn.loading"
></i>
<span *ngIf="!loading && !submitBtn.loading">{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto">
<button
#deleteBtn
type="button"
(click)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="deleteBtn.loading"
>
<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>

View File

@@ -1,103 +1,104 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { EmergencyAccessType } from 'jslib-common/enums/emergencyAccessType';
import { EmergencyAccessInviteRequest } from 'jslib-common/models/request/emergencyAccessInviteRequest';
import { EmergencyAccessUpdateRequest } from 'jslib-common/models/request/emergencyAccessUpdateRequest';
import { EmergencyAccessType } from "jslib-common/enums/emergencyAccessType";
import { EmergencyAccessInviteRequest } from "jslib-common/models/request/emergencyAccessInviteRequest";
import { EmergencyAccessUpdateRequest } from "jslib-common/models/request/emergencyAccessUpdateRequest";
@Component({
selector: 'emergency-access-add-edit',
templateUrl: 'emergency-access-add-edit.component.html',
selector: "emergency-access-add-edit",
templateUrl: "emergency-access-add-edit.component.html",
})
export class EmergencyAccessAddEditComponent implements OnInit {
@Input() name: string;
@Input() emergencyAccessId: string;
@Output() onSaved = new EventEmitter();
@Output() onDeleted = new EventEmitter();
@Input() name: string;
@Input() emergencyAccessId: string;
@Output() onSaved = new EventEmitter();
@Output() onDeleted = new EventEmitter();
loading = true;
readOnly: boolean = false;
editMode: boolean = false;
title: string;
email: string;
type: EmergencyAccessType = EmergencyAccessType.View;
loading = true;
readOnly: boolean = false;
editMode: boolean = false;
title: string;
email: string;
type: EmergencyAccessType = EmergencyAccessType.View;
formPromise: Promise<any>;
formPromise: Promise<any>;
emergencyAccessType = EmergencyAccessType;
waitTimes: { name: string; value: number; }[];
waitTime: number;
emergencyAccessType = EmergencyAccessType;
waitTimes: { name: string; value: number }[];
waitTime: number;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
this.editMode = this.loading = this.emergencyAccessId != null;
async ngOnInit() {
this.editMode = this.loading = this.emergencyAccessId != null;
this.waitTimes = [
{ name: this.i18nService.t('oneDay'), value: 1 },
{ name: this.i18nService.t('days', '2'), value: 2 },
{ name: this.i18nService.t('days', '7'), value: 7 },
{ name: this.i18nService.t('days', '14'), value: 14 },
{ name: this.i18nService.t('days', '30'), value: 30 },
{ name: this.i18nService.t('days', '90'), value: 90 },
];
this.waitTimes = [
{ name: this.i18nService.t("oneDay"), value: 1 },
{ name: this.i18nService.t("days", "2"), value: 2 },
{ name: this.i18nService.t("days", "7"), value: 7 },
{ name: this.i18nService.t("days", "14"), value: 14 },
{ name: this.i18nService.t("days", "30"), value: 30 },
{ name: this.i18nService.t("days", "90"), value: 90 },
];
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editEmergencyContact');
try {
const emergencyAccess = await this.apiService.getEmergencyAccess(this.emergencyAccessId);
this.type = emergencyAccess.type;
this.waitTime = emergencyAccess.waitTimeDays;
} catch (e) {
this.logService.error(e);
}
} else {
this.title = this.i18nService.t('inviteEmergencyContact');
this.waitTime = this.waitTimes[2].value;
}
this.loading = false;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t("editEmergencyContact");
try {
const emergencyAccess = await this.apiService.getEmergencyAccess(this.emergencyAccessId);
this.type = emergencyAccess.type;
this.waitTime = emergencyAccess.waitTimeDays;
} catch (e) {
this.logService.error(e);
}
} else {
this.title = this.i18nService.t("inviteEmergencyContact");
this.waitTime = this.waitTimes[2].value;
}
async submit() {
try {
if (this.editMode) {
const request = new EmergencyAccessUpdateRequest();
request.type = this.type;
request.waitTimeDays = this.waitTime;
this.loading = false;
}
this.formPromise = this.apiService.putEmergencyAccess(this.emergencyAccessId, request);
} else {
const request = new EmergencyAccessInviteRequest();
request.email = this.email.trim();
request.type = this.type;
request.waitTimeDays = this.waitTime;
async submit() {
try {
if (this.editMode) {
const request = new EmergencyAccessUpdateRequest();
request.type = this.type;
request.waitTimeDays = this.waitTime;
this.formPromise = this.apiService.postEmergencyAccessInvite(request);
}
this.formPromise = this.apiService.putEmergencyAccess(this.emergencyAccessId, request);
} else {
const request = new EmergencyAccessInviteRequest();
request.email = this.email.trim();
request.type = this.type;
request.waitTimeDays = this.waitTime;
await this.formPromise;
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
this.onSaved.emit();
} catch (e) {
this.logService.error(e);
}
this.formPromise = this.apiService.postEmergencyAccessInvite(request);
}
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
);
this.onSaved.emit();
} catch (e) {
this.logService.error(e);
}
}
async delete() {
this.onDeleted.emit();
}
async delete() {
this.onDeleted.emit();
}
}

View File

@@ -1,36 +1,51 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { AttachmentView } from 'jslib-common/models/view/attachmentView';
import { AttachmentView } from "jslib-common/models/view/attachmentView";
import { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib-angular/components/attachments.component';
import { AttachmentsComponent as BaseAttachmentsComponent } from "jslib-angular/components/attachments.component";
@Component({
selector: 'emergency-access-attachments',
templateUrl: '../vault/attachments.component.html',
selector: "emergency-access-attachments",
templateUrl: "../vault/attachments.component.html",
})
export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponent {
viewOnly = true;
canAccessAttachments = true;
viewOnly = true;
canAccessAttachments = true;
constructor(cipherService: CipherService, i18nService: I18nService,
cryptoService: CryptoService, stateService: StateService,
platformUtilsService: PlatformUtilsService, apiService: ApiService, logService: LogService) {
super(cipherService, i18nService, cryptoService, platformUtilsService, apiService, window, logService, stateService);
}
constructor(
cipherService: CipherService,
i18nService: I18nService,
cryptoService: CryptoService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
logService: LogService
) {
super(
cipherService,
i18nService,
cryptoService,
platformUtilsService,
apiService,
window,
logService,
stateService
);
}
protected async init() {
// Do nothing since cipher is already decoded
}
protected async init() {
// Do nothing since cipher is already decoded
}
protected showFixOldAttachments(attachment: AttachmentView) {
return false;
}
protected showFixOldAttachments(attachment: AttachmentView) {
return false;
}
}

View File

@@ -1,38 +1,56 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="confirmUserTitle">
{{'confirmUser' | i18n}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
{{'fingerprintEnsureIntegrityVerify' | i18n}}
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener">
{{'learnMore' | i18n}}</a>
</p>
<p><code>{{fingerprint}}</code></p>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dontAskAgain" name="DontAskAgain"
[(ngModel)]="dontAskAgain">
<label class="form-check-label" for="dontAskAgain">
{{'dontAskFingerprintAgain' | i18n}}
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'confirm' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="confirmUserTitle">
{{ "confirmUser" | i18n }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
<a
href="https://help.bitwarden.com/article/fingerprint-phrase/"
target="_blank"
rel="noopener"
>
{{ "learnMore" | i18n }}</a
>
</p>
<p>
<code>{{ fingerprint }}</code>
</p>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="dontAskAgain"
name="DontAskAgain"
[(ngModel)]="dontAskAgain"
/>
<label class="form-check-label" for="dontAskAgain">
{{ "dontAskFingerprintAgain" | i18n }}
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "confirm" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,65 +1,63 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Utils } from 'jslib-common/misc/utils';
import { Utils } from "jslib-common/misc/utils";
@Component({
selector: 'emergency-access-confirm',
templateUrl: 'emergency-access-confirm.component.html',
selector: "emergency-access-confirm",
templateUrl: "emergency-access-confirm.component.html",
})
export class EmergencyAccessConfirmComponent implements OnInit {
@Input() name: string;
@Input() userId: string;
@Input() emergencyAccessId: string;
@Input() formPromise: Promise<any>;
@Output() onConfirmed = new EventEmitter();
@Input() name: string;
@Input() userId: string;
@Input() emergencyAccessId: string;
@Input() formPromise: Promise<any>;
@Output() onConfirmed = new EventEmitter();
dontAskAgain = false;
loading = true;
fingerprint: string;
dontAskAgain = false;
loading = true;
fingerprint: string;
constructor(private apiService: ApiService, private cryptoService: CryptoService,
private stateService: StateService, private logService: LogService) { }
constructor(
private apiService: ApiService,
private cryptoService: CryptoService,
private stateService: StateService,
private logService: LogService
) {}
async ngOnInit() {
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
if (publicKeyResponse != null) {
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const fingerprint = await this.cryptoService.getFingerprint(this.userId, publicKey.buffer);
if (fingerprint != null) {
this.fingerprint = fingerprint.join('-');
}
}
} catch (e) {
this.logService.error(e);
async ngOnInit() {
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
if (publicKeyResponse != null) {
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const fingerprint = await this.cryptoService.getFingerprint(this.userId, publicKey.buffer);
if (fingerprint != null) {
this.fingerprint = fingerprint.join("-");
}
this.loading = false;
}
} catch (e) {
this.logService.error(e);
}
this.loading = false;
}
async submit() {
if (this.loading) {
return;
}
async submit() {
if (this.loading) {
return;
}
if (this.dontAskAgain) {
await this.stateService.setAutoConfirmFingerprints(true);
}
try {
this.onConfirmed.emit();
} catch (e) {
this.logService.error(e);
}
if (this.dontAskAgain) {
await this.stateService.setAutoConfirmFingerprints(true);
}
try {
this.onConfirmed.emit();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,50 +1,79 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
{{'takeover' | i18n}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
{{ "takeover" | i18n }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="row">
<div class="col-6">
<div class="form-group">
<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 class="modal-body">
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
<app-callout type="info" [enforcedPolicyOptions]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
</app-callout>
<div class="row">
<div class="col-6">
<div class="form-group">
<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="masterPasswordRetype">{{'confirmNewMasterPass' | i18n}}</label>
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
class="form-control" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
autocomplete="new-password">
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="form-group">
<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 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>
</form>
</div>
</div>
</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>
</form>
</div>
</div>

View File

@@ -1,104 +1,113 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { KdfType } from 'jslib-common/enums/kdfType';
import { PolicyData } from 'jslib-common/models/data/policyData';
import { Policy } from 'jslib-common/models/domain/policy';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { EmergencyAccessPasswordRequest } from 'jslib-common/models/request/emergencyAccessPasswordRequest';
import { PolicyResponse } from 'jslib-common/models/response/policyResponse';
import { KdfType } from "jslib-common/enums/kdfType";
import { PolicyData } from "jslib-common/models/data/policyData";
import { Policy } from "jslib-common/models/domain/policy";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { EmergencyAccessPasswordRequest } from "jslib-common/models/request/emergencyAccessPasswordRequest";
import { PolicyResponse } from "jslib-common/models/response/policyResponse";
import { ChangePasswordComponent } from 'jslib-angular/components/change-password.component';
import { ChangePasswordComponent } from "jslib-angular/components/change-password.component";
@Component({
selector: 'emergency-access-takeover',
templateUrl: 'emergency-access-takeover.component.html',
selector: "emergency-access-takeover",
templateUrl: "emergency-access-takeover.component.html",
})
export class EmergencyAccessTakeoverComponent extends ChangePasswordComponent implements OnInit {
@Output() onDone = new EventEmitter();
@Input() emergencyAccessId: string;
@Input() name: string;
@Input() email: string;
@Input() kdf: KdfType;
@Input() kdfIterations: number;
@Output() onDone = new EventEmitter();
@Input() emergencyAccessId: string;
@Input() name: string;
@Input() email: string;
@Input() kdf: KdfType;
@Input() kdfIterations: number;
formPromise: Promise<any>;
formPromise: Promise<any>;
constructor(
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
stateService: StateService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
private apiService: ApiService,
private logService: LogService
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyService,
stateService,
);
constructor(
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
stateService: StateService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
private apiService: ApiService,
private logService: LogService
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyService,
stateService
);
}
async ngOnInit() {
const response = await this.apiService.getEmergencyGrantorPolicies(this.emergencyAccessId);
if (response.data != null && response.data.length > 0) {
const policies = response.data.map(
(policyResponse: PolicyResponse) => new Policy(new PolicyData(policyResponse))
);
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(
policies
);
}
}
async submit() {
if (!(await this.strongPassword())) {
return;
}
async ngOnInit() {
const response = await this.apiService.getEmergencyGrantorPolicies(this.emergencyAccessId);
if (response.data != null && response.data.length > 0) {
const policies = response.data.map((policyResponse: PolicyResponse) => new Policy(new PolicyData(policyResponse)));
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(policies);
}
const takeoverResponse = await this.apiService.postEmergencyAccessTakeover(
this.emergencyAccessId
);
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
if (oldEncKey == null) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("unexpectedError")
);
return;
}
async submit() {
if (!await this.strongPassword()) {
return;
}
const key = await this.cryptoService.makeKey(
this.masterPassword,
this.email,
takeoverResponse.kdf,
takeoverResponse.kdfIterations
);
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
const takeoverResponse = await this.apiService.postEmergencyAccessTakeover(this.emergencyAccessId);
const encKey = await this.cryptoService.remakeEncKey(key, oldEncKey);
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
const request = new EmergencyAccessPasswordRequest();
request.newMasterPasswordHash = masterPasswordHash;
request.key = encKey[1].encryptedString;
if (oldEncKey == null) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('unexpectedError'));
return;
}
this.apiService.postEmergencyAccessPassword(this.emergencyAccessId, request);
const key = await this.cryptoService.makeKey(this.masterPassword, this.email, takeoverResponse.kdf, takeoverResponse.kdfIterations);
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
const encKey = await this.cryptoService.remakeEncKey(key, oldEncKey);
const request = new EmergencyAccessPasswordRequest();
request.newMasterPasswordHash = masterPasswordHash;
request.key = encKey[1].encryptedString;
this.apiService.postEmergencyAccessPassword(this.emergencyAccessId, request);
try {
this.onDone.emit();
} catch (e) {
this.logService.error(e);
}
try {
this.onDone.emit();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,51 +1,72 @@
<div class="page-header">
<h1>{{'vault' | i18n}}</h1>
<h1>{{ "vault" | i18n }}</h1>
</div>
<div class="mt-4">
<ng-container *ngIf="ciphers.length">
<table class="table table-hover table-list table-ciphers">
<tbody>
<tr *ngFor="let c of ciphers">
<td class="table-list-icon">
<app-vault-icon [cipher]="c"></app-vault-icon>
</td>
<td class="reduced-lh wrap">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
<ng-container *ngIf="!organization && c.organizationId">
<i class="fa fa-cube" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'shared' | i18n}}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i class="fa fa-paperclip" appStopProp title="{{'attachments' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'attachments' | i18n}}</span>
</ng-container>
<br>
<small>{{c.subTitle}}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown *ngIf="c.hasAttachments">
<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)="viewAttachments(c)">
<i class="fa fa-fw fa-paperclip" aria-hidden="true"></i>
{{'attachments' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
<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="ciphers.length">
<table class="table table-hover table-list table-ciphers">
<tbody>
<tr *ngFor="let c of ciphers">
<td class="table-list-icon">
<app-vault-icon [cipher]="c"></app-vault-icon>
</td>
<td class="reduced-lh wrap">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
c.name
}}</a>
<ng-container *ngIf="!organization && c.organizationId">
<i
class="fa fa-cube"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i
class="fa fa-paperclip"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
<br />
<small>{{ c.subTitle }}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown *ngIf="c.hasAttachments">
<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)="viewAttachments(c)">
<i class="fa fa-fw fa-paperclip" aria-hidden="true"></i>
{{ "attachments" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
<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>
</div>
<ng-template #cipherAddEdit></ng-template>
<ng-template #attachments></ng-template>

View File

@@ -1,93 +1,103 @@
import {
Component,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { ModalService } from 'jslib-angular/services/modal.service';
import { ModalService } from "jslib-angular/services/modal.service";
import { CipherData } from 'jslib-common/models/data/cipherData';
import { Cipher } from 'jslib-common/models/domain/cipher';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { EmergencyAccessViewResponse } from 'jslib-common/models/response/emergencyAccessResponse';
import { CipherView } from 'jslib-common/models/view/cipherView';
import { CipherData } from "jslib-common/models/data/cipherData";
import { Cipher } from "jslib-common/models/domain/cipher";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { EmergencyAccessViewResponse } from "jslib-common/models/response/emergencyAccessResponse";
import { CipherView } from "jslib-common/models/view/cipherView";
import { EmergencyAccessAttachmentsComponent } from './emergency-access-attachments.component';
import { EmergencyAddEditComponent } from './emergency-add-edit.component';
import { EmergencyAccessAttachmentsComponent } from "./emergency-access-attachments.component";
import { EmergencyAddEditComponent } from "./emergency-add-edit.component";
@Component({
selector: 'emergency-access-view',
templateUrl: 'emergency-access-view.component.html',
selector: "emergency-access-view",
templateUrl: "emergency-access-view.component.html",
})
export class EmergencyAccessViewComponent implements OnInit {
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('attachments', { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef;
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
id: string;
ciphers: CipherView[] = [];
loaded = false;
id: string;
ciphers: CipherView[] = [];
loaded = false;
constructor(private cipherService: CipherService, private cryptoService: CryptoService,
private modalService: ModalService, private router: Router,
private route: ActivatedRoute, private apiService: ApiService) { }
constructor(
private cipherService: CipherService,
private cryptoService: CryptoService,
private modalService: ModalService,
private router: Router,
private route: ActivatedRoute,
private apiService: ApiService
) {}
ngOnInit() {
this.route.params.subscribe(qParams => {
if (qParams.id == null) {
return this.router.navigate(['settings/emergency-access']);
}
ngOnInit() {
this.route.params.subscribe((qParams) => {
if (qParams.id == null) {
return this.router.navigate(["settings/emergency-access"]);
}
this.id = qParams.id;
this.id = qParams.id;
this.load();
});
}
this.load();
});
}
async selectCipher(cipher: CipherView) {
const [_, childComponent] = await this.modalService.openViewRef(EmergencyAddEditComponent, this.cipherAddEditModalRef, comp => {
comp.cipherId = cipher == null ? null : cipher.id;
comp.cipher = cipher;
});
async selectCipher(cipher: CipherView) {
const [_, childComponent] = await this.modalService.openViewRef(
EmergencyAddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = cipher == null ? null : cipher.id;
comp.cipher = cipher;
}
);
return childComponent;
}
return childComponent;
}
async load() {
const response = await this.apiService.postEmergencyAccessView(this.id);
this.ciphers = await this.getAllCiphers(response);
this.loaded = true;
}
async load() {
const response = await this.apiService.postEmergencyAccessView(this.id);
this.ciphers = await this.getAllCiphers(response);
this.loaded = true;
}
async viewAttachments(cipher: CipherView) {
await this.modalService.openViewRef(EmergencyAccessAttachmentsComponent, this.attachmentsModalRef, comp => {
comp.cipher = cipher;
comp.emergencyAccessId = this.id;
});
}
async viewAttachments(cipher: CipherView) {
await this.modalService.openViewRef(
EmergencyAccessAttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.cipher = cipher;
comp.emergencyAccessId = this.id;
}
);
}
protected async getAllCiphers(response: EmergencyAccessViewResponse): Promise<CipherView[]> {
const ciphers = response.ciphers;
protected async getAllCiphers(response: EmergencyAccessViewResponse): Promise<CipherView[]> {
const ciphers = response.ciphers;
const decCiphers: CipherView[] = [];
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
const decCiphers: CipherView[] = [];
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
const promises: any[] = [];
ciphers.forEach(cipherResponse => {
const cipherData = new CipherData(cipherResponse);
const cipher = new Cipher(cipherData);
promises.push(cipher.decrypt(oldEncKey).then(c => decCiphers.push(c)));
});
const promises: any[] = [];
ciphers.forEach((cipherResponse) => {
const cipherData = new CipherData(cipherResponse);
const cipher = new Cipher(cipherData);
promises.push(cipher.decrypt(oldEncKey).then((c) => decCiphers.push(c)));
});
await Promise.all(promises);
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
await Promise.all(promises);
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
return decCiphers;
}
return decCiphers;
}
}

View File

@@ -1,162 +1,259 @@
<div class="page-header">
<h1>{{'emergencyAccess' | i18n}}</h1>
<h1>{{ "emergencyAccess" | i18n }}</h1>
</div>
<p>
{{'emergencyAccessDesc' | i18n}}
<a href="https://bitwarden.com/help/article/emergency-access/" target="_blank" rel="noopener">
{{'learnMore' | i18n}}.
</a>
{{ "emergencyAccessDesc" | i18n }}
<a href="https://bitwarden.com/help/article/emergency-access/" target="_blank" rel="noopener">
{{ "learnMore" | i18n }}.
</a>
</p>
<p *ngIf="isOrganizationOwner">
<b>{{'warning' | i18n }}:</b> {{'emergencyAccessOwnerWarning' | i18n}}
<b>{{ "warning" | i18n }}:</b> {{ "emergencyAccessOwnerWarning" | i18n }}
</p>
<div class="page-header d-flex">
<h2>
{{'trustedEmergencyContacts' | i18n}}
<a href="#" appStopClick class="badge badge-primary" *ngIf="!canAccessPremium" (click)="premiumRequired()">
{{'premium' | i18n}}
</a>
</h2>
<div class="ml-auto d-flex">
<button class="btn btn-sm btn-outline-primary ml-3" type="button" (click)="invite()" [disabled]="!canAccessPremium">
<i aria-hidden="true" class="fa fa-plus fa-fw"></i>
{{'addEmergencyContact' |i18n}}
</button>
</div>
<h2>
{{ "trustedEmergencyContacts" | i18n }}
<a
href="#"
appStopClick
class="badge badge-primary"
*ngIf="!canAccessPremium"
(click)="premiumRequired()"
>
{{ "premium" | i18n }}
</a>
</h2>
<div class="ml-auto d-flex">
<button
class="btn btn-sm btn-outline-primary ml-3"
type="button"
(click)="invite()"
[disabled]="!canAccessPremium"
>
<i aria-hidden="true" class="fa fa-plus fa-fw"></i>
{{ "addEmergencyContact" | i18n }}
</button>
</div>
</div>
<table class="table table-hover table-list mb-0" *ngIf="trustedContacts && trustedContacts.length">
<tbody>
<tr *ngFor="let c of trustedContacts; let i = index">
<td width="30">
<app-avatar [data]="c | userName" [email]="c.email" size="25" [circle]="true"
[fontSize]="14"></app-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(c)">{{c.email}}</a>
<span class="badge badge-secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited">{{'invited' | i18n}}</span>
<span class="badge badge-warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted">{{'accepted' | i18n}}</span>
<span class="badge badge-warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated">{{'emergencyAccessRecoveryInitiated' | i18n}}</span>
<span class="badge badge-success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{'emergencyAccessRecoveryApproved' | i18n}}</span>
<span class="badge badge-primary"
*ngIf="c.type === emergencyAccessType.View">{{'view' | i18n}}</span>
<span class="badge badge-primary"
*ngIf="c.type === emergencyAccessType.Takeover">{{'takeover' | i18n}}</span>
<tbody>
<tr *ngFor="let c of trustedContacts; let i = index">
<td width="30">
<app-avatar
[data]="c | userName"
[email]="c.email"
size="25"
[circle]="true"
[fontSize]="14"
></app-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
<span
class="badge badge-secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span class="badge badge-warning" *ngIf="c.status === emergencyAccessStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<span
class="badge badge-warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span
class="badge badge-success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
>
<small class="text-muted d-block" *ngIf="c.name">{{c.name}}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(c)"
*ngIf="c.status === emergencyAccessStatusType.Invited">
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{'resendInvitation' | i18n}}
</a>
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(c)"
*ngIf="c.status === emergencyAccessStatusType.Accepted">
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
{{'confirm' | i18n}}
</a>
<a class="dropdown-item text-success" href="#" appStopClick (click)="approve(c)"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated">
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
{{'approve' | i18n}}
</a>
<a class="dropdown-item text-warning" href="#" appStopClick (click)="reject(c)"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated || c.status === emergencyAccessStatusType.RecoveryApproved">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'reject' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(c)">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'remove' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
<span class="badge badge-primary" *ngIf="c.type === emergencyAccessType.View">{{
"view" | i18n
}}</span>
<span class="badge badge-primary" *ngIf="c.type === emergencyAccessType.Takeover">{{
"takeover" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a
class="dropdown-item"
href="#"
appStopClick
(click)="reinvite(c)"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{ "resendInvitation" | i18n }}
</a>
<a
class="dropdown-item text-success"
href="#"
appStopClick
(click)="confirm(c)"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
{{ "confirm" | i18n }}
</a>
<a
class="dropdown-item text-success"
href="#"
appStopClick
(click)="approve(c)"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
{{ "approve" | i18n }}
</a>
<a
class="dropdown-item text-warning"
href="#"
appStopClick
(click)="reject(c)"
*ngIf="
c.status === emergencyAccessStatusType.RecoveryInitiated ||
c.status === emergencyAccessStatusType.RecoveryApproved
"
>
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{ "reject" | i18n }}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(c)">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<p *ngIf="!trustedContacts || !trustedContacts.length">{{'noTrustedContacts' | i18n}}</p>
<p *ngIf="!trustedContacts || !trustedContacts.length">{{ "noTrustedContacts" | i18n }}</p>
<div class="page-header spaced-header">
<h2>{{'designatedEmergencyContacts' | i18n}}</h2>
<h2>{{ "designatedEmergencyContacts" | i18n }}</h2>
</div>
<table class="table table-hover table-list mb-0" *ngIf="grantedContacts && grantedContacts.length">
<tbody>
<tr *ngFor="let c of grantedContacts; let i = index">
<td width="30">
<app-avatar [data]="c | userName" [email]="c.email" size="25" [circle]="true"
[fontSize]="14"></app-avatar>
</td>
<td>
<span>{{c.email}}</span>
<span class="badge badge-secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited">{{'invited' | i18n}}</span>
<span class="badge badge-warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted">{{'accepted' | i18n}}</span>
<span class="badge badge-warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated">{{'emergencyAccessRecoveryInitiated' | i18n}}</span>
<span class="badge badge-success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{'emergencyAccessRecoveryApproved' | i18n}}</span>
<span class="badge badge-primary"
*ngIf="c.type === emergencyAccessType.View">{{'view' | i18n}}</span>
<span class="badge badge-primary"
*ngIf="c.type === emergencyAccessType.Takeover">{{'takeover' | i18n}}</span>
<tbody>
<tr *ngFor="let c of grantedContacts; let i = index">
<td width="30">
<app-avatar
[data]="c | userName"
[email]="c.email"
size="25"
[circle]="true"
[fontSize]="14"
></app-avatar>
</td>
<td>
<span>{{ c.email }}</span>
<span
class="badge badge-secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span class="badge badge-warning" *ngIf="c.status === emergencyAccessStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<span
class="badge badge-warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span
class="badge badge-success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
>
<small class="text-muted d-block" *ngIf="c.name">{{c.name}}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" appStopClick (click)="requestAccess(c)"
*ngIf="c.status === emergencyAccessStatusType.Confirmed">
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{'requestAccess' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="takeover(c)"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved && c.type === emergencyAccessType.Takeover">
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
{{'takeover' | i18n}}
</a>
<a class="dropdown-item" [routerLink]="c.id"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved && c.type === emergencyAccessType.View">
<i class="fa fa-fw fa-eye" aria-hidden="true"></i>
{{'view' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(c)">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'remove' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
<span class="badge badge-primary" *ngIf="c.type === emergencyAccessType.View">{{
"view" | i18n
}}</span>
<span class="badge badge-primary" *ngIf="c.type === emergencyAccessType.Takeover">{{
"takeover" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a
class="dropdown-item"
href="#"
appStopClick
(click)="requestAccess(c)"
*ngIf="c.status === emergencyAccessStatusType.Confirmed"
>
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{ "requestAccess" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="takeover(c)"
*ngIf="
c.status === emergencyAccessStatusType.RecoveryApproved &&
c.type === emergencyAccessType.Takeover
"
>
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
{{ "takeover" | i18n }}
</a>
<a
class="dropdown-item"
[routerLink]="c.id"
*ngIf="
c.status === emergencyAccessStatusType.RecoveryApproved &&
c.type === emergencyAccessType.View
"
>
<i class="fa fa-fw fa-eye" aria-hidden="true"></i>
{{ "view" | i18n }}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(c)">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<p *ngIf="!grantedContacts || !grantedContacts.length">{{'noGrantedAccess' | i18n}}</p>
<p *ngIf="!grantedContacts || !grantedContacts.length">{{ "noGrantedAccess" | i18n }}</p>
<ng-template #addEdit></ng-template>
<ng-template #takeoverTemplate></ng-template>

View File

@@ -1,262 +1,316 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { EmergencyAccessConfirmRequest } from "jslib-common/models/request/emergencyAccessConfirmRequest";
import {
Component,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
EmergencyAccessGranteeDetailsResponse,
EmergencyAccessGrantorDetailsResponse,
} from "jslib-common/models/response/emergencyAccessResponse";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { EmergencyAccessStatusType } from "jslib-common/enums/emergencyAccessStatusType";
import { EmergencyAccessType } from "jslib-common/enums/emergencyAccessType";
import { Utils } from "jslib-common/misc/utils";
import { EmergencyAccessConfirmRequest } from 'jslib-common/models/request/emergencyAccessConfirmRequest';
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
import { EmergencyAccessGranteeDetailsResponse, EmergencyAccessGrantorDetailsResponse } from 'jslib-common/models/response/emergencyAccessResponse';
import { EmergencyAccessAddEditComponent } from "./emergency-access-add-edit.component";
import { EmergencyAccessConfirmComponent } from "./emergency-access-confirm.component";
import { EmergencyAccessTakeoverComponent } from "./emergency-access-takeover.component";
import { EmergencyAccessStatusType } from 'jslib-common/enums/emergencyAccessStatusType';
import { EmergencyAccessType } from 'jslib-common/enums/emergencyAccessType';
import { Utils } from 'jslib-common/misc/utils';
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { EmergencyAccessAddEditComponent } from './emergency-access-add-edit.component';
import { EmergencyAccessConfirmComponent } from './emergency-access-confirm.component';
import { EmergencyAccessTakeoverComponent } from './emergency-access-takeover.component';
import { ModalService } from 'jslib-angular/services/modal.service';
import { ModalService } from "jslib-angular/services/modal.service";
@Component({
selector: 'emergency-access',
templateUrl: 'emergency-access.component.html',
selector: "emergency-access",
templateUrl: "emergency-access.component.html",
})
export class EmergencyAccessComponent implements OnInit {
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild('takeoverTemplate', { read: ViewContainerRef, static: true }) takeoverModalRef: ViewContainerRef;
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("takeoverTemplate", { read: ViewContainerRef, static: true })
takeoverModalRef: ViewContainerRef;
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
confirmModalRef: ViewContainerRef;
canAccessPremium: boolean;
trustedContacts: EmergencyAccessGranteeDetailsResponse[];
grantedContacts: EmergencyAccessGrantorDetailsResponse[];
emergencyAccessType = EmergencyAccessType;
emergencyAccessStatusType = EmergencyAccessStatusType;
actionPromise: Promise<any>;
isOrganizationOwner: boolean;
canAccessPremium: boolean;
trustedContacts: EmergencyAccessGranteeDetailsResponse[];
grantedContacts: EmergencyAccessGrantorDetailsResponse[];
emergencyAccessType = EmergencyAccessType;
emergencyAccessStatusType = EmergencyAccessStatusType;
actionPromise: Promise<any>;
isOrganizationOwner: boolean;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private modalService: ModalService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private userNamePipe: UserNamePipe,
private logService: LogService,
private stateService: StateService,
private organizationService: OrganizationService,
) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private modalService: ModalService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private userNamePipe: UserNamePipe,
private logService: LogService,
private stateService: StateService,
private organizationService: OrganizationService
) {}
async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
const orgs = await this.organizationService.getAll();
this.isOrganizationOwner = orgs.some(o => o.isOwner);
this.load();
async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
const orgs = await this.organizationService.getAll();
this.isOrganizationOwner = orgs.some((o) => o.isOwner);
this.load();
}
async load() {
this.trustedContacts = (await this.apiService.getEmergencyAccessTrusted()).data;
this.grantedContacts = (await this.apiService.getEmergencyAccessGranted()).data;
}
async premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send("premiumRequired");
return;
}
}
async load() {
this.trustedContacts = (await this.apiService.getEmergencyAccessTrusted()).data;
this.grantedContacts = (await this.apiService.getEmergencyAccessGranted()).data;
}
async premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send('premiumRequired');
return;
}
}
async edit(details: EmergencyAccessGranteeDetailsResponse) {
const [modal] = await this.modalService.openViewRef(EmergencyAccessAddEditComponent, this.addEditModalRef, comp => {
comp.name = this.userNamePipe.transform(details);
comp.emergencyAccessId = details?.id;
comp.readOnly = !this.canAccessPremium;
comp.onSaved.subscribe(() => {
modal.close();
this.load();
});
comp.onDeleted.subscribe(() => {
modal.close();
this.remove(details);
});
async edit(details: EmergencyAccessGranteeDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
EmergencyAccessAddEditComponent,
this.addEditModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(details);
comp.emergencyAccessId = details?.id;
comp.readOnly = !this.canAccessPremium;
comp.onSaved.subscribe(() => {
modal.close();
this.load();
});
}
invite() {
this.edit(null);
}
async reinvite(contact: EmergencyAccessGranteeDetailsResponse) {
if (this.actionPromise != null) {
return;
}
this.actionPromise = this.apiService.postEmergencyAccessReinvite(contact.id);
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('hasBeenReinvited', contact.email));
this.actionPromise = null;
}
async confirm(contact: EmergencyAccessGranteeDetailsResponse) {
function updateUser() {
contact.status = EmergencyAccessStatusType.Confirmed;
}
if (this.actionPromise != null) {
return;
}
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
if (autoConfirm == null || !autoConfirm) {
const [modal] = await this.modalService.openViewRef(EmergencyAccessConfirmComponent, this.confirmModalRef, comp => {
comp.name = this.userNamePipe.transform(contact);
comp.emergencyAccessId = contact.id;
comp.userId = contact?.granteeId;
comp.onConfirmed.subscribe(async () => {
modal.close();
comp.formPromise = this.doConfirmation(contact);
await comp.formPromise;
updateUser();
this.platformUtilsService.showToast('success', null, this.i18nService.t('hasBeenConfirmed', this.userNamePipe.transform(contact)));
});
});
return;
}
this.actionPromise = this.doConfirmation(contact);
await this.actionPromise;
updateUser();
this.platformUtilsService.showToast('success', null, this.i18nService.t('hasBeenConfirmed', this.userNamePipe.transform(contact)));
this.actionPromise = null;
}
async remove(details: EmergencyAccessGranteeDetailsResponse | EmergencyAccessGrantorDetailsResponse) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('removeUserConfirmation'), this.userNamePipe.transform(details),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
await this.apiService.deleteEmergencyAccess(details.id);
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedUserId', this.userNamePipe.transform(details)));
if (details instanceof EmergencyAccessGranteeDetailsResponse) {
this.removeGrantee(details);
} else {
this.removeGrantor(details);
}
} catch (e) {
this.logService.error(e);
}
}
async requestAccess(details: EmergencyAccessGrantorDetailsResponse) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('requestAccessConfirmation', details.waitTimeDays.toString()),
this.userNamePipe.transform(details),
this.i18nService.t('requestAccess'),
this.i18nService.t('no'),
'warning',
);
if (!confirmed) {
return false;
}
await this.apiService.postEmergencyAccessInitiate(details.id);
details.status = EmergencyAccessStatusType.RecoveryInitiated;
this.platformUtilsService.showToast('success', null, this.i18nService.t('requestSent', this.userNamePipe.transform(details)));
}
async approve(details: EmergencyAccessGranteeDetailsResponse) {
const type = this.i18nService.t(details.type === EmergencyAccessType.View ? 'view' : 'takeover');
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('approveAccessConfirmation', this.userNamePipe.transform(details), type),
this.userNamePipe.transform(details),
this.i18nService.t('approve'),
this.i18nService.t('no'),
'warning',
);
if (!confirmed) {
return false;
}
await this.apiService.postEmergencyAccessApprove(details.id);
details.status = EmergencyAccessStatusType.RecoveryApproved;
this.platformUtilsService.showToast('success', null, this.i18nService.t('emergencyApproved', this.userNamePipe.transform(details)));
}
async reject(details: EmergencyAccessGranteeDetailsResponse) {
await this.apiService.postEmergencyAccessReject(details.id);
details.status = EmergencyAccessStatusType.Confirmed;
this.platformUtilsService.showToast('success', null, this.i18nService.t('emergencyRejected', this.userNamePipe.transform(details)));
}
async takeover(details: EmergencyAccessGrantorDetailsResponse) {
const [modal] = await this.modalService.openViewRef(EmergencyAccessTakeoverComponent, this.takeoverModalRef, comp => {
comp.name = this.userNamePipe.transform(details);
comp.email = details.email;
comp.emergencyAccessId = details != null ? details.id : null;
comp.onDone.subscribe(() => {
modal.close();
this.platformUtilsService.showToast('success', null, this.i18nService.t('passwordResetFor', this.userNamePipe.transform(details)));
});
comp.onDeleted.subscribe(() => {
modal.close();
this.remove(details);
});
}
);
}
invite() {
this.edit(null);
}
async reinvite(contact: EmergencyAccessGranteeDetailsResponse) {
if (this.actionPromise != null) {
return;
}
this.actionPromise = this.apiService.postEmergencyAccessReinvite(contact.id);
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenReinvited", contact.email)
);
this.actionPromise = null;
}
async confirm(contact: EmergencyAccessGranteeDetailsResponse) {
function updateUser() {
contact.status = EmergencyAccessStatusType.Confirmed;
}
private removeGrantee(details: EmergencyAccessGranteeDetailsResponse) {
const index = this.trustedContacts.indexOf(details);
if (index > -1) {
this.trustedContacts.splice(index, 1);
if (this.actionPromise != null) {
return;
}
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
if (autoConfirm == null || !autoConfirm) {
const [modal] = await this.modalService.openViewRef(
EmergencyAccessConfirmComponent,
this.confirmModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(contact);
comp.emergencyAccessId = contact.id;
comp.userId = contact?.granteeId;
comp.onConfirmed.subscribe(async () => {
modal.close();
comp.formPromise = this.doConfirmation(contact);
await comp.formPromise;
updateUser();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact))
);
});
}
);
return;
}
private removeGrantor(details: EmergencyAccessGrantorDetailsResponse) {
const index = this.grantedContacts.indexOf(details);
if (index > -1) {
this.grantedContacts.splice(index, 1);
}
this.actionPromise = this.doConfirmation(contact);
await this.actionPromise;
updateUser();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact))
);
this.actionPromise = null;
}
async remove(
details: EmergencyAccessGranteeDetailsResponse | EmergencyAccessGrantorDetailsResponse
) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("removeUserConfirmation"),
this.userNamePipe.transform(details),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
// Encrypt the master password hash using the grantees public key, and send it to bitwarden for escrow.
private async doConfirmation(details: EmergencyAccessGranteeDetailsResponse) {
const encKey = await this.cryptoService.getEncKey();
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
await this.apiService.deleteEmergencyAccess(details.id);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("removedUserId", this.userNamePipe.transform(details))
);
try {
this.logService.debug('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(details.granteeId, publicKey.buffer)).join('-'));
} catch {
// Ignore errors since it's just a debug message
}
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
const request = new EmergencyAccessConfirmRequest();
request.key = encryptedKey.encryptedString;
await this.apiService.postEmergencyAccessConfirm(details.id, request);
if (details instanceof EmergencyAccessGranteeDetailsResponse) {
this.removeGrantee(details);
} else {
this.removeGrantor(details);
}
} catch (e) {
this.logService.error(e);
}
}
async requestAccess(details: EmergencyAccessGrantorDetailsResponse) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("requestAccessConfirmation", details.waitTimeDays.toString()),
this.userNamePipe.transform(details),
this.i18nService.t("requestAccess"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
await this.apiService.postEmergencyAccessInitiate(details.id);
details.status = EmergencyAccessStatusType.RecoveryInitiated;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("requestSent", this.userNamePipe.transform(details))
);
}
async approve(details: EmergencyAccessGranteeDetailsResponse) {
const type = this.i18nService.t(
details.type === EmergencyAccessType.View ? "view" : "takeover"
);
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("approveAccessConfirmation", this.userNamePipe.transform(details), type),
this.userNamePipe.transform(details),
this.i18nService.t("approve"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
await this.apiService.postEmergencyAccessApprove(details.id);
details.status = EmergencyAccessStatusType.RecoveryApproved;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details))
);
}
async reject(details: EmergencyAccessGranteeDetailsResponse) {
await this.apiService.postEmergencyAccessReject(details.id);
details.status = EmergencyAccessStatusType.Confirmed;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details))
);
}
async takeover(details: EmergencyAccessGrantorDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
EmergencyAccessTakeoverComponent,
this.takeoverModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(details);
comp.email = details.email;
comp.emergencyAccessId = details != null ? details.id : null;
comp.onDone.subscribe(() => {
modal.close();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details))
);
});
}
);
}
private removeGrantee(details: EmergencyAccessGranteeDetailsResponse) {
const index = this.trustedContacts.indexOf(details);
if (index > -1) {
this.trustedContacts.splice(index, 1);
}
}
private removeGrantor(details: EmergencyAccessGrantorDetailsResponse) {
const index = this.grantedContacts.indexOf(details);
if (index > -1) {
this.grantedContacts.splice(index, 1);
}
}
// Encrypt the master password hash using the grantees public key, and send it to bitwarden for escrow.
private async doConfirmation(details: EmergencyAccessGranteeDetailsResponse) {
const encKey = await this.cryptoService.getEncKey();
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
this.logService.debug(
"User's fingerprint: " +
(await this.cryptoService.getFingerprint(details.granteeId, publicKey.buffer)).join("-")
);
} catch {
// Ignore errors since it's just a debug message
}
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
const request = new EmergencyAccessConfirmRequest();
request.key = encryptedKey.encryptedString;
await this.apiService.postEmergencyAccessConfirm(details.id, request);
}
}

View File

@@ -1,50 +1,74 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { AuditService } from 'jslib-common/abstractions/audit.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { EventService } from 'jslib-common/abstractions/event.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { TotpService } from 'jslib-common/abstractions/totp.service';
import { AuditService } from "jslib-common/abstractions/audit.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TotpService } from "jslib-common/abstractions/totp.service";
import { Cipher } from 'jslib-common/models/domain/cipher';
import { Cipher } from "jslib-common/models/domain/cipher";
import { AddEditComponent as BaseAddEditComponent } from '../vault/add-edit.component';
import { AddEditComponent as BaseAddEditComponent } from "../vault/add-edit.component";
@Component({
selector: 'app-org-vault-add-edit',
templateUrl: '../vault/add-edit.component.html',
selector: "app-org-vault-add-edit",
templateUrl: "../vault/add-edit.component.html",
})
export class EmergencyAddEditComponent extends BaseAddEditComponent {
originalCipher: Cipher = null;
viewOnly = true;
originalCipher: Cipher = null;
viewOnly = true;
constructor(cipherService: CipherService, folderService: FolderService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
auditService: AuditService, stateService: StateService, collectionService: CollectionService,
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
messagingService: MessagingService, eventService: EventService,
policyService: PolicyService, passwordRepromptService: PasswordRepromptService,
organizationService: OrganizationService, logService: LogService) {
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
collectionService, totpService, passwordGenerationService, messagingService,
eventService, policyService, organizationService, logService, passwordRepromptService);
}
constructor(
cipherService: CipherService,
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
stateService: StateService,
collectionService: CollectionService,
totpService: TotpService,
passwordGenerationService: PasswordGenerationService,
messagingService: MessagingService,
eventService: EventService,
policyService: PolicyService,
passwordRepromptService: PasswordRepromptService,
organizationService: OrganizationService,
logService: LogService
) {
super(
cipherService,
folderService,
i18nService,
platformUtilsService,
auditService,
stateService,
collectionService,
totpService,
passwordGenerationService,
messagingService,
eventService,
policyService,
organizationService,
logService,
passwordRepromptService
);
}
async load() {
this.title = this.i18nService.t('viewItem');
}
async load() {
this.title = this.i18nService.t("viewItem");
}
protected async loadCipher() {
return Promise.resolve(this.originalCipher);
}
protected async loadCipher() {
return Promise.resolve(this.originalCipher);
}
}

View File

@@ -1,4 +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}}
<i class="fa fa-fw fa-link" aria-hidden="true"></i>
{{ "linkSso" | i18n }}
</a>

View File

@@ -1,48 +1,61 @@
import {
AfterContentInit,
Component,
Input,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AfterContentInit, Component, Input } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SsoComponent } from 'jslib-angular/components/sso.component';
import { SsoComponent } from "jslib-angular/components/sso.component";
import { Organization } from 'jslib-common/models/domain/organization';
import { Organization } from "jslib-common/models/domain/organization";
@Component({
selector: 'app-link-sso',
templateUrl: 'link-sso.component.html',
selector: "app-link-sso",
templateUrl: "link-sso.component.html",
})
export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
@Input() organization: Organization;
returnUri: string = '/settings/organizations';
@Input() organization: Organization;
returnUri: string = "/settings/organizations";
constructor(platformUtilsService: PlatformUtilsService, i18nService: I18nService,
apiService: ApiService, authService: AuthService,
router: Router, route: ActivatedRoute,
cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService,
stateService: StateService, environmentService: EnvironmentService, logService: LogService) {
super(authService, router,
i18nService, route, stateService,
platformUtilsService, apiService,
cryptoFunctionService, environmentService, passwordGenerationService, logService);
constructor(
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
apiService: ApiService,
authService: AuthService,
router: Router,
route: ActivatedRoute,
cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationService,
stateService: StateService,
environmentService: EnvironmentService,
logService: LogService
) {
super(
authService,
router,
i18nService,
route,
stateService,
platformUtilsService,
apiService,
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
);
this.returnUri = '/settings/organizations';
this.redirectUri = window.location.origin + '/sso-connector.html';
this.clientId = 'web';
}
this.returnUri = "/settings/organizations";
this.redirectUri = window.location.origin + "/sso-connector.html";
this.clientId = "web";
}
async ngAfterContentInit() {
this.identifier = this.organization.identifier;
}
async ngAfterContentInit() {
this.identifier = this.organization.identifier;
}
}

View File

@@ -1,99 +1,145 @@
<div class="page-header">
<h1>{{'options' | i18n}}</h1>
<h1>{{ "options" | i18n }}</h1>
</div>
<p>{{'optionsDesc' | i18n}}</p>
<p>{{ "optionsDesc" | i18n }}</p>
<form (ngSubmit)="submit()" ngNativeValidate>
<div class="row">
<div class="col-6">
<app-vault-timeout-input [vaultTimeouts]="vaultTimeouts" [formControl]="vaultTimeout" ngDefaultControl>
</app-vault-timeout-input>
</div>
<div class="row">
<div class="col-6">
<app-vault-timeout-input
[vaultTimeouts]="vaultTimeouts"
[formControl]="vaultTimeout"
ngDefaultControl
>
</app-vault-timeout-input>
</div>
<div class="form-group">
<label>{{'vaultTimeoutAction' | i18n}}</label>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="vaultTimeoutAction" id="vaultTimeoutActionLock"
value="lock" [(ngModel)]="vaultTimeoutAction">
<label class="form-check-label" for="vaultTimeoutActionLock">
{{'lock' | i18n}}
<small>{{'vaultTimeoutActionLockDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="vaultTimeoutAction" id="vaultTimeoutActionLogOut"
value="logOut" [(ngModel)]="vaultTimeoutAction" (ngModelChange)="vaultTimeoutActionChanged($event)">
<label class="form-check-label" for="vaultTimeoutActionLogOut">
{{'logOut' | i18n}}
<small>{{'vaultTimeoutActionLogOutDesc' | i18n}}</small>
</label>
</div>
</div>
<div class="form-group">
<label>{{ "vaultTimeoutAction" | i18n }}</label>
<div class="form-check form-check-block">
<input
class="form-check-input"
type="radio"
name="vaultTimeoutAction"
id="vaultTimeoutActionLock"
value="lock"
[(ngModel)]="vaultTimeoutAction"
/>
<label class="form-check-label" for="vaultTimeoutActionLock">
{{ "lock" | i18n }}
<small>{{ "vaultTimeoutActionLockDesc" | i18n }}</small>
</label>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<div class="d-flex">
<label for="locale">{{'language' | i18n}}</label>
<a class="ml-auto" href="https://help.bitwarden.com/article/localization/" target="_blank"
rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<select id="locale" name="Locale" [(ngModel)]="locale" class="form-control">
<option *ngFor="let o of localeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
<small class="form-text text-muted">{{'languageDesc' | i18n}}</small>
</div>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="vaultTimeoutAction"
id="vaultTimeoutActionLogOut"
value="logOut"
[(ngModel)]="vaultTimeoutAction"
(ngModelChange)="vaultTimeoutActionChanged($event)"
/>
<label class="form-check-label" for="vaultTimeoutActionLogOut">
{{ "logOut" | i18n }}
<small>{{ "vaultTimeoutActionLogOutDesc" | i18n }}</small>
</label>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="disableIcons" name="DisableIcons"
[(ngModel)]="disableIcons">
<label class="form-check-label" for="disableIcons">
{{'disableIcons' | i18n}}
</label>
<a href="https://help.bitwarden.com/article/website-icons/" target="_blank" rel="noopener"
appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<div class="d-flex">
<label for="locale">{{ "language" | i18n }}</label>
<a
class="ml-auto"
href="https://help.bitwarden.com/article/localization/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<small class="form-text text-muted">{{'disableIconsDesc' | i18n}}</small>
<select id="locale" name="Locale" [(ngModel)]="locale" class="form-control">
<option *ngFor="let o of localeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="form-text text-muted">{{ "languageDesc" | i18n }}</small>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enableGravatars" name="enableGravatars"
[(ngModel)]="enableGravatars">
<label class="form-check-label" for="enableGravatars">
{{'enableGravatars' | i18n}}
</label>
<a href="https://gravatar.com/" target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<small class="form-text text-muted">{{'enableGravatarsDesc' | i18n}}</small>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="disableIcons"
name="DisableIcons"
[(ngModel)]="disableIcons"
/>
<label class="form-check-label" for="disableIcons">
{{ "disableIcons" | i18n }}
</label>
<a
href="https://help.bitwarden.com/article/website-icons/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enableFullWidth" name="enableFullWidth"
[(ngModel)]="enableFullWidth">
<label class="form-check-label" for="enableFullWidth">
{{'enableFullWidth' | i18n}}
</label>
</div>
<small class="form-text text-muted">{{'enableFullWidthDesc' | i18n}}</small>
<small class="form-text text-muted">{{ "disableIconsDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enableGravatars"
name="enableGravatars"
[(ngModel)]="enableGravatars"
/>
<label class="form-check-label" for="enableGravatars">
{{ "enableGravatars" | i18n }}
</label>
<a
href="https://gravatar.com/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="theme">{{'theme' | i18n}}</label>
<select id="theme" name="theme" [(ngModel)]="theme" class="form-control">
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
<small class="form-text text-muted">{{'themeDesc' | i18n}}</small>
</div>
</div>
<small class="form-text text-muted">{{ "enableGravatarsDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enableFullWidth"
name="enableFullWidth"
[(ngModel)]="enableFullWidth"
/>
<label class="form-check-label" for="enableFullWidth">
{{ "enableFullWidth" | i18n }}
</label>
</div>
<button type="submit" class="btn btn-primary">
{{'save' | i18n}}
</button>
<small class="form-text text-muted">{{ "enableFullWidthDesc" | i18n }}</small>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="theme">{{ "theme" | i18n }}</label>
<select id="theme" name="theme" [(ngModel)]="theme" class="form-control">
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="form-text text-muted">{{ "themeDesc" | i18n }}</small>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
{{ "save" | i18n }}
</button>
</form>

View File

@@ -1,124 +1,130 @@
import {
Component,
OnInit,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { Component, OnInit } from "@angular/core";
import { FormControl } from "@angular/forms";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { ThemeType } from 'jslib-common/enums/themeType';
import { Utils } from 'jslib-common/misc/utils';
import { ThemeType } from "jslib-common/enums/themeType";
import { Utils } from "jslib-common/misc/utils";
@Component({
selector: 'app-options',
templateUrl: 'options.component.html',
selector: "app-options",
templateUrl: "options.component.html",
})
export class OptionsComponent implements OnInit {
vaultTimeoutAction: string = 'lock';
disableIcons: boolean;
enableGravatars: boolean;
enableFullWidth: boolean;
theme: string = null;
locale: string;
vaultTimeouts: { name: string; value: number; }[];
localeOptions: any[];
themeOptions: any[];
vaultTimeoutAction: string = "lock";
disableIcons: boolean;
enableGravatars: boolean;
enableFullWidth: boolean;
theme: string = null;
locale: string;
vaultTimeouts: { name: string; value: number }[];
localeOptions: any[];
themeOptions: any[];
vaultTimeout: FormControl = new FormControl(null);
vaultTimeout: FormControl = new FormControl(null);
private startingLocale: string;
private startingTheme: string;
private startingLocale: string;
private startingTheme: string;
constructor(
private stateService: StateService,
private i18nService: I18nService,
private vaultTimeoutService: VaultTimeoutService,
private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService
) {
this.vaultTimeouts = [
{ name: i18nService.t('oneMinute'), value: 1 },
{ name: i18nService.t('fiveMinutes'), value: 5 },
{ name: i18nService.t('fifteenMinutes'), value: 15 },
{ name: i18nService.t('thirtyMinutes'), value: 30 },
{ name: i18nService.t('oneHour'), value: 60 },
{ name: i18nService.t('fourHours'), value: 240 },
{ name: i18nService.t('onRefresh'), value: -1 },
];
if (this.platformUtilsService.isDev()) {
this.vaultTimeouts.push({ name: i18nService.t('never'), value: null });
}
const localeOptions: any[] = [];
i18nService.supportedTranslationLocales.forEach(locale => {
let name = locale;
if (i18nService.localeNames.has(locale)) {
name += (' - ' + i18nService.localeNames.get(locale));
}
localeOptions.push({ name: name, value: locale });
});
localeOptions.sort(Utils.getSortFunction(i18nService, 'name'));
localeOptions.splice(0, 0, { name: i18nService.t('default'), value: null });
this.localeOptions = localeOptions;
this.themeOptions = [
{ name: i18nService.t('themeLight'), value: ThemeType.Light },
{ name: i18nService.t('themeDark'), value: ThemeType.Dark },
{ name: i18nService.t('themeSystem'), value: ThemeType.System },
];
constructor(
private stateService: StateService,
private i18nService: I18nService,
private vaultTimeoutService: VaultTimeoutService,
private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService
) {
this.vaultTimeouts = [
{ name: i18nService.t("oneMinute"), value: 1 },
{ name: i18nService.t("fiveMinutes"), value: 5 },
{ name: i18nService.t("fifteenMinutes"), value: 15 },
{ name: i18nService.t("thirtyMinutes"), value: 30 },
{ name: i18nService.t("oneHour"), value: 60 },
{ name: i18nService.t("fourHours"), value: 240 },
{ name: i18nService.t("onRefresh"), value: -1 },
];
if (this.platformUtilsService.isDev()) {
this.vaultTimeouts.push({ name: i18nService.t("never"), value: null });
}
async ngOnInit() {
this.vaultTimeout.setValue(await this.vaultTimeoutService.getVaultTimeout());
this.vaultTimeoutAction = await this.stateService.getVaultTimeoutAction();
this.disableIcons = await this.stateService.getDisableFavicon();
this.enableGravatars = await this.stateService.getEnableGravitars();
this.enableFullWidth = await this.stateService.getEnableFullWidth();
this.locale = await this.stateService.getLocale() ?? this.startingLocale;
this.theme = await this.stateService.getTheme() ?? this.startingTheme;
const localeOptions: any[] = [];
i18nService.supportedTranslationLocales.forEach((locale) => {
let name = locale;
if (i18nService.localeNames.has(locale)) {
name += " - " + i18nService.localeNames.get(locale);
}
localeOptions.push({ name: name, value: locale });
});
localeOptions.sort(Utils.getSortFunction(i18nService, "name"));
localeOptions.splice(0, 0, { name: i18nService.t("default"), value: null });
this.localeOptions = localeOptions;
this.themeOptions = [
{ name: i18nService.t("themeLight"), value: ThemeType.Light },
{ name: i18nService.t("themeDark"), value: ThemeType.Dark },
{ name: i18nService.t("themeSystem"), value: ThemeType.System },
];
}
async ngOnInit() {
this.vaultTimeout.setValue(await this.vaultTimeoutService.getVaultTimeout());
this.vaultTimeoutAction = await this.stateService.getVaultTimeoutAction();
this.disableIcons = await this.stateService.getDisableFavicon();
this.enableGravatars = await this.stateService.getEnableGravitars();
this.enableFullWidth = await this.stateService.getEnableFullWidth();
this.locale = (await this.stateService.getLocale()) ?? this.startingLocale;
this.theme = (await this.stateService.getTheme()) ?? this.startingTheme;
}
async submit() {
if (!this.vaultTimeout.valid) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("vaultTimeoutToLarge"));
return;
}
async submit() {
if (!this.vaultTimeout.valid) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('vaultTimeoutToLarge'));
return;
}
await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout.value, this.vaultTimeoutAction);
await this.stateService.setDisableFavicon(this.disableIcons);
await this.stateService.setEnableGravitars(this.enableGravatars);
await this.stateService.setEnableFullWidth(this.enableFullWidth);
this.messagingService.send('setFullWidth');
if (this.theme !== this.startingTheme) {
await this.stateService.setTheme(this.theme);
this.startingTheme = this.theme;
const effectiveTheme = await this.platformUtilsService.getEffectiveTheme();
const htmlEl = window.document.documentElement;
htmlEl.classList.remove('theme_' + ThemeType.Light, 'theme_' + ThemeType.Dark);
htmlEl.classList.add('theme_' + effectiveTheme);
}
await this.stateService.setLocale(this.locale);
if (this.locale !== this.startingLocale) {
window.location.reload();
} else {
this.platformUtilsService.showToast('success', null, [this.i18nService.t('optionsUpdated'), this.i18nService.t('optionsUpdated')]);
}
await this.vaultTimeoutService.setVaultTimeoutOptions(
this.vaultTimeout.value,
this.vaultTimeoutAction
);
await this.stateService.setDisableFavicon(this.disableIcons);
await this.stateService.setEnableGravitars(this.enableGravatars);
await this.stateService.setEnableFullWidth(this.enableFullWidth);
this.messagingService.send("setFullWidth");
if (this.theme !== this.startingTheme) {
await this.stateService.setTheme(this.theme);
this.startingTheme = this.theme;
const effectiveTheme = await this.platformUtilsService.getEffectiveTheme();
const htmlEl = window.document.documentElement;
htmlEl.classList.remove("theme_" + ThemeType.Light, "theme_" + ThemeType.Dark);
htmlEl.classList.add("theme_" + effectiveTheme);
}
async vaultTimeoutActionChanged(newValue: string) {
if (newValue === 'logOut') {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('vaultTimeoutLogOutConfirmation'),
this.i18nService.t('vaultTimeoutLogOutConfirmationTitle'),
this.i18nService.t('yes'), this.i18nService.t('cancel'), 'warning');
if (!confirmed) {
this.vaultTimeoutAction = 'lock';
return;
}
}
this.vaultTimeoutAction = newValue;
await this.stateService.setLocale(this.locale);
if (this.locale !== this.startingLocale) {
window.location.reload();
} else {
this.platformUtilsService.showToast("success", null, [
this.i18nService.t("optionsUpdated"),
this.i18nService.t("optionsUpdated"),
]);
}
}
async vaultTimeoutActionChanged(newValue: string) {
if (newValue === "logOut") {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("vaultTimeoutLogOutConfirmation"),
this.i18nService.t("vaultTimeoutLogOutConfirmationTitle"),
this.i18nService.t("yes"),
this.i18nService.t("cancel"),
"warning"
);
if (!confirmed) {
this.vaultTimeoutAction = "lock";
return;
}
}
this.vaultTimeoutAction = newValue;
}
}

View File

@@ -1,261 +1,373 @@
<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>
<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>
<div class="form-group">
<label for="file">{{'licenseFile' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required>
<small class="form-text text-muted">{{'licenseFileDesc' | i18n :
'bitwarden_organization_license.json'}}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
</form>
<p>{{ "uploadLicenseFileOrg" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="form-group">
<label for="file">{{ "licenseFile" | i18n }}</label>
<input type="file" id="file" class="form-control-file" name="file" required />
<small class="form-text text-muted">{{
"licenseFileDesc" | i18n: "bitwarden_organization_license.json"
}}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span>
</button>
</form>
</ng-container>
<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">
<label for="name">{{'organizationName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
</div>
<div class="form-group col-6">
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail"
required>
</div>
<div class="form-group col-6" *ngIf="!!providerId">
<label for="email">{{'clientOwnerEmail' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="clientOwnerEmail" required>
<small class="text-muted">{{'clientOwnerDesc' | i18n : '20'}}</small>
</div>
<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">
<label for="name">{{ "organizationName" | i18n }}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required />
</div>
<div *ngIf="!providerId && !acceptingSponsorship">
<div class="form-group form-check">
<input id="ownedBusiness" class="form-check-input" type="checkbox" name="OwnedBusiness"
[(ngModel)]="ownedBusiness" (change)="changedOwnedBusiness()">
<label for="ownedBusiness" class="form-check-label">{{'accountOwnedBusiness' | i18n}}</label>
</div>
<div class="row" *ngIf="ownedBusiness">
<div class="form-group col-6">
<label for="businessName">{{'businessName' | i18n}}</label>
<input id="businessName" class="form-control" type="text" name="BusinessName"
[(ngModel)]="businessName">
</div>
</div>
<div class="form-group col-6">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="billingEmail"
required
/>
</div>
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
<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>
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization">
{{'xDayFreeTrial' | i18n : selectableProduct.trialPeriodDays }}
</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 && createOrganization">
{{'xDayFreeTrial' | i18n : selectableProduct.trialPeriodDays }}
</small>
</ng-template>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container *ngIf="selectableProduct.basePrice && !acceptingSponsorship">
{{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 class="form-group col-6" *ngIf="!!providerId">
<label for="email">{{ "clientOwnerEmail" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="clientOwnerEmail"
required
/>
<small class="text-muted">{{ "clientOwnerDesc" | i18n: "20" }}</small>
</div>
<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">
<label for="additionalSeats">{{'userSeats' | i18n}}</label>
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
[(ngModel)]="additionalSeats" min="1" max="100000" placeholder="{{'userSeatsDesc' | i18n}}"
required>
<small class="text-muted form-text">{{'userSeatsHowManyDesc' | i18n}}</small>
</div>
</div>
</ng-container>
<h2 class="mt-5">{{'addons' | i18n}}</h2>
<div class="row" *ngIf="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 : selectedPlan.baseSeats :
(seatPriceMonthly(selectedPlan) | currency:'$')}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6">
<label for="additionalStorage">{{'additionalStorageGb' | i18n}}</label>
<input id="additionalStorage" class="form-control" type="number" name="AdditionalStorageGb"
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
placeholder="{{'additionalStorageGbDesc' | i18n}}">
<small class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' :
(additionalStoragePriceMonthly(selectedPlan) | currency:'$') : ('month' | i18n)}}</small>
</div>
</div>
<div class="row">
<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">
<label for="premiumAccess" class="form-check-label bold">{{'premiumAccess' | i18n}}</label>
</div>
<small class="text-muted form-text">{{'premiumAccessDesc' | i18n : (3.33 | currency:'$') : ('month' |
i18n)}}</small>
</div>
</div>
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
<div class="form-check form-check-block" *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:'$'}} &times; 12
{{'monthAbbr' | i18n}}
=
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
<span style="text-decoration: line-through;">{{selectablePlan.basePrice | currency:'$'}}</span>
{{'freeWithSponsorship' | i18n}}
</ng-container>
<ng-template #notAcceptingSponsorship>
{{selectablePlan.basePrice | currency:'$'}}
/{{'year' | i18n}}
</ng-template>
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{selectablePlan.seatPrice / 12 | currency:'$'}} &times; 12
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
| currency:'$'}} /{{'year' | i18n}}
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{selectablePlan.additionalStoragePricePerGb / 12 | currency:'$'}} &times; 12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{selectablePlan.premiumAccessOptionCost / 12 | currency:'$'}} &times; 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}} &times; {{selectablePlan.seatPrice | currency:'$'}}
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
| currency:'$'}} /{{'month' | i18n}}
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{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">
<h2 class="spaced-header mb-4">{{ (createOrganization ? 'paymentInformation' : 'billingInformation') | i18n}}
</h2>
<small class="text-muted font-italic mb-3 d-block">
{{paymentDesc}}
</div>
<div *ngIf="!providerId && !acceptingSponsorship">
<div class="form-group form-check">
<input
id="ownedBusiness"
class="form-check-input"
type="checkbox"
name="OwnedBusiness"
[(ngModel)]="ownedBusiness"
(change)="changedOwnedBusiness()"
/>
<label for="ownedBusiness" class="form-check-label">{{
"accountOwnedBusiness" | i18n
}}</label>
</div>
<div class="row" *ngIf="ownedBusiness">
<div class="form-group col-6">
<label for="businessName">{{ "businessName" | i18n }}</label>
<input
id="businessName"
class="form-control"
type="text"
name="BusinessName"
[(ngModel)]="businessName"
/>
</div>
</div>
</div>
<h2 class="mt-5">{{ "chooseYourPlan" | i18n }}</h2>
<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
>
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
>
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
</small>
<app-payment *ngIf="createOrganization" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
<div id="price" class="my-4">
<div class="text-muted text-sm">
{{ 'planPrice' | i18n }}: {{ subtotal | currency: 'USD $' }}
<br />
<ng-container>
{{ 'estimatedTax' | i18n }}: {{ taxCharges | currency: 'USD $' }}
</ng-container>
</div>
<hr class="my-1 col-3 ml-0">
<p class="text-lg"><strong>{{'total' | i18n}}:</strong>
{{total | currency:'USD $'}}/{{selectedPlanInterval | i18n}}</p>
</div>
<ng-container *ngIf="!createOrganization">
<app-payment [showMethods]="false"></app-payment>
</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 && createOrganization"
>
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
</small>
</ng-template>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container *ngIf="selectableProduct.basePrice && !acceptingSponsorship">
{{ 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 *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">
<label for="additionalSeats">{{ "userSeats" | i18n }}</label>
<input
id="additionalSeats"
class="form-control"
type="number"
name="AdditionalSeats"
[(ngModel)]="additionalSeats"
min="1"
max="100000"
placeholder="{{ 'userSeatsDesc' | i18n }}"
required
/>
<small class="text-muted form-text">{{ "userSeatsHowManyDesc" | i18n }}</small>
</div>
</div>
</ng-container>
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
<div class="row" *ngIf="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: selectedPlan.baseSeats:(seatPriceMonthly(selectedPlan) | currency: "$")
}}</small>
</div>
</div>
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
<app-callout [type]="'error'">{{'singleOrgBlockCreateMessage' | i18n}}</app-callout>
<div class="row">
<div class="form-group col-6">
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
<input
id="additionalStorage"
class="form-control"
type="number"
name="AdditionalStorageGb"
[(ngModel)]="additionalStorage"
min="0"
max="99"
step="1"
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
/>
<small class="text-muted form-text">{{
"additionalStorageIntervalDesc"
| i18n
: "1 GB"
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
: ("month" | i18n)
}}</small>
</div>
</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>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
{{'cancel' | i18n}}
</button>
<div class="row">
<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"
/>
<label for="premiumAccess" class="form-check-label bold">{{
"premiumAccess" | i18n
}}</label>
</div>
<small class="text-muted form-text">{{
"premiumAccessDesc" | i18n: (3.33 | currency: "$"):("month" | i18n)
}}</small>
</div>
</div>
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
<div class="form-check form-check-block" *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: "$" }} &times; 12
{{ "monthAbbr" | i18n }}
=
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
<span style="text-decoration: line-through">{{
selectablePlan.basePrice | currency: "$"
}}</span>
{{ "freeWithSponsorship" | i18n }}
</ng-container>
<ng-template #notAcceptingSponsorship>
{{ selectablePlan.basePrice | currency: "$" }}
/{{ "year" | i18n }}
</ng-template>
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{ "additionalUsers" | i18n }}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
{{ additionalSeats || 0 }} &times;
{{ selectablePlan.seatPrice / 12 | currency: "$" }} &times; 12
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency: "$" }} /{{
"year" | i18n
}}
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} &times;
{{ selectablePlan.additionalStoragePricePerGb / 12 | currency: "$" }} &times; 12
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
{{ "premiumAccess" | i18n }}:
{{ selectablePlan.premiumAccessOptionCost / 12 | currency: "$" }} &times; 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 }} &times; {{ selectablePlan.seatPrice | currency: "$" }}
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency: "$" }} /{{
"month" | i18n
}}
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} &times;
{{ 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" />
<h2 class="spaced-header mb-4">
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
</h2>
<small class="text-muted font-italic mb-3 d-block">
{{ paymentDesc }}
</small>
<app-payment *ngIf="createOrganization" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
<div id="price" class="my-4">
<div class="text-muted text-sm">
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
<br />
<ng-container>
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
</ng-container>
</div>
<hr class="my-1 col-3 ml-0" />
<p class="text-lg">
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{
selectedPlanInterval | i18n
}}
</p>
</div>
<ng-container *ngIf="!createOrganization">
<app-payment [showMethods]="false"></app-payment>
</ng-container>
</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>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
{{ "cancel" | i18n }}
</button>
</div>
</form>

View File

@@ -1,388 +1,413 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import { Router } from '@angular/router';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
import { Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
import { PaymentComponent } from "./payment.component";
import { TaxInfoComponent } from "./tax-info.component";
import { EncString } from 'jslib-common/models/domain/encString';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { EncString } from "jslib-common/models/domain/encString";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
import { PlanType } from 'jslib-common/enums/planType';
import { PolicyType } from 'jslib-common/enums/policyType';
import { ProductType } from 'jslib-common/enums/productType';
import { PaymentMethodType } from "jslib-common/enums/paymentMethodType";
import { PlanType } from "jslib-common/enums/planType";
import { PolicyType } from "jslib-common/enums/policyType";
import { ProductType } from "jslib-common/enums/productType";
import { OrganizationCreateRequest } from 'jslib-common/models/request/organizationCreateRequest';
import { OrganizationKeysRequest } from 'jslib-common/models/request/organizationKeysRequest';
import { OrganizationUpgradeRequest } from 'jslib-common/models/request/organizationUpgradeRequest';
import { ProviderOrganizationCreateRequest } from 'jslib-common/models/request/provider/providerOrganizationCreateRequest';
import { OrganizationCreateRequest } from "jslib-common/models/request/organizationCreateRequest";
import { OrganizationKeysRequest } from "jslib-common/models/request/organizationKeysRequest";
import { OrganizationUpgradeRequest } from "jslib-common/models/request/organizationUpgradeRequest";
import { ProviderOrganizationCreateRequest } from "jslib-common/models/request/provider/providerOrganizationCreateRequest";
import { PlanResponse } from 'jslib-common/models/response/planResponse';
import { PlanResponse } from "jslib-common/models/response/planResponse";
@Component({
selector: 'app-organization-plans',
templateUrl: 'organization-plans.component.html',
selector: "app-organization-plans",
templateUrl: "organization-plans.component.html",
})
export class OrganizationPlansComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@Input() organizationId: string;
@Input() showFree = true;
@Input() showCancel = false;
@Input() acceptingSponsorship = false;
@Input() product: ProductType = ProductType.Free;
@Input() plan: PlanType = PlanType.Free;
@Input() providerId: string;
@Output() onSuccess = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@Input() organizationId: string;
@Input() showFree = true;
@Input() showCancel = false;
@Input() acceptingSponsorship = false;
@Input() product: ProductType = ProductType.Free;
@Input() plan: PlanType = PlanType.Free;
@Input() providerId: string;
@Output() onSuccess = new EventEmitter();
@Output() onCanceled = new EventEmitter();
loading: boolean = true;
selfHosted: boolean = false;
ownedBusiness: boolean = false;
premiumAccessAddon: boolean = false;
additionalStorage: number = 0;
additionalSeats: number = 0;
name: string;
billingEmail: string;
clientOwnerEmail: string;
businessName: string;
productTypes = ProductType;
formPromise: Promise<any>;
singleOrgPolicyBlock: boolean = false;
discount = 0;
loading: boolean = true;
selfHosted: boolean = false;
ownedBusiness: boolean = false;
premiumAccessAddon: boolean = false;
additionalStorage: number = 0;
additionalSeats: number = 0;
name: string;
billingEmail: string;
clientOwnerEmail: string;
businessName: string;
productTypes = ProductType;
formPromise: Promise<any>;
singleOrgPolicyBlock: boolean = false;
discount = 0;
plans: PlanResponse[];
plans: PlanResponse[];
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService, private router: Router, private syncService: SyncService,
private policyService: PolicyService, private organizationService: OrganizationService, private logService: LogService) {
this.selfHosted = platformUtilsService.isSelfHost();
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private router: Router,
private syncService: SyncService,
private policyService: PolicyService,
private organizationService: OrganizationService,
private logService: LogService
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
if (!this.selfHosted) {
const plans = await this.apiService.getPlans();
this.plans = plans.data;
if (this.product === ProductType.Enterprise || this.product === ProductType.Teams) {
this.ownedBusiness = true;
}
}
async ngOnInit() {
if (!this.selfHosted) {
const plans = await this.apiService.getPlans();
this.plans = plans.data;
if (this.product === ProductType.Enterprise || this.product === ProductType.Teams) {
this.ownedBusiness = true;
}
}
if (this.providerId) {
this.ownedBusiness = true;
this.changedOwnedBusiness();
}
this.loading = false;
if (this.providerId) {
this.ownedBusiness = true;
this.changedOwnedBusiness();
}
get createOrganization() {
return this.organizationId == null;
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);
}
get selectedPlan() {
return this.plans.find(plan => plan.type === this.plan);
if (!this.showFree) {
validPlans = validPlans.filter((plan) => plan.product !== ProductType.Free);
}
get selectedPlanInterval() {
return this.selectedPlan.isAnnual
? 'year'
: 'month';
validPlans = validPlans.filter(
(plan) =>
!plan.legacyYear &&
!plan.disabled &&
(plan.isAnnual || plan.product === this.productTypes.Free)
);
if (this.acceptingSponsorship) {
const familyPlan = this.plans.find((plan) => plan.type === PlanType.FamiliesAnnually);
this.discount = familyPlan.basePrice;
validPlans = [familyPlan];
}
get selectableProducts() {
let validPlans = this.plans.filter(plan => plan.type !== PlanType.Custom);
return validPlans;
}
if (this.ownedBusiness) {
validPlans = validPlans.filter(plan => plan.canBeUsedByBusiness);
}
get selectablePlans() {
return this.plans.filter(
(plan) => !plan.legacyYear && !plan.disabled && plan.product === this.product
);
}
if (!this.showFree) {
validPlans = validPlans.filter(plan => plan.product !== ProductType.Free);
}
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.additionalStoragePricePerGb;
}
return selectedPlan.additionalStoragePricePerGb / 12;
}
validPlans = validPlans
.filter(plan => !plan.legacyYear
&& !plan.disabled
&& (plan.isAnnual || plan.product === this.productTypes.Free));
seatPriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.seatPrice;
}
return selectedPlan.seatPrice / 12;
}
if (this.acceptingSponsorship) {
const familyPlan = this.plans.find(plan => plan.type === PlanType.FamiliesAnnually);
this.discount = familyPlan.basePrice;
validPlans = [
familyPlan,
];
}
return validPlans;
additionalStorageTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalStorageOption) {
return 0;
}
get selectablePlans() {
return this.plans.filter(plan => !plan.legacyYear && !plan.disabled && plan.product === this.product);
return plan.additionalStoragePricePerGb * Math.abs(this.additionalStorage || 0);
}
seatTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalSeatsOption) {
return 0;
}
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.additionalStoragePricePerGb;
}
return selectedPlan.additionalStoragePricePerGb / 12;
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 - this.discount;
}
get freeTrial() {
return this.selectedPlan.trialPeriodDays != null;
}
get taxCharges() {
return this.taxComponent != null && this.taxComponent.taxRate != null
? (this.taxComponent.taxRate / 100) * this.subtotal
: 0;
}
get total() {
return this.subtotal + this.taxCharges || 0;
}
get paymentDesc() {
if (this.acceptingSponsorship) {
return this.i18nService.t("paymentSponsored");
} else if (this.freeTrial && this.createOrganization) {
return this.i18nService.t("paymentChargedWithTrial");
} else {
return this.i18nService.t("paymentCharged", this.i18nService.t(this.selectedPlanInterval));
}
}
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.product = ProductType.Teams;
this.plan = PlanType.TeamsAnnually;
}
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() {
this.singleOrgPolicyBlock = await this.userHasBlockingSingleOrgPolicy();
if (this.singleOrgPolicyBlock) {
return;
}
seatPriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.seatPrice;
}
return selectedPlan.seatPrice / 12;
}
try {
const doSubmit = async (): Promise<string> => {
let orgId: string = null;
if (this.createOrganization) {
const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString;
const collection = await this.cryptoService.encrypt(
this.i18nService.t("defaultCollection"),
shareKey[1]
);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(shareKey[1]);
additionalStorageTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalStorageOption) {
return 0;
}
if (this.selfHosted) {
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
} else {
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, shareKey[1]);
}
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 - this.discount;
}
get freeTrial() {
return this.selectedPlan.trialPeriodDays != null;
}
get taxCharges() {
return this.taxComponent != null && this.taxComponent.taxRate != null ?
(this.taxComponent.taxRate / 100) * this.subtotal :
0;
}
get total() {
return (this.subtotal + this.taxCharges) || 0;
}
get paymentDesc() {
if (this.acceptingSponsorship) {
return this.i18nService.t('paymentSponsored');
} else if (this.freeTrial && this.createOrganization) {
return this.i18nService.t('paymentChargedWithTrial');
this.platformUtilsService.showToast(
"success",
this.i18nService.t("organizationCreated"),
this.i18nService.t("organizationReadyToGo")
);
} else {
return this.i18nService.t('paymentCharged', this.i18nService.t(this.selectedPlanInterval));
}
}
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.product = ProductType.Teams;
this.plan = PlanType.TeamsAnnually;
}
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() {
this.singleOrgPolicyBlock = await this.userHasBlockingSingleOrgPolicy();
if (this.singleOrgPolicyBlock) {
return;
orgId = await this.updateOrganization(orgId);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationUpgraded")
);
}
try {
const doSubmit = async (): Promise<string> => {
let orgId: string = null;
if (this.createOrganization) {
const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString;
const collection = await this.cryptoService.encrypt(
this.i18nService.t('defaultCollection'), shareKey[1]);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(shareKey[1]);
if (this.selfHosted) {
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
} else {
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, shareKey[1]);
}
this.platformUtilsService.showToast('success', this.i18nService.t('organizationCreated'), this.i18nService.t('organizationReadyToGo'));
} else {
orgId = await this.updateOrganization(orgId);
this.platformUtilsService.showToast('success', null, this.i18nService.t('organizationUpgraded'));
}
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
if (!this.acceptingSponsorship) {
this.router.navigate(['/organizations/' + orgId]);
}
return orgId;
};
this.formPromise = doSubmit();
const organizationId = await this.formPromise;
this.onSuccess.emit({ organizationId: organizationId });
} catch (e) {
this.logService.error(e);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
if (!this.acceptingSponsorship) {
this.router.navigate(["/organizations/" + orgId]);
}
}
private async userHasBlockingSingleOrgPolicy() {
return this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
}
private async updateOrganization(orgId: string) {
const request = new OrganizationUpgradeRequest();
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon;
request.planType = this.selectedPlan.type;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
// Retrieve org info to backfill pub/priv key if necessary
const org = await this.organizationService.get(this.organizationId);
if (!org.hasPublicAndPrivateKeys) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
}
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
if (!result.success && result.paymentIntentClientSecret != null) {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
}
return this.organizationId;
}
private async createCloudHosted(key: string, collectionCt: string, orgKeys: [string, EncString], orgKey: SymmetricCryptoKey) {
const request = new OrganizationCreateRequest();
request.key = key;
request.collectionName = collectionCt;
request.name = this.name;
request.billingEmail = this.billingEmail;
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
if (this.selectedPlan.type === PlanType.Free) {
request.planType = PlanType.Free;
} else {
const tokenResult = await this.paymentComponent.createPaymentToken();
request.paymentToken = tokenResult[0];
request.paymentMethodType = tokenResult[1];
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
this.premiumAccessAddon;
request.planType = this.selectedPlan.type;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
if (this.taxComponent.taxInfo.includeTaxId) {
request.taxIdNumber = this.taxComponent.taxInfo.taxId;
request.billingAddressLine1 = this.taxComponent.taxInfo.line1;
request.billingAddressLine2 = this.taxComponent.taxInfo.line2;
request.billingAddressCity = this.taxComponent.taxInfo.city;
request.billingAddressState = this.taxComponent.taxInfo.state;
}
}
if (this.providerId) {
const providerRequest = new ProviderOrganizationCreateRequest(this.clientOwnerEmail, request);
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
providerRequest.organizationCreateRequest.key = (await this.cryptoService.encrypt(orgKey.key, providerKey)).encryptedString;
const orgId = (await this.apiService.postProviderCreateOrganization(this.providerId, providerRequest)).organizationId;
return orgId;
} else {
return (await this.apiService.postOrganization(request)).id;
}
}
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
const fileEl = document.getElementById('file') as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
throw new Error(this.i18nService.t('selectFile'));
}
const fd = new FormData();
fd.append('license', files[0]);
fd.append('key', key);
fd.append('collectionName', collectionCt);
const response = await this.apiService.postOrganizationLicense(fd);
const orgId = response.id;
// Org Keys live outside of the OrganizationLicense - add the keys to the org here
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
await this.apiService.postOrganizationKeys(orgId, request);
return orgId;
};
this.formPromise = doSubmit();
const organizationId = await this.formPromise;
this.onSuccess.emit({ organizationId: organizationId });
} catch (e) {
this.logService.error(e);
}
}
private async userHasBlockingSingleOrgPolicy() {
return this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
}
private async updateOrganization(orgId: string) {
const request = new OrganizationUpgradeRequest();
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon =
this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon;
request.planType = this.selectedPlan.type;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
// Retrieve org info to backfill pub/priv key if necessary
const org = await this.organizationService.get(this.organizationId);
if (!org.hasPublicAndPrivateKeys) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
}
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
if (!result.success && result.paymentIntentClientSecret != null) {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
}
return this.organizationId;
}
private async createCloudHosted(
key: string,
collectionCt: string,
orgKeys: [string, EncString],
orgKey: SymmetricCryptoKey
) {
const request = new OrganizationCreateRequest();
request.key = key;
request.collectionName = collectionCt;
request.name = this.name;
request.billingEmail = this.billingEmail;
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
if (this.selectedPlan.type === PlanType.Free) {
request.planType = PlanType.Free;
} else {
const tokenResult = await this.paymentComponent.createPaymentToken();
request.paymentToken = tokenResult[0];
request.paymentMethodType = tokenResult[1];
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon =
this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon;
request.planType = this.selectedPlan.type;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
if (this.taxComponent.taxInfo.includeTaxId) {
request.taxIdNumber = this.taxComponent.taxInfo.taxId;
request.billingAddressLine1 = this.taxComponent.taxInfo.line1;
request.billingAddressLine2 = this.taxComponent.taxInfo.line2;
request.billingAddressCity = this.taxComponent.taxInfo.city;
request.billingAddressState = this.taxComponent.taxInfo.state;
}
}
if (this.providerId) {
const providerRequest = new ProviderOrganizationCreateRequest(this.clientOwnerEmail, request);
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
providerRequest.organizationCreateRequest.key = (
await this.cryptoService.encrypt(orgKey.key, providerKey)
).encryptedString;
const orgId = (
await this.apiService.postProviderCreateOrganization(this.providerId, providerRequest)
).organizationId;
return orgId;
} else {
return (await this.apiService.postOrganization(request)).id;
}
}
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
const fileEl = document.getElementById("file") as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
throw new Error(this.i18nService.t("selectFile"));
}
const fd = new FormData();
fd.append("license", files[0]);
fd.append("key", key);
fd.append("collectionName", collectionCt);
const response = await this.apiService.postOrganizationLicense(fd);
const orgId = response.id;
// Org Keys live outside of the OrganizationLicense - add the keys to the org here
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
await this.apiService.postOrganizationKeys(orgId, request);
return orgId;
}
}

View File

@@ -1,114 +1,155 @@
<ng-container *ngIf="vault">
<p *ngIf="!loaded" class="text-muted">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</p>
<ng-container *ngIf="loaded">
<ul class="fa-ul card-ul carets" *ngIf="organizations && organizations.length">
<li *ngFor="let o of organizations">
<a [routerLink]="['/organizations', o.id]" class="text-body">
<i class="fa-li fa fa-caret-right" aria-hidden="true"></i> {{o.name}}
<ng-container *ngIf="!o.enabled">
<i class="fa fa-exclamation-triangle text-danger" title="{{'organizationIsDisabled' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'organizationIsDisabled' | i18n}}</span>
</ng-container>
</a>
</li>
</ul>
<p *ngIf="!organizations || !organizations.length">{{'noOrganizationsList' | i18n}}</p>
</ng-container>
<a href="#" routerLink="/settings/create-organization" class="btn btn-block btn-outline-primary">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'newOrganization' | i18n}}
</a>
<p *ngIf="!loaded" class="text-muted">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
<ng-container *ngIf="loaded">
<ul class="fa-ul card-ul carets" *ngIf="organizations && organizations.length">
<li *ngFor="let o of organizations">
<a [routerLink]="['/organizations', o.id]" class="text-body">
<i class="fa-li fa fa-caret-right" aria-hidden="true"></i> {{ o.name }}
<ng-container *ngIf="!o.enabled">
<i
class="fa fa-exclamation-triangle text-danger"
title="{{ 'organizationIsDisabled' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "organizationIsDisabled" | i18n }}</span>
</ng-container>
</a>
</li>
</ul>
<p *ngIf="!organizations || !organizations.length">{{ "noOrganizationsList" | i18n }}</p>
</ng-container>
<a href="#" routerLink="/settings/create-organization" class="btn btn-block btn-outline-primary">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{ "newOrganization" | i18n }}
</a>
</ng-container>
<ng-container *ngIf="!vault">
<div class="page-header d-flex">
<h1>
{{'organizations' | i18n}}
<small [appApiAction]="actionPromise" #action>
<ng-container *ngIf="action.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>
<a href="#" routerLink="/settings/create-organization" class="btn btn-sm btn-outline-primary ml-auto"
*ngIf="!loaded || (organizations && organizations.length)">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'newOrganization' | i18n}}
</a>
</div>
<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">
<ng-container *ngIf="!organizations || !organizations.length">
<p>{{'noOrganizationsList' | i18n}}</p>
<a href="#" routerLink="/settings/create-organization" class="btn btn-outline-primary">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'newOrganization' | i18n}}
</a>
<div class="page-header d-flex">
<h1>
{{ "organizations" | i18n }}
<small [appApiAction]="actionPromise" #action>
<ng-container *ngIf="action.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>
<table class="table table-hover table-list" *ngIf="organizations && organizations.length">
<tbody>
<tr *ngFor="let o of organizations">
<td width="30">
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
<a href="#" [routerLink]="['/organizations', o.id]">{{o.name}}</a>
<ng-container *ngIf="!o.enabled">
<i class="fa fa-exclamation-triangle text-danger"
title="{{'organizationIsDisabled' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'organizationIsDisabled' | i18n}}</span>
</ng-container>
<ng-container *ngIf="showEnrolledStatus(o)">
<i class="fa fa-key" appStopProp title="{{'enrolledPasswordReset' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'enrolledPasswordReset' | i18n}}</span>
</ng-container>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a *ngIf="allowEnrollmentChanges(o) && !o.resetPasswordEnrolled" class="dropdown-item"
href="#" appStopClick (click)="toggleResetPasswordEnrollment(o)">
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
{{'enrollPasswordReset' | i18n}}
</a>
<a *ngIf="allowEnrollmentChanges(o) && o.resetPasswordEnrolled" class="dropdown-item"
href="#" appStopClick (click)="toggleResetPasswordEnrollment(o)">
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{'withdrawPasswordReset' | i18n}}
</a>
<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}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</small>
</h1>
<a
href="#"
routerLink="/settings/create-organization"
class="btn btn-sm btn-outline-primary ml-auto"
*ngIf="!loaded || (organizations && organizations.length)"
>
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{ "newOrganization" | i18n }}
</a>
</div>
<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">
<ng-container *ngIf="!organizations || !organizations.length">
<p>{{ "noOrganizationsList" | i18n }}</p>
<a href="#" routerLink="/settings/create-organization" class="btn btn-outline-primary">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{ "newOrganization" | i18n }}
</a>
</ng-container>
<table class="table table-hover table-list" *ngIf="organizations && organizations.length">
<tbody>
<tr *ngFor="let o of organizations">
<td width="30">
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
<a href="#" [routerLink]="['/organizations', o.id]">{{ o.name }}</a>
<ng-container *ngIf="!o.enabled">
<i
class="fa fa-exclamation-triangle text-danger"
title="{{ 'organizationIsDisabled' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "organizationIsDisabled" | i18n }}</span>
</ng-container>
<ng-container *ngIf="showEnrolledStatus(o)">
<i
class="fa fa-key"
appStopProp
title="{{ 'enrolledPasswordReset' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "enrolledPasswordReset" | i18n }}</span>
</ng-container>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a
*ngIf="allowEnrollmentChanges(o) && !o.resetPasswordEnrolled"
class="dropdown-item"
href="#"
appStopClick
(click)="toggleResetPasswordEnrollment(o)"
>
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
{{ "enrollPasswordReset" | i18n }}
</a>
<a
*ngIf="allowEnrollmentChanges(o) && o.resetPasswordEnrolled"
class="dropdown-item"
href="#"
appStopClick
(click)="toggleResetPasswordEnrollment(o)"
>
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{ "withdrawPasswordReset" | i18n }}
</a>
<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 }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>

View File

@@ -1,169 +1,192 @@
import {
Component,
Input,
OnInit,
} from '@angular/core';
import { Component, Input, OnInit } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { Organization } from 'jslib-common/models/domain/organization';
import { Policy } from 'jslib-common/models/domain/policy';
import { Organization } from "jslib-common/models/domain/organization";
import { Policy } from "jslib-common/models/domain/policy";
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest';
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
import { Utils } from 'jslib-common/misc/utils';
import { Utils } from "jslib-common/misc/utils";
import { PolicyType } from 'jslib-common/enums/policyType';
import { PolicyType } from "jslib-common/enums/policyType";
@Component({
selector: 'app-organizations',
templateUrl: 'organizations.component.html',
selector: "app-organizations",
templateUrl: "organizations.component.html",
})
export class OrganizationsComponent implements OnInit {
@Input() vault = false;
@Input() vault = false;
organizations: Organization[];
policies: Policy[];
loaded: boolean = false;
actionPromise: Promise<any>;
organizations: Organization[];
policies: Policy[];
loaded: boolean = false;
actionPromise: Promise<any>;
constructor(private organizationService: OrganizationService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private apiService: ApiService,
private syncService: SyncService,
private cryptoService: CryptoService, private policyService: PolicyService,
private logService: LogService) { }
constructor(
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private apiService: ApiService,
private syncService: SyncService,
private cryptoService: CryptoService,
private policyService: PolicyService,
private logService: LogService
) {}
async ngOnInit() {
if (!this.vault) {
await this.syncService.fullSync(true);
await this.load();
}
async ngOnInit() {
if (!this.vault) {
await this.syncService.fullSync(true);
await this.load();
}
}
async load() {
const orgs = await this.organizationService.getAll();
orgs.sort(Utils.getSortFunction(this.i18nService, "name"));
this.organizations = orgs;
this.policies = await this.policyService.getAll(PolicyType.ResetPassword);
this.loaded = true;
}
allowEnrollmentChanges(org: Organization): boolean {
if (org.usePolicies && org.useResetPassword && org.hasPublicAndPrivateKeys) {
const policy = this.policies.find((p) => p.organizationId === org.id);
if (policy != null && policy.enabled) {
return org.resetPasswordEnrolled && policy.data.autoEnrollEnabled ? false : true;
}
}
async load() {
const orgs = await this.organizationService.getAll();
orgs.sort(Utils.getSortFunction(this.i18nService, 'name'));
this.organizations = orgs;
this.policies = await this.policyService.getAll(PolicyType.ResetPassword);
this.loaded = true;
return false;
}
showEnrolledStatus(org: Organization): boolean {
return (
org.useResetPassword &&
org.resetPasswordEnrolled &&
this.policies.some((p) => p.organizationId === org.id && p.enabled)
);
}
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;
}
allowEnrollmentChanges(org: Organization): boolean {
if (org.usePolicies && org.useResetPassword && org.hasPublicAndPrivateKeys) {
const policy = this.policies.find(p => p.organizationId === org.id);
if (policy != null && policy.enabled) {
return org.resetPasswordEnrolled && policy.data.autoEnrollEnabled ? false : true;
}
}
try {
this.actionPromise = this.apiService.deleteSsoUser(org.id).then(() => {
return this.syncService.fullSync(true);
});
await this.actionPromise;
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
await this.load();
} catch (e) {
this.logService.error(e);
}
}
return false;
async leave(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("leaveOrganizationConfirmation"),
org.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
showEnrolledStatus(org: Organization): boolean {
return org.useResetPassword && org.resetPasswordEnrolled && this.policies.some(p => p.organizationId === org.id && p.enabled);
try {
this.actionPromise = this.apiService.postLeaveOrganization(org.id).then(() => {
return this.syncService.fullSync(true);
});
await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
await this.load();
} catch (e) {
this.logService.error(e);
}
}
async toggleResetPasswordEnrollment(org: Organization) {
// Set variables
let keyString: string = null;
let toastStringRef = "withdrawPasswordResetSuccess";
// Enrolling
if (!org.resetPasswordEnrolled) {
// Alert user about enrollment
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("resetPasswordEnrollmentWarning"),
null,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return;
}
// Retrieve Public Key
this.actionPromise = this.apiService
.getOrganizationKeys(org.id)
.then(async (response) => {
if (response == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user's encKey.key with organization public key
const encKey = await this.cryptoService.getEncKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
keyString = encryptedKey.encryptedString;
toastStringRef = "enrollPasswordResetSuccess";
// Create request and execute enrollment
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = keyString;
return this.apiService.putOrganizationUserResetPasswordEnrollment(
org.id,
org.userId,
request
);
})
.then(() => {
return this.syncService.fullSync(true);
});
} else {
// Withdrawal
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = keyString;
this.actionPromise = this.apiService
.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request)
.then(() => {
return this.syncService.fullSync(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.platformUtilsService.showToast('success', null, 'Unlinked SSO');
await this.load();
} catch (e) {
this.logService.error(e);
}
}
async leave(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('leaveOrganizationConfirmation'), org.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.postLeaveOrganization(org.id).then(() => {
return this.syncService.fullSync(true);
});
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('leftOrganization'));
await this.load();
} catch (e) {
this.logService.error(e);
}
}
async toggleResetPasswordEnrollment(org: Organization) {
// Set variables
let keyString: string = null;
let toastStringRef = 'withdrawPasswordResetSuccess';
// Enrolling
if (!org.resetPasswordEnrolled) {
// Alert user about enrollment
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('resetPasswordEnrollmentWarning'), null,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
// Retrieve Public Key
this.actionPromise = this.apiService.getOrganizationKeys(org.id)
.then(async response => {
if (response == null) {
throw new Error(this.i18nService.t('resetPasswordOrgKeysError'));
}
const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user's encKey.key with organization public key
const encKey = await this.cryptoService.getEncKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
keyString = encryptedKey.encryptedString;
toastStringRef = 'enrollPasswordResetSuccess';
// Create request and execute enrollment
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = keyString;
return this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request);
})
.then(() => {
return this.syncService.fullSync(true);
});
} else {
// Withdrawal
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = keyString;
this.actionPromise = this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request)
.then(() => {
return this.syncService.fullSync(true);
});
}
try {
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t(toastStringRef));
await this.load();
} catch (e) {
this.logService.error(e);
}
try {
await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef));
await this.load();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,96 +1,163 @@
<div class="mb-4 text-lg" *ngIf="showOptions && showMethods">
<div class="form-check form-check-inline mr-4">
<input class="form-check-input" type="radio" name="Method" id="method-card" [value]="paymentMethodType.Card"
[(ngModel)]="method" (change)="changeMethod()">
<label class="form-check-label" for="method-card">
<i class="fa fa-fw fa-credit-card" aria-hidden="true"></i> {{'creditCard' | i18n}}</label>
</div>
<div class="form-check form-check-inline mr-4" *ngIf="!hideBank">
<input class="form-check-input" type="radio" name="Method" id="method-bank"
[value]="paymentMethodType.BankAccount" [(ngModel)]="method" (change)="changeMethod()">
<label class="form-check-label" for="method-bank">
<i class="fa fa-fw fa-university" aria-hidden="true"></i> {{'bankAccount' | i18n}}</label>
</div>
<div class="form-check form-check-inline" *ngIf="!hidePaypal">
<input class="form-check-input" type="radio" name="Method" id="method-paypal" [value]="paymentMethodType.PayPal"
[(ngModel)]="method" (change)="changeMethod()">
<label class="form-check-label" for="method-paypal">
<i class="fa fa-fw fa-paypal" aria-hidden="true"></i> PayPal</label>
</div>
<div class="form-check form-check-inline" *ngIf="!hideCredit">
<input class="form-check-input" type="radio" name="Method" id="method-credit" [value]="paymentMethodType.Credit"
[(ngModel)]="method" (change)="changeMethod()">
<label class="form-check-label" for="method-credit">
<i class="fa fa-fw fa-dollar" aria-hidden="true"></i> {{'accountCredit' | i18n}}</label>
</div>
<div class="form-check form-check-inline mr-4">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-card"
[value]="paymentMethodType.Card"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-card">
<i class="fa fa-fw fa-credit-card" aria-hidden="true"></i> {{ "creditCard" | i18n }}</label
>
</div>
<div class="form-check form-check-inline mr-4" *ngIf="!hideBank">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-bank"
[value]="paymentMethodType.BankAccount"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-bank">
<i class="fa fa-fw fa-university" aria-hidden="true"></i> {{ "bankAccount" | i18n }}</label
>
</div>
<div class="form-check form-check-inline" *ngIf="!hidePaypal">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-paypal"
[value]="paymentMethodType.PayPal"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-paypal">
<i class="fa fa-fw fa-paypal" aria-hidden="true"></i> PayPal</label
>
</div>
<div class="form-check form-check-inline" *ngIf="!hideCredit">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-credit"
[value]="paymentMethodType.Credit"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-credit">
<i class="fa fa-fw fa-dollar" aria-hidden="true"></i> {{ "accountCredit" | i18n }}</label
>
</div>
</div>
<ng-container *ngIf="showMethods && method === paymentMethodType.Card">
<div class="row">
<div class="form-group col-4">
<label for="stripe-card-number-element">{{'number' | i18n}}</label>
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-8 d-flex align-items-end">
<img src="../../images/cards.png" alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
width="323" height="32">
</div>
<div class="form-group col-4">
<label for="stripe-card-expiry-element">{{'expiration' | i18n}}</label>
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-4">
<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>
</div>
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
</div>
<div class="row">
<div class="form-group col-4">
<label for="stripe-card-number-element">{{ "number" | i18n }}</label>
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-8 d-flex align-items-end">
<img
src="../../images/cards.png"
alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
width="323"
height="32"
/>
</div>
<div class="form-group col-4">
<label for="stripe-card-expiry-element">{{ "expiration" | i18n }}</label>
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-4">
<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>
</div>
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
</div>
</div>
</ng-container>
<ng-container *ngIf="showMethods && method === paymentMethodType.BankAccount">
<app-callout type="warning" title="{{'verifyBankAccount' | i18n}}">
{{'verifyBankAccountInitialDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}
</app-callout>
<div class="row">
<div class="form-group col-6">
<label for="routing_number">{{'routingNumber' | i18n}}</label>
<input id="routing_number" class="form-control" type="text" name="routing_number"
[(ngModel)]="bank.routing_number" required appInputVerbatim>
</div>
<div class="form-group col-6">
<label for="account_number">{{'accountNumber' | i18n}}</label>
<input id="account_number" class="form-control" type="text" name="account_number"
[(ngModel)]="bank.account_number" required appInputVerbatim>
</div>
<div class="form-group col-6">
<label for="account_holder_name">{{'accountHolderName' | i18n}}</label>
<input id="account_holder_name" class="form-control" type="text" name="account_holder_name"
[(ngModel)]="bank.account_holder_name" required>
</div>
<div class="form-group col-6">
<label for="account_holder_type">{{'bankAccountType' | i18n}}</label>
<select id="account_holder_type" class="form-control" name="account_holder_type"
[(ngModel)]="bank.account_holder_type" required>
<option value="">-- {{'select' | i18n}} --</option>
<option value="company">{{'bankAccountTypeCompany' | i18n}}</option>
<option value="individual">{{'bankAccountTypeIndividual' | i18n}}</option>
</select>
</div>
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
{{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}
</app-callout>
<div class="row">
<div class="form-group col-6">
<label for="routing_number">{{ "routingNumber" | i18n }}</label>
<input
id="routing_number"
class="form-control"
type="text"
name="routing_number"
[(ngModel)]="bank.routing_number"
required
appInputVerbatim
/>
</div>
<div class="form-group col-6">
<label for="account_number">{{ "accountNumber" | i18n }}</label>
<input
id="account_number"
class="form-control"
type="text"
name="account_number"
[(ngModel)]="bank.account_number"
required
appInputVerbatim
/>
</div>
<div class="form-group col-6">
<label for="account_holder_name">{{ "accountHolderName" | i18n }}</label>
<input
id="account_holder_name"
class="form-control"
type="text"
name="account_holder_name"
[(ngModel)]="bank.account_holder_name"
required
/>
</div>
<div class="form-group col-6">
<label for="account_holder_type">{{ "bankAccountType" | i18n }}</label>
<select
id="account_holder_type"
class="form-control"
name="account_holder_type"
[(ngModel)]="bank.account_holder_type"
required
>
<option value="">-- {{ "select" | i18n }} --</option>
<option value="company">{{ "bankAccountTypeCompany" | i18n }}</option>
<option value="individual">{{ "bankAccountTypeIndividual" | i18n }}</option>
</select>
</div>
</div>
</ng-container>
<ng-container *ngIf="showMethods && method === paymentMethodType.PayPal">
<div class="mb-3">
<div id="bt-dropin-container" class="mb-1"></div>
<small class="text-muted">{{'paypalClickSubmit' | i18n}}</small>
</div>
<div class="mb-3">
<div id="bt-dropin-container" class="mb-1"></div>
<small class="text-muted">{{ "paypalClickSubmit" | i18n }}</small>
</div>
</ng-container>
<ng-container *ngIf="showMethods && method === paymentMethodType.Credit">
<app-callout type="note">
{{'makeSureEnoughCredit' | i18n}}
</app-callout>
<app-callout type="note">
{{ "makeSureEnoughCredit" | i18n }}
</app-callout>
</ng-container>

View File

@@ -1,18 +1,14 @@
import {
Component,
Input,
OnInit,
} from '@angular/core';
import { Component, Input, OnInit } from "@angular/core";
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
import { PaymentMethodType } from "jslib-common/enums/paymentMethodType";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ThemeType } from 'jslib-common/enums/themeType';
import { ThemeType } from "jslib-common/enums/themeType";
import ThemeVariables from 'src/scss/export.module.scss';
import ThemeVariables from "src/scss/export.module.scss";
const lightInputColor = ThemeVariables.lightInputColor;
const lightInputPlaceholderColor = ThemeVariables.lightInputPlaceholderColor;
@@ -20,260 +16,276 @@ const darkInputColor = ThemeVariables.darkInputColor;
const darkInputPlaceholderColor = ThemeVariables.darkInputPlaceholderColor;
@Component({
selector: 'app-payment',
templateUrl: 'payment.component.html',
selector: "app-payment",
templateUrl: "payment.component.html",
})
export class PaymentComponent implements OnInit {
@Input() showMethods = true;
@Input() showOptions = true;
@Input() method = PaymentMethodType.Card;
@Input() hideBank = false;
@Input() hidePaypal = false;
@Input() hideCredit = false;
@Input() showMethods = true;
@Input() showOptions = true;
@Input() method = PaymentMethodType.Card;
@Input() hideBank = false;
@Input() hidePaypal = false;
@Input() hideCredit = false;
bank: any = {
routing_number: null,
account_number: null,
account_holder_name: null,
account_holder_type: '',
currency: 'USD',
country: 'US',
bank: any = {
routing_number: null,
account_number: null,
account_holder_name: null,
account_holder_type: "",
currency: "USD",
country: "US",
};
paymentMethodType = PaymentMethodType;
private btScript: HTMLScriptElement;
private btInstance: any = null;
private stripeScript: HTMLScriptElement;
private stripe: any = null;
private stripeElements: any = null;
private stripeCardNumberElement: any = null;
private stripeCardExpiryElement: any = null;
private stripeCardCvcElement: any = null;
private StripeElementStyle: any;
private StripeElementClasses: any;
constructor(
private platformUtilsService: PlatformUtilsService,
private apiService: ApiService,
private logService: LogService
) {
this.stripeScript = window.document.createElement("script");
this.stripeScript.src = "https://js.stripe.com/v3/";
this.stripeScript.async = true;
this.stripeScript.onload = () => {
this.stripe = (window as any).Stripe(process.env.STRIPE_KEY);
this.stripeElements = this.stripe.elements();
this.setStripeElement();
};
this.btScript = window.document.createElement("script");
this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`;
this.btScript.async = true;
this.StripeElementStyle = {
base: {
color: null,
fontFamily:
'"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
fontSize: "14px",
fontSmoothing: "antialiased",
"::placeholder": {
color: null,
},
},
invalid: {
color: null,
},
};
this.StripeElementClasses = {
focus: "is-focused",
empty: "is-empty",
invalid: "is-invalid",
};
}
paymentMethodType = PaymentMethodType;
private btScript: HTMLScriptElement;
private btInstance: any = null;
private stripeScript: HTMLScriptElement;
private stripe: any = null;
private stripeElements: any = null;
private stripeCardNumberElement: any = null;
private stripeCardExpiryElement: any = null;
private stripeCardCvcElement: any = null;
private StripeElementStyle: any;
private StripeElementClasses: any;
constructor(private platformUtilsService: PlatformUtilsService, private apiService: ApiService,
private logService: LogService) {
this.stripeScript = window.document.createElement('script');
this.stripeScript.src = 'https://js.stripe.com/v3/';
this.stripeScript.async = true;
this.stripeScript.onload = () => {
this.stripe = (window as any).Stripe(process.env.STRIPE_KEY);
this.stripeElements = this.stripe.elements();
this.setStripeElement();
};
this.btScript = window.document.createElement('script');
this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`;
this.btScript.async = true;
this.StripeElementStyle = {
base: {
color: null,
fontFamily: '"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
fontSize: '14px',
fontSmoothing: 'antialiased',
'::placeholder': {
color: null,
},
},
invalid: {
color: null,
},
};
this.StripeElementClasses = {
focus: 'is-focused',
empty: 'is-empty',
invalid: 'is-invalid',
};
async ngOnInit() {
if (!this.showOptions) {
this.hidePaypal = this.method !== PaymentMethodType.PayPal;
this.hideBank = this.method !== PaymentMethodType.BankAccount;
this.hideCredit = this.method !== PaymentMethodType.Credit;
}
async ngOnInit() {
if (!this.showOptions) {
this.hidePaypal = this.method !== PaymentMethodType.PayPal;
this.hideBank = this.method !== PaymentMethodType.BankAccount;
this.hideCredit = this.method !== PaymentMethodType.Credit;
}
await this.setTheme();
window.document.head.appendChild(this.stripeScript);
if (!this.hidePaypal) {
window.document.head.appendChild(this.btScript);
}
await this.setTheme();
window.document.head.appendChild(this.stripeScript);
if (!this.hidePaypal) {
window.document.head.appendChild(this.btScript);
}
}
ngOnDestroy() {
window.document.head.removeChild(this.stripeScript);
window.setTimeout(() => {
Array.from(window.document.querySelectorAll('iframe')).forEach(el => {
if (el.src != null && el.src.indexOf('stripe') > -1) {
try {
window.document.body.removeChild(el);
} catch (e) {
this.logService.error(e);
}
}
});
}, 500);
if (!this.hidePaypal) {
window.document.head.removeChild(this.btScript);
window.setTimeout(() => {
Array.from(window.document.head.querySelectorAll('script')).forEach(el => {
if (el.src != null && el.src.indexOf('paypal') > -1) {
try {
window.document.head.removeChild(el);
} catch (e) {
this.logService.error(e);
}
}
});
const btStylesheet = window.document.head.querySelector('#braintree-dropin-stylesheet');
if (btStylesheet != null) {
try {
window.document.head.removeChild(btStylesheet);
} catch (e) {
this.logService.error(e);
}
}
}, 500);
ngOnDestroy() {
window.document.head.removeChild(this.stripeScript);
window.setTimeout(() => {
Array.from(window.document.querySelectorAll("iframe")).forEach((el) => {
if (el.src != null && el.src.indexOf("stripe") > -1) {
try {
window.document.body.removeChild(el);
} catch (e) {
this.logService.error(e);
}
}
}
changeMethod() {
this.btInstance = null;
if (this.method === PaymentMethodType.PayPal) {
window.setTimeout(() => {
(window as any).braintree.dropin.create({
authorization: process.env.BRAINTREE_KEY,
container: '#bt-dropin-container',
paymentOptionPriority: ['paypal'],
paypal: {
flow: 'vault',
buttonStyle: {
label: 'pay',
size: 'medium',
shape: 'pill',
color: 'blue',
tagline: 'false',
},
},
}, (createErr: any, instance: any) => {
if (createErr != null) {
// tslint:disable-next-line
console.error(createErr);
return;
}
this.btInstance = instance;
});
}, 250);
} else {
this.setStripeElement();
}
}
createPaymentToken(): Promise<[string, PaymentMethodType]> {
return new Promise((resolve, reject) => {
if (this.method === PaymentMethodType.Credit) {
resolve([null, this.method]);
} else if (this.method === PaymentMethodType.PayPal) {
this.btInstance.requestPaymentMethod().then((payload: any) => {
resolve([payload.nonce, this.method]);
}).catch((err: any) => {
reject(err.message);
});
} else if (this.method === PaymentMethodType.Card || this.method === PaymentMethodType.BankAccount) {
if (this.method === PaymentMethodType.Card) {
this.apiService.postSetupPayment().then(clientSecret =>
this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement))
.then((result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.setupIntent && result.setupIntent.status === 'succeeded') {
resolve([result.setupIntent.payment_method, this.method]);
} else {
reject();
}
});
} else {
this.stripe.createToken('bank_account', this.bank).then((result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.token && result.token.id != null) {
resolve([result.token.id, this.method]);
} else {
reject();
}
});
}
});
}, 500);
if (!this.hidePaypal) {
window.document.head.removeChild(this.btScript);
window.setTimeout(() => {
Array.from(window.document.head.querySelectorAll("script")).forEach((el) => {
if (el.src != null && el.src.indexOf("paypal") > -1) {
try {
window.document.head.removeChild(el);
} catch (e) {
this.logService.error(e);
}
}
});
const btStylesheet = window.document.head.querySelector("#braintree-dropin-stylesheet");
if (btStylesheet != null) {
try {
window.document.head.removeChild(btStylesheet);
} catch (e) {
this.logService.error(e);
}
}
}, 500);
}
}
handleStripeCardPayment(clientSecret: string, successCallback: () => Promise<any>): Promise<any> {
return new Promise<void>((resolve, reject) => {
if (this.showMethods && this.stripeCardNumberElement == null) {
changeMethod() {
this.btInstance = null;
if (this.method === PaymentMethodType.PayPal) {
window.setTimeout(() => {
(window as any).braintree.dropin.create(
{
authorization: process.env.BRAINTREE_KEY,
container: "#bt-dropin-container",
paymentOptionPriority: ["paypal"],
paypal: {
flow: "vault",
buttonStyle: {
label: "pay",
size: "medium",
shape: "pill",
color: "blue",
tagline: "false",
},
},
},
(createErr: any, instance: any) => {
if (createErr != null) {
// tslint:disable-next-line
console.error(createErr);
return;
}
this.btInstance = instance;
}
);
}, 250);
} else {
this.setStripeElement();
}
}
createPaymentToken(): Promise<[string, PaymentMethodType]> {
return new Promise((resolve, reject) => {
if (this.method === PaymentMethodType.Credit) {
resolve([null, this.method]);
} else if (this.method === PaymentMethodType.PayPal) {
this.btInstance
.requestPaymentMethod()
.then((payload: any) => {
resolve([payload.nonce, this.method]);
})
.catch((err: any) => {
reject(err.message);
});
} else if (
this.method === PaymentMethodType.Card ||
this.method === PaymentMethodType.BankAccount
) {
if (this.method === PaymentMethodType.Card) {
this.apiService
.postSetupPayment()
.then((clientSecret) =>
this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement)
)
.then((result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.setupIntent && result.setupIntent.status === "succeeded") {
resolve([result.setupIntent.payment_method, this.method]);
} else {
reject();
return;
}
const handleCardPayment = () => this.showMethods ?
this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement) :
this.stripe.handleCardSetup(clientSecret);
return handleCardPayment().then(async (result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.paymentIntent && result.paymentIntent.status === 'succeeded') {
if (successCallback != null) {
await successCallback();
}
resolve();
} else {
reject();
}
}
});
});
}
private setStripeElement() {
window.setTimeout(() => {
if (this.showMethods && this.method === PaymentMethodType.Card) {
if (this.stripeCardNumberElement == null) {
this.stripeCardNumberElement = this.stripeElements.create('cardNumber', {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
placeholder: '',
});
}
if (this.stripeCardExpiryElement == null) {
this.stripeCardExpiryElement = this.stripeElements.create('cardExpiry', {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
});
}
if (this.stripeCardCvcElement == null) {
this.stripeCardCvcElement = this.stripeElements.create('cardCvc', {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
placeholder: '',
});
}
this.stripeCardNumberElement.mount('#stripe-card-number-element');
this.stripeCardExpiryElement.mount('#stripe-card-expiry-element');
this.stripeCardCvcElement.mount('#stripe-card-cvc-element');
}
}, 50);
}
private async setTheme() {
const theme = await this.platformUtilsService.getEffectiveTheme();
if (theme === ThemeType.Dark) {
this.StripeElementStyle.base.color = darkInputColor;
this.StripeElementStyle.base['::placeholder'].color = darkInputPlaceholderColor;
this.StripeElementStyle.invalid.color = darkInputColor;
} else {
this.StripeElementStyle.base.color = lightInputColor;
this.StripeElementStyle.base['::placeholder'].color = lightInputPlaceholderColor;
this.StripeElementStyle.invalid.color = lightInputColor;
this.stripe.createToken("bank_account", this.bank).then((result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.token && result.token.id != null) {
resolve([result.token.id, this.method]);
} else {
reject();
}
});
}
}
});
}
handleStripeCardPayment(clientSecret: string, successCallback: () => Promise<any>): Promise<any> {
return new Promise<void>((resolve, reject) => {
if (this.showMethods && this.stripeCardNumberElement == null) {
reject();
return;
}
const handleCardPayment = () =>
this.showMethods
? this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement)
: this.stripe.handleCardSetup(clientSecret);
return handleCardPayment().then(async (result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.paymentIntent && result.paymentIntent.status === "succeeded") {
if (successCallback != null) {
await successCallback();
}
resolve();
} else {
reject();
}
});
});
}
private setStripeElement() {
window.setTimeout(() => {
if (this.showMethods && this.method === PaymentMethodType.Card) {
if (this.stripeCardNumberElement == null) {
this.stripeCardNumberElement = this.stripeElements.create("cardNumber", {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
placeholder: "",
});
}
if (this.stripeCardExpiryElement == null) {
this.stripeCardExpiryElement = this.stripeElements.create("cardExpiry", {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
});
}
if (this.stripeCardCvcElement == null) {
this.stripeCardCvcElement = this.stripeElements.create("cardCvc", {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
placeholder: "",
});
}
this.stripeCardNumberElement.mount("#stripe-card-number-element");
this.stripeCardExpiryElement.mount("#stripe-card-expiry-element");
this.stripeCardCvcElement.mount("#stripe-card-cvc-element");
}
}, 50);
}
private async setTheme() {
const theme = await this.platformUtilsService.getEffectiveTheme();
if (theme === ThemeType.Dark) {
this.StripeElementStyle.base.color = darkInputColor;
this.StripeElementStyle.base["::placeholder"].color = darkInputPlaceholderColor;
this.StripeElementStyle.invalid.color = darkInputColor;
} else {
this.StripeElementStyle.base.color = lightInputColor;
this.StripeElementStyle.base["::placeholder"].color = lightInputPlaceholderColor;
this.StripeElementStyle.invalid.color = lightInputColor;
}
}
}

View File

@@ -1,96 +1,122 @@
<div class="page-header">
<h1>{{'goPremium' | i18n}}</h1>
<h1>{{ "goPremium" | i18n }}</h1>
</div>
<app-callout type="info" *ngIf="canAccessPremium" title="{{'youHavePremiumAccess' | i18n}}" icon="fa-star">
{{'alreadyPremiumFromOrg' | i18n}}
<app-callout
type="info"
*ngIf="canAccessPremium"
title="{{ 'youHavePremiumAccess' | i18n }}"
icon="fa-star"
>
{{ "alreadyPremiumFromOrg" | i18n }}
</app-callout>
<app-callout type="success">
<p>{{'premiumUpgradeUnlockFeatures' | i18n}}</p>
<ul class="fa-ul">
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{'premiumSignUpStorage' | i18n}}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{'premiumSignUpTwoStep' | i18n}}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{'premiumSignUpEmergency' |i18n}}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{'premiumSignUpReports' | i18n}}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{'premiumSignUpTotp' | i18n}}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{'premiumSignUpSupport' | i18n}}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{'premiumSignUpFuture' | i18n}}
</li>
</ul>
<p class="text-lg" [ngClass]="{'mb-0':!selfHosted}">{{'premiumPrice' | i18n : (premiumPrice | currency:'$')}}</p>
<a href="https://vault.bitwarden.com/#/settings/premium" target="_blank" rel="noopener"
class="btn btn-outline-secondary" *ngIf="selfHosted">
{{'purchasePremium' | i18n}}
</a>
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<ul class="fa-ul">
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{ "premiumSignUpStorage" | i18n }}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{ "premiumSignUpTwoStep" | i18n }}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{ "premiumSignUpEmergency" | i18n }}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{ "premiumSignUpReports" | i18n }}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{ "premiumSignUpTotp" | i18n }}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{ "premiumSignUpSupport" | i18n }}
</li>
<li>
<i class="fa fa-check text-success fa-li" aria-hidden="true"></i>
{{ "premiumSignUpFuture" | i18n }}
</li>
</ul>
<p class="text-lg" [ngClass]="{ 'mb-0': !selfHosted }">
{{ "premiumPrice" | i18n: (premiumPrice | currency: "$") }}
</p>
<a
href="https://vault.bitwarden.com/#/settings/premium"
target="_blank"
rel="noopener"
class="btn btn-outline-secondary"
*ngIf="selfHosted"
>
{{ "purchasePremium" | i18n }}
</a>
</app-callout>
<ng-container *ngIf="selfHosted">
<p>{{'uploadLicenseFilePremium' | i18n}}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="form-group">
<label for="file">{{'licenseFile' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required>
<small class="form-text text-muted">{{'licenseFileDesc' | i18n : 'bitwarden_premium_license.json'}}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
</form>
<p>{{ "uploadLicenseFilePremium" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="form-group">
<label for="file">{{ "licenseFile" | i18n }}</label>
<input type="file" id="file" class="form-control-file" name="file" required />
<small class="form-text text-muted">{{
"licenseFileDesc" | i18n: "bitwarden_premium_license.json"
}}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span>
</button>
</form>
</ng-container>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
<h2 class="mt-5">{{'addons' | i18n}}</h2>
<div class="row">
<div class="form-group col-6">
<label for="additionalStorage">{{'additionalStorageGb' | i18n}}</label>
<input id="additionalStorage" class="form-control" type="number" name="AdditionalStorageGb"
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
placeholder="{{'additionalStorageGbDesc' | i18n}}">
<small
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$') : ('year' | i18n)}}</small>
</div>
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
<div class="row">
<div class="form-group col-6">
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
<input
id="additionalStorage"
class="form-control"
type="number"
name="AdditionalStorageGb"
[(ngModel)]="additionalStorage"
min="0"
max="99"
step="1"
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
/>
<small class="text-muted form-text">{{
"additionalStorageIntervalDesc"
| i18n: "1 GB":(storageGbPrice | currency: "$"):("year" | i18n)
}}</small>
</div>
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
{{'premiumMembership' | i18n}}: {{premiumPrice | currency:'$'}}
<br> {{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} GB &times; {{storageGbPrice | currency:'$'}} = {{additionalStorageTotal
| currency:'$'}}
<hr class="my-3">
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
<app-payment [hideBank]="true"></app-payment>
<app-tax-info></app-tax-info>
<div id="price" class="my-4">
<div class="text-muted text-sm">
{{ 'planPrice' | i18n }}: {{ subtotal | currency: 'USD $' }}
<br />
<ng-container>
{{ 'estimatedTax' | i18n }}: {{ taxCharges | currency: 'USD $' }}
</ng-container>
</div>
<hr class="my-1 col-3 ml-0">
<p class="text-lg"><strong>{{'total' | i18n}}:</strong>
{{total | currency:'USD $'}}/{{'year' | i18n}}</p>
</div>
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB &times;
{{ storageGbPrice | currency: "$" }} =
{{ additionalStorageTotal | currency: "$" }}
<hr class="my-3" />
<h2 class="spaced-header mb-4">{{ "paymentInformation" | i18n }}</h2>
<app-payment [hideBank]="true"></app-payment>
<app-tax-info></app-tax-info>
<div id="price" class="my-4">
<div class="text-muted text-sm">
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
<br />
<ng-container>
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
</ng-container>
</div>
<small class="text-muted font-italic">{{'paymentChargedAnnually' | i18n}}</small>
<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>
</button>
<hr class="my-1 col-3 ml-0" />
<p class="text-lg">
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
</p>
</div>
<small class="text-muted font-italic">{{ "paymentChargedAnnually" | i18n }}</small>
<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>
</button>
</form>

View File

@@ -1,129 +1,142 @@
import {
Component,
OnInit,
ViewChild,
} from '@angular/core';
import { Router } from '@angular/router';
import { Component, OnInit, ViewChild } from "@angular/core";
import { Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
import { PaymentComponent } from "./payment.component";
import { TaxInfoComponent } from "./tax-info.component";
@Component({
selector: 'app-premium',
templateUrl: 'premium.component.html',
selector: "app-premium",
templateUrl: "premium.component.html",
})
export class PremiumComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
canAccessPremium = false;
selfHosted = false;
premiumPrice = 10;
storageGbPrice = 4;
additionalStorage = 0;
canAccessPremium = false;
selfHosted = false;
premiumPrice = 10;
storageGbPrice = 4;
additionalStorage = 0;
formPromise: Promise<any>;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private tokenService: TokenService, private router: Router,
private messagingService: MessagingService, private syncService: SyncService,
private logService: LogService, private stateService: StateService) {
this.selfHosted = platformUtilsService.isSelfHost();
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private tokenService: TokenService,
private router: Router,
private messagingService: MessagingService,
private syncService: SyncService,
private logService: LogService,
private stateService: StateService
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
const premium = await this.tokenService.getPremium();
if (premium) {
this.router.navigate(["/settings/subscription"]);
return;
}
}
async submit() {
let files: FileList = null;
if (this.selfHosted) {
const fileEl = document.getElementById("file") as HTMLInputElement;
files = fileEl.files;
if (files == null || files.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("selectFile")
);
return;
}
}
async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
const premium = await this.tokenService.getPremium();
if (premium) {
this.router.navigate(['/settings/subscription']);
return;
try {
if (this.selfHosted) {
if (!this.tokenService.getEmailVerified()) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("verifyEmailFirst")
);
return;
}
}
async submit() {
let files: FileList = null;
if (this.selfHosted) {
const fileEl = document.getElementById('file') as HTMLInputElement;
files = fileEl.files;
if (files == null || files.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('selectFile'));
return;
const fd = new FormData();
fd.append("license", files[0]);
this.formPromise = this.apiService.postAccountLicense(fd).then(() => {
return this.finalizePremium();
});
} else {
this.formPromise = this.paymentComponent
.createPaymentToken()
.then((result) => {
const fd = new FormData();
fd.append("paymentMethodType", result[1].toString());
if (result[0] != null) {
fd.append("paymentToken", result[0]);
}
}
try {
if (this.selfHosted) {
if (!this.tokenService.getEmailVerified()) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('verifyEmailFirst'));
return;
}
const fd = new FormData();
fd.append('license', files[0]);
this.formPromise = this.apiService.postAccountLicense(fd).then(() => {
return this.finalizePremium();
});
fd.append("additionalStorageGb", (this.additionalStorage || 0).toString());
fd.append("country", this.taxInfoComponent.taxInfo.country);
fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode);
return this.apiService.postPremium(fd);
})
.then((paymentResponse) => {
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
return this.paymentComponent.handleStripeCardPayment(
paymentResponse.paymentIntentClientSecret,
() => this.finalizePremium()
);
} else {
this.formPromise = this.paymentComponent.createPaymentToken().then(result => {
const fd = new FormData();
fd.append('paymentMethodType', result[1].toString());
if (result[0] != null) {
fd.append('paymentToken', result[0]);
}
fd.append('additionalStorageGb', (this.additionalStorage || 0).toString());
fd.append('country', this.taxInfoComponent.taxInfo.country);
fd.append('postalCode', this.taxInfoComponent.taxInfo.postalCode);
return this.apiService.postPremium(fd);
}).then(paymentResponse => {
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
return this.paymentComponent.handleStripeCardPayment(paymentResponse.paymentIntentClientSecret,
() => this.finalizePremium());
} else {
return this.finalizePremium();
}
});
return this.finalizePremium();
}
await this.formPromise;
} catch (e) {
this.logService.error(e);
}
});
}
await this.formPromise;
} catch (e) {
this.logService.error(e);
}
}
async finalizePremium() {
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.platformUtilsService.showToast('success', null, this.i18nService.t('premiumUpdated'));
this.messagingService.send('purchasedPremium');
this.router.navigate(['/settings/subscription']);
}
async finalizePremium() {
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.platformUtilsService.showToast("success", null, this.i18nService.t("premiumUpdated"));
this.messagingService.send("purchasedPremium");
this.router.navigate(["/settings/subscription"]);
}
get additionalStorageTotal(): number {
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
}
get additionalStorageTotal(): number {
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
}
get subtotal(): number {
return this.premiumPrice + this.additionalStorageTotal;
}
get subtotal(): number {
return this.premiumPrice + this.additionalStorageTotal;
}
get taxCharges(): number {
return this.taxInfoComponent != null && this.taxInfoComponent.taxRate != null ?
(this.taxInfoComponent.taxRate / 100) * this.subtotal :
0;
}
get taxCharges(): number {
return this.taxInfoComponent != null && this.taxInfoComponent.taxRate != null
? (this.taxInfoComponent.taxRate / 100) * this.subtotal
: 0;
}
get total(): number {
return (this.subtotal + this.taxCharges) || 0;
}
get total(): number {
return this.subtotal + this.taxCharges || 0;
}
}

View File

@@ -1,41 +1,69 @@
<div *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>
<i class="fa fa-spinner fa-spin text-muted" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<form *ngIf="profile && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="profile.name">
</div>
<div class="form-group">
<label for="email">{{'email' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="profile.email" readonly>
</div>
<div class="form-group" *ngIf="!hidePasswordHint">
<label for="masterPasswordHint">{{'masterPassHintLabel' | i18n}}</label>
<input id="masterPasswordHint" class="form-control" type="text" name="MasterPasswordHint"
[(ngModel)]="profile.masterPasswordHint">
</div>
</div>
<div class="col-6">
<div class="mb-3">
<app-avatar data="{{profile | userName}}" [email]="profile.email" dynamic="true" size="75"
fontSize="35"></app-avatar>
</div>
<hr>
<p *ngIf="fingerprint">
{{'yourAccountsFingerprint' | i18n}}:
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener"
appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i></a><br>
<code>{{fingerprint}}</code>
</p>
</div>
<form
*ngIf="profile && !loading"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="name">{{ "name" | i18n }}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="profile.name" />
</div>
<div class="form-group">
<label for="email">{{ "email" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="profile.email"
readonly
/>
</div>
<div class="form-group" *ngIf="!hidePasswordHint">
<label for="masterPasswordHint">{{ "masterPassHintLabel" | i18n }}</label>
<input
id="masterPasswordHint"
class="form-control"
type="text"
name="MasterPasswordHint"
[(ngModel)]="profile.masterPasswordHint"
/>
</div>
</div>
<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>
<div class="col-6">
<div class="mb-3">
<app-avatar
data="{{ profile | userName }}"
[email]="profile.email"
dynamic="true"
size="75"
fontSize="35"
></app-avatar>
</div>
<hr />
<p *ngIf="fingerprint">
{{ "yourAccountsFingerprint" | i18n }}:
<a
href="https://help.bitwarden.com/article/fingerprint-phrase/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i></a
><br />
<code>{{ fingerprint }}</code>
</p>
</div>
</div>
<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>
</form>

View File

@@ -1,60 +1,59 @@
import {
Component,
OnInit,
} from '@angular/core';
import { Component, OnInit } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { UpdateProfileRequest } from 'jslib-common/models/request/updateProfileRequest';
import { UpdateProfileRequest } from "jslib-common/models/request/updateProfileRequest";
import { ProfileResponse } from 'jslib-common/models/response/profileResponse';
import { ProfileResponse } from "jslib-common/models/response/profileResponse";
@Component({
selector: 'app-profile',
templateUrl: 'profile.component.html',
selector: "app-profile",
templateUrl: "profile.component.html",
})
export class ProfileComponent implements OnInit {
loading = true;
profile: ProfileResponse;
fingerprint: string;
hidePasswordHint = false;
loading = true;
profile: ProfileResponse;
fingerprint: string;
hidePasswordHint = false;
formPromise: Promise<any>;
formPromise: Promise<any>;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private logService: LogService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private logService: LogService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService
) {}
async ngOnInit() {
this.profile = await this.apiService.getProfile();
this.loading = false;
const fingerprint = await this.cryptoService.getFingerprint(await this.stateService.getUserId());
if (fingerprint != null) {
this.fingerprint = fingerprint.join('-');
}
this.hidePasswordHint = await this.keyConnectorService.getUsesKeyConnector();
async ngOnInit() {
this.profile = await this.apiService.getProfile();
this.loading = false;
const fingerprint = await this.cryptoService.getFingerprint(
await this.stateService.getUserId()
);
if (fingerprint != null) {
this.fingerprint = fingerprint.join("-");
}
this.hidePasswordHint = await this.keyConnectorService.getUsesKeyConnector();
}
async submit() {
try {
const request = new UpdateProfileRequest(this.profile.name, this.profile.masterPasswordHint);
this.formPromise = this.apiService.putProfile(request);
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('accountUpdated'));
} catch (e) {
this.logService.error(e);
}
async submit() {
try {
const request = new UpdateProfileRequest(this.profile.name, this.profile.masterPasswordHint);
this.formPromise = this.apiService.putProfile(request);
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated"));
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,25 +1,38 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="purgeVaultTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="purgeVaultTitle">{{'purgeVault' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{(organizationId ? 'purgeOrgVaultDesc' : 'purgeVaultDesc') | i18n}}</p>
<app-callout type="warning">{{'purgeVaultWarning' | i18n}}</app-callout>
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-verify-master-password>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'purgeVault' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="purgeVaultTitle">{{ "purgeVault" | i18n }}</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ (organizationId ? "purgeOrgVaultDesc" : "purgeVaultDesc") | i18n }}</p>
<app-callout type="warning">{{ "purgeVaultWarning" | i18n }}</app-callout>
<app-verify-master-password [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-verify-master-password>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "purgeVault" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,44 +1,47 @@
import {
Component,
Input,
} from '@angular/core';
import { Router } from '@angular/router';
import { Component, Input } from "@angular/core";
import { Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { Verification } from 'jslib-common/types/verification';
import { Verification } from "jslib-common/types/verification";
@Component({
selector: 'app-purge-vault',
templateUrl: 'purge-vault.component.html',
selector: "app-purge-vault",
templateUrl: "purge-vault.component.html",
})
export class PurgeVaultComponent {
@Input() organizationId?: string = null;
@Input() organizationId?: string = null;
masterPassword: Verification;
formPromise: Promise<any>;
masterPassword: Verification;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private userVerificationService: UserVerificationService,
private router: Router, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private userVerificationService: UserVerificationService,
private router: Router,
private logService: LogService
) {}
async submit() {
try {
this.formPromise = this.userVerificationService.buildRequest(this.masterPassword)
.then(request => this.apiService.postPurgeCiphers(request, this.organizationId));
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('vaultPurged'));
if (this.organizationId != null) {
this.router.navigate(['organizations', this.organizationId, 'vault']);
} else {
this.router.navigate(['vault']);
}
} catch (e) {
this.logService.error(e);
}
async submit() {
try {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword)
.then((request) => this.apiService.postPurgeCiphers(request, this.organizationId));
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("vaultPurged"));
if (this.organizationId != null) {
this.router.navigate(["organizations", this.organizationId, "vault"]);
} else {
this.router.navigate(["vault"]);
}
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,44 +1,64 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{'settings' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{'myAccount' | i18n}}
</a>
<a routerLink="options" class="list-group-item" routerLinkActive="active">
{{'options' | i18n}}
</a>
<a routerLink="organizations" class="list-group-item" routerLinkActive="active">
{{'organizations' | i18n}}
</a>
<a routerLink="subscription" class="list-group-item" routerLinkActive="active" *ngIf="premium">
{{'premiumMembership' | i18n}}
</a>
<a routerLink="premium" class="list-group-item" routerLinkActive="active" *ngIf="!premium">
{{'goPremium' | i18n}}
</a>
<a routerLink="billing" class="list-group-item" routerLinkActive="active" *ngIf="!selfHosted">
{{'billing' | i18n}}
</a>
<a routerLink="two-factor" class="list-group-item" routerLinkActive="active">
{{'twoStepLogin' | i18n}}
</a>
<a routerLink="domain-rules" class="list-group-item" routerLinkActive="active">
{{'domainRules' | i18n}}
</a>
<a routerLink="emergency-access" class="list-group-item" routerLinkActive="active">
{{'emergencyAccess' | i18n}}
</a>
<a routerLink="sponsored-families" class="list-group-item" routerLinkActive="active" *ngIf="hasFamilySponsorshipAvailable">
{{'sponsoredFamilies' | i18n}}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{ "settings" | i18n }}</div>
<div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{ "myAccount" | i18n }}
</a>
<a routerLink="options" class="list-group-item" routerLinkActive="active">
{{ "options" | i18n }}
</a>
<a routerLink="organizations" class="list-group-item" routerLinkActive="active">
{{ "organizations" | i18n }}
</a>
<a
routerLink="subscription"
class="list-group-item"
routerLinkActive="active"
*ngIf="premium"
>
{{ "premiumMembership" | i18n }}
</a>
<a
routerLink="premium"
class="list-group-item"
routerLinkActive="active"
*ngIf="!premium"
>
{{ "goPremium" | i18n }}
</a>
<a
routerLink="billing"
class="list-group-item"
routerLinkActive="active"
*ngIf="!selfHosted"
>
{{ "billing" | i18n }}
</a>
<a routerLink="two-factor" class="list-group-item" routerLinkActive="active">
{{ "twoStepLogin" | i18n }}
</a>
<a routerLink="domain-rules" class="list-group-item" routerLinkActive="active">
{{ "domainRules" | i18n }}
</a>
<a routerLink="emergency-access" class="list-group-item" routerLinkActive="active">
{{ "emergencyAccess" | i18n }}
</a>
<a
routerLink="sponsored-families"
class="list-group-item"
routerLinkActive="active"
*ngIf="hasFamilySponsorshipAvailable"
>
{{ "sponsoredFamilies" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -1,52 +1,51 @@
import {
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service";
const BroadcasterSubscriptionId = 'SettingsComponent';
const BroadcasterSubscriptionId = "SettingsComponent";
@Component({
selector: 'app-settings',
templateUrl: 'settings.component.html',
selector: "app-settings",
templateUrl: "settings.component.html",
})
export class SettingsComponent implements OnInit, OnDestroy {
premium: boolean;
selfHosted: boolean;
hasFamilySponsorshipAvailable: boolean;
premium: boolean;
selfHosted: boolean;
hasFamilySponsorshipAvailable: boolean;
constructor(private tokenService: TokenService, private broadcasterService: BroadcasterService,
private ngZone: NgZone, private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService) { }
constructor(
private tokenService: TokenService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService
) {}
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'purchasedPremium':
await this.load();
break;
default:
}
});
});
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "purchasedPremium":
await this.load();
break;
default:
}
});
});
this.selfHosted = await this.platformUtilsService.isSelfHost();
await this.load();
}
this.selfHosted = await this.platformUtilsService.isSelfHost();
await this.load();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async load() {
this.premium = await this.tokenService.getPremium();
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
}
async load() {
this.premium = await this.tokenService.getPremium();
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
}
}

View File

@@ -1,57 +1,74 @@
<div class="page-header">
<h1>{{'sponsoredFamilies' | i18n}}</h1>
<h1>{{ "sponsoredFamilies" | i18n }}</h1>
</div>
<ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<i class="fa fa-spinner fa-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="!loading">
<p>
{{'sponsoredFamiliesEligible' | i18n}}
</p>
<div>
{{'sponsoredFamiliesInclude' | i18n}}:
<ul class="inset-list">
<li>{{'sponsoredFamiliesPremiumAccess' | i18n}}</li>
<li>{{'sponsoredFamiliesSharedCollections' | i18n}}</li>
</ul>
<p>
{{ "sponsoredFamiliesEligible" | i18n }}
</p>
<div>
{{ "sponsoredFamiliesInclude" | i18n }}:
<ul class="inset-list">
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
</ul>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="anyOrgsAvailable"
>
<div *ngIf="moreThanOneOrgAvailable" class="form-group col-7">
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
<select
id="availableSponsorshipOrg"
name="Available Sponsorship Organization"
[(ngModel)]="selectedSponsorshipOrgId"
class="form-control"
required
>
<option value="">-- {{ "select" | i18n }} --</option>
<option *ngFor="let o of availableSponsorshipOrgs" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="anyOrgsAvailable">
<div *ngIf="moreThanOneOrgAvailable" class="form-group col-7">
<label for="availableSponsorshipOrg">{{ 'familiesSponsoringOrgSelect' | i18n}}</label>
<select id="availableSponsorshipOrg" name="Available Sponsorship Organization"
[(ngModel)]="selectedSponsorshipOrgId" class="form-control" required>
<option value="">-- {{'select' | i18n}} --</option>
<option *ngFor="let o of availableSponsorshipOrgs" [ngValue]="o.id">{{o.name}}</option>
</select>
</div>
<div class="form-group col-7">
<label for="accountEmail">{{'sponsoredFamiliesEmail' | i18n}}:</label>
<input id="accountEmail" class="form-control" inputmode="email" [(ngModel)]="sponsorshipEmail"
name="sponsorshipEmail" required>
<button class="btn btn-primary btn-submit mt-4" type="submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'redeem' | i18n}}</span>
</button>
</div>
</form>
<ng-container *ngIf="anyActiveSponsorships">
<div class="border-bottom">
<table class="table table-hover table-list">
<thead>
<tr>
<th>{{'recipient' | i18n}}</th>
<th>{{'sponsoringOrg' | i18n}}</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let o of activeSponsorshipOrgs">
<tr sponsoring-org-row [sponsoringOrg]="o" (sponsorshipRemoved)="load(true)"></tr>
</ng-container>
</tbody>
</table>
</div>
<small>{{'sponsoredFamiliesLeaveCopy' | i18n}}</small>
</ng-container>
<div class="form-group col-7">
<label for="accountEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
<input
id="accountEmail"
class="form-control"
inputmode="email"
[(ngModel)]="sponsorshipEmail"
name="sponsorshipEmail"
required
/>
<button class="btn btn-primary btn-submit mt-4" type="submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "redeem" | i18n }}</span>
</button>
</div>
</form>
<ng-container *ngIf="anyActiveSponsorships">
<div class="border-bottom">
<table class="table table-hover table-list">
<thead>
<tr>
<th>{{ "recipient" | i18n }}</th>
<th>{{ "sponsoringOrg" | i18n }}</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let o of activeSponsorshipOrgs">
<tr sponsoring-org-row [sponsoringOrg]="o" (sponsorshipRemoved)="load(true)"></tr>
</ng-container>
</tbody>
</table>
</div>
<small>{{ "sponsoredFamiliesLeaveCopy" | i18n }}</small>
</ng-container>
</ng-container>

View File

@@ -1,93 +1,91 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { Component, OnInit } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { PlanSponsorshipType } from 'jslib-common/enums/planSponsorshipType';
import { Organization } from 'jslib-common/models/domain/organization';
import { PlanSponsorshipType } from "jslib-common/enums/planSponsorshipType";
import { Organization } from "jslib-common/models/domain/organization";
@Component({
selector: 'app-sponsored-families',
templateUrl: 'sponsored-families.component.html',
selector: "app-sponsored-families",
templateUrl: "sponsored-families.component.html",
})
export class SponsoredFamiliesComponent implements OnInit {
loading = false;
loading = false;
availableSponsorshipOrgs: Organization[] = [];
activeSponsorshipOrgs: Organization[] = [];
selectedSponsorshipOrgId: string = '';
sponsorshipEmail: string = '';
availableSponsorshipOrgs: Organization[] = [];
activeSponsorshipOrgs: Organization[] = [];
selectedSponsorshipOrgId: string = "";
sponsorshipEmail: string = "";
// Conditional display properties
formPromise: Promise<any>;
// Conditional display properties
formPromise: Promise<any>;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private syncService: SyncService,
private organizationService: OrganizationService,
) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private syncService: SyncService,
private organizationService: OrganizationService
) {}
async ngOnInit() {
await this.load();
async ngOnInit() {
await this.load();
}
async submit() {
this.formPromise = this.apiService.postCreateSponsorship(this.selectedSponsorshipOrgId, {
sponsoredEmail: this.sponsorshipEmail,
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
friendlyName: this.sponsorshipEmail,
});
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated"));
this.formPromise = null;
this.resetForm();
await this.load(true);
}
async load(forceReload: boolean = false) {
if (this.loading) {
return;
}
async submit() {
this.formPromise = this.apiService.postCreateSponsorship(this.selectedSponsorshipOrgId, {
sponsoredEmail: this.sponsorshipEmail,
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
friendlyName: this.sponsorshipEmail,
});
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('sponsorshipCreated'));
this.formPromise = null;
this.resetForm();
await this.load(true);
this.loading = true;
if (forceReload) {
await this.syncService.fullSync(true);
}
async load(forceReload: boolean = false) {
if (this.loading) {
return;
}
const allOrgs = await this.organizationService.getAll();
this.availableSponsorshipOrgs = allOrgs.filter((org) => org.familySponsorshipAvailable);
this.loading = true;
if (forceReload) {
await this.syncService.fullSync(true);
}
this.activeSponsorshipOrgs = allOrgs.filter(
(org) => org.familySponsorshipFriendlyName !== null
);
const allOrgs = await this.organizationService.getAll();
this.availableSponsorshipOrgs = allOrgs.filter(org => org.familySponsorshipAvailable);
this.activeSponsorshipOrgs = allOrgs.filter(org => org.familySponsorshipFriendlyName !== null);
if (this.availableSponsorshipOrgs.length === 1) {
this.selectedSponsorshipOrgId = this.availableSponsorshipOrgs[0].id;
}
this.loading = false;
if (this.availableSponsorshipOrgs.length === 1) {
this.selectedSponsorshipOrgId = this.availableSponsorshipOrgs[0].id;
}
this.loading = false;
}
private async resetForm() {
this.sponsorshipEmail = "";
this.selectedSponsorshipOrgId = "";
}
private async resetForm() {
this.sponsorshipEmail = '';
this.selectedSponsorshipOrgId = '';
}
get anyActiveSponsorships(): boolean {
return this.activeSponsorshipOrgs.length > 0;
}
get anyActiveSponsorships(): boolean {
return this.activeSponsorshipOrgs.length > 0;
}
get anyOrgsAvailable(): boolean {
return this.availableSponsorshipOrgs.length > 0;
}
get anyOrgsAvailable(): boolean {
return this.availableSponsorshipOrgs.length > 0;
}
get moreThanOneOrgAvailable(): boolean {
return this.availableSponsorshipOrgs.length > 1;
}
get moreThanOneOrgAvailable(): boolean {
return this.availableSponsorshipOrgs.length > 1;
}
}

View File

@@ -1,26 +1,43 @@
<td>
{{sponsoringOrg.familySponsorshipFriendlyName}}
{{ sponsoringOrg.familySponsorshipFriendlyName }}
</td>
<td>{{sponsoringOrg.name}}</td>
<td>{{ sponsoringOrg.name }}</td>
<td class="table-action-right">
<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">
<button #resendEmailBtn [appApiAction]="resendEmailPromise" class="dropdown-item btn-submit"
[disabled]="resendEmailBtn.loading" (click)="resendEmail()"
[attr.aria-label]="'resendEmailLabel' | i18n : sponsoringOrg.familySponsorshipFriendlyName">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'resendEmail' | i18n }}</span>
</button>
<button #revokeSponsorshipBtn [appApiAction]="revokeSponsorshipPromise" class="dropdown-item text-danger btn-submit"
[disabled]="revokeSponsorshipBtn.loading" (click)="revokeSponsorship()"
[attr.aria-label]="'revokeAccount' | i18n : sponsoringOrg.familySponsorshipFriendlyName">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'remove' | i18n}}</span>
</button>
</div>
<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">
<button
#resendEmailBtn
[appApiAction]="resendEmailPromise"
class="dropdown-item btn-submit"
[disabled]="resendEmailBtn.loading"
(click)="resendEmail()"
[attr.aria-label]="'resendEmailLabel' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "resendEmail" | i18n }}</span>
</button>
<button
#revokeSponsorshipBtn
[appApiAction]="revokeSponsorshipPromise"
class="dropdown-item text-danger btn-submit"
[disabled]="revokeSponsorshipBtn.loading"
(click)="revokeSponsorship()"
[attr.aria-label]="'revokeAccount' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "remove" | i18n }}</span>
</button>
</div>
</div>
</td>

View File

@@ -1,62 +1,63 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { Organization } from 'jslib-common/models/domain/organization';
import { Organization } from "jslib-common/models/domain/organization";
@Component({
selector: '[sponsoring-org-row]',
templateUrl: 'sponsoring-org-row.component.html',
selector: "[sponsoring-org-row]",
templateUrl: "sponsoring-org-row.component.html",
})
export class SponsoringOrgRowComponent {
@Input() sponsoringOrg: Organization = null;
@Input() sponsoringOrg: Organization = null;
@Output() sponsorshipRemoved = new EventEmitter();
@Output() sponsorshipRemoved = new EventEmitter();
revokeSponsorshipPromise: Promise<any>;
resendEmailPromise: Promise<any>;
revokeSponsorshipPromise: Promise<any>;
resendEmailPromise: Promise<any>;
constructor(private apiService: ApiService,
private i18nService: I18nService, private logService: LogService,
private platformUtilsService: PlatformUtilsService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private logService: LogService,
private platformUtilsService: PlatformUtilsService
) {}
async revokeSponsorship() {
try {
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
await this.revokeSponsorshipPromise;
} catch (e) {
this.logService.error(e);
}
this.revokeSponsorshipPromise = null;
async revokeSponsorship() {
try {
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
await this.revokeSponsorshipPromise;
} catch (e) {
this.logService.error(e);
}
async resendEmail() {
this.resendEmailPromise = this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
await this.resendEmailPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('emailSent'));
this.resendEmailPromise = null;
this.revokeSponsorshipPromise = null;
}
async resendEmail() {
this.resendEmailPromise = this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
await this.resendEmailPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailSent"));
this.resendEmailPromise = null;
}
private async doRevokeSponsorship() {
const isConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("revokeSponsorshipConfirmation"),
`${this.i18nService.t("remove")} ${this.sponsoringOrg.familySponsorshipFriendlyName}?`,
this.i18nService.t("remove"),
this.i18nService.t("cancel"),
"warning"
);
if (!isConfirmed) {
return;
}
private async doRevokeSponsorship() {
const isConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('revokeSponsorshipConfirmation'),
`${this.i18nService.t('remove')} ${this.sponsoringOrg.familySponsorshipFriendlyName}?`,
this.i18nService.t('remove'), this.i18nService.t('cancel'), 'warning');
if (!isConfirmed) {
return;
}
await this.apiService.deleteRevokeSponsorship(this.sponsoringOrg.id);
this.platformUtilsService.showToast('success', null, this.i18nService.t('reclaimedFreePlan'));
this.sponsorshipRemoved.emit();
}
await this.apiService.deleteRevokeSponsorship(this.sponsoringOrg.id);
this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan"));
this.sponsorshipRemoved.emit();
}
}

View File

@@ -1,313 +1,356 @@
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="addressCountry">{{'country' | i18n}}</label>
<select id="addressCountry" class="form-control" [(ngModel)]="taxInfo.country" required name="addressCountry"
autocomplete="country" (change)="changeCountry()">
<option value="">-- Select --</option>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="FR">France</option>
<option value="DE">Germany</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
<option value="IN">India</option>
<option value="-" disabled></option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland Islands</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<option value="AI">Anguilla</option>
<option value="AQ">Antarctica</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AR">Argentina</option>
<option value="AM">Armenia</option>
<option value="AW">Aruba</option>
<option value="AT">Austria</option>
<option value="AZ">Azerbaijan</option>
<option value="BS">Bahamas</option>
<option value="BH">Bahrain</option>
<option value="BD">Bangladesh</option>
<option value="BB">Barbados</option>
<option value="BY">Belarus</option>
<option value="BE">Belgium</option>
<option value="BZ">Belize</option>
<option value="BJ">Benin</option>
<option value="BM">Bermuda</option>
<option value="BT">Bhutan</option>
<option value="BO">Bolivia, Plurinational State of</option>
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BW">Botswana</option>
<option value="BV">Bouvet Island</option>
<option value="BR">Brazil</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="BN">Brunei Darussalam</option>
<option value="BG">Bulgaria</option>
<option value="BF">Burkina Faso</option>
<option value="BI">Burundi</option>
<option value="KH">Cambodia</option>
<option value="CM">Cameroon</option>
<option value="CV">Cape Verde</option>
<option value="KY">Cayman Islands</option>
<option value="CF">Central African Republic</option>
<option value="TD">Chad</option>
<option value="CL">Chile</option>
<option value="CX">Christmas Island</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CO">Colombia</option>
<option value="KM">Comoros</option>
<option value="CG">Congo</option>
<option value="CD">Congo, the Democratic Republic of the</option>
<option value="CK">Cook Islands</option>
<option value="CR">Costa Rica</option>
<option value="CI">Côte d'Ivoire</option>
<option value="HR">Croatia</option>
<option value="CU">Cuba</option>
<option value="CW">Curaçao</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DK">Denmark</option>
<option value="DJ">Djibouti</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="EC">Ecuador</option>
<option value="EG">Egypt</option>
<option value="SV">El Salvador</option>
<option value="GQ">Equatorial Guinea</option>
<option value="ER">Eritrea</option>
<option value="EE">Estonia</option>
<option value="ET">Ethiopia</option>
<option value="FK">Falkland Islands (Malvinas)</option>
<option value="FO">Faroe Islands</option>
<option value="FJ">Fiji</option>
<option value="FI">Finland</option>
<option value="GF">French Guiana</option>
<option value="PF">French Polynesia</option>
<option value="TF">French Southern Territories</option>
<option value="GA">Gabon</option>
<option value="GM">Gambia</option>
<option value="GE">Georgia</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GR">Greece</option>
<option value="GL">Greenland</option>
<option value="GD">Grenada</option>
<option value="GP">Guadeloupe</option>
<option value="GU">Guam</option>
<option value="GT">Guatemala</option>
<option value="GG">Guernsey</option>
<option value="GN">Guinea</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HT">Haiti</option>
<option value="HM">Heard Island and McDonald Islands</option>
<option value="VA">Holy See (Vatican City State)</option>
<option value="HN">Honduras</option>
<option value="HK">Hong Kong</option>
<option value="HU">Hungary</option>
<option value="IS">Iceland</option>
<option value="ID">Indonesia</option>
<option value="IR">Iran, Islamic Republic of</option>
<option value="IQ">Iraq</option>
<option value="IE">Ireland</option>
<option value="IM">Isle of Man</option>
<option value="IL">Israel</option>
<option value="IT">Italy</option>
<option value="JM">Jamaica</option>
<option value="JP">Japan</option>
<option value="JE">Jersey</option>
<option value="JO">Jordan</option>
<option value="KZ">Kazakhstan</option>
<option value="KE">Kenya</option>
<option value="KI">Kiribati</option>
<option value="KP">Korea, Democratic People's Republic of</option>
<option value="KR">Korea, Republic of</option>
<option value="KW">Kuwait</option>
<option value="KG">Kyrgyzstan</option>
<option value="LA">Lao People's Democratic Republic</option>
<option value="LV">Latvia</option>
<option value="LB">Lebanon</option>
<option value="LS">Lesotho</option>
<option value="LR">Liberia</option>
<option value="LY">Libya</option>
<option value="LI">Liechtenstein</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="MO">Macao</option>
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
<option value="MG">Madagascar</option>
<option value="MW">Malawi</option>
<option value="MY">Malaysia</option>
<option value="MV">Maldives</option>
<option value="ML">Mali</option>
<option value="MT">Malta</option>
<option value="MH">Marshall Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MU">Mauritius</option>
<option value="YT">Mayotte</option>
<option value="MX">Mexico</option>
<option value="FM">Micronesia, Federated States of</option>
<option value="MD">Moldova, Republic of</option>
<option value="MC">Monaco</option>
<option value="MN">Mongolia</option>
<option value="ME">Montenegro</option>
<option value="MS">Montserrat</option>
<option value="MA">Morocco</option>
<option value="MZ">Mozambique</option>
<option value="MM">Myanmar</option>
<option value="NA">Namibia</option>
<option value="NR">Nauru</option>
<option value="NP">Nepal</option>
<option value="NL">Netherlands</option>
<option value="NC">New Caledonia</option>
<option value="NZ">New Zealand</option>
<option value="NI">Nicaragua</option>
<option value="NE">Niger</option>
<option value="NG">Nigeria</option>
<option value="NU">Niue</option>
<option value="NF">Norfolk Island</option>
<option value="MP">Northern Mariana Islands</option>
<option value="NO">Norway</option>
<option value="OM">Oman</option>
<option value="PK">Pakistan</option>
<option value="PW">Palau</option>
<option value="PS">Palestinian Territory, Occupied</option>
<option value="PA">Panama</option>
<option value="PG">Papua New Guinea</option>
<option value="PY">Paraguay</option>
<option value="PE">Peru</option>
<option value="PH">Philippines</option>
<option value="PN">Pitcairn</option>
<option value="PL">Poland</option>
<option value="PT">Portugal</option>
<option value="PR">Puerto Rico</option>
<option value="QA">Qatar</option>
<option value="RE">Réunion</option>
<option value="RO">Romania</option>
<option value="RU">Russian Federation</option>
<option value="RW">Rwanda</option>
<option value="BL">Saint Barthélemy</option>
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="LC">Saint Lucia</option>
<option value="MF">Saint Martin (French part)</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="WS">Samoa</option>
<option value="SM">San Marino</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SA">Saudi Arabia</option>
<option value="SN">Senegal</option>
<option value="RS">Serbia</option>
<option value="SC">Seychelles</option>
<option value="SL">Sierra Leone</option>
<option value="SG">Singapore</option>
<option value="SX">Sint Maarten (Dutch part)</option>
<option value="SK">Slovakia</option>
<option value="SI">Slovenia</option>
<option value="SB">Solomon Islands</option>
<option value="SO">Somalia</option>
<option value="ZA">South Africa</option>
<option value="GS">South Georgia and the South Sandwich Islands</option>
<option value="SS">South Sudan</option>
<option value="ES">Spain</option>
<option value="LK">Sri Lanka</option>
<option value="SD">Sudan</option>
<option value="SR">Suriname</option>
<option value="SJ">Svalbard and Jan Mayen</option>
<option value="SZ">Swaziland</option>
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syrian Arab Republic</option>
<option value="TW">Taiwan</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania, United Republic of</option>
<option value="TH">Thailand</option>
<option value="TL">Timor-Leste</option>
<option value="TG">Togo</option>
<option value="TK">Tokelau</option>
<option value="TO">Tonga</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TN">Tunisia</option>
<option value="TR">Turkey</option>
<option value="TM">Turkmenistan</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TV">Tuvalu</option>
<option value="UG">Uganda</option>
<option value="UA">Ukraine</option>
<option value="AE">United Arab Emirates</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VU">Vanuatu</option>
<option value="VE">Venezuela, Bolivarian Republic of</option>
<option value="VN">Viet Nam</option>
<option value="VG">Virgin Islands, British</option>
<option value="VI">Virgin Islands, U.S.</option>
<option value="WF">Wallis and Futuna</option>
<option value="EH">Western Sahara</option>
<option value="YE">Yemen</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>
</select>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressCountry">{{ "country" | i18n }}</label>
<select
id="addressCountry"
class="form-control"
[(ngModel)]="taxInfo.country"
required
name="addressCountry"
autocomplete="country"
(change)="changeCountry()"
>
<option value="">-- Select --</option>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="FR">France</option>
<option value="DE">Germany</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
<option value="IN">India</option>
<option value="-" disabled></option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland Islands</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<option value="AI">Anguilla</option>
<option value="AQ">Antarctica</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AR">Argentina</option>
<option value="AM">Armenia</option>
<option value="AW">Aruba</option>
<option value="AT">Austria</option>
<option value="AZ">Azerbaijan</option>
<option value="BS">Bahamas</option>
<option value="BH">Bahrain</option>
<option value="BD">Bangladesh</option>
<option value="BB">Barbados</option>
<option value="BY">Belarus</option>
<option value="BE">Belgium</option>
<option value="BZ">Belize</option>
<option value="BJ">Benin</option>
<option value="BM">Bermuda</option>
<option value="BT">Bhutan</option>
<option value="BO">Bolivia, Plurinational State of</option>
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BW">Botswana</option>
<option value="BV">Bouvet Island</option>
<option value="BR">Brazil</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="BN">Brunei Darussalam</option>
<option value="BG">Bulgaria</option>
<option value="BF">Burkina Faso</option>
<option value="BI">Burundi</option>
<option value="KH">Cambodia</option>
<option value="CM">Cameroon</option>
<option value="CV">Cape Verde</option>
<option value="KY">Cayman Islands</option>
<option value="CF">Central African Republic</option>
<option value="TD">Chad</option>
<option value="CL">Chile</option>
<option value="CX">Christmas Island</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CO">Colombia</option>
<option value="KM">Comoros</option>
<option value="CG">Congo</option>
<option value="CD">Congo, the Democratic Republic of the</option>
<option value="CK">Cook Islands</option>
<option value="CR">Costa Rica</option>
<option value="CI">Côte d'Ivoire</option>
<option value="HR">Croatia</option>
<option value="CU">Cuba</option>
<option value="CW">Curaçao</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DK">Denmark</option>
<option value="DJ">Djibouti</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="EC">Ecuador</option>
<option value="EG">Egypt</option>
<option value="SV">El Salvador</option>
<option value="GQ">Equatorial Guinea</option>
<option value="ER">Eritrea</option>
<option value="EE">Estonia</option>
<option value="ET">Ethiopia</option>
<option value="FK">Falkland Islands (Malvinas)</option>
<option value="FO">Faroe Islands</option>
<option value="FJ">Fiji</option>
<option value="FI">Finland</option>
<option value="GF">French Guiana</option>
<option value="PF">French Polynesia</option>
<option value="TF">French Southern Territories</option>
<option value="GA">Gabon</option>
<option value="GM">Gambia</option>
<option value="GE">Georgia</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GR">Greece</option>
<option value="GL">Greenland</option>
<option value="GD">Grenada</option>
<option value="GP">Guadeloupe</option>
<option value="GU">Guam</option>
<option value="GT">Guatemala</option>
<option value="GG">Guernsey</option>
<option value="GN">Guinea</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HT">Haiti</option>
<option value="HM">Heard Island and McDonald Islands</option>
<option value="VA">Holy See (Vatican City State)</option>
<option value="HN">Honduras</option>
<option value="HK">Hong Kong</option>
<option value="HU">Hungary</option>
<option value="IS">Iceland</option>
<option value="ID">Indonesia</option>
<option value="IR">Iran, Islamic Republic of</option>
<option value="IQ">Iraq</option>
<option value="IE">Ireland</option>
<option value="IM">Isle of Man</option>
<option value="IL">Israel</option>
<option value="IT">Italy</option>
<option value="JM">Jamaica</option>
<option value="JP">Japan</option>
<option value="JE">Jersey</option>
<option value="JO">Jordan</option>
<option value="KZ">Kazakhstan</option>
<option value="KE">Kenya</option>
<option value="KI">Kiribati</option>
<option value="KP">Korea, Democratic People's Republic of</option>
<option value="KR">Korea, Republic of</option>
<option value="KW">Kuwait</option>
<option value="KG">Kyrgyzstan</option>
<option value="LA">Lao People's Democratic Republic</option>
<option value="LV">Latvia</option>
<option value="LB">Lebanon</option>
<option value="LS">Lesotho</option>
<option value="LR">Liberia</option>
<option value="LY">Libya</option>
<option value="LI">Liechtenstein</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="MO">Macao</option>
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
<option value="MG">Madagascar</option>
<option value="MW">Malawi</option>
<option value="MY">Malaysia</option>
<option value="MV">Maldives</option>
<option value="ML">Mali</option>
<option value="MT">Malta</option>
<option value="MH">Marshall Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MU">Mauritius</option>
<option value="YT">Mayotte</option>
<option value="MX">Mexico</option>
<option value="FM">Micronesia, Federated States of</option>
<option value="MD">Moldova, Republic of</option>
<option value="MC">Monaco</option>
<option value="MN">Mongolia</option>
<option value="ME">Montenegro</option>
<option value="MS">Montserrat</option>
<option value="MA">Morocco</option>
<option value="MZ">Mozambique</option>
<option value="MM">Myanmar</option>
<option value="NA">Namibia</option>
<option value="NR">Nauru</option>
<option value="NP">Nepal</option>
<option value="NL">Netherlands</option>
<option value="NC">New Caledonia</option>
<option value="NZ">New Zealand</option>
<option value="NI">Nicaragua</option>
<option value="NE">Niger</option>
<option value="NG">Nigeria</option>
<option value="NU">Niue</option>
<option value="NF">Norfolk Island</option>
<option value="MP">Northern Mariana Islands</option>
<option value="NO">Norway</option>
<option value="OM">Oman</option>
<option value="PK">Pakistan</option>
<option value="PW">Palau</option>
<option value="PS">Palestinian Territory, Occupied</option>
<option value="PA">Panama</option>
<option value="PG">Papua New Guinea</option>
<option value="PY">Paraguay</option>
<option value="PE">Peru</option>
<option value="PH">Philippines</option>
<option value="PN">Pitcairn</option>
<option value="PL">Poland</option>
<option value="PT">Portugal</option>
<option value="PR">Puerto Rico</option>
<option value="QA">Qatar</option>
<option value="RE">Réunion</option>
<option value="RO">Romania</option>
<option value="RU">Russian Federation</option>
<option value="RW">Rwanda</option>
<option value="BL">Saint Barthélemy</option>
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="LC">Saint Lucia</option>
<option value="MF">Saint Martin (French part)</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="WS">Samoa</option>
<option value="SM">San Marino</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SA">Saudi Arabia</option>
<option value="SN">Senegal</option>
<option value="RS">Serbia</option>
<option value="SC">Seychelles</option>
<option value="SL">Sierra Leone</option>
<option value="SG">Singapore</option>
<option value="SX">Sint Maarten (Dutch part)</option>
<option value="SK">Slovakia</option>
<option value="SI">Slovenia</option>
<option value="SB">Solomon Islands</option>
<option value="SO">Somalia</option>
<option value="ZA">South Africa</option>
<option value="GS">South Georgia and the South Sandwich Islands</option>
<option value="SS">South Sudan</option>
<option value="ES">Spain</option>
<option value="LK">Sri Lanka</option>
<option value="SD">Sudan</option>
<option value="SR">Suriname</option>
<option value="SJ">Svalbard and Jan Mayen</option>
<option value="SZ">Swaziland</option>
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syrian Arab Republic</option>
<option value="TW">Taiwan</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania, United Republic of</option>
<option value="TH">Thailand</option>
<option value="TL">Timor-Leste</option>
<option value="TG">Togo</option>
<option value="TK">Tokelau</option>
<option value="TO">Tonga</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TN">Tunisia</option>
<option value="TR">Turkey</option>
<option value="TM">Turkmenistan</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TV">Tuvalu</option>
<option value="UG">Uganda</option>
<option value="UA">Ukraine</option>
<option value="AE">United Arab Emirates</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VU">Vanuatu</option>
<option value="VE">Venezuela, Bolivarian Republic of</option>
<option value="VN">Viet Nam</option>
<option value="VG">Virgin Islands, British</option>
<option value="VI">Virgin Islands, U.S.</option>
<option value="WF">Wallis and Futuna</option>
<option value="EH">Western Sahara</option>
<option value="YE">Yemen</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>
</select>
</div>
<div class="col-3">
<div class="form-group">
<label for="addressPostalCode">{{'zipPostalCode' | i18n}}</label>
<input id="addressPostalCode" class="form-control" type="text" name="addressPostalCode"
[(ngModel)]="taxInfo.postalCode" [required]="taxInfo.country === 'US'" autocomplete="postal-code">
</div>
</div>
<div class="col-3">
<div class="form-group">
<label for="addressPostalCode">{{ "zipPostalCode" | i18n }}</label>
<input
id="addressPostalCode"
class="form-control"
type="text"
name="addressPostalCode"
[(ngModel)]="taxInfo.postalCode"
[required]="taxInfo.country === 'US'"
autocomplete="postal-code"
/>
</div>
<div class="col-6" *ngIf="organizationId && taxInfo.country !== 'US'">
<div class="form-group form-check">
<input class="form-check-input" id="addressIncludeTaxId" name="addressIncludeTaxId" type="checkbox"
[(ngModel)]="taxInfo.includeTaxId">
<label class="form-check-label" for="addressIncludeTaxId">{{'includeVAT' | i18n}}</label>
</div>
</div>
<div class="col-6" *ngIf="organizationId && taxInfo.country !== 'US'">
<div class="form-group form-check">
<input
class="form-check-input"
id="addressIncludeTaxId"
name="addressIncludeTaxId"
type="checkbox"
[(ngModel)]="taxInfo.includeTaxId"
/>
<label class="form-check-label" for="addressIncludeTaxId">{{ "includeVAT" | i18n }}</label>
</div>
</div>
</div>
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
<div class="col-6">
<div class="form-group">
<label for="taxId">{{'taxIdNumber' | i18n}}</label>
<input id="taxId" class="form-control" type="text" name="taxId" [(ngModel)]="taxInfo.taxId">
</div>
<div class="col-6">
<div class="form-group">
<label for="taxId">{{ "taxIdNumber" | i18n }}</label>
<input id="taxId" class="form-control" type="text" name="taxId" [(ngModel)]="taxInfo.taxId" />
</div>
</div>
</div>
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
<div class="col-6">
<div class="form-group">
<label for="addressLine1">{{'address1' | i18n}}</label>
<input id="addressLine1" class="form-control" type="text" name="addressLine1"
[(ngModel)]="taxInfo.line1" autocomplete="address-line1">
</div>
<div class="col-6">
<div class="form-group">
<label for="addressLine1">{{ "address1" | i18n }}</label>
<input
id="addressLine1"
class="form-control"
type="text"
name="addressLine1"
[(ngModel)]="taxInfo.line1"
autocomplete="address-line1"
/>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressLine2">{{'address2' | i18n}}</label>
<input id="addressLine2" class="form-control" type="text" name="addressLine2"
[(ngModel)]="taxInfo.line2" autocomplete="address-line2">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressLine2">{{ "address2" | i18n }}</label>
<input
id="addressLine2"
class="form-control"
type="text"
name="addressLine2"
[(ngModel)]="taxInfo.line2"
autocomplete="address-line2"
/>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressCity">{{'cityTown' | i18n}}</label>
<input id="addressCity" class="form-control" type="text" name="addressCity"
[(ngModel)]="taxInfo.city" autocomplete="address-level2">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressCity">{{ "cityTown" | i18n }}</label>
<input
id="addressCity"
class="form-control"
type="text"
name="addressCity"
[(ngModel)]="taxInfo.city"
autocomplete="address-level2"
/>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressState">{{'stateProvince' | i18n}}</label>
<input id="addressState" class="form-control" type="text" name="addressState"
[(ngModel)]="taxInfo.state" autocomplete="address-level1">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressState">{{ "stateProvince" | i18n }}</label>
<input
id="addressState"
class="form-control"
type="text"
name="addressState"
[(ngModel)]="taxInfo.state"
autocomplete="address-level1"
/>
</div>
</div>
</div>

View File

@@ -1,151 +1,157 @@
import {
Component,
EventEmitter,
Output,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { OrganizationTaxInfoUpdateRequest } from 'jslib-common/models/request/organizationTaxInfoUpdateRequest';
import { TaxInfoUpdateRequest } from 'jslib-common/models/request/taxInfoUpdateRequest';
import { TaxRateResponse } from 'jslib-common/models/response/taxRateResponse';
import { Component, EventEmitter, Output } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "jslib-common/abstractions/api.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationTaxInfoUpdateRequest } from "jslib-common/models/request/organizationTaxInfoUpdateRequest";
import { TaxInfoUpdateRequest } from "jslib-common/models/request/taxInfoUpdateRequest";
import { TaxRateResponse } from "jslib-common/models/response/taxRateResponse";
@Component({
selector: 'app-tax-info',
templateUrl: 'tax-info.component.html',
selector: "app-tax-info",
templateUrl: "tax-info.component.html",
})
export class TaxInfoComponent {
@Output() onCountryChanged = new EventEmitter();
@Output() onCountryChanged = new EventEmitter();
loading: boolean = true;
organizationId: string;
taxInfo: any = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: 'US',
includeTaxId: false,
};
loading: boolean = true;
organizationId: string;
taxInfo: any = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: "US",
includeTaxId: false,
};
taxRates: TaxRateResponse[];
taxRates: TaxRateResponse[];
private pristine: any = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: 'US',
includeTaxId: false,
};
private pristine: any = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: "US",
includeTaxId: false,
};
constructor(private apiService: ApiService, private route: ActivatedRoute, private logService: LogService) { }
constructor(
private apiService: ApiService,
private route: ActivatedRoute,
private logService: LogService
) {}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
if (this.organizationId) {
try {
const taxInfo = await this.apiService.getOrganizationTaxInfo(this.organizationId);
if (taxInfo) {
this.taxInfo.taxId = taxInfo.taxId;
this.taxInfo.state = taxInfo.state;
this.taxInfo.line1 = taxInfo.line1;
this.taxInfo.line2 = taxInfo.line2;
this.taxInfo.city = taxInfo.city;
this.taxInfo.state = taxInfo.state;
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || 'US';
this.taxInfo.includeTaxId = this.taxInfo.country !== 'US' && (
!!taxInfo.taxId
|| !!taxInfo.line1
|| !!taxInfo.line2
|| !!taxInfo.city
|| !!taxInfo.state);
}
} catch (e) {
this.logService.error(e);
}
} else {
const taxInfo = await this.apiService.getTaxInfo();
if (taxInfo) {
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || 'US';
}
}
this.pristine = Object.assign({}, this.taxInfo);
// If not the default (US) then trigger onCountryChanged
if (this.taxInfo.country !== 'US') {
this.onCountryChanged.emit();
}
});
const taxRates = await this.apiService.getTaxRates();
this.taxRates = taxRates.data;
this.loading = false;
}
get taxRate() {
if (this.taxRates != null) {
const localTaxRate = this.taxRates.find(x =>
x.country === this.taxInfo.country &&
x.postalCode === this.taxInfo.postalCode
);
return localTaxRate?.rate ?? null;
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
if (this.organizationId) {
try {
const taxInfo = await this.apiService.getOrganizationTaxInfo(this.organizationId);
if (taxInfo) {
this.taxInfo.taxId = taxInfo.taxId;
this.taxInfo.state = taxInfo.state;
this.taxInfo.line1 = taxInfo.line1;
this.taxInfo.line2 = taxInfo.line2;
this.taxInfo.city = taxInfo.city;
this.taxInfo.state = taxInfo.state;
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || "US";
this.taxInfo.includeTaxId =
this.taxInfo.country !== "US" &&
(!!taxInfo.taxId ||
!!taxInfo.line1 ||
!!taxInfo.line2 ||
!!taxInfo.city ||
!!taxInfo.state);
}
} catch (e) {
this.logService.error(e);
}
}
getTaxInfoRequest(): TaxInfoUpdateRequest {
if (this.organizationId) {
const request = new OrganizationTaxInfoUpdateRequest();
request.taxId = this.taxInfo.taxId;
request.state = this.taxInfo.state;
request.line1 = this.taxInfo.line1;
request.line2 = this.taxInfo.line2;
request.city = this.taxInfo.city;
request.state = this.taxInfo.state;
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
} else {
const request = new TaxInfoUpdateRequest();
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
}
}
submitTaxInfo(): Promise<any> {
if (!this.hasChanged()) {
return new Promise<void>(resolve => { resolve(); });
}
const request = this.getTaxInfoRequest();
return this.organizationId ? this.apiService.putOrganizationTaxInfo(this.organizationId,
request as OrganizationTaxInfoUpdateRequest) : this.apiService.putTaxInfo(request);
}
changeCountry() {
if (this.taxInfo.country === 'US') {
this.taxInfo.includeTaxId = false;
this.taxInfo.taxId = null;
this.taxInfo.line1 = null;
this.taxInfo.line2 = null;
this.taxInfo.city = null;
this.taxInfo.state = null;
} else {
const taxInfo = await this.apiService.getTaxInfo();
if (taxInfo) {
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || "US";
}
}
this.pristine = Object.assign({}, this.taxInfo);
// If not the default (US) then trigger onCountryChanged
if (this.taxInfo.country !== "US") {
this.onCountryChanged.emit();
}
}
});
private hasChanged(): boolean {
for (const key in this.taxInfo) {
if (this.pristine.hasOwnProperty(key) && this.pristine[key] !== this.taxInfo[key]) {
return true;
}
}
return false;
const taxRates = await this.apiService.getTaxRates();
this.taxRates = taxRates.data;
this.loading = false;
}
get taxRate() {
if (this.taxRates != null) {
const localTaxRate = this.taxRates.find(
(x) => x.country === this.taxInfo.country && x.postalCode === this.taxInfo.postalCode
);
return localTaxRate?.rate ?? null;
}
}
getTaxInfoRequest(): TaxInfoUpdateRequest {
if (this.organizationId) {
const request = new OrganizationTaxInfoUpdateRequest();
request.taxId = this.taxInfo.taxId;
request.state = this.taxInfo.state;
request.line1 = this.taxInfo.line1;
request.line2 = this.taxInfo.line2;
request.city = this.taxInfo.city;
request.state = this.taxInfo.state;
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
} else {
const request = new TaxInfoUpdateRequest();
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
}
}
submitTaxInfo(): Promise<any> {
if (!this.hasChanged()) {
return new Promise<void>((resolve) => {
resolve();
});
}
const request = this.getTaxInfoRequest();
return this.organizationId
? this.apiService.putOrganizationTaxInfo(
this.organizationId,
request as OrganizationTaxInfoUpdateRequest
)
: this.apiService.putTaxInfo(request);
}
changeCountry() {
if (this.taxInfo.country === "US") {
this.taxInfo.includeTaxId = false;
this.taxInfo.taxId = null;
this.taxInfo.line1 = null;
this.taxInfo.line2 = null;
this.taxInfo.city = null;
this.taxInfo.state = null;
}
this.onCountryChanged.emit();
}
private hasChanged(): boolean {
for (const key in this.taxInfo) {
if (this.pristine.hasOwnProperty(key) && this.pristine[key] !== this.taxInfo[key]) {
return true;
}
}
return false;
}
}

View File

@@ -1,77 +1,112 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faAuthenticatorTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faAuthenticatorTitle">
{{'twoStepLogin' | i18n}}
<small>{{'authenticatorAppTitle' | i18n}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
*ngIf="!authed">
</app-two-factor-verify>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
<div class="modal-body">
<ng-container *ngIf="!enabled">
<img class="float-right mfaType0" alt="Authenticator app logo">
<p>{{'twoStepAuthenticatorDesc' | i18n}}</p>
<p>
<strong>1. {{'twoStepAuthenticatorDownloadApp' | i18n}}</strong>
</p>
</ng-container>
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle">
<p>{{'twoStepLoginProviderEnabled' | i18n}}</p>
{{'twoStepAuthenticatorReaddDesc' | i18n}}
</app-callout>
<img class="float-right mfaType0" alt="Authenticator app logo">
<p>{{'twoStepAuthenticatorNeedApp' | i18n}}</p>
</ng-container>
<ul class="fa-ul">
<li>
<i class="fa-li fa fa-apple"></i>{{'iosDevices' | i18n}}:
<a href="https://itunes.apple.com/us/app/authy/id494168017?mt=8" target="_blank"
rel="noopener">Authy</a>
</li>
<li>
<i class="fa-li fa fa-android"></i>{{'androidDevices' | i18n}}:
<a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank"
rel="noopener">Authy</a>
</li>
<li>
<i class="fa-li fa fa-windows"></i>{{'windowsDevices' | i18n}}:
<a href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj" target="_blank"
rel="noopener">Microsoft Authenticator</a>
</li>
</ul>
<p>{{'twoStepAuthenticatorAppsRecommended' | i18n}}</p>
<p *ngIf="!enabled">
<strong>2. {{'twoStepAuthenticatorScanCode' | i18n}}</strong>
</p>
<hr *ngIf="enabled">
<p class="text-center" [ngClass]="{'mb-0': enabled}">
<canvas id="qr"></canvas><br>
<code appA11yTitle="{{'key' | i18n}}">{{key}}</code>
</p>
<ng-container *ngIf="!enabled">
<label for="token">3. {{'twoStepAuthenticatorEnterCode' | i18n}}</label>
<input id="token" type="text" name="Token" class="form-control" [(ngModel)]="token" required
appInputVerbatim>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
<span *ngIf="enabled">{{'disable' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faAuthenticatorTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "authenticatorAppTitle" | i18n }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($event)"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
>
<div class="modal-body">
<ng-container *ngIf="!enabled">
<img class="float-right mfaType0" alt="Authenticator app logo" />
<p>{{ "twoStepAuthenticatorDesc" | i18n }}</p>
<p>
<strong>1. {{ "twoStepAuthenticatorDownloadApp" | i18n }}</strong>
</p>
</ng-container>
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="fa-check-circle">
<p>{{ "twoStepLoginProviderEnabled" | i18n }}</p>
{{ "twoStepAuthenticatorReaddDesc" | i18n }}
</app-callout>
<img class="float-right mfaType0" alt="Authenticator app logo" />
<p>{{ "twoStepAuthenticatorNeedApp" | i18n }}</p>
</ng-container>
<ul class="fa-ul">
<li>
<i class="fa-li fa fa-apple"></i>{{ "iosDevices" | i18n }}:
<a
href="https://itunes.apple.com/us/app/authy/id494168017?mt=8"
target="_blank"
rel="noopener"
>Authy</a
>
</li>
<li>
<i class="fa-li fa fa-android"></i>{{ "androidDevices" | i18n }}:
<a
href="https://play.google.com/store/apps/details?id=com.authy.authy"
target="_blank"
rel="noopener"
>Authy</a
>
</li>
<li>
<i class="fa-li fa fa-windows"></i>{{ "windowsDevices" | i18n }}:
<a
href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj"
target="_blank"
rel="noopener"
>Microsoft Authenticator</a
>
</li>
</ul>
<p>{{ "twoStepAuthenticatorAppsRecommended" | i18n }}</p>
<p *ngIf="!enabled">
<strong>2. {{ "twoStepAuthenticatorScanCode" | i18n }}</strong>
</p>
<hr *ngIf="enabled" />
<p class="text-center" [ngClass]="{ 'mb-0': enabled }">
<canvas id="qr"></canvas><br />
<code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code>
</p>
<ng-container *ngIf="!enabled">
<label for="token">3. {{ "twoStepAuthenticatorEnterCode" | i18n }}</label>
<input
id="token"
type="text"
name="Token"
class="form-control"
[(ngModel)]="token"
required
appInputVerbatim
/>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,100 +1,97 @@
import {
Component,
OnDestroy,
OnInit,
} from '@angular/core';
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { UpdateTwoFactorAuthenticatorRequest } from 'jslib-common/models/request/updateTwoFactorAuthenticatorRequest';
import { TwoFactorAuthenticatorResponse } from 'jslib-common/models/response/twoFactorAuthenticatorResponse';
import { UpdateTwoFactorAuthenticatorRequest } from "jslib-common/models/request/updateTwoFactorAuthenticatorRequest";
import { TwoFactorAuthenticatorResponse } from "jslib-common/models/response/twoFactorAuthenticatorResponse";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { TwoFactorBaseComponent } from './two-factor-base.component';
import { TwoFactorBaseComponent } from "./two-factor-base.component";
@Component({
selector: 'app-two-factor-authenticator',
templateUrl: 'two-factor-authenticator.component.html',
selector: "app-two-factor-authenticator",
templateUrl: "two-factor-authenticator.component.html",
})
export class TwoFactorAuthenticatorComponent extends TwoFactorBaseComponent implements OnInit, OnDestroy {
type = TwoFactorProviderType.Authenticator;
key: string;
token: string;
formPromise: Promise<any>;
export class TwoFactorAuthenticatorComponent
extends TwoFactorBaseComponent
implements OnInit, OnDestroy
{
type = TwoFactorProviderType.Authenticator;
key: string;
token: string;
formPromise: Promise<any>;
private qrScript: HTMLScriptElement;
private qrScript: HTMLScriptElement;
constructor(
apiService: ApiService,
i18nService: I18nService,
userVerificationService: UserVerificationService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
private stateService: StateService
) {
super(
apiService,
i18nService,
platformUtilsService,
logService,
userVerificationService
);
this.qrScript = window.document.createElement('script');
this.qrScript.src = 'scripts/qrious.min.js';
this.qrScript.async = true;
constructor(
apiService: ApiService,
i18nService: I18nService,
userVerificationService: UserVerificationService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
private stateService: StateService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
this.qrScript = window.document.createElement("script");
this.qrScript.src = "scripts/qrious.min.js";
this.qrScript.async = true;
}
ngOnInit() {
window.document.body.appendChild(this.qrScript);
}
ngOnDestroy() {
window.document.body.removeChild(this.qrScript);
}
auth(authResponse: any) {
super.auth(authResponse);
return this.processResponse(authResponse.response);
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
ngOnInit() {
window.document.body.appendChild(this.qrScript);
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
request.token = this.token;
request.key = this.key;
ngOnDestroy() {
window.document.body.removeChild(this.qrScript);
}
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorAuthenticator(request);
const response = await this.formPromise;
await this.processResponse(response);
});
}
auth(authResponse: any) {
super.auth(authResponse);
return this.processResponse(authResponse.response);
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
request.token = this.token;
request.key = this.key;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorAuthenticator(request);
const response = await this.formPromise;
await this.processResponse(response);
});
}
private async processResponse(response: TwoFactorAuthenticatorResponse) {
this.token = null;
this.enabled = response.enabled;
this.key = response.key;
const email = await this.stateService.getEmail();
window.setTimeout(() => {
const qr = new (window as any).QRious({
element: document.getElementById('qr'),
value: 'otpauth://totp/Bitwarden:' + encodeURIComponent(email) +
'?secret=' + encodeURIComponent(this.key) + '&issuer=Bitwarden',
size: 160,
});
}, 100);
}
private async processResponse(response: TwoFactorAuthenticatorResponse) {
this.token = null;
this.enabled = response.enabled;
this.key = response.key;
const email = await this.stateService.getEmail();
window.setTimeout(() => {
const qr = new (window as any).QRious({
element: document.getElementById("qr"),
value:
"otpauth://totp/Bitwarden:" +
encodeURIComponent(email) +
"?secret=" +
encodeURIComponent(this.key) +
"&issuer=Bitwarden",
size: 160,
});
}, 100);
}
}

View File

@@ -1,81 +1,92 @@
import {
Directive,
EventEmitter,
Output,
} from '@angular/core';
import { Directive, EventEmitter, Output } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { VerificationType } from 'jslib-common/enums/verificationType';
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { VerificationType } from "jslib-common/enums/verificationType";
import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest';
import { TwoFactorProviderRequest } from 'jslib-common/models/request/twoFactorProviderRequest';
import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
import { TwoFactorProviderRequest } from "jslib-common/models/request/twoFactorProviderRequest";
@Directive()
export abstract class TwoFactorBaseComponent {
@Output() onUpdated = new EventEmitter<boolean>();
@Output() onUpdated = new EventEmitter<boolean>();
type: TwoFactorProviderType;
organizationId: string;
twoFactorProviderType = TwoFactorProviderType;
enabled = false;
authed = false;
type: TwoFactorProviderType;
organizationId: string;
twoFactorProviderType = TwoFactorProviderType;
enabled = false;
authed = false;
protected hashedSecret: string;
protected verificationType: VerificationType;
protected hashedSecret: string;
protected verificationType: VerificationType;
constructor(protected apiService: ApiService, protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected logService: LogService, protected userVerificationService: UserVerificationService) { }
constructor(
protected apiService: ApiService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected logService: LogService,
protected userVerificationService: UserVerificationService
) {}
protected auth(authResponse: any) {
this.hashedSecret = authResponse.secret;
this.verificationType = authResponse.verificationType;
this.authed = true;
protected auth(authResponse: any) {
this.hashedSecret = authResponse.secret;
this.verificationType = authResponse.verificationType;
this.authed = true;
}
protected async enable(enableFunction: () => Promise<void>) {
try {
await enableFunction();
this.onUpdated.emit(true);
} catch (e) {
this.logService.error(e);
}
}
protected async disable(promise: Promise<any>) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("twoStepDisableDesc"),
this.i18nService.t("disable"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return;
}
protected async enable(enableFunction: () => Promise<void>) {
try {
await enableFunction();
this.onUpdated.emit(true);
} catch (e) {
this.logService.error(e);
}
try {
const request = await this.buildRequestModel(TwoFactorProviderRequest);
request.type = this.type;
if (this.organizationId != null) {
promise = this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
} else {
promise = this.apiService.putTwoFactorDisable(request);
}
await promise;
this.enabled = false;
this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled"));
this.onUpdated.emit(false);
} catch (e) {
this.logService.error(e);
}
}
protected async disable(promise: Promise<any>) {
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('twoStepDisableDesc'),
this.i18nService.t('disable'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
try {
const request = await this.buildRequestModel(TwoFactorProviderRequest);
request.type = this.type;
if (this.organizationId != null) {
promise = this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
} else {
promise = this.apiService.putTwoFactorDisable(request);
}
await promise;
this.enabled = false;
this.platformUtilsService.showToast('success', null, this.i18nService.t('twoStepDisabled'));
this.onUpdated.emit(false);
} catch (e) {
this.logService.error(e);
}
}
protected async buildRequestModel<T extends SecretVerificationRequest>(requestClass: new() => T) {
return this.userVerificationService.buildRequest({
secret: this.hashedSecret,
type: this.verificationType,
}, requestClass, true);
}
protected async buildRequestModel<T extends SecretVerificationRequest>(
requestClass: new () => T
) {
return this.userVerificationService.buildRequest(
{
secret: this.hashedSecret,
type: this.verificationType,
},
requestClass,
true
);
}
}

View File

@@ -1,62 +1,101 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faDuoTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" title="2faDuoTitle">
{{'twoStepLogin' | i18n}}
<small>Duo</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" title="2faDuoTitle">
{{ "twoStepLogin" | i18n }}
<small>Duo</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($event)"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
autocomplete="off"
>
<div class="modal-body">
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="fa-check-circle">
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<img class="float-right ml-3 mfaType2" alt="Duo logo" />
<strong>{{ "twoFactorDuoIntegrationKey" | i18n }}:</strong> {{ ikey }}
<br />
<strong>{{ "twoFactorDuoSecretKey" | i18n }}:</strong> {{ skey }}
<br />
<strong>{{ "twoFactorDuoApiHostname" | i18n }}:</strong> {{ host }}
</ng-container>
<ng-container *ngIf="!enabled">
<img class="float-right ml-3 mfaType2" alt="Duo logo" />
<p>{{ "twoFactorDuoDesc" | i18n }}</p>
<div class="form-group">
<label for="ikey">{{ "twoFactorDuoIntegrationKey" | i18n }}</label>
<input
id="ikey"
type="text"
name="IntegrationKey"
class="form-control"
[(ngModel)]="ikey"
required
appInputVerbatim
/>
</div>
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
*ngIf="!authed">
</app-two-factor-verify>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed"
autocomplete="off">
<div class="modal-body">
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle">
{{'twoStepLoginProviderEnabled' | i18n}}
</app-callout>
<img class="float-right ml-3 mfaType2" alt="Duo logo">
<strong>{{'twoFactorDuoIntegrationKey' | i18n}}:</strong> {{ikey}}
<br>
<strong>{{'twoFactorDuoSecretKey' | i18n}}:</strong> {{skey}}
<br>
<strong>{{'twoFactorDuoApiHostname' | i18n}}:</strong> {{host}}
</ng-container>
<ng-container *ngIf="!enabled">
<img class="float-right ml-3 mfaType2" alt="Duo logo">
<p>{{'twoFactorDuoDesc' | i18n}}</p>
<div class="form-group">
<label for="ikey">{{'twoFactorDuoIntegrationKey' | i18n}}</label>
<input id="ikey" type="text" name="IntegrationKey" class="form-control" [(ngModel)]="ikey"
required appInputVerbatim>
</div>
<div class="form-group">
<label for="skey">{{'twoFactorDuoSecretKey' | i18n}}</label>
<input id="skey" type="password" name="SecretKey" class="form-control" [(ngModel)]="skey"
required appInputVerbatim autocomplete="new-password">
</div>
<div class="form-group">
<label for="host">{{'twoFactorDuoApiHostname' | i18n}}</label>
<input id="host" type="text" name="Host" class="form-control" [(ngModel)]="host"
placeholder="{{'ex' | i18n}} api-xxxxxxxx.duosecurity.com" required appInputVerbatim>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
<span *ngIf="enabled">{{'disable' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
<div class="form-group">
<label for="skey">{{ "twoFactorDuoSecretKey" | i18n }}</label>
<input
id="skey"
type="password"
name="SecretKey"
class="form-control"
[(ngModel)]="skey"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
<div class="form-group">
<label for="host">{{ "twoFactorDuoApiHostname" | i18n }}</label>
<input
id="host"
type="text"
name="Host"
class="form-control"
[(ngModel)]="host"
placeholder="{{ 'ex' | i18n }} api-xxxxxxxx.duosecurity.com"
required
appInputVerbatim
/>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,68 +1,75 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { UpdateTwoFactorDuoRequest } from 'jslib-common/models/request/updateTwoFactorDuoRequest';
import { TwoFactorDuoResponse } from 'jslib-common/models/response/twoFactorDuoResponse';
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { UpdateTwoFactorDuoRequest } from "jslib-common/models/request/updateTwoFactorDuoRequest";
import { TwoFactorDuoResponse } from "jslib-common/models/response/twoFactorDuoResponse";
import { TwoFactorBaseComponent } from './two-factor-base.component';
import { TwoFactorBaseComponent } from "./two-factor-base.component";
@Component({
selector: 'app-two-factor-duo',
templateUrl: 'two-factor-duo.component.html',
selector: "app-two-factor-duo",
templateUrl: "two-factor-duo.component.html",
})
export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Duo;
ikey: string;
skey: string;
host: string;
formPromise: Promise<any>;
type = TwoFactorProviderType.Duo;
ikey: string;
skey: string;
host: string;
formPromise: Promise<any>;
constructor(apiService: ApiService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService, userVerificationService: UserVerificationService) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
userVerificationService: UserVerificationService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
}
auth(authResponse: any) {
super.auth(authResponse);
this.processResponse(authResponse.response);
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
auth(authResponse: any) {
super.auth(authResponse);
this.processResponse(authResponse.response);
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest);
request.integrationKey = this.ikey;
request.secretKey = this.skey;
request.host = this.host;
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
return super.enable(async () => {
if (this.organizationId != null) {
this.formPromise = this.apiService.putTwoFactorOrganizationDuo(
this.organizationId,
request
);
} else {
this.formPromise = this.apiService.putTwoFactorDuo(request);
}
const response = await this.formPromise;
await this.processResponse(response);
});
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest);
request.integrationKey = this.ikey;
request.secretKey = this.skey;
request.host = this.host;
return super.enable(async () => {
if (this.organizationId != null) {
this.formPromise = this.apiService.putTwoFactorOrganizationDuo(this.organizationId, request);
} else {
this.formPromise = this.apiService.putTwoFactorDuo(request);
}
const response = await this.formPromise;
await this.processResponse(response);
});
}
private processResponse(response: TwoFactorDuoResponse) {
this.ikey = response.integrationKey;
this.skey = response.secretKey;
this.host = response.host;
this.enabled = response.enabled;
}
private processResponse(response: TwoFactorDuoResponse) {
this.ikey = response.integrationKey;
this.skey = response.secretKey;
this.host = response.host;
this.enabled = response.enabled;
}
}

View File

@@ -1,64 +1,104 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faEmailTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faEmailTitle">
{{'twoStepLogin' | i18n}}
<small>{{'emailTitle' | i18n}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faEmailTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "emailTitle" | i18n }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($event)"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
>
<div class="modal-body">
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="fa-check-circle">
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<strong>{{ "email" | i18n }}:</strong> {{ email }}
</ng-container>
<ng-container *ngIf="!enabled">
<p class="d-flex">
<span class="mr-3">{{ "twoFactorEmailDesc" | i18n }}</span>
<img class="float-right ml-auto mfaType1" alt="Email logo" />
</p>
<div class="form-group">
<label for="email">1. {{ "twoFactorEmailEnterEmail" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
class="form-control"
[(ngModel)]="email"
required
inputmode="email"
appInputVerbatim="false"
/>
</div>
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
*ngIf="!authed">
</app-two-factor-verify>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
<div class="modal-body">
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle">
{{'twoStepLoginProviderEnabled' | i18n}}
</app-callout>
<strong>{{'email' | i18n}}:</strong> {{email}}
</ng-container>
<ng-container *ngIf="!enabled">
<p class="d-flex">
<span class="mr-3">{{'twoFactorEmailDesc' | i18n}}</span>
<img class="float-right ml-auto mfaType1" alt="Email logo">
</p>
<div class="form-group">
<label for="email">1. {{'twoFactorEmailEnterEmail' | i18n}}</label>
<input id="email" type="text" name="Email" class="form-control" [(ngModel)]="email" required
inputmode="email" appInputVerbatim="false">
</div>
<div class="mb-3 d-flex">
<button #sendBtn type="button"
class="btn btn-outline-primary btn-sm btn-submit align-self-start" (click)="sendEmail()"
[appApiAction]="emailPromise" [disabled]="sendBtn.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'sendEmail' | i18n}}</span>
</button>
<span class="text-success ml-3" *ngIf="sentEmail">
{{'verificationCodeEmailSent' | i18n : sentEmail}}
</span>
</div>
<div class="form-group">
<label for="token">2. {{'twoFactorEmailEnterCode' | i18n}}</label>
<input id="token" type="text" name="Token" class="form-control" [(ngModel)]="token" required
appInputVerbatim>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
<span *ngIf="enabled">{{'disable' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
<div class="mb-3 d-flex">
<button
#sendBtn
type="button"
class="btn btn-outline-primary btn-sm btn-submit align-self-start"
(click)="sendEmail()"
[appApiAction]="emailPromise"
[disabled]="sendBtn.loading"
>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "sendEmail" | i18n }}</span>
</button>
<span class="text-success ml-3" *ngIf="sentEmail">
{{ "verificationCodeEmailSent" | i18n: sentEmail }}
</span>
</div>
<div class="form-group">
<label for="token">2. {{ "twoFactorEmailEnterCode" | i18n }}</label>
<input
id="token"
type="text"
name="Token"
class="form-control"
[(ngModel)]="token"
required
appInputVerbatim
/>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,92 +1,86 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { TwoFactorEmailRequest } from 'jslib-common/models/request/twoFactorEmailRequest';
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { UpdateTwoFactorEmailRequest } from 'jslib-common/models/request/updateTwoFactorEmailRequest';
import { TwoFactorEmailResponse } from 'jslib-common/models/response/twoFactorEmailResponse';
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { UpdateTwoFactorEmailRequest } from "jslib-common/models/request/updateTwoFactorEmailRequest";
import { TwoFactorEmailResponse } from "jslib-common/models/response/twoFactorEmailResponse";
import { TwoFactorBaseComponent } from './two-factor-base.component';
import { TwoFactorBaseComponent } from "./two-factor-base.component";
@Component({
selector: 'app-two-factor-email',
templateUrl: 'two-factor-email.component.html',
selector: "app-two-factor-email",
templateUrl: "two-factor-email.component.html",
})
export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Email;
email: string;
token: string;
sentEmail: string;
formPromise: Promise<any>;
emailPromise: Promise<any>;
type = TwoFactorProviderType.Email;
email: string;
token: string;
sentEmail: string;
formPromise: Promise<any>;
emailPromise: Promise<any>;
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
userVerificationService: UserVerificationService,
private stateService: StateService,
) {
super(
apiService,
i18nService,
platformUtilsService,
logService,
userVerificationService
);
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
userVerificationService: UserVerificationService,
private stateService: StateService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
}
auth(authResponse: any) {
super.auth(authResponse);
return this.processResponse(authResponse.response);
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
auth(authResponse: any) {
super.auth(authResponse);
return this.processResponse(authResponse.response);
async sendEmail() {
try {
const request = await this.buildRequestModel(TwoFactorEmailRequest);
request.email = this.email;
this.emailPromise = this.apiService.postTwoFactorEmailSetup(request);
await this.emailPromise;
this.sentEmail = this.email;
} catch (e) {
this.logService.error(e);
}
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
}
}
async sendEmail() {
try {
const request = await this.buildRequestModel(TwoFactorEmailRequest);
request.email = this.email;
this.emailPromise = this.apiService.postTwoFactorEmailSetup(request);
await this.emailPromise;
this.sentEmail = this.email;
} catch (e) {
this.logService.error(e);
}
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorEmailRequest);
request.email = this.email;
request.token = this.token;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorEmail(request);
const response = await this.formPromise;
await this.processResponse(response);
});
}
private async processResponse(response: TwoFactorEmailResponse) {
this.token = null;
this.email = response.email;
this.enabled = response.enabled;
if (!this.enabled && (this.email == null || this.email === '')) {
this.email = await this.stateService.getEmail();
}
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorEmailRequest);
request.email = this.email;
request.token = this.token;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorEmail(request);
const response = await this.formPromise;
await this.processResponse(response);
});
}
private async processResponse(response: TwoFactorEmailResponse) {
this.token = null;
this.email = response.email;
this.enabled = response.enabled;
if (!this.enabled && (this.email == null || this.email === "")) {
this.email = await this.stateService.getEmail();
}
}
}

View File

@@ -1,35 +1,46 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faRecoveryTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faRecoveryTitle">
{{'twoStepLogin' | i18n}}
<small>{{'recoveryCodeTitle' | i18n}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
*ngIf="!authed">
</app-two-factor-verify>
<ng-container *ngIf="authed">
<div class="modal-body text-center">
<ng-container *ngIf="code">
<p>{{'twoFactorRecoveryYourCode' | i18n}}:</p>
<code class="text-lg">{{code}}</code>
</ng-container>
<ng-container *ngIf="!code">
{{'twoFactorRecoveryNoCode' | i18n}}
</ng-container>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="print()"
*ngIf="code">{{'printCode' | i18n}}</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</ng-container>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faRecoveryTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "recoveryCodeTitle" | i18n }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($event)"
*ngIf="!authed"
>
</app-two-factor-verify>
<ng-container *ngIf="authed">
<div class="modal-body text-center">
<ng-container *ngIf="code">
<p>{{ "twoFactorRecoveryYourCode" | i18n }}:</p>
<code class="text-lg">{{ code }}</code>
</ng-container>
<ng-container *ngIf="!code">
{{ "twoFactorRecoveryNoCode" | i18n }}
</ng-container>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="print()" *ngIf="code">
{{ "printCode" | i18n }}
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</ng-container>
</div>
</div>
</div>

View File

@@ -1,47 +1,57 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { TwoFactorRecoverResponse } from 'jslib-common/models/response/twoFactorRescoverResponse';
import { TwoFactorRecoverResponse } from "jslib-common/models/response/twoFactorRescoverResponse";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
@Component({
selector: 'app-two-factor-recovery',
templateUrl: 'two-factor-recovery.component.html',
selector: "app-two-factor-recovery",
templateUrl: "two-factor-recovery.component.html",
})
export class TwoFactorRecoveryComponent {
type = -1;
code: string;
authed: boolean;
twoFactorProviderType = TwoFactorProviderType;
type = -1;
code: string;
authed: boolean;
twoFactorProviderType = TwoFactorProviderType;
constructor(private i18nService: I18nService) { }
constructor(private i18nService: I18nService) {}
auth(authResponse: any) {
this.authed = true;
this.processResponse(authResponse.response);
auth(authResponse: any) {
this.authed = true;
this.processResponse(authResponse.response);
}
print() {
const w = window.open();
w.document.write(
'<div style="font-size: 18px; text-align: center;">' +
"<p>" +
this.i18nService.t("twoFactorRecoveryYourCode") +
":</p>" +
"<code style=\"font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;\">" +
this.code +
"</code></div>" +
'<p style="text-align: center;">' +
new Date() +
"</p>"
);
w.onafterprint = () => w.close();
w.print();
}
private formatString(s: string) {
if (s == null) {
return null;
}
return s
.replace(/(.{4})/g, "$1 ")
.trim()
.toUpperCase();
}
print() {
const w = window.open();
w.document.write('<div style="font-size: 18px; text-align: center;">' +
'<p>' + this.i18nService.t('twoFactorRecoveryYourCode') + ':</p>' +
'<code style="font-family: Menlo, Monaco, Consolas, \'Courier New\', monospace;">' +
this.code + '</code></div>' +
'<p style="text-align: center;">' + new Date() + '</p>');
w.onafterprint = () => w.close();
w.print();
}
private formatString(s: string) {
if (s == null) {
return null;
}
return s.replace(/(.{4})/g, '$1 ').trim().toUpperCase();
}
private processResponse(response: TwoFactorRecoverResponse) {
this.code = this.formatString(response.code);
}
private processResponse(response: TwoFactorRecoverResponse) {
this.code = this.formatString(response.code);
}
}

View File

@@ -1,49 +1,67 @@
<div class="page-header">
<h1>{{'twoStepLogin' | i18n}}</h1>
<h1>{{ "twoStepLogin" | i18n }}</h1>
</div>
<p *ngIf="!organizationId">{{'twoStepLoginDesc' | i18n}}</p>
<p *ngIf="organizationId">{{'twoStepLoginOrganizationDesc' | i18n}}</p>
<p *ngIf="!organizationId">{{ "twoStepLoginDesc" | i18n }}</p>
<p *ngIf="organizationId">{{ "twoStepLoginOrganizationDesc" | i18n }}</p>
<app-callout type="warning" *ngIf="!organizationId">
<p>{{'twoStepLoginRecoveryWarning' | i18n}}</p>
<button type="button" class="btn btn-outline-secondary"
(click)="recoveryCode()">{{'viewRecoveryCode' | i18n}}</button>
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
<button type="button" class="btn btn-outline-secondary" (click)="recoveryCode()">
{{ "viewRecoveryCode" | i18n }}
</button>
</app-callout>
<h2 [ngClass]="{'mt-5':!organizationId}">
{{'providers' | i18n}}
<small *ngIf="loading">
<i class="fa fa-spinner fa-spin fa-fw text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</small>
<h2 [ngClass]="{ 'mt-5': !organizationId }">
{{ "providers" | i18n }}
<small *ngIf="loading">
<i
class="fa fa-spinner fa-spin fa-fw text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h2>
<app-callout type="warning" *ngIf="showPolicyWarning">
{{'twoStepLoginPolicyUserWarning' | i18n}}
{{ "twoStepLoginPolicyUserWarning" | i18n }}
</app-callout>
<ul class="list-group list-group-2fa">
<li *ngFor="let p of providers" class="list-group-item d-flex align-items-center">
<div class="logo-2fa d-flex justify-content-center">
<img [class]="'mfaType' + p.type" [alt]="p.name + ' logo'">
</div>
<div class="mx-4">
<h3 class="mb-0">
{{p.name}}
<ng-container *ngIf="p.enabled">
<i class="fa fa-check text-success fa-fw" title="{{'enabled' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'enabled' | i18n}}</span>
</ng-container>
<a href="#" appStopClick class="badge badge-primary" *ngIf="!canAccessPremium && p.premium"
(click)="premiumRequired()">
{{'premium' | i18n}}
</a>
</h3>
{{p.description}}
</div>
<div class="ml-auto">
<button type="button" class="btn btn-outline-secondary btn-sm" [disabled]="!canAccessPremium && p.premium"
(click)="manage(p.type)">
{{'manage' | i18n}}
</button>
</div>
</li>
<li *ngFor="let p of providers" class="list-group-item d-flex align-items-center">
<div class="logo-2fa d-flex justify-content-center">
<img [class]="'mfaType' + p.type" [alt]="p.name + ' logo'" />
</div>
<div class="mx-4">
<h3 class="mb-0">
{{ p.name }}
<ng-container *ngIf="p.enabled">
<i
class="fa fa-check text-success fa-fw"
title="{{ 'enabled' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "enabled" | i18n }}</span>
</ng-container>
<a
href="#"
appStopClick
class="badge badge-primary"
*ngIf="!canAccessPremium && p.premium"
(click)="premiumRequired()"
>
{{ "premium" | i18n }}
</a>
</h3>
{{ p.description }}
</div>
<div class="ml-auto">
<button
type="button"
class="btn btn-outline-secondary btn-sm"
[disabled]="!canAccessPremium && p.premium"
(click)="manage(p.type)"
>
{{ "manage" | i18n }}
</button>
</div>
</li>
</ul>
<ng-template #authenticatorTemplate></ng-template>

View File

@@ -1,176 +1,187 @@
import {
Component,
OnInit,
Type,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Component, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TwoFactorProviders } from 'jslib-common/services/auth.service';
import { TwoFactorProviders } from "jslib-common/services/auth.service";
import { ModalService } from 'jslib-angular/services/modal.service';
import { ModalService } from "jslib-angular/services/modal.service";
import { ModalRef } from 'jslib-angular/components/modal/modal.ref';
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
import { PolicyType } from 'jslib-common/enums/policyType';
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { PolicyType } from "jslib-common/enums/policyType";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { TwoFactorAuthenticatorComponent } from './two-factor-authenticator.component';
import { TwoFactorDuoComponent } from './two-factor-duo.component';
import { TwoFactorEmailComponent } from './two-factor-email.component';
import { TwoFactorRecoveryComponent } from './two-factor-recovery.component';
import { TwoFactorWebAuthnComponent } from './two-factor-webauthn.component';
import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
import { TwoFactorDuoComponent } from "./two-factor-duo.component";
import { TwoFactorEmailComponent } from "./two-factor-email.component";
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component";
import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
@Component({
selector: 'app-two-factor-setup',
templateUrl: 'two-factor-setup.component.html',
selector: "app-two-factor-setup",
templateUrl: "two-factor-setup.component.html",
})
export class TwoFactorSetupComponent implements OnInit {
@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('duoTemplate', { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
@ViewChild('emailTemplate', { read: ViewContainerRef, static: true }) emailModalRef: ViewContainerRef;
@ViewChild('webAuthnTemplate', { read: ViewContainerRef, static: true }) webAuthnModalRef: 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("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
@ViewChild("emailTemplate", { read: ViewContainerRef, static: true })
emailModalRef: ViewContainerRef;
@ViewChild("webAuthnTemplate", { read: ViewContainerRef, static: true })
webAuthnModalRef: ViewContainerRef;
organizationId: string;
providers: any[] = [];
canAccessPremium: boolean;
showPolicyWarning = false;
loading = true;
modal: ModalRef;
organizationId: string;
providers: any[] = [];
canAccessPremium: boolean;
showPolicyWarning = false;
loading = true;
modal: ModalRef;
constructor(protected apiService: ApiService, protected modalService: ModalService,
protected messagingService: MessagingService, protected policyService: PolicyService,
private stateService: StateService) { }
constructor(
protected apiService: ApiService,
protected modalService: ModalService,
protected messagingService: MessagingService,
protected policyService: PolicyService,
private stateService: StateService
) {}
async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
for (const key in TwoFactorProviders) {
if (!TwoFactorProviders.hasOwnProperty(key)) {
continue;
}
for (const key in TwoFactorProviders) {
if (!TwoFactorProviders.hasOwnProperty(key)) {
continue;
}
const p = (TwoFactorProviders as any)[key];
if (this.filterProvider(p.type)) {
continue;
}
const p = (TwoFactorProviders as any)[key];
if (this.filterProvider(p.type)) {
continue;
}
this.providers.push({
type: p.type,
name: p.name,
description: p.description,
enabled: false,
premium: p.premium,
sort: p.sort,
});
}
this.providers.sort((a: any, b: any) => a.sort - b.sort);
await this.load();
this.providers.push({
type: p.type,
name: p.name,
description: p.description,
enabled: false,
premium: p.premium,
sort: p.sort,
});
}
async load() {
this.loading = true;
const providerList = await this.getTwoFactorProviders();
providerList.data.forEach(p => {
this.providers.forEach(p2 => {
if (p.type === p2.type) {
p2.enabled = p.enabled;
}
});
this.providers.sort((a: any, b: any) => a.sort - b.sort);
await this.load();
}
async load() {
this.loading = true;
const providerList = await this.getTwoFactorProviders();
providerList.data.forEach((p) => {
this.providers.forEach((p2) => {
if (p.type === p2.type) {
p2.enabled = p.enabled;
}
});
});
this.evaluatePolicies();
this.loading = false;
}
async manage(type: TwoFactorProviderType) {
switch (type) {
case TwoFactorProviderType.Authenticator:
const authComp = await this.openModal(
this.authenticatorModalRef,
TwoFactorAuthenticatorComponent
);
authComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
});
this.evaluatePolicies();
this.loading = false;
}
async manage(type: TwoFactorProviderType) {
switch (type) {
case TwoFactorProviderType.Authenticator:
const authComp = await this.openModal(this.authenticatorModalRef, TwoFactorAuthenticatorComponent);
authComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
});
break;
case TwoFactorProviderType.Yubikey:
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
yubiComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
});
break;
case TwoFactorProviderType.Duo:
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
duoComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Duo);
});
break;
case TwoFactorProviderType.Email:
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
emailComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Email);
});
break;
case TwoFactorProviderType.WebAuthn:
const webAuthnComp = await this.openModal(this.webAuthnModalRef, TwoFactorWebAuthnComponent);
webAuthnComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
});
break;
default:
break;
}
}
recoveryCode() {
this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
}
async premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send('premiumRequired');
return;
}
}
protected getTwoFactorProviders() {
return this.apiService.getTwoFactorProviders();
}
protected filterProvider(type: TwoFactorProviderType) {
return type === TwoFactorProviderType.OrganizationDuo;
}
protected async openModal<T>(ref: ViewContainerRef, type: Type<T>): Promise<T> {
const [modal, childComponent] = await this.modalService.openViewRef(type, ref);
this.modal = modal;
return childComponent;
}
protected updateStatus(enabled: boolean, type: TwoFactorProviderType) {
if (!enabled && this.modal != null) {
this.modal.close();
}
this.providers.forEach(p => {
if (p.type === type) {
p.enabled = enabled;
}
break;
case TwoFactorProviderType.Yubikey:
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
yubiComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
});
this.evaluatePolicies();
break;
case TwoFactorProviderType.Duo:
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
duoComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Duo);
});
break;
case TwoFactorProviderType.Email:
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
emailComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Email);
});
break;
case TwoFactorProviderType.WebAuthn:
const webAuthnComp = await this.openModal(
this.webAuthnModalRef,
TwoFactorWebAuthnComponent
);
webAuthnComp.onUpdated.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
});
break;
default:
break;
}
}
private async evaluatePolicies() {
if (this.organizationId == null && this.providers.filter(p => p.enabled).length === 1) {
this.showPolicyWarning = await this.policyService.policyAppliesToUser(PolicyType.TwoFactorAuthentication);
} else {
this.showPolicyWarning = false;
}
recoveryCode() {
this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
}
async premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send("premiumRequired");
return;
}
}
protected getTwoFactorProviders() {
return this.apiService.getTwoFactorProviders();
}
protected filterProvider(type: TwoFactorProviderType) {
return type === TwoFactorProviderType.OrganizationDuo;
}
protected async openModal<T>(ref: ViewContainerRef, type: Type<T>): Promise<T> {
const [modal, childComponent] = await this.modalService.openViewRef(type, ref);
this.modal = modal;
return childComponent;
}
protected updateStatus(enabled: boolean, type: TwoFactorProviderType) {
if (!enabled && this.modal != null) {
this.modal.close();
}
this.providers.forEach((p) => {
if (p.type === type) {
p.enabled = enabled;
}
});
this.evaluatePolicies();
}
private async evaluatePolicies() {
if (this.organizationId == null && this.providers.filter((p) => p.enabled).length === 1) {
this.showPolicyWarning = await this.policyService.policyAppliesToUser(
PolicyType.TwoFactorAuthentication
);
} else {
this.showPolicyWarning = false;
}
}
}

View File

@@ -1,14 +1,16 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-body">
<p>{{'twoStepLoginAuthDesc' | i18n}}</p>
<app-verify-master-password [(ngModel)]="secret" ngDefaultControl name="secret">
</app-verify-master-password>
</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>{{'continue' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
<div class="modal-body">
<p>{{ "twoStepLoginAuthDesc" | i18n }}</p>
<app-verify-master-password [(ngModel)]="secret" ngDefaultControl name="secret">
</app-verify-master-password>
</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>{{ "continue" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>

View File

@@ -1,88 +1,91 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { VerificationType } from 'jslib-common/enums/verificationType';
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { VerificationType } from "jslib-common/enums/verificationType";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { Verification } from 'jslib-common/types/verification';
import { Verification } from "jslib-common/types/verification";
import { TwoFactorAuthenticatorResponse } from 'jslib-common/models/response/twoFactorAuthenticatorResponse';
import { TwoFactorDuoResponse } from 'jslib-common/models/response/twoFactorDuoResponse';
import { TwoFactorEmailResponse } from 'jslib-common/models/response/twoFactorEmailResponse';
import { TwoFactorRecoverResponse } from 'jslib-common/models/response/twoFactorRescoverResponse';
import { TwoFactorWebAuthnResponse } from 'jslib-common/models/response/twoFactorWebAuthnResponse';
import { TwoFactorYubiKeyResponse } from 'jslib-common/models/response/twoFactorYubiKeyResponse';
import { TwoFactorAuthenticatorResponse } from "jslib-common/models/response/twoFactorAuthenticatorResponse";
import { TwoFactorDuoResponse } from "jslib-common/models/response/twoFactorDuoResponse";
import { TwoFactorEmailResponse } from "jslib-common/models/response/twoFactorEmailResponse";
import { TwoFactorRecoverResponse } from "jslib-common/models/response/twoFactorRescoverResponse";
import { TwoFactorWebAuthnResponse } from "jslib-common/models/response/twoFactorWebAuthnResponse";
import { TwoFactorYubiKeyResponse } from "jslib-common/models/response/twoFactorYubiKeyResponse";
import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest';
import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
type TwoFactorResponse = TwoFactorRecoverResponse | TwoFactorDuoResponse | TwoFactorEmailResponse |
TwoFactorWebAuthnResponse | TwoFactorAuthenticatorResponse | TwoFactorYubiKeyResponse;
type TwoFactorResponse =
| TwoFactorRecoverResponse
| TwoFactorDuoResponse
| TwoFactorEmailResponse
| TwoFactorWebAuthnResponse
| TwoFactorAuthenticatorResponse
| TwoFactorYubiKeyResponse;
@Component({
selector: 'app-two-factor-verify',
templateUrl: 'two-factor-verify.component.html',
selector: "app-two-factor-verify",
templateUrl: "two-factor-verify.component.html",
})
export class TwoFactorVerifyComponent {
@Input() type: TwoFactorProviderType;
@Input() organizationId: string;
@Output() onAuthed = new EventEmitter<any>();
@Input() type: TwoFactorProviderType;
@Input() organizationId: string;
@Output() onAuthed = new EventEmitter<any>();
secret: Verification;
formPromise: Promise<TwoFactorResponse>;
secret: Verification;
formPromise: Promise<TwoFactorResponse>;
constructor(private apiService: ApiService, private logService: LogService,
private userVerificationService: UserVerificationService) { }
constructor(
private apiService: ApiService,
private logService: LogService,
private userVerificationService: UserVerificationService
) {}
async submit() {
let hashedSecret: string;
async submit() {
let hashedSecret: string;
try {
this.formPromise = this.userVerificationService.buildRequest(this.secret)
.then(request => {
hashedSecret = this.secret.type === VerificationType.MasterPassword
? request.masterPasswordHash
: request.otp;
return this.apiCall(request);
});
try {
this.formPromise = this.userVerificationService.buildRequest(this.secret).then((request) => {
hashedSecret =
this.secret.type === VerificationType.MasterPassword
? request.masterPasswordHash
: request.otp;
return this.apiCall(request);
});
const response = await this.formPromise;
this.onAuthed.emit({
response: response,
secret: hashedSecret,
verificationType: this.secret.type,
});
} catch (e) {
this.logService.error(e);
}
const response = await this.formPromise;
this.onAuthed.emit({
response: response,
secret: hashedSecret,
verificationType: this.secret.type,
});
} catch (e) {
this.logService.error(e);
}
}
private apiCall(request: SecretVerificationRequest): Promise<TwoFactorResponse> {
switch (this.type) {
case -1 as TwoFactorProviderType:
return this.apiService.getTwoFactorRecover(request);
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo:
if (this.organizationId != null) {
return this.apiService.getTwoFactorOrganizationDuo(this.organizationId, request);
} else {
return this.apiService.getTwoFactorDuo(request);
}
case TwoFactorProviderType.Email:
return this.apiService.getTwoFactorEmail(request);
case TwoFactorProviderType.WebAuthn:
return this.apiService.getTwoFactorWebAuthn(request);
case TwoFactorProviderType.Authenticator:
return this.apiService.getTwoFactorAuthenticator(request);
case TwoFactorProviderType.Yubikey:
return this.apiService.getTwoFactorYubiKey(request);
private apiCall(request: SecretVerificationRequest): Promise<TwoFactorResponse> {
switch (this.type) {
case -1 as TwoFactorProviderType:
return this.apiService.getTwoFactorRecover(request);
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo:
if (this.organizationId != null) {
return this.apiService.getTwoFactorOrganizationDuo(this.organizationId, request);
} else {
return this.apiService.getTwoFactorDuo(request);
}
case TwoFactorProviderType.Email:
return this.apiService.getTwoFactorEmail(request);
case TwoFactorProviderType.WebAuthn:
return this.apiService.getTwoFactorWebAuthn(request);
case TwoFactorProviderType.Authenticator:
return this.apiService.getTwoFactorAuthenticator(request);
case TwoFactorProviderType.Yubikey:
return this.apiService.getTwoFactorYubiKey(request);
}
}
}

View File

@@ -1,102 +1,156 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faU2fTitle">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faU2fTitle">
{{'twoStepLogin' | i18n}}
<small>{{'webAuthnTitle' | i18n}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faU2fTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "webAuthnTitle" | i18n }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($event)"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
>
<div class="modal-body">
<app-callout
type="success"
title="{{ 'enabled' | i18n }}"
icon="fa-check-circle"
*ngIf="enabled"
>
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
<p>{{ "twoFactorWebAuthnWarning" | i18n }}</p>
<ul class="mb-0">
<li>{{ "twoFactorWebAuthnSupportWeb" | i18n }}</li>
</ul>
</app-callout>
<img class="float-right ml-5 mfaType7" alt="FIDO2 WebAuthn logo'" />
<ul class="fa-ul">
<li
*ngFor="let k of keys; let i = index"
#removeKeyBtn
[appApiAction]="k.removePromise"
>
<i class="fa-li fa fa-key"></i>
<strong *ngIf="!k.configured || !k.name">{{ "webAuthnkeyX" | i18n: i + 1 }}</strong>
<strong *ngIf="k.configured && k.name">{{ k.name }}</strong>
<ng-container *ngIf="k.configured && !removeKeyBtn.loading">
<ng-container *ngIf="k.migrated">
<span>{{ "webAuthnMigrated" | i18n }}</span>
</ng-container>
</ng-container>
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
<i
class="fa fa-spin fa-spinner text-muted fa-fw"
title="{{ 'loading' | i18n }}"
*ngIf="removeKeyBtn.loading"
aria-hidden="true"
></i>
-
<a href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
</ng-container>
</li>
</ul>
<hr />
<p>{{ "twoFactorWebAuthnAdd" | i18n }}:</p>
<ol>
<li>{{ "twoFactorU2fGiveName" | i18n }}</li>
<li>{{ "twoFactorU2fPlugInReadKey" | i18n }}</li>
<li>{{ "twoFactorU2fTouchButton" | i18n }}</li>
<li>{{ "twoFactorU2fSaveForm" | i18n }}</li>
</ol>
<div class="row">
<div class="form-group col-6">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
class="form-control"
[(ngModel)]="name"
[disabled]="!keyIdAvailable"
/>
</div>
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
*ngIf="!authed">
</app-two-factor-verify>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
<div class="modal-body">
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle" *ngIf="enabled">
{{'twoStepLoginProviderEnabled' | i18n}}
</app-callout>
<app-callout type="warning">
<p>{{'twoFactorWebAuthnWarning' | i18n}}</p>
<ul class="mb-0">
<li>{{'twoFactorWebAuthnSupportWeb' | i18n}}</li>
</ul>
</app-callout>
<img class="float-right ml-5 mfaType7" alt="FIDO2 WebAuthn logo'">
<ul class="fa-ul">
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
<i class="fa-li fa fa-key"></i>
<strong *ngIf="!k.configured || !k.name">{{'webAuthnkeyX' | i18n : i + 1}}</strong>
<strong *ngIf="k.configured && k.name">{{k.name}}</strong>
<ng-container *ngIf="k.configured && !removeKeyBtn.loading">
<ng-container *ngIf="k.migrated">
<span>{{'webAuthnMigrated' | i18n}}</span>
</ng-container>
</ng-container>
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
<i class="fa fa-spin fa-spinner text-muted fa-fw" title="{{'loading' | i18n}}"
*ngIf="removeKeyBtn.loading" aria-hidden="true"></i>
-
<a href="#" appStopClick (click)="remove(k)">{{'remove' | i18n}}</a>
</ng-container>
</li>
</ul>
<hr>
<p>{{'twoFactorWebAuthnAdd' | i18n}}:</p>
<ol>
<li>{{'twoFactorU2fGiveName' | i18n}}</li>
<li>{{'twoFactorU2fPlugInReadKey' | i18n}}</li>
<li>{{'twoFactorU2fTouchButton' | i18n}}</li>
<li>{{'twoFactorU2fSaveForm' | i18n}}</li>
</ol>
<div class="row">
<div class="form-group col-6">
<label for="name">{{'name' | i18n}}</label>
<input id="name" type="text" name="Name" class="form-control" [(ngModel)]="name"
[disabled]="!keyIdAvailable">
</div>
</div>
<button type="button" (click)="readKey()" class="btn btn-outline-secondary mr-2"
[disabled]="readKeyBtn.loading || webAuthnListening || !keyIdAvailable" #readKeyBtn
[appApiAction]="challengePromise">
{{'readKey' | i18n}}
</button>
<ng-container *ngIf="readKeyBtn.loading">
<i class="fa fa-spinner fa-spin text-muted" aria-hidden="true"></i>
</ng-container>
<ng-container *ngIf="!readKeyBtn.loading">
<ng-container *ngIf="webAuthnListening">
<i class="fa fa-spinner fa-spin text-muted" aria-hidden="true"></i>
{{'twoFactorU2fWaiting' | i18n}}...
</ng-container>
<ng-container *ngIf="webAuthnResponse">
<i class="fa fa-check-circle text-success" aria-hidden="true"></i>
{{'twoFactorU2fClickSave' | i18n}}
</ng-container>
<ng-container *ngIf="webAuthnError">
<i class="fa fa-warning text-danger" aria-hidden="true"></i>
{{'twoFactorU2fProblemReadingTryAgain' | i18n}}
</ng-container>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" [disabled]="form.loading || !webAuthnResponse">
<i class="fa fa-spinner fa-spin" *ngIf="form.loading" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span *ngIf="!form.loading">{{'save' | i18n}}</span>
</button>
<button #disableBtn type="button" class="btn btn-outline-secondary btn-submit"
[appApiAction]="disablePromise" [disabled]="disableBtn.loading" (click)="disable()"
*ngIf="enabled">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'disableAllKeys' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
<button
type="button"
(click)="readKey()"
class="btn btn-outline-secondary mr-2"
[disabled]="readKeyBtn.loading || webAuthnListening || !keyIdAvailable"
#readKeyBtn
[appApiAction]="challengePromise"
>
{{ "readKey" | i18n }}
</button>
<ng-container *ngIf="readKeyBtn.loading">
<i class="fa fa-spinner fa-spin text-muted" aria-hidden="true"></i>
</ng-container>
<ng-container *ngIf="!readKeyBtn.loading">
<ng-container *ngIf="webAuthnListening">
<i class="fa fa-spinner fa-spin text-muted" aria-hidden="true"></i>
{{ "twoFactorU2fWaiting" | i18n }}...
</ng-container>
<ng-container *ngIf="webAuthnResponse">
<i class="fa fa-check-circle text-success" aria-hidden="true"></i>
{{ "twoFactorU2fClickSave" | i18n }}
</ng-container>
<ng-container *ngIf="webAuthnError">
<i class="fa fa-warning text-danger" aria-hidden="true"></i>
{{ "twoFactorU2fProblemReadingTryAgain" | i18n }}
</ng-container>
</ng-container>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary"
[disabled]="form.loading || !webAuthnResponse"
>
<i
class="fa fa-spinner fa-spin"
*ngIf="form.loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span *ngIf="!form.loading">{{ "save" | i18n }}</span>
</button>
<button
#disableBtn
type="button"
class="btn btn-outline-secondary btn-submit"
[appApiAction]="disablePromise"
[disabled]="disableBtn.loading"
(click)="disable()"
*ngIf="enabled"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "disableAllKeys" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,163 +1,173 @@
import { Component, NgZone } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "jslib-common/models/request/updateTwoFactorWebAuthnDeleteRequest";
import { UpdateTwoFactorWebAuthnRequest } from "jslib-common/models/request/updateTwoFactorWebAuthnRequest";
import {
Component,
NgZone,
} from '@angular/core';
ChallengeResponse,
TwoFactorWebAuthnResponse,
} from "jslib-common/models/response/twoFactorWebAuthnResponse";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest';
import { UpdateTwoFactorWebAuthnDeleteRequest } from 'jslib-common/models/request/updateTwoFactorWebAuthnDeleteRequest';
import { UpdateTwoFactorWebAuthnRequest } from 'jslib-common/models/request/updateTwoFactorWebAuthnRequest';
import {
ChallengeResponse,
TwoFactorWebAuthnResponse,
} from 'jslib-common/models/response/twoFactorWebAuthnResponse';
import { TwoFactorBaseComponent } from './two-factor-base.component';
import { TwoFactorBaseComponent } from "./two-factor-base.component";
@Component({
selector: 'app-two-factor-webauthn',
templateUrl: 'two-factor-webauthn.component.html',
selector: "app-two-factor-webauthn",
templateUrl: "two-factor-webauthn.component.html",
})
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.WebAuthn;
name: string;
keys: any[];
keyIdAvailable: number = null;
keysConfiguredCount = 0;
webAuthnError: boolean;
webAuthnListening: boolean;
webAuthnResponse: PublicKeyCredential;
challengePromise: Promise<ChallengeResponse>;
formPromise: Promise<any>;
type = TwoFactorProviderType.WebAuthn;
name: string;
keys: any[];
keyIdAvailable: number = null;
keysConfiguredCount = 0;
webAuthnError: boolean;
webAuthnListening: boolean;
webAuthnResponse: PublicKeyCredential;
challengePromise: Promise<ChallengeResponse>;
formPromise: Promise<any>;
constructor(apiService: ApiService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
private ngZone: NgZone, logService: LogService, userVerificationService: UserVerificationService) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
private ngZone: NgZone,
logService: LogService,
userVerificationService: UserVerificationService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
}
auth(authResponse: any) {
super.auth(authResponse);
this.processResponse(authResponse.response);
}
async submit() {
if (this.webAuthnResponse == null || this.keyIdAvailable == null) {
// Should never happen.
return Promise.reject();
}
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
request.deviceResponse = this.webAuthnResponse;
request.id = this.keyIdAvailable;
request.name = this.name;
auth(authResponse: any) {
super.auth(authResponse);
this.processResponse(authResponse.response);
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorWebAuthn(request);
const response = await this.formPromise;
await this.processResponse(response);
});
}
disable() {
return super.disable(this.formPromise);
}
async remove(key: any) {
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
return;
}
const name = key.name != null ? key.name : this.i18nService.t("webAuthnkeyX", key.id);
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("removeU2fConfirmation"),
name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return;
}
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnDeleteRequest);
request.id = key.id;
try {
key.removePromise = this.apiService.deleteTwoFactorWebAuthn(request);
const response = await key.removePromise;
key.removePromise = null;
await this.processResponse(response);
} catch (e) {
this.logService.error(e);
}
}
async submit() {
if (this.webAuthnResponse == null || this.keyIdAvailable == null) {
// Should never happen.
return Promise.reject();
}
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
request.deviceResponse = this.webAuthnResponse;
request.id = this.keyIdAvailable;
request.name = this.name;
async readKey() {
if (this.keyIdAvailable == null) {
return;
}
const request = await this.buildRequestModel(SecretVerificationRequest);
try {
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
const challenge = await this.challengePromise;
this.readDevice(challenge);
} catch (e) {
this.logService.error(e);
}
}
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorWebAuthn(request);
const response = await this.formPromise;
await this.processResponse(response);
private readDevice(webAuthnChallenge: ChallengeResponse) {
// tslint:disable-next-line
console.log("listening for key...");
this.resetWebAuthn(true);
navigator.credentials
.create({
publicKey: webAuthnChallenge,
})
.then((data: PublicKeyCredential) => {
this.ngZone.run(() => {
this.webAuthnListening = false;
this.webAuthnResponse = data;
});
}
disable() {
return super.disable(this.formPromise);
}
async remove(key: any) {
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
return;
}
const name = key.name != null ? key.name : this.i18nService.t('webAuthnkeyX', key.id);
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('removeU2fConfirmation'), name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnDeleteRequest);
request.id = key.id;
try {
key.removePromise = this.apiService.deleteTwoFactorWebAuthn(request);
const response = await key.removePromise;
key.removePromise = null;
await this.processResponse(response);
} catch (e) {
this.logService.error(e);
}
}
async readKey() {
if (this.keyIdAvailable == null) {
return;
}
const request = await this.buildRequestModel(SecretVerificationRequest);
try {
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
const challenge = await this.challengePromise;
this.readDevice(challenge);
} catch (e) {
this.logService.error(e);
}
}
private readDevice(webAuthnChallenge: ChallengeResponse) {
})
.catch((err) => {
// tslint:disable-next-line
console.log('listening for key...');
this.resetWebAuthn(true);
console.error(err);
this.resetWebAuthn(false);
// TODO: Should we display the actual error?
this.webAuthnError = true;
});
}
navigator.credentials.create({
publicKey: webAuthnChallenge,
}).then((data: PublicKeyCredential) => {
this.ngZone.run(() => {
this.webAuthnListening = false;
this.webAuthnResponse = data;
});
}).catch(err => {
// tslint:disable-next-line
console.error(err);
this.resetWebAuthn(false);
// TODO: Should we display the actual error?
this.webAuthnError = true;
});
}
private resetWebAuthn(listening = false) {
this.webAuthnResponse = null;
this.webAuthnError = false;
this.webAuthnListening = listening;
}
private resetWebAuthn(listening = false) {
this.webAuthnResponse = null;
this.webAuthnError = false;
this.webAuthnListening = listening;
}
private processResponse(response: TwoFactorWebAuthnResponse) {
this.resetWebAuthn();
this.keys = [];
this.keyIdAvailable = null;
this.name = null;
this.keysConfiguredCount = 0;
for (let i = 1; i <= 5; i++) {
if (response.keys != null) {
const key = response.keys.filter(k => k.id === i);
if (key.length > 0) {
this.keysConfiguredCount++;
this.keys.push({
id: i, name: key[0].name,
configured: true,
migrated: key[0].migrated,
removePromise: null,
});
continue;
}
}
this.keys.push({ id: i, name: null, configured: false, removePromise: null });
if (this.keyIdAvailable == null) {
this.keyIdAvailable = i;
}
private processResponse(response: TwoFactorWebAuthnResponse) {
this.resetWebAuthn();
this.keys = [];
this.keyIdAvailable = null;
this.name = null;
this.keysConfiguredCount = 0;
for (let i = 1; i <= 5; i++) {
if (response.keys != null) {
const key = response.keys.filter((k) => k.id === i);
if (key.length > 0) {
this.keysConfiguredCount++;
this.keys.push({
id: i,
name: key[0].name,
configured: true,
migrated: key[0].migrated,
removePromise: null,
});
continue;
}
this.enabled = response.enabled;
}
this.keys.push({ id: i, name: null, configured: false, removePromise: null });
if (this.keyIdAvailable == null) {
this.keyIdAvailable = i;
}
}
this.enabled = response.enabled;
}
}

View File

@@ -1,76 +1,117 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faYubiKeyTitle">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faYubiKeyTitle">
{{'twoStepLogin' | i18n}}
<small>YubiKey</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="2faYubiKeyTitle">
{{ "twoStepLogin" | i18n }}
<small>YubiKey</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($event)"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
autocomplete="off"
>
<div class="modal-body">
<app-callout
type="success"
title="{{ 'enabled' | i18n }}"
icon="fa-check-circle"
*ngIf="enabled"
>
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
<p>{{ "twoFactorYubikeyWarning" | i18n }}</p>
<ul class="mb-0">
<li>{{ "twoFactorYubikeySupportUsb" | i18n }}</li>
<li>{{ "twoFactorYubikeySupportMobile" | i18n }}</li>
</ul>
</app-callout>
<img class="float-right mfaType3" alt="YubiKey OTP security key logo" />
<p>{{ "twoFactorYubikeyAdd" | i18n }}:</p>
<ol>
<li>{{ "twoFactorYubikeyPlugIn" | i18n }}</li>
<li>{{ "twoFactorYubikeySelectKey" | i18n }}</li>
<li>{{ "twoFactorYubikeyTouchButton" | i18n }}</li>
<li>{{ "twoFactorYubikeySaveForm" | i18n }}</li>
</ol>
<hr />
<div class="row">
<div class="form-group col-6" *ngFor="let k of keys; let i = index">
<label for="key{{ i + 1 }}">{{ "yubikeyX" | i18n: i + 1 }}</label>
<input
id="key{{ i + 1 }}"
type="password"
name="Key{{ i + 1 }}"
class="form-control"
[(ngModel)]="k.key"
*ngIf="!k.existingKey"
appInputVerbatim
autocomplete="new-password"
/>
<div class="d-flex" *ngIf="k.existingKey">
<span class="mr-2">{{ k.existingKey }}</span>
<button
type="button"
class="btn btn-link text-danger ml-auto"
(click)="remove(k)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
</button>
</div>
</div>
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)"
*ngIf="!authed">
</app-two-factor-verify>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed"
autocomplete="off">
<div class="modal-body">
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle" *ngIf="enabled">
{{'twoStepLoginProviderEnabled' | i18n}}
</app-callout>
<app-callout type="warning">
<p>{{'twoFactorYubikeyWarning' | i18n}}</p>
<ul class="mb-0">
<li>{{'twoFactorYubikeySupportUsb' | i18n}}</li>
<li>{{'twoFactorYubikeySupportMobile' | i18n}}</li>
</ul>
</app-callout>
<img class="float-right mfaType3" alt="YubiKey OTP security key logo">
<p>{{'twoFactorYubikeyAdd' | i18n}}:</p>
<ol>
<li>{{'twoFactorYubikeyPlugIn' | i18n}}</li>
<li>{{'twoFactorYubikeySelectKey' | i18n}}</li>
<li>{{'twoFactorYubikeyTouchButton' | i18n}}</li>
<li>{{'twoFactorYubikeySaveForm' | i18n}}</li>
</ol>
<hr>
<div class="row">
<div class="form-group col-6" *ngFor="let k of keys; let i = index">
<label for="key{{i + 1}}">{{'yubikeyX' | i18n : i + 1}}</label>
<input id="key{{i + 1}}" type="password" name="Key{{i + 1}}" class="form-control"
[(ngModel)]="k.key" *ngIf="!k.existingKey" appInputVerbatim autocomplete="new-password">
<div class="d-flex" *ngIf="k.existingKey">
<span class="mr-2">{{k.existingKey}}</span>
<button type="button" class="btn btn-link text-danger ml-auto" (click)="remove(k)"
appA11yTitle="{{'remove' | i18n}}">
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<strong class="d-block mb-2">{{'nfcSupport' | i18n}}</strong>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="nfc" name="Nfc" [(ngModel)]="nfc">
<label class="form-check-label" for="nfc">{{'twoFactorYubikeySupportsNfc' | i18n}}</label>
</div>
<small class="form-text text-muted">{{'twoFactorYubikeySupportsNfcDesc' | i18n}}</small>
</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 #disableBtn type="button" class="btn btn-outline-secondary btn-submit"
[appApiAction]="disablePromise" [disabled]="disableBtn.loading" (click)="disable()"
*ngIf="enabled">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'disableAllKeys' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
<strong class="d-block mb-2">{{ "nfcSupport" | i18n }}</strong>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="nfc" name="Nfc" [(ngModel)]="nfc" />
<label class="form-check-label" for="nfc">{{
"twoFactorYubikeySupportsNfc" | i18n
}}</label>
</div>
<small class="form-text text-muted">{{ "twoFactorYubikeySupportsNfcDesc" | i18n }}</small>
</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
#disableBtn
type="button"
class="btn btn-outline-secondary btn-submit"
[appApiAction]="disablePromise"
[disabled]="disableBtn.loading"
(click)="disable()"
*ngIf="enabled"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "disableAllKeys" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,87 +1,91 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { UpdateTwoFactorYubioOtpRequest } from 'jslib-common/models/request/updateTwoFactorYubioOtpRequest';
import { TwoFactorYubiKeyResponse } from 'jslib-common/models/response/twoFactorYubiKeyResponse';
import { UpdateTwoFactorYubioOtpRequest } from "jslib-common/models/request/updateTwoFactorYubioOtpRequest";
import { TwoFactorYubiKeyResponse } from "jslib-common/models/response/twoFactorYubiKeyResponse";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { TwoFactorBaseComponent } from './two-factor-base.component';
import { TwoFactorBaseComponent } from "./two-factor-base.component";
@Component({
selector: 'app-two-factor-yubikey',
templateUrl: 'two-factor-yubikey.component.html',
selector: "app-two-factor-yubikey",
templateUrl: "two-factor-yubikey.component.html",
})
export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent {
type = TwoFactorProviderType.Yubikey;
keys: any[];
nfc = false;
type = TwoFactorProviderType.Yubikey;
keys: any[];
nfc = false;
formPromise: Promise<any>;
disablePromise: Promise<any>;
formPromise: Promise<any>;
disablePromise: Promise<any>;
constructor(apiService: ApiService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService, userVerificationService: UserVerificationService) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
userVerificationService: UserVerificationService
) {
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
}
auth(authResponse: any) {
super.auth(authResponse);
this.processResponse(authResponse.response);
}
async submit() {
const request = await this.buildRequestModel(UpdateTwoFactorYubioOtpRequest);
request.key1 = this.keys != null && this.keys.length > 0 ? this.keys[0].key : null;
request.key2 = this.keys != null && this.keys.length > 1 ? this.keys[1].key : null;
request.key3 = this.keys != null && this.keys.length > 2 ? this.keys[2].key : null;
request.key4 = this.keys != null && this.keys.length > 3 ? this.keys[3].key : null;
request.key5 = this.keys != null && this.keys.length > 4 ? this.keys[4].key : null;
request.nfc = this.nfc;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorYubiKey(request);
const response = await this.formPromise;
await this.processResponse(response);
this.platformUtilsService.showToast("success", null, this.i18nService.t("yubikeysUpdated"));
});
}
disable() {
return super.disable(this.disablePromise);
}
remove(key: any) {
key.existingKey = null;
key.key = null;
}
private processResponse(response: TwoFactorYubiKeyResponse) {
this.enabled = response.enabled;
this.keys = [
{ key: response.key1, existingKey: this.padRight(response.key1) },
{ key: response.key2, existingKey: this.padRight(response.key2) },
{ key: response.key3, existingKey: this.padRight(response.key3) },
{ key: response.key4, existingKey: this.padRight(response.key4) },
{ key: response.key5, existingKey: this.padRight(response.key5) },
];
this.nfc = response.nfc || !response.enabled;
}
private padRight(str: string, character = "•", size = 44) {
if (str == null || character == null || str.length >= size) {
return str;
}
auth(authResponse: any) {
super.auth(authResponse);
this.processResponse(authResponse.response);
}
async submit() {
const request = await this.buildRequestModel(UpdateTwoFactorYubioOtpRequest);
request.key1 = this.keys != null && this.keys.length > 0 ? this.keys[0].key : null;
request.key2 = this.keys != null && this.keys.length > 1 ? this.keys[1].key : null;
request.key3 = this.keys != null && this.keys.length > 2 ? this.keys[2].key : null;
request.key4 = this.keys != null && this.keys.length > 3 ? this.keys[3].key : null;
request.key5 = this.keys != null && this.keys.length > 4 ? this.keys[4].key : null;
request.nfc = this.nfc;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorYubiKey(request);
const response = await this.formPromise;
await this.processResponse(response);
this.platformUtilsService.showToast('success', null, this.i18nService.t('yubikeysUpdated'));
});
}
disable() {
return super.disable(this.disablePromise);
}
remove(key: any) {
key.existingKey = null;
key.key = null;
}
private processResponse(response: TwoFactorYubiKeyResponse) {
this.enabled = response.enabled;
this.keys = [
{ key: response.key1, existingKey: this.padRight(response.key1) },
{ key: response.key2, existingKey: this.padRight(response.key2) },
{ key: response.key3, existingKey: this.padRight(response.key3) },
{ key: response.key4, existingKey: this.padRight(response.key4) },
{ key: response.key5, existingKey: this.padRight(response.key5) },
];
this.nfc = response.nfc || !response.enabled;
}
private padRight(str: string, character = '•', size = 44) {
if (str == null || character == null || str.length >= size) {
return str;
}
const max = (size - str.length) / character.length;
for (let i = 0; i < max; i++) {
str += character;
}
return str;
const max = (size - str.length) / character.length;
for (let i = 0; i < max; i++) {
str += character;
}
return str;
}
}

View File

@@ -1,29 +1,55 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="updateEncKeyTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="updateEncKeyTitle">{{'updateEncryptionKey' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'updateEncryptionKeyShortDesc' | i18n}} {{'updateEncryptionKeyDesc' | i18n}}
<a href="https://help.bitwarden.com/article/update-encryption-key/" target="_blank"
rel="noopener">{{'learnMore' | i18n}}</a>
</p>
<app-callout type="warning">{{'updateEncryptionKeyWarning' | i18n}}</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'updateEncryptionKey' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="updateEncKeyTitle">{{ "updateEncryptionKey" | i18n }}</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
{{ "updateEncryptionKeyShortDesc" | i18n }} {{ "updateEncryptionKeyDesc" | i18n }}
<a
href="https://help.bitwarden.com/article/update-encryption-key/"
target="_blank"
rel="noopener"
>{{ "learnMore" | i18n }}</a
>
</p>
<app-callout type="warning">{{ "updateEncryptionKeyWarning" | i18n }}</app-callout>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[(ngModel)]="masterPassword"
required
appAutofocus
appInputVerbatim
/>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "updateEncryptionKey" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,93 +1,106 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { EncString } from 'jslib-common/models/domain/encString';
import { EncString } from "jslib-common/models/domain/encString";
import { CipherWithIdRequest } from 'jslib-common/models/request/cipherWithIdRequest';
import { FolderWithIdRequest } from 'jslib-common/models/request/folderWithIdRequest';
import { UpdateKeyRequest } from 'jslib-common/models/request/updateKeyRequest';
import { CipherWithIdRequest } from "jslib-common/models/request/cipherWithIdRequest";
import { FolderWithIdRequest } from "jslib-common/models/request/folderWithIdRequest";
import { UpdateKeyRequest } from "jslib-common/models/request/updateKeyRequest";
@Component({
selector: 'app-update-key',
templateUrl: 'update-key.component.html',
selector: "app-update-key",
templateUrl: "update-key.component.html",
})
export class UpdateKeyComponent {
masterPassword: string;
formPromise: Promise<any>;
masterPassword: string;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
private messagingService: MessagingService, private syncService: SyncService,
private folderService: FolderService, private cipherService: CipherService,
private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private syncService: SyncService,
private folderService: FolderService,
private cipherService: CipherService,
private logService: LogService
) {}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (hasEncKey) {
return;
}
if (this.masterPassword == null || this.masterPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
try {
this.formPromise = this.makeRequest().then(request => {
return this.apiService.postAccountKey(request);
});
await this.formPromise;
this.platformUtilsService.showToast('success', this.i18nService.t('keyUpdated'),
this.i18nService.t('logBackInOthersToo'), { timeout: 15000 });
this.messagingService.send('logout');
} catch (e) {
this.logService.error(e);
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (hasEncKey) {
return;
}
private async makeRequest(): Promise<UpdateKeyRequest> {
const key = await this.cryptoService.getKey();
const encKey = await this.cryptoService.makeEncKey(key);
const privateKey = await this.cryptoService.getPrivateKey();
let encPrivateKey: EncString = null;
if (privateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(privateKey, encKey[0]);
}
const request = new UpdateKeyRequest();
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
request.key = encKey[1].encryptedString;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
await this.syncService.fullSync(true);
const folders = await this.folderService.getAllDecrypted();
for (let i = 0; i < folders.length; i++) {
if (folders[i].id == null) {
continue;
}
const folder = await this.folderService.encrypt(folders[i], encKey[0]);
request.folders.push(new FolderWithIdRequest(folder));
}
const ciphers = await this.cipherService.getAllDecrypted();
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId != null) {
continue;
}
const cipher = await this.cipherService.encrypt(ciphers[i], encKey[0]);
request.ciphers.push(new CipherWithIdRequest(cipher));
}
return request;
if (this.masterPassword == null || this.masterPassword === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPassRequired")
);
return;
}
try {
this.formPromise = this.makeRequest().then((request) => {
return this.apiService.postAccountKey(request);
});
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("keyUpdated"),
this.i18nService.t("logBackInOthersToo"),
{ timeout: 15000 }
);
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
}
}
private async makeRequest(): Promise<UpdateKeyRequest> {
const key = await this.cryptoService.getKey();
const encKey = await this.cryptoService.makeEncKey(key);
const privateKey = await this.cryptoService.getPrivateKey();
let encPrivateKey: EncString = null;
if (privateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(privateKey, encKey[0]);
}
const request = new UpdateKeyRequest();
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
request.key = encKey[1].encryptedString;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
await this.syncService.fullSync(true);
const folders = await this.folderService.getAllDecrypted();
for (let i = 0; i < folders.length; i++) {
if (folders[i].id == null) {
continue;
}
const folder = await this.folderService.encrypt(folders[i], encKey[0]);
request.folders.push(new FolderWithIdRequest(folder));
}
const ciphers = await this.cipherService.getAllDecrypted();
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId != null) {
continue;
}
const cipher = await this.cipherService.encrypt(ciphers[i], encKey[0]);
request.ciphers.push(new CipherWithIdRequest(cipher));
}
return request;
}
}

View File

@@ -1,15 +1,20 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="form-group">
<label for="file" class="sr-only">{{'licenseFile' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required>
<small
class="form-text text-muted">{{'licenseFileDesc' | i18n : (!organizationId ? 'bitwarden_premium_license.json' : 'bitwarden_organization_license.json')}}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{'cancel' | i18n}}
</button>
<div class="form-group">
<label for="file" class="sr-only">{{ "licenseFile" | i18n }}</label>
<input type="file" id="file" class="form-control-file" name="file" required />
<small class="form-text text-muted">{{
"licenseFileDesc"
| i18n
: (!organizationId
? "bitwarden_premium_license.json"
: "bitwarden_organization_license.json")
}}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
</form>

View File

@@ -1,62 +1,64 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: 'app-update-license',
templateUrl: 'update-license.component.html',
selector: "app-update-license",
templateUrl: "update-license.component.html",
})
export class UpdateLicenseComponent {
@Input() organizationId: string;
@Output() onUpdated = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@Input() organizationId: string;
@Output() onUpdated = new EventEmitter();
@Output() onCanceled = new EventEmitter();
formPromise: Promise<any>;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async submit() {
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;
}
try {
const fd = new FormData();
fd.append('license', files[0]);
let updatePromise: Promise<any> = null;
if (this.organizationId == null) {
updatePromise = this.apiService.postAccountLicense(fd);
} else {
updatePromise = this.apiService.postOrganizationLicenseUpdate(this.organizationId, fd);
}
this.formPromise = updatePromise.then(() => {
return this.apiService.refreshIdentityToken();
});
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('updatedLicense'));
this.onUpdated.emit();
} catch (e) {
this.logService.error(e);
}
async submit() {
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;
}
cancel() {
this.onCanceled.emit();
try {
const fd = new FormData();
fd.append("license", files[0]);
let updatePromise: Promise<any> = null;
if (this.organizationId == null) {
updatePromise = this.apiService.postAccountLicense(fd);
} else {
updatePromise = this.apiService.postOrganizationLicenseUpdate(this.organizationId, fd);
}
this.formPromise = updatePromise.then(() => {
return this.apiService.refreshIdentityToken();
});
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("updatedLicense"));
this.onUpdated.emit();
} catch (e) {
this.logService.error(e);
}
}
cancel() {
this.onCanceled.emit();
}
}

View File

@@ -1,127 +1,208 @@
<div class="page-header d-flex">
<h1>
{{'billing' | i18n}}
</h1>
<button (click)="load()" class="btn btn-sm btn-outline-primary ml-auto" *ngIf="firstLoaded" [disabled]="loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loading}" aria-hidden="true"></i>
{{'refresh' | i18n}}
</button>
<h1>
{{ "billing" | i18n }}
</h1>
<button
(click)="load()"
class="btn btn-sm btn-outline-primary ml-auto"
*ngIf="firstLoaded"
[disabled]="loading"
>
<i class="fa fa-refresh fa-fw" [ngClass]="{ 'fa-spin': loading }" aria-hidden="true"></i>
{{ "refresh" | i18n }}
</button>
</div>
<ng-container *ngIf="!firstLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<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="billing">
<h2>{{(isCreditBalance ? 'accountCredit' : 'accountBalance') | i18n}}</h2>
<p class="text-lg"><strong>{{creditOrBalance | currency:'$'}}</strong></p>
<p>{{'creditAppliedDesc' | i18n}}</p>
<button type="button" class="btn btn-outline-secondary" (click)="addCredit()" *ngIf="!showAddCredit">
{{'addCredit' | i18n}}
</button>
<app-add-credit [organizationId]="organizationId" (onAdded)="closeAddCredit(true)"
(onCanceled)="closeAddCredit(false)" *ngIf="showAddCredit">
</app-add-credit>
<h2 class="spaced-header">{{'paymentMethod' | i18n}}</h2>
<p *ngIf="!paymentSource">{{'noPaymentMethod' | i18n}}</p>
<ng-container *ngIf="paymentSource">
<app-callout type="warning" title="{{'verifyBankAccount' | i18n}}"
*ngIf="paymentSource.type === paymentMethodType.BankAccount && paymentSource.needsVerification">
<p>{{'verifyBankAccountDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}</p>
<form #verifyForm class="form-inline" (ngSubmit)="verifyBank()" [appApiAction]="verifyBankPromise"
ngNativeValidate>
<label class="sr-only" for="verifyAmount1">{{'amount' | i18n : '1'}}</label>
<div class="input-group mr-2">
<div class="input-group-prepend">
<div class="input-group-text">$0.</div>
</div>
<input type="number" class="form-control" id="verifyAmount1" placeholder="xx" name="Amount1"
[(ngModel)]="verifyAmount1" min="1" max="99" step="1" required>
</div>
<label class="sr-only" for="verifyAmount2">{{'amount' | i18n : '2'}}</label>
<div class="input-group mr-2">
<div class="input-group-prepend">
<div class="input-group-text">$0.</div>
</div>
<input type="number" class="form-control" id="verifyAmount2" placeholder="xx" name="Amount2"
[(ngModel)]="verifyAmount2" min="1" max="99" step="1" required>
</div>
<button type="submit" class="btn btn-outline-primary btn-submit" [disabled]="verifyForm.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'verifyBankAccount' | i18n}}</span>
</button>
</form>
</app-callout>
<p>
<i class="fa fa-fw" [ngClass]="{'fa-credit-card': paymentSource.type === paymentMethodType.Card,
'fa-university': paymentSource.type === paymentMethodType.BankAccount,
'fa-money': paymentSource.type === paymentMethodType.Check,
'fa-paypal text-primary': paymentSource.type === paymentMethodType.PayPal,
'fa-apple text-muted': paymentSource.type === paymentMethodType.AppleInApp,
'fa-google text-muted': paymentSource.type === paymentMethodType.GoogleInApp}"></i>
<span *ngIf="paymentSourceInApp">{{'inAppPurchase' | i18n}}</span>
{{paymentSource.description}}
</p>
</ng-container>
<button type="button" class="btn btn-outline-secondary" (click)="changePayment()" *ngIf="!showAdjustPayment">
{{(paymentSource ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}
</button>
<app-adjust-payment [currentType]="paymentSource != null ? paymentSource.type : null"
[organizationId]="organizationId" (onAdjusted)="closePayment(true)" (onCanceled)="closePayment(false)"
*ngIf="showAdjustPayment">
</app-adjust-payment>
<h2 class="spaced-header">{{'invoices' | i18n}}</h2>
<p *ngIf="!invoices || !invoices.length">{{'noInvoices' | i18n}}</p>
<table class="table mb-2" *ngIf="invoices && invoices.length">
<tbody>
<tr *ngFor="let i of invoices">
<td>{{i.date | date:'mediumDate'}}</td>
<td>
<a href="{{i.pdfUrl}}" target="_blank" rel="noopener" class="mr-2"
appA11yTitle="{{'downloadInvoice' | i18n}}">
<i class="fa fa-file-pdf-o" aria-hidden="true"></i></a>
<a href="{{i.url}}" target="_blank" rel="noopener" title="{{'viewInvoice' | i18n}}">
{{'invoiceNumber' | i18n : i.number}}</a>
</td>
<td>{{i.amount | currency:'$'}}</td>
<td>
<span *ngIf="i.paid">
<i class="fa fa-check text-success" aria-hidden="true"></i>
{{'paid' | i18n}}
</span>
<span *ngIf="!i.paid">
<i class="fa fa-exclamation-circle text-muted" aria-hidden="true"></i>
{{'unpaid' | i18n}}
</span>
</td>
</tr>
</tbody>
</table>
<h2 class="spaced-header">{{'transactions' | i18n}}</h2>
<p *ngIf="!transactions || !transactions.length">{{'noTransactions' | i18n}}</p>
<table class="table mb-2" *ngIf="transactions && transactions.length">
<tbody>
<tr *ngFor="let t of transactions">
<td>{{t.createdDate | date:'mediumDate'}}</td>
<td>
<span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit">
{{'chargeNoun' | i18n}}
</span>
<span *ngIf="t.type === transactionType.Refund">{{'refundNoun' | i18n}}</span>
</td>
<td>
<i class="fa fa-fw" *ngIf="t.paymentMethodType" aria-hidden="true" [ngClass]="{
'fa-credit-card': t.paymentMethodType === paymentMethodType.Card,
'fa-university': t.paymentMethodType === paymentMethodType.BankAccount ||
t.paymentMethodType === paymentMethodType.WireTransfer,
'fa-bitcoin text-warning': t.paymentMethodType === paymentMethodType.BitPay,
'fa-paypal text-primary': t.paymentMethodType === paymentMethodType.PayPal
}"></i>
{{t.details}}
</td>
<td [ngClass]="{'text-strike': t.refunded}" title="{{(t.refunded ? 'refunded' : '') | i18n}}">
{{t.amount | currency:'$'}}</td>
</tr>
</tbody>
</table>
<small class="text-muted">* {{'chargesStatement' | i18n : 'BITWARDEN'}}</small>
<h2>{{ (isCreditBalance ? "accountCredit" : "accountBalance") | i18n }}</h2>
<p class="text-lg">
<strong>{{ creditOrBalance | currency: "$" }}</strong>
</p>
<p>{{ "creditAppliedDesc" | i18n }}</p>
<button
type="button"
class="btn btn-outline-secondary"
(click)="addCredit()"
*ngIf="!showAddCredit"
>
{{ "addCredit" | i18n }}
</button>
<app-add-credit
[organizationId]="organizationId"
(onAdded)="closeAddCredit(true)"
(onCanceled)="closeAddCredit(false)"
*ngIf="showAddCredit"
>
</app-add-credit>
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
<ng-container *ngIf="paymentSource">
<app-callout
type="warning"
title="{{ 'verifyBankAccount' | i18n }}"
*ngIf="
paymentSource.type === paymentMethodType.BankAccount && paymentSource.needsVerification
"
>
<p>{{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}</p>
<form
#verifyForm
class="form-inline"
(ngSubmit)="verifyBank()"
[appApiAction]="verifyBankPromise"
ngNativeValidate
>
<label class="sr-only" for="verifyAmount1">{{ "amount" | i18n: "1" }}</label>
<div class="input-group mr-2">
<div class="input-group-prepend">
<div class="input-group-text">$0.</div>
</div>
<input
type="number"
class="form-control"
id="verifyAmount1"
placeholder="xx"
name="Amount1"
[(ngModel)]="verifyAmount1"
min="1"
max="99"
step="1"
required
/>
</div>
<label class="sr-only" for="verifyAmount2">{{ "amount" | i18n: "2" }}</label>
<div class="input-group mr-2">
<div class="input-group-prepend">
<div class="input-group-text">$0.</div>
</div>
<input
type="number"
class="form-control"
id="verifyAmount2"
placeholder="xx"
name="Amount2"
[(ngModel)]="verifyAmount2"
min="1"
max="99"
step="1"
required
/>
</div>
<button
type="submit"
class="btn btn-outline-primary btn-submit"
[disabled]="verifyForm.loading"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "verifyBankAccount" | i18n }}</span>
</button>
</form>
</app-callout>
<p>
<i
class="fa fa-fw"
[ngClass]="{
'fa-credit-card': paymentSource.type === paymentMethodType.Card,
'fa-university': paymentSource.type === paymentMethodType.BankAccount,
'fa-money': paymentSource.type === paymentMethodType.Check,
'fa-paypal text-primary': paymentSource.type === paymentMethodType.PayPal,
'fa-apple text-muted': paymentSource.type === paymentMethodType.AppleInApp,
'fa-google text-muted': paymentSource.type === paymentMethodType.GoogleInApp
}"
></i>
<span *ngIf="paymentSourceInApp">{{ "inAppPurchase" | i18n }}</span>
{{ paymentSource.description }}
</p>
</ng-container>
<button
type="button"
class="btn btn-outline-secondary"
(click)="changePayment()"
*ngIf="!showAdjustPayment"
>
{{ (paymentSource ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
</button>
<app-adjust-payment
[currentType]="paymentSource != null ? paymentSource.type : null"
[organizationId]="organizationId"
(onAdjusted)="closePayment(true)"
(onCanceled)="closePayment(false)"
*ngIf="showAdjustPayment"
>
</app-adjust-payment>
<h2 class="spaced-header">{{ "invoices" | i18n }}</h2>
<p *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p>
<table class="table mb-2" *ngIf="invoices && invoices.length">
<tbody>
<tr *ngFor="let i of invoices">
<td>{{ i.date | date: "mediumDate" }}</td>
<td>
<a
href="{{ i.pdfUrl }}"
target="_blank"
rel="noopener"
class="mr-2"
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
>
<i class="fa fa-file-pdf-o" aria-hidden="true"></i
></a>
<a href="{{ i.url }}" target="_blank" rel="noopener" title="{{ 'viewInvoice' | i18n }}">
{{ "invoiceNumber" | i18n: i.number }}</a
>
</td>
<td>{{ i.amount | currency: "$" }}</td>
<td>
<span *ngIf="i.paid">
<i class="fa fa-check text-success" aria-hidden="true"></i>
{{ "paid" | i18n }}
</span>
<span *ngIf="!i.paid">
<i class="fa fa-exclamation-circle text-muted" aria-hidden="true"></i>
{{ "unpaid" | i18n }}
</span>
</td>
</tr>
</tbody>
</table>
<h2 class="spaced-header">{{ "transactions" | i18n }}</h2>
<p *ngIf="!transactions || !transactions.length">{{ "noTransactions" | i18n }}</p>
<table class="table mb-2" *ngIf="transactions && transactions.length">
<tbody>
<tr *ngFor="let t of transactions">
<td>{{ t.createdDate | date: "mediumDate" }}</td>
<td>
<span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit">
{{ "chargeNoun" | i18n }}
</span>
<span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span>
</td>
<td>
<i
class="fa fa-fw"
*ngIf="t.paymentMethodType"
aria-hidden="true"
[ngClass]="{
'fa-credit-card': t.paymentMethodType === paymentMethodType.Card,
'fa-university':
t.paymentMethodType === paymentMethodType.BankAccount ||
t.paymentMethodType === paymentMethodType.WireTransfer,
'fa-bitcoin text-warning': t.paymentMethodType === paymentMethodType.BitPay,
'fa-paypal text-primary': t.paymentMethodType === paymentMethodType.PayPal
}"
></i>
{{ t.details }}
</td>
<td
[ngClass]="{ 'text-strike': t.refunded }"
title="{{ (t.refunded ? 'refunded' : '') | i18n }}"
>
{{ t.amount | currency: "$" }}
</td>
</tr>
</tbody>
</table>
<small class="text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small>
</ng-container>

View File

@@ -1,132 +1,151 @@
import {
Component,
OnInit,
} from '@angular/core';
import { Component, OnInit } from "@angular/core";
import { BillingResponse } from 'jslib-common/models/response/billingResponse';
import { BillingResponse } from "jslib-common/models/response/billingResponse";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
import { TransactionType } from 'jslib-common/enums/transactionType';
import { VerifyBankRequest } from 'jslib-common/models/request/verifyBankRequest';
import { PaymentMethodType } from "jslib-common/enums/paymentMethodType";
import { TransactionType } from "jslib-common/enums/transactionType";
import { VerifyBankRequest } from "jslib-common/models/request/verifyBankRequest";
@Component({
selector: 'app-user-billing',
templateUrl: 'user-billing.component.html',
selector: "app-user-billing",
templateUrl: "user-billing.component.html",
})
export class UserBillingComponent implements OnInit {
loading = false;
firstLoaded = false;
showAdjustPayment = false;
showAddCredit = false;
billing: BillingResponse;
paymentMethodType = PaymentMethodType;
transactionType = TransactionType;
organizationId: string;
verifyAmount1: number;
verifyAmount2: number;
loading = false;
firstLoaded = false;
showAdjustPayment = false;
showAddCredit = false;
billing: BillingResponse;
paymentMethodType = PaymentMethodType;
transactionType = TransactionType;
organizationId: string;
verifyAmount1: number;
verifyAmount2: number;
verifyBankPromise: Promise<any>;
verifyBankPromise: Promise<any>;
constructor(protected apiService: ApiService, protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
private logService: LogService) { }
constructor(
protected apiService: ApiService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
await this.load();
this.firstLoaded = true;
async ngOnInit() {
await this.load();
this.firstLoaded = true;
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
if (this.organizationId != null) {
this.billing = await this.apiService.getOrganizationBilling(this.organizationId);
} else {
this.billing = await this.apiService.getUserBilling();
}
this.loading = false;
}
async verifyBank() {
if (this.loading) {
return;
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
if (this.organizationId != null) {
this.billing = await this.apiService.getOrganizationBilling(this.organizationId);
} else {
this.billing = await this.apiService.getUserBilling();
}
this.loading = false;
try {
const request = new VerifyBankRequest();
request.amount1 = this.verifyAmount1;
request.amount2 = this.verifyAmount2;
this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(
this.organizationId,
request
);
await this.verifyBankPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("verifiedBankAccount")
);
this.load();
} catch (e) {
this.logService.error(e);
}
}
async verifyBank() {
if (this.loading) {
return;
}
try {
const request = new VerifyBankRequest();
request.amount1 = this.verifyAmount1;
request.amount2 = this.verifyAmount2;
this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(this.organizationId, request);
await this.verifyBankPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('verifiedBankAccount'));
this.load();
} catch (e) {
this.logService.error(e);
}
addCredit() {
if (this.paymentSourceInApp) {
this.platformUtilsService.showDialog(
this.i18nService.t("cannotPerformInAppPurchase"),
this.i18nService.t("addCredit"),
null,
null,
"warning"
);
return;
}
this.showAddCredit = true;
}
addCredit() {
if (this.paymentSourceInApp) {
this.platformUtilsService.showDialog(this.i18nService.t('cannotPerformInAppPurchase'),
this.i18nService.t('addCredit'), null, null, 'warning');
return;
}
this.showAddCredit = true;
closeAddCredit(load: boolean) {
this.showAddCredit = false;
if (load) {
this.load();
}
}
closeAddCredit(load: boolean) {
this.showAddCredit = false;
if (load) {
this.load();
}
changePayment() {
if (this.paymentSourceInApp) {
this.platformUtilsService.showDialog(
this.i18nService.t("cannotPerformInAppPurchase"),
this.i18nService.t("changePaymentMethod"),
null,
null,
"warning"
);
return;
}
this.showAdjustPayment = true;
}
changePayment() {
if (this.paymentSourceInApp) {
this.platformUtilsService.showDialog(this.i18nService.t('cannotPerformInAppPurchase'),
this.i18nService.t('changePaymentMethod'), null, null, 'warning');
return;
}
this.showAdjustPayment = true;
closePayment(load: boolean) {
this.showAdjustPayment = false;
if (load) {
this.load();
}
}
closePayment(load: boolean) {
this.showAdjustPayment = false;
if (load) {
this.load();
}
}
get isCreditBalance() {
return this.billing == null || this.billing.balance <= 0;
}
get isCreditBalance() {
return this.billing == null || this.billing.balance <= 0;
}
get creditOrBalance() {
return Math.abs(this.billing != null ? this.billing.balance : 0);
}
get creditOrBalance() {
return Math.abs(this.billing != null ? this.billing.balance : 0);
}
get paymentSource() {
return this.billing != null ? this.billing.paymentSource : null;
}
get paymentSource() {
return this.billing != null ? this.billing.paymentSource : null;
}
get paymentSourceInApp() {
return (
this.paymentSource != null &&
(this.paymentSource.type === PaymentMethodType.AppleInApp ||
this.paymentSource.type === PaymentMethodType.GoogleInApp)
);
}
get paymentSourceInApp() {
return this.paymentSource != null &&
(this.paymentSource.type === PaymentMethodType.AppleInApp ||
this.paymentSource.type === PaymentMethodType.GoogleInApp);
}
get invoices() {
return this.billing != null ? this.billing.invoices : null;
}
get invoices() {
return this.billing != null ? this.billing.invoices : null;
}
get transactions() {
return this.billing != null ? this.billing.transactions : null;
}
get transactions() {
return this.billing != null ? this.billing.transactions : null;
}
}

View File

@@ -1,116 +1,180 @@
<div class="page-header">
<h1>
{{'premiumMembership' | i18n}}
<small *ngIf="firstLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</small>
</h1>
<h1>
{{ "premiumMembership" | i18n }}
<small *ngIf="firstLoaded && loading">
<i
class="fa fa-spinner fa-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
<ng-container *ngIf="!firstLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<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="sub">
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">
{{'subscriptionCanceled' | i18n}}</app-callout>
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()"
[appApiAction]="reinstatePromise" [disabled]="reinstateBtn.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'reinstateSubscription' | i18n}}</span>
</button>
</app-callout>
<dl *ngIf="selfHosted">
<dt>{{'expiration' | i18n}}</dt>
<dd *ngIf="sub.expiration">{{sub.expiration | date:'mediumDate'}}</dd>
<dd *ngIf="!sub.expiration">{{'neverExpires' | i18n}}</dd>
</dl>
<div class="row" *ngIf="!selfHosted">
<div class="col-4">
<dl>
<dt>{{'status' | i18n}}</dt>
<dd>
<span class="text-capitalize">{{(subscription && subscription.status) || '-'}}</span>
<span class="badge badge-warning"
*ngIf="subscriptionMarkedForCancel">{{'pendingCancellation' | i18n}}</span>
</dd>
<dt>{{'nextCharge' | i18n}}</dt>
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$')) :
'-'}}
</dd>
</dl>
</div>
<div class="col-8" *ngIf="subscription">
<strong class="d-block mb-1">{{'details' | i18n}}</strong>
<table class="table">
<tbody>
<tr *ngFor="let i of subscription.items">
<td>
{{i.name}} {{i.quantity > 1 ? '&times;' + i.quantity : ''}} @ {{i.amount | currency:'$'}}
</td>
<td>
{{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}}
</td>
</tr>
</tbody>
</table>
</div>
<app-callout
type="warning"
title="{{ 'canceled' | i18n }}"
*ngIf="subscription && subscription.cancelled"
>
{{ "subscriptionCanceled" | i18n }}</app-callout
>
<app-callout
type="warning"
title="{{ 'pendingCancellation' | i18n }}"
*ngIf="subscriptionMarkedForCancel"
>
<p>{{ "subscriptionPendingCanceled" | i18n }}</p>
<button
#reinstateBtn
type="button"
class="btn btn-outline-secondary btn-submit"
(click)="reinstate()"
[appApiAction]="reinstatePromise"
[disabled]="reinstateBtn.loading"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "reinstateSubscription" | i18n }}</span>
</button>
</app-callout>
<dl *ngIf="selfHosted">
<dt>{{ "expiration" | i18n }}</dt>
<dd *ngIf="sub.expiration">{{ sub.expiration | date: "mediumDate" }}</dd>
<dd *ngIf="!sub.expiration">{{ "neverExpires" | i18n }}</dd>
</dl>
<div class="row" *ngIf="!selfHosted">
<div class="col-4">
<dl>
<dt>{{ "status" | i18n }}</dt>
<dd>
<span class="text-capitalize">{{ (subscription && subscription.status) || "-" }}</span>
<span class="badge badge-warning" *ngIf="subscriptionMarkedForCancel">{{
"pendingCancellation" | i18n
}}</span>
</dd>
<dt>{{ "nextCharge" | i18n }}</dt>
<dd>
{{
nextInvoice
? (nextInvoice.date | date: "mediumDate") +
", " +
(nextInvoice.amount | currency: "$")
: "-"
}}
</dd>
</dl>
</div>
<ng-container *ngIf="selfHosted">
<div>
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
{{'updateLicense' | i18n}}
</button>
<a href="https://vault.bitwarden.com/#/settings/subscription" target="_blank" rel="noopener"
class="btn btn-outline-secondary">
{{'manageSubscription' | i18n}}
</a>
</div>
<div class="card mt-3" *ngIf="showUpdateLicense">
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}"
(click)="closeUpdateLicense(false)"><span aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{'updateLicense' | i18n}}</h3>
<app-update-license (onUpdated)="closeUpdateLicense(true)" (onCanceled)="closeUpdateLicense(false)">
</app-update-license>
</div>
<div class="col-8" *ngIf="subscription">
<strong class="d-block mb-1">{{ "details" | i18n }}</strong>
<table class="table">
<tbody>
<tr *ngFor="let i of subscription.items">
<td>
{{ i.name }} {{ i.quantity > 1 ? "&times;" + i.quantity : "" }} @
{{ i.amount | currency: "$" }}
</td>
<td>{{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<ng-container *ngIf="selfHosted">
<div>
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
{{ "updateLicense" | i18n }}
</button>
<a
href="https://vault.bitwarden.com/#/settings/subscription"
target="_blank"
rel="noopener"
class="btn btn-outline-secondary"
>
{{ "manageSubscription" | i18n }}
</a>
</div>
<div class="card mt-3" *ngIf="showUpdateLicense">
<div class="card-body">
<button
type="button"
class="close"
appA11yTitle="{{ 'cancel' | i18n }}"
(click)="closeUpdateLicense(false)"
>
<span aria-hidden="true">&times;</span>
</button>
<h3 class="card-body-header">{{ "updateLicense" | i18n }}</h3>
<app-update-license
(onUpdated)="closeUpdateLicense(true)"
(onCanceled)="closeUpdateLicense(false)"
>
</app-update-license>
</div>
</div>
</ng-container>
<ng-container *ngIf="!selfHosted">
<div class="d-flex">
<button
type="button"
class="btn btn-outline-secondary"
(click)="downloadLicense()"
*ngIf="!subscription || !subscription.cancelled"
>
{{ "downloadLicense" | i18n }}
</button>
<button
#cancelBtn
type="button"
class="btn btn-outline-danger btn-submit ml-auto"
(click)="cancel()"
[appApiAction]="cancelPromise"
[disabled]="cancelBtn.loading"
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "cancelSubscription" | i18n }}</span>
</button>
</div>
<h2 class="spaced-header">{{ "storage" | i18n }}</h2>
<p>{{ "subscriptionStorage" | i18n: sub.maxStorageGb || 0:sub.storageName || "0 MB" }}</p>
<div class="progress">
<div
class="progress-bar bg-success"
role="progressbar"
[ngStyle]="{ width: storageProgressWidth + '%' }"
[attr.aria-valuenow]="storagePercentage"
aria-valuemin="0"
aria-valuemax="100"
>
{{ storagePercentage / 100 | percent }}
</div>
</div>
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustStorage">
<button type="button" class="btn btn-outline-secondary" (click)="adjustStorage(true)">
{{ "addStorage" | i18n }}
</button>
<button
type="button"
class="ml-1 btn btn-outline-secondary"
(click)="adjustStorage(false)"
>
{{ "removeStorage" | i18n }}
</button>
</div>
<app-adjust-storage
[storageGbPrice]="4"
[add]="adjustStorageAdd"
(onAdjusted)="closeStorage(true)"
(onCanceled)="closeStorage(false)"
*ngIf="showAdjustStorage"
></app-adjust-storage>
</div>
</ng-container>
<ng-container *ngIf="!selfHosted">
<div class="d-flex">
<button type="button" class="btn btn-outline-secondary" (click)="downloadLicense()"
*ngIf="!subscription || !subscription.cancelled">
{{'downloadLicense' | i18n}}
</button>
<button #cancelBtn type="button" class="btn btn-outline-danger btn-submit ml-auto" (click)="cancel()"
[appApiAction]="cancelPromise" [disabled]="cancelBtn.loading"
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'cancelSubscription' | i18n}}</span>
</button>
</div>
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
<p>{{'subscriptionStorage' | i18n : sub.maxStorageGb || 0 : sub.storageName || '0 MB'}}</p>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar" [ngStyle]="{width: storageProgressWidth + '%' }"
[attr.aria-valuenow]="storagePercentage" aria-valuemin="0" aria-valuemax="100">
{{(storagePercentage / 100) | percent}}</div>
</div>
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustStorage">
<button type="button" class="btn btn-outline-secondary" (click)="adjustStorage(true)">
{{'addStorage' | i18n}}
</button>
<button type="button" class="ml-1 btn btn-outline-secondary" (click)="adjustStorage(false)">
{{'removeStorage' | i18n}}
</button>
</div>
<app-adjust-storage [storageGbPrice]="4" [add]="adjustStorageAdd" (onAdjusted)="closeStorage(true)"
(onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
</div>
</ng-container>
</ng-container>
</ng-container>
</ng-container>

View File

@@ -1,176 +1,214 @@
import {
Component,
OnInit,
} from '@angular/core';
import { Router } from '@angular/router';
import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { SubscriptionResponse } from 'jslib-common/models/response/subscriptionResponse';
import { SubscriptionResponse } from "jslib-common/models/response/subscriptionResponse";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service";
@Component({
selector: 'app-user-subscription',
templateUrl: 'user-subscription.component.html',
selector: "app-user-subscription",
templateUrl: "user-subscription.component.html",
})
export class UserSubscriptionComponent implements OnInit {
loading = false;
firstLoaded = false;
adjustStorageAdd = true;
showAdjustStorage = false;
showUpdateLicense = false;
sub: SubscriptionResponse;
selfHosted = false;
loading = false;
firstLoaded = false;
adjustStorageAdd = true;
showAdjustStorage = false;
showUpdateLicense = false;
sub: SubscriptionResponse;
selfHosted = false;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
constructor(private tokenService: TokenService, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private router: Router, private logService: LogService) {
this.selfHosted = platformUtilsService.isSelfHost();
constructor(
private tokenService: TokenService,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private router: Router,
private logService: LogService
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
await this.load();
this.firstLoaded = true;
}
async load() {
if (this.loading) {
return;
}
async ngOnInit() {
await this.load();
this.firstLoaded = true;
if (this.tokenService.getPremium()) {
this.loading = true;
this.sub = await this.apiService.getUserSubscription();
} else {
this.router.navigate(["/settings/premium"]);
return;
}
async load() {
if (this.loading) {
return;
}
this.loading = false;
}
if (this.tokenService.getPremium()) {
this.loading = true;
this.sub = await this.apiService.getUserSubscription();
} else {
this.router.navigate(['/settings/premium']);
return;
}
this.loading = false;
async reinstate() {
if (this.loading) {
return;
}
async reinstate() {
if (this.loading) {
return;
}
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(this.i18nService.t('manageSubscriptionFromStore'),
this.i18nService.t('cancelSubscription'), null, null, 'warning');
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('reinstateConfirmation'),
this.i18nService.t('reinstateSubscription'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
if (!confirmed) {
return;
}
try {
this.reinstatePromise = this.apiService.postReinstatePremium();
await this.reinstatePromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('reinstated'));
this.load();
} catch (e) {
this.logService.error(e);
}
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(
this.i18nService.t("manageSubscriptionFromStore"),
this.i18nService.t("cancelSubscription"),
null,
null,
"warning"
);
return;
}
async cancel() {
if (this.loading) {
return;
}
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(this.i18nService.t('manageSubscriptionFromStore'),
this.i18nService.t('cancelSubscription'), null, null, 'warning');
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('cancelConfirmation'),
this.i18nService.t('cancelSubscription'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
try {
this.cancelPromise = this.apiService.postCancelPremium();
await this.cancelPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('canceledSubscription'));
this.load();
} catch (e) {
this.logService.error(e);
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("reinstateConfirmation"),
this.i18nService.t("reinstateSubscription"),
this.i18nService.t("yes"),
this.i18nService.t("cancel")
);
if (!confirmed) {
return;
}
downloadLicense() {
if (this.loading) {
return;
}
try {
this.reinstatePromise = this.apiService.postReinstatePremium();
await this.reinstatePromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("reinstated"));
this.load();
} catch (e) {
this.logService.error(e);
}
}
const licenseString = JSON.stringify(this.sub.license, null, 2);
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_premium_license.json');
async cancel() {
if (this.loading) {
return;
}
updateLicense() {
if (this.loading) {
return;
}
this.showUpdateLicense = true;
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(
this.i18nService.t("manageSubscriptionFromStore"),
this.i18nService.t("cancelSubscription"),
null,
null,
"warning"
);
return;
}
closeUpdateLicense(load: boolean) {
this.showUpdateLicense = false;
if (load) {
this.load();
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("cancelConfirmation"),
this.i18nService.t("cancelSubscription"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return;
}
adjustStorage(add: boolean) {
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(this.i18nService.t('cannotPerformInAppPurchase'),
this.i18nService.t(add ? 'addStorage' : 'removeStorage'), null, null, 'warning');
return;
}
this.adjustStorageAdd = add;
this.showAdjustStorage = true;
try {
this.cancelPromise = this.apiService.postCancelPremium();
await this.cancelPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("canceledSubscription")
);
this.load();
} catch (e) {
this.logService.error(e);
}
}
downloadLicense() {
if (this.loading) {
return;
}
closeStorage(load: boolean) {
this.showAdjustStorage = false;
if (load) {
this.load();
}
}
const licenseString = JSON.stringify(this.sub.license, null, 2);
this.platformUtilsService.saveFile(
window,
licenseString,
null,
"bitwarden_premium_license.json"
);
}
get subscriptionMarkedForCancel() {
return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
updateLicense() {
if (this.loading) {
return;
}
this.showUpdateLicense = true;
}
get subscription() {
return this.sub != null ? this.sub.subscription : null;
closeUpdateLicense(load: boolean) {
this.showUpdateLicense = false;
if (load) {
this.load();
}
}
get nextInvoice() {
return this.sub != null ? this.sub.upcomingInvoice : null;
adjustStorage(add: boolean) {
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(
this.i18nService.t("cannotPerformInAppPurchase"),
this.i18nService.t(add ? "addStorage" : "removeStorage"),
null,
null,
"warning"
);
return;
}
this.adjustStorageAdd = add;
this.showAdjustStorage = true;
}
get storagePercentage() {
return this.sub != null && this.sub.maxStorageGb ?
+(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2) : 0;
closeStorage(load: boolean) {
this.showAdjustStorage = false;
if (load) {
this.load();
}
}
get storageProgressWidth() {
return this.storagePercentage < 5 ? 5 : 0;
}
get subscriptionMarkedForCancel() {
return (
this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate
);
}
get usingInAppPurchase() {
return this.sub != null ? this.sub.usingInAppPurchase : false;
}
get subscription() {
return this.sub != null ? this.sub.subscription : null;
}
get nextInvoice() {
return this.sub != null ? this.sub.upcomingInvoice : null;
}
get storagePercentage() {
return this.sub != null && this.sub.maxStorageGb
? +(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2)
: 0;
}
get storageProgressWidth() {
return this.storagePercentage < 5 ? 5 : 0;
}
get usingInAppPurchase() {
return this.sub != null ? this.sub.usingInAppPurchase : false;
}
}

View File

@@ -1,28 +1,45 @@
<app-callout type="info" *ngIf="vaultTimeoutPolicy">
{{'vaultTimeoutPolicyInEffect' | i18n : vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes}}
{{ "vaultTimeoutPolicyInEffect" | i18n: vaultTimeoutPolicyHours:vaultTimeoutPolicyMinutes }}
</app-callout>
<div [formGroup]="form">
<div class="form-group">
<label for="vaultTimeout">{{'vaultTimeout' | i18n}}</label>
<select id="vaultTimeout" name="VaultTimeout" formControlName="vaultTimeout" class="form-control">
<option *ngFor="let o of vaultTimeouts" [ngValue]="o.value">{{o.name}}</option>
</select>
<small class="form-text text-muted">{{'vaultTimeoutDesc' | i18n}}</small>
</div>
<div class="form-group" *ngIf="showCustom" formGroupName="custom">
<label for="customVaultTimeout">{{'customVaultTimeout' | i18n}}</label>
<div class="row">
<div class="col-6">
<input id="hours" class="form-control" type="number" min="0" name="hours"
formControlName="hours">
<small>{{'hours' | i18n }}</small>
</div>
<div class="col-6">
<input id="minutes" class="form-control" type="number" min="0" name="minutes"
formControlName="minutes">
<small>{{'minutes' | i18n }}</small>
</div>
</div>
<div class="form-group">
<label for="vaultTimeout">{{ "vaultTimeout" | i18n }}</label>
<select
id="vaultTimeout"
name="VaultTimeout"
formControlName="vaultTimeout"
class="form-control"
>
<option *ngFor="let o of vaultTimeouts" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="form-text text-muted">{{ "vaultTimeoutDesc" | i18n }}</small>
</div>
<div class="form-group" *ngIf="showCustom" formGroupName="custom">
<label for="customVaultTimeout">{{ "customVaultTimeout" | i18n }}</label>
<div class="row">
<div class="col-6">
<input
id="hours"
class="form-control"
type="number"
min="0"
name="hours"
formControlName="hours"
/>
<small>{{ "hours" | i18n }}</small>
</div>
<div class="col-6">
<input
id="minutes"
class="form-control"
type="number"
min="0"
name="minutes"
formControlName="minutes"
/>
<small>{{ "minutes" | i18n }}</small>
</div>
</div>
</div>
</div>

View File

@@ -1,28 +1,22 @@
import { Component } from '@angular/core';
import {
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { Component } from "@angular/core";
import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from "@angular/forms";
import {
VaultTimeoutInputComponent as VaultTimeoutInputComponentBase
} from 'jslib-angular/components/settings/vault-timeout-input.component';
import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "jslib-angular/components/settings/vault-timeout-input.component";
@Component({
selector: 'app-vault-timeout-input',
templateUrl: 'vault-timeout-input.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
],
selector: "app-vault-timeout-input",
templateUrl: "vault-timeout-input.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
],
})
export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {
}
export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {}

View File

@@ -1,15 +1,21 @@
<div class="card border-warning">
<div class="card-header bg-warning text-white">
<i class="fa fa-envelope-o fa-fw" aria-hidden="true"></i> {{'verifyEmail' | i18n}}
</div>
<div class="card-body">
<p>{{'verifyEmailDesc' | i18n}}</p>
<button type="button" class="btn btn-block btn-outline-secondary btn-submit" #sendBtn
[appApiAction]="actionPromise" [disabled]="sendBtn.loading" (click)="send()">
<i class="fa fa-spin fa-spinner" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>
{{'sendEmail' | i18n}}
</span>
</button>
</div>
<div class="card-header bg-warning text-white">
<i class="fa fa-envelope-o fa-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }}
</div>
<div class="card-body">
<p>{{ "verifyEmailDesc" | i18n }}</p>
<button
type="button"
class="btn btn-block btn-outline-secondary btn-submit"
#sendBtn
[appApiAction]="actionPromise"
[disabled]="sendBtn.loading"
(click)="send()"
>
<i class="fa fa-spin fa-spinner" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>
{{ "sendEmail" | i18n }}
</span>
</button>
</div>
</div>

View File

@@ -1,31 +1,39 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: 'app-verify-email',
templateUrl: 'verify-email.component.html',
selector: "app-verify-email",
templateUrl: "verify-email.component.html",
})
export class VerifyEmailComponent {
actionPromise: Promise<any>;
actionPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async send() {
if (this.actionPromise != null) {
return;
}
try {
this.actionPromise = this.apiService.postAccountVerifyEmail();
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('checkInboxForVerification'));
} catch (e) {
this.logService.error(e);
}
this.actionPromise = null;
async send() {
if (this.actionPromise != null) {
return;
}
try {
this.actionPromise = this.apiService.postAccountVerifyEmail();
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("checkInboxForVerification")
);
} catch (e) {
this.logService.error(e);
}
this.actionPromise = null;
}
}