mirror of
https://github.com/bitwarden/browser
synced 2025-12-28 14:13:22 +00:00
Apply Prettier (#1347)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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 × {{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">×</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 ×
|
||||
{{ 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:'$'}} × 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}} × {{selectablePlan.seatPrice / 12 | currency:'$'}} × 12
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
|
||||
| currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{selectablePlan.additionalStoragePricePerGb / 12 | currency:'$'}} × 12 {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{selectablePlan.premiumAccessOptionCost / 12 | currency:'$'}} × 12 {{'monthAbbr' | i18n}}
|
||||
=
|
||||
{{40 | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
||||
{{'monthly' | i18n}}
|
||||
<small *ngIf="selectablePlan.basePrice">
|
||||
{{'basePrice' | i18n}}: {{selectablePlan.basePrice | currency:'$'}} {{'monthAbbr' | i18n}}
|
||||
=
|
||||
{{selectablePlan.basePrice | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
|
||||
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{selectablePlan.seatPrice | currency:'$'}}
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
|
||||
| currency:'$'}} /{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{selectablePlan.additionalStoragePricePerGb | currency:'$'}} {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{selectablePlan.premiumAccessOptionCost | currency:'$'}} {{'monthAbbr' | i18n}} =
|
||||
{{40 | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
</ng-container>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<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: "$" }} × 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 }} ×
|
||||
{{ selectablePlan.seatPrice / 12 | currency: "$" }} × 12
|
||||
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency: "$" }} /{{
|
||||
"year" | i18n
|
||||
}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} ×
|
||||
{{ selectablePlan.additionalStoragePricePerGb / 12 | currency: "$" }} × 12
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{ "premiumAccess" | i18n }}:
|
||||
{{ selectablePlan.premiumAccessOptionCost / 12 | currency: "$" }} × 12
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
{{ 40 | currency: "$" }}
|
||||
/{{ "year" | i18n }}
|
||||
</small>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
||||
{{ "monthly" | i18n }}
|
||||
<small *ngIf="selectablePlan.basePrice">
|
||||
{{ "basePrice" | i18n }}: {{ selectablePlan.basePrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
{{ selectablePlan.basePrice | currency: "$" }}
|
||||
/{{ "month" | i18n }}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
|
||||
<span *ngIf="selectablePlan.baseSeats">{{ "additionalUsers" | i18n }}:</span>
|
||||
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ additionalSeats || 0 }} × {{ selectablePlan.seatPrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency: "$" }} /{{
|
||||
"month" | i18n
|
||||
}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} ×
|
||||
{{ selectablePlan.additionalStoragePricePerGb | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{ "premiumAccess" | i18n }}:
|
||||
{{ selectablePlan.premiumAccessOptionCost | currency: "$" }} {{ "monthAbbr" | i18n }} =
|
||||
{{ 40 | currency: "$" }}
|
||||
/{{ "month" | i18n }}
|
||||
</small>
|
||||
</ng-container>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-3" />
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 × {{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 ×
|
||||
{{ 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ? '×' + 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">×</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 ? "×" + 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">×</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user