mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 19:23:52 +00:00
Auth/ps 2298 reorg auth (#4564)
* Move auth service factories to Auth team * Move authentication componenets to Auth team * Move auth guard services to Auth team * Move Duo content script to Auth team * Move auth CLI commands to Auth team * Move Desktop Account components to Auth Team * Move Desktop guards to Auth team * Move two-factor provider images to Auth team * Move web Accounts components to Auth Team * Move web settings components to Auth Team * Move web two factor images to Auth Team * Fix missed import changes for Auth Team * Fix Linting errors * Fix missed CLI imports * Fix missed Desktop imports * Revert images move * Fix missed imports in Web * Move angular lib components to Auth Team * Move angular auth guards to Auth team * Move strategy specs to Auth team * Update .eslintignore for new paths * Move lib common abstractions to Auth team * Move services to Auth team * Move common lib enums to Auth team * Move webauthn iframe to Auth team * Move lib common domain models to Auth team * Move common lib requests to Auth team * Move response models to Auth team * Clean up whitelist * Move bit web components to Auth team * Move SSO and SCIM files to Auth team * Revert move SCIM to Auth team SCIM belongs to Admin Console team * Move captcha to Auth team * Move key connector to Auth team * Move emergency access to auth team * Delete extra file * linter fixes * Move kdf config to auth team * Fix whitelist * Fix duo autoformat * Complete two factor provider request move * Fix whitelist names * Fix login capitalization * Revert hint dependency reordering * Revert hint dependency reordering * Revert hint component This components is being picked up as a move between clients * Move web hint component to Auth team * Move new files to auth team * Fix desktop build * Fix browser build
This commit is contained in:
@@ -2,10 +2,11 @@ import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
|
||||
import { DeauthorizeSessionsComponent } from "../../auth/settings/deauthorize-sessions.component";
|
||||
|
||||
import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component";
|
||||
import { DeleteAccountComponent } from "./delete-account.component";
|
||||
import { PurgeVaultComponent } from "./purge-vault.component";
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Component } from "@angular/core";
|
||||
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "@bitwarden/common/models/response/api-key.response";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -7,9 +7,9 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { EmailTokenRequest } from "@bitwarden/common/models/request/email-token.request";
|
||||
import { EmailRequest } from "@bitwarden/common/models/request/email.request";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
|
||||
|
||||
@Component({
|
||||
selector: "app-change-email",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import {
|
||||
DEFAULT_KDF_CONFIG,
|
||||
DEFAULT_PBKDF2_ITERATIONS,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
DEFAULT_ARGON2_PARALLELISM,
|
||||
KdfType,
|
||||
} from "@bitwarden/common/enums/kdfType";
|
||||
import { KdfConfig } from "@bitwarden/common/models/domain/kdf-config";
|
||||
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -2,11 +2,10 @@ import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/components/change-password.component";
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
|
||||
@@ -17,12 +16,13 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/abstractions/send.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { EmergencyAccessStatusType } from "@bitwarden/common/enums/emergencyAccessStatusType";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
|
||||
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/models/request/emergency-access-update.request";
|
||||
import { PasswordRequest } from "@bitwarden/common/models/request/password.request";
|
||||
import { SendWithIdRequest } from "@bitwarden/common/models/request/send-with-id.request";
|
||||
import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="deAuthTitle">{{ "deauthorizeSessions" | i18n }}</h1>
|
||||
<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-user-verification [(ngModel)]="masterPassword" ngDefaultControl name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-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,44 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
@Component({
|
||||
selector: "app-deauthorize-sessions",
|
||||
templateUrl: "deauthorize-sessions.component.html",
|
||||
})
|
||||
export class DeauthorizeSessionsComponent {
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<unknown>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { AccountApiService } from "@bitwarden/common/abstractions/account/account-api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="userAddEditTitle">
|
||||
<app-premium-badge *ngIf="readOnly"></app-premium-badge>
|
||||
{{ title }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h1>
|
||||
<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="bwi bwi-spinner bwi-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/emergency-access/#user-access"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" 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
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="loading || form.loading"
|
||||
[disabled]="readOnly"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" data-dismiss="modal">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
#deleteBtn
|
||||
bitButton
|
||||
buttonType="danger"
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
*ngIf="editMode"
|
||||
[disabled]="$any(deleteBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
||||
[hidden]="$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,103 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { EmergencyAccessType } from "@bitwarden/common/enums/emergencyAccessType";
|
||||
import { EmergencyAccessInviteRequest } from "@bitwarden/common/models/request/emergency-access-invite.request";
|
||||
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/models/request/emergency-access-update.request";
|
||||
|
||||
@Component({
|
||||
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();
|
||||
|
||||
loading = true;
|
||||
readOnly = false;
|
||||
editMode = false;
|
||||
title: string;
|
||||
email: string;
|
||||
type: EmergencyAccessType = EmergencyAccessType.View;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
emergencyAccessType = EmergencyAccessType;
|
||||
waitTimes: { name: string; value: number }[];
|
||||
waitTime: number;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (this.editMode) {
|
||||
const request = new EmergencyAccessUpdateRequest();
|
||||
request.type = this.type;
|
||||
request.waitTimeDays = this.waitTime;
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-attachments",
|
||||
templateUrl: "../../vault/app/vault/attachments.component.html",
|
||||
})
|
||||
export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponent {
|
||||
viewOnly = true;
|
||||
canAccessAttachments = true;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
cryptoService: CryptoService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
i18nService,
|
||||
cryptoService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
window,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService
|
||||
);
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
// Do nothing since cipher is already decoded
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="confirmUserTitle">
|
||||
{{ "confirmUser" | i18n }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h1>
|
||||
<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://bitwarden.com/help/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="bwi bwi-spinner bwi-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,62 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
@Component({
|
||||
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();
|
||||
|
||||
dontAskAgain = false;
|
||||
loading = true;
|
||||
fingerprint: string;
|
||||
|
||||
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);
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dontAskAgain) {
|
||||
await this.stateService.setAutoConfirmFingerprints(true);
|
||||
}
|
||||
|
||||
try {
|
||||
this.onConfirmed.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="userAddEditTitle">
|
||||
{{ "takeover" | i18n }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h1>
|
||||
<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"
|
||||
required
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<app-password-strength
|
||||
[password]="masterPassword"
|
||||
[email]="email"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
>
|
||||
</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="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-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,128 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { takeUntil } from "rxjs";
|
||||
|
||||
import { ChangePasswordComponent } from "@bitwarden/angular/components/change-password.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { KdfType } from "@bitwarden/common/enums/kdfType";
|
||||
import { PolicyData } from "@bitwarden/common/models/data/policy.data";
|
||||
import { KdfConfig } from "@bitwarden/common/models/domain/kdf-config";
|
||||
import { Policy } from "@bitwarden/common/models/domain/policy";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { EmergencyAccessPasswordRequest } from "@bitwarden/common/models/request/emergency-access-password.request";
|
||||
import { PolicyResponse } from "@bitwarden/common/models/response/policy.response";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-takeover",
|
||||
templateUrl: "emergency-access-takeover.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessTakeoverComponent
|
||||
extends ChangePasswordComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
@Output() onDone = new EventEmitter();
|
||||
@Input() emergencyAccessId: string;
|
||||
@Input() name: string;
|
||||
@Input() email: string;
|
||||
@Input() kdf: KdfType;
|
||||
@Input() kdfIterations: number;
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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.policyService
|
||||
.masterPasswordPolicyOptions$(policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions));
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const key = await this.cryptoService.makeKey(
|
||||
this.masterPassword,
|
||||
this.email,
|
||||
takeoverResponse.kdf,
|
||||
new KdfConfig(
|
||||
takeoverResponse.kdfIterations,
|
||||
takeoverResponse.kdfMemory,
|
||||
takeoverResponse.kdfParallelism
|
||||
)
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<div class="page-header">
|
||||
<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="c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-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="bwi bwi-cog bwi-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="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||
{{ "attachments" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-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,104 +0,0 @@
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { EmergencyAccessViewResponse } from "@bitwarden/common/models/response/emergency-access.response";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
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",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessViewComponent implements OnInit {
|
||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||
cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
|
||||
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
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.route.params.subscribe((qParams) => {
|
||||
if (qParams.id == null) {
|
||||
return this.router.navigate(["settings/emergency-access"]);
|
||||
}
|
||||
|
||||
this.id = qParams.id;
|
||||
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
// eslint-disable-next-line
|
||||
const [_, childComponent] = await this.modalService.openViewRef(
|
||||
EmergencyAddEditComponent,
|
||||
this.cipherAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher == null ? null : cipher.id;
|
||||
comp.cipher = cipher;
|
||||
}
|
||||
);
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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 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());
|
||||
|
||||
return decCiphers;
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<div class="page-header">
|
||||
<h1>{{ "emergencyAccess" | i18n }}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{ "emergencyAccessDesc" | i18n }}
|
||||
<a href="https://bitwarden.com/help/emergency-access/" target="_blank" rel="noopener">
|
||||
{{ "learnMore" | i18n }}.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p *ngIf="isOrganizationOwner">
|
||||
<b>{{ "warning" | i18n }}:</b> {{ "emergencyAccessOwnerWarning" | i18n }}
|
||||
</p>
|
||||
|
||||
<div class="page-header d-flex">
|
||||
<h2>
|
||||
{{ "trustedEmergencyContacts" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</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="bwi bwi-plus bwi-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">
|
||||
<bit-avatar
|
||||
[text]="c | userName"
|
||||
[id]="c.granteeId"
|
||||
[color]="c.avatarColor"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="secondary"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Invited"
|
||||
>{{ "invited" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted"
|
||||
>{{ "accepted" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
|
||||
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
|
||||
>
|
||||
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{
|
||||
"emergencyAccessRecoveryApproved" | i18n
|
||||
}}</span>
|
||||
|
||||
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
|
||||
<span bitBadge *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">
|
||||
<button
|
||||
[bitMenuTriggerFor]="trustedContactOptions"
|
||||
class="tw-border-none tw-bg-transparent tw-text-main"
|
||||
type="button"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #trustedContactOptions>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.status === emergencyAccessStatusType.Invited"
|
||||
(click)="reinvite(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted"
|
||||
(click)="confirm(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirm" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
|
||||
(click)="approve(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "approve" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="
|
||||
c.status === emergencyAccessStatusType.RecoveryInitiated ||
|
||||
c.status === emergencyAccessStatusType.RecoveryApproved
|
||||
"
|
||||
(click)="reject(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "reject" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem (click)="remove(c)">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ng-container *ngIf="!trustedContacts || !trustedContacts.length">
|
||||
<p *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="page-header spaced-header">
|
||||
<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">
|
||||
<bit-avatar
|
||||
[text]="c | userName"
|
||||
[id]="c.grantorId"
|
||||
[color]="c.avatarColor"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ c.email }}</span>
|
||||
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{
|
||||
"invited" | i18n
|
||||
}}</span>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted"
|
||||
>{{ "accepted" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
|
||||
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="success"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
|
||||
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
|
||||
>
|
||||
|
||||
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
|
||||
<span bitBadge *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">
|
||||
<button
|
||||
[bitMenuTriggerFor]="grantedContactOptions"
|
||||
class="tw-border-none tw-bg-transparent tw-text-main"
|
||||
type="button"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #grantedContactOptions>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.status === emergencyAccessStatusType.Confirmed"
|
||||
(click)="requestAccess(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "requestAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="
|
||||
c.status === emergencyAccessStatusType.RecoveryApproved &&
|
||||
c.type === emergencyAccessType.Takeover
|
||||
"
|
||||
(click)="takeover(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
||||
{{ "takeover" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="
|
||||
c.status === emergencyAccessStatusType.RecoveryApproved &&
|
||||
c.type === emergencyAccessType.View
|
||||
"
|
||||
[routerLink]="c.id"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
|
||||
{{ "view" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem (click)="remove(c)">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ng-container *ngIf="!grantedContacts || !grantedContacts.length">
|
||||
<p *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #takeoverTemplate></ng-template>
|
||||
<ng-template #confirmTemplate></ng-template>
|
||||
@@ -1,318 +0,0 @@
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { EmergencyAccessStatusType } from "@bitwarden/common/enums/emergencyAccessStatusType";
|
||||
import { EmergencyAccessType } from "@bitwarden/common/enums/emergencyAccessType";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EmergencyAccessConfirmRequest } from "@bitwarden/common/models/request/emergency-access-confirm.request";
|
||||
import {
|
||||
EmergencyAccessGranteeDetailsResponse,
|
||||
EmergencyAccessGrantorDetailsResponse,
|
||||
} from "@bitwarden/common/models/response/emergency-access.response";
|
||||
|
||||
import { EmergencyAccessAddEditComponent } from "./emergency-access-add-edit.component";
|
||||
import { EmergencyAccessConfirmComponent } from "./emergency-access-confirm.component";
|
||||
import { EmergencyAccessTakeoverComponent } from "./emergency-access-takeover.component";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access",
|
||||
templateUrl: "emergency-access.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
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;
|
||||
|
||||
loaded = false;
|
||||
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
|
||||
) {}
|
||||
|
||||
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;
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
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;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onSaved.subscribe(() => {
|
||||
modal.close();
|
||||
this.load();
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
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;
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
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,74 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from "../../vault/app/vault/add-edit.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-vault-add-edit",
|
||||
templateUrl: "../../vault/app/vault/add-edit.component.html",
|
||||
})
|
||||
export class EmergencyAddEditComponent extends BaseAddEditComponent {
|
||||
originalCipher: Cipher = null;
|
||||
viewOnly = true;
|
||||
protected override componentName = "app-org-vault-add-edit";
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
stateService: StateService,
|
||||
collectionService: CollectionService,
|
||||
totpService: TotpService,
|
||||
passwordGenerationService: PasswordGenerationService,
|
||||
messagingService: MessagingService,
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
organizationService: OrganizationService,
|
||||
logService: LogService
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
stateService,
|
||||
collectionService,
|
||||
totpService,
|
||||
passwordGenerationService,
|
||||
messagingService,
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
organizationService,
|
||||
logService,
|
||||
passwordRepromptService
|
||||
);
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.title = this.i18nService.t("viewItem");
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
return Promise.resolve(this.originalCipher);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
import { PaymentComponent } from "./payment.component";
|
||||
|
||||
@@ -5,11 +5,11 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/models/request/update-profile.request";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
|
||||
import { ChangeAvatarComponent } from "./change-avatar.component";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
|
||||
import { ApiKeyComponent } from "./api-key.component";
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { TwoFactorSetupComponent } from "../../auth/settings/two-factor-setup.component";
|
||||
|
||||
import { ChangePasswordComponent } from "./change-password.component";
|
||||
import { SecurityKeysComponent } from "./security-keys.component";
|
||||
import { SecurityComponent } from "./security.component";
|
||||
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-security",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
|
||||
import { StateService } from "../core";
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="2faAuthenticatorTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "authenticatorAppTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<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($any($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="bwi bwi-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="bwi-ul">
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-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="bwi bwi-li bwi-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="bwi bwi-li bwi-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="bwi bwi-spinner bwi-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,111 +0,0 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/models/request/update-two-factor-authenticator.request";
|
||||
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/models/response/two-factor-authenticator.response";
|
||||
import { AuthResponse } from "@bitwarden/common/types/authResponse";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
// NOTE: There are additional options available but these are just the ones we are current using.
|
||||
// See: https://github.com/neocotic/qrious#examples
|
||||
interface QRiousOptions {
|
||||
element: HTMLElement;
|
||||
value: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
QRious: new (options: QRiousOptions) => unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
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<TwoFactorAuthenticatorResponse>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
window.document.body.appendChild(this.qrScript);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
window.document.body.removeChild(this.qrScript);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorAuthenticatorResponse>) {
|
||||
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(() => {
|
||||
new window.QRious({
|
||||
element: document.getElementById("qr"),
|
||||
value:
|
||||
"otpauth://totp/Bitwarden:" +
|
||||
Utils.encodeRFC3986URIComponent(email) +
|
||||
"?secret=" +
|
||||
encodeURIComponent(this.key) +
|
||||
"&issuer=Bitwarden",
|
||||
size: 160,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { VerificationType } from "@bitwarden/common/enums/verificationType";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secret-verification.request";
|
||||
import { TwoFactorProviderRequest } from "@bitwarden/common/models/request/two-factor-provider.request";
|
||||
import { AuthResponseBase } from "@bitwarden/common/types/authResponse";
|
||||
|
||||
@Directive()
|
||||
export abstract class TwoFactorBaseComponent {
|
||||
@Output() onUpdated = new EventEmitter<boolean>();
|
||||
|
||||
type: TwoFactorProviderType;
|
||||
organizationId: string;
|
||||
twoFactorProviderType = TwoFactorProviderType;
|
||||
enabled = false;
|
||||
authed = false;
|
||||
|
||||
protected hashedSecret: string;
|
||||
protected verificationType: VerificationType;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
protected userVerificationService: UserVerificationService
|
||||
) {}
|
||||
|
||||
protected auth(authResponse: AuthResponseBase) {
|
||||
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<unknown>) {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="2faDuoTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>Duo</small>
|
||||
</h1>
|
||||
<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($any($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="bwi bwi-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="bwi bwi-spinner bwi-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,75 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/models/request/update-two-factor-duo.request";
|
||||
import { TwoFactorDuoResponse } from "@bitwarden/common/models/response/two-factor-duo.response";
|
||||
import { AuthResponse } from "@bitwarden/common/types/authResponse";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
@Component({
|
||||
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<TwoFactorDuoResponse>;
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService
|
||||
) {
|
||||
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorDuoResponse>) {
|
||||
super.auth(authResponse);
|
||||
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(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;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="2faEmailTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "emailTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<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($any($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="bwi bwi-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]="$any(sendBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-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="bwi bwi-spinner bwi-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,85 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/models/request/two-factor-email.request";
|
||||
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/models/request/update-two-factor-email.request";
|
||||
import { TwoFactorEmailResponse } from "@bitwarden/common/models/response/two-factor-email.response";
|
||||
import { AuthResponse } from "@bitwarden/common/types/authResponse";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
@Component({
|
||||
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<TwoFactorEmailResponse>;
|
||||
emailPromise: Promise<unknown>;
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
private stateService: StateService
|
||||
) {
|
||||
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorEmailResponse>) {
|
||||
super.auth(authResponse);
|
||||
return this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="2faRecoveryTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "recoveryCodeTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify [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,55 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { TwoFactorRecoverResponse } from "@bitwarden/common/models/response/two-factor-recover.response";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-recovery",
|
||||
templateUrl: "two-factor-recovery.component.html",
|
||||
})
|
||||
export class TwoFactorRecoveryComponent {
|
||||
type = -1;
|
||||
code: string;
|
||||
authed: boolean;
|
||||
twoFactorProviderType = TwoFactorProviderType;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private processResponse(response: TwoFactorRecoverResponse) {
|
||||
this.code = this.formatString(response.code);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<div class="tabbed-header">
|
||||
<h1 *ngIf="!organizationId">{{ "twoStepLogin" | i18n }}</h1>
|
||||
<h1 *ngIf="organizationId">{{ "twoStepLoginEnforcement" | i18n }}</h1>
|
||||
</div>
|
||||
<p *ngIf="!organizationId">{{ "twoStepLoginDesc" | i18n }}</p>
|
||||
<ng-container *ngIf="organizationId">
|
||||
<p>
|
||||
{{ "twoStepLoginOrganizationDescStart" | i18n }}
|
||||
<a routerLink="../policies">{{ "twoStepLoginPolicy" | i18n }}.</a>
|
||||
<br />
|
||||
{{ "twoStepLoginOrganizationDuoDesc" | i18n }}
|
||||
</p>
|
||||
<p>{{ "twoStepLoginOrganizationSsoDesc" | i18n }}</p>
|
||||
</ng-container>
|
||||
<bit-callout type="warning" *ngIf="!organizationId">
|
||||
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
|
||||
<button bitButton buttonType="secondary" (click)="recoveryCode()">
|
||||
{{ "viewRecoveryCode" | i18n }}
|
||||
</button>
|
||||
</bit-callout>
|
||||
<h2 [ngClass]="{ 'mt-5': !organizationId }">
|
||||
{{ "providers" | i18n }}
|
||||
<small *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-fw text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</small>
|
||||
</h2>
|
||||
<bit-callout type="warning" *ngIf="showPolicyWarning">
|
||||
{{ "twoStepLoginPolicyUserWarning" | i18n }}
|
||||
</bit-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="bwi bwi-check text-success bwi-fw"
|
||||
title="{{ 'enabled' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "enabled" | i18n }}</span>
|
||||
</ng-container>
|
||||
<app-premium-badge *ngIf="p.premium"></app-premium-badge>
|
||||
</h3>
|
||||
{{ p.description }}
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!canAccessPremium && p.premium"
|
||||
(click)="manage(p.type)"
|
||||
>
|
||||
{{ "manage" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ng-template #authenticatorTemplate></ng-template>
|
||||
<ng-template #recoveryTemplate></ng-template>
|
||||
<ng-template #duoTemplate></ng-template>
|
||||
<ng-template #emailTemplate></ng-template>
|
||||
<ng-template #yubikeyTemplate></ng-template>
|
||||
<ng-template #webAuthnTemplate></ng-template>
|
||||
@@ -1,210 +0,0 @@
|
||||
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { TwoFactorProviders } from "@bitwarden/common/services/twoFactor.service";
|
||||
|
||||
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",
|
||||
})
|
||||
export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
@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;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private twoFactorAuthPolicyAppliesToActiveUser: boolean;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected modalService: ModalService,
|
||||
protected messagingService: MessagingService,
|
||||
protected policyService: PolicyService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
|
||||
for (const key in TwoFactorProviders) {
|
||||
// eslint-disable-next-line
|
||||
if (!TwoFactorProviders.hasOwnProperty(key)) {
|
||||
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);
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.TwoFactorAuthentication)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((policyAppliesToActiveUser) => {
|
||||
this.twoFactorAuthPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
||||
});
|
||||
|
||||
await this.load();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
authComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Yubikey: {
|
||||
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
yubiComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Duo: {
|
||||
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
duoComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Duo);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Email: {
|
||||
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
emailComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.WebAuthn: {
|
||||
const webAuthnComp = await this.openModal(
|
||||
this.webAuthnModalRef,
|
||||
TwoFactorWebAuthnComponent
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
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;
|
||||
}
|
||||
});
|
||||
this.evaluatePolicies();
|
||||
}
|
||||
|
||||
private async evaluatePolicies() {
|
||||
if (this.organizationId == null && this.providers.filter((p) => p.enabled).length === 1) {
|
||||
this.showPolicyWarning = this.twoFactorAuthPolicyAppliesToActiveUser;
|
||||
} else {
|
||||
this.showPolicyWarning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-body">
|
||||
<p>{{ "twoStepLoginAuthDesc" | i18n }}</p>
|
||||
<app-user-verification [(ngModel)]="secret" ngDefaultControl name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-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,75 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { VerificationType } from "@bitwarden/common/enums/verificationType";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secret-verification.request";
|
||||
import { AuthResponse } from "@bitwarden/common/types/authResponse";
|
||||
import { TwoFactorResponse } from "@bitwarden/common/types/twoFactorResponse";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-verify",
|
||||
templateUrl: "two-factor-verify.component.html",
|
||||
})
|
||||
export class TwoFactorVerifyComponent {
|
||||
@Input() type: TwoFactorProviderType;
|
||||
@Input() organizationId: string;
|
||||
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
|
||||
|
||||
secret: Verification;
|
||||
formPromise: Promise<TwoFactorResponse>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
private userVerificationService: UserVerificationService
|
||||
) {}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="2faU2fTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "webAuthnTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<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($any($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="bwi bwi-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="bwi-ul">
|
||||
<li
|
||||
*ngFor="let k of keys; let i = index"
|
||||
#removeKeyBtn
|
||||
[appApiAction]="k.removePromise"
|
||||
>
|
||||
<i class="bwi bwi-li bwi-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 && !$any(removeKeyBtn).loading">
|
||||
<ng-container *ngIf="k.migrated">
|
||||
<span>{{ "webAuthnMigrated" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
||||
<i
|
||||
class="bwi bwi-spin bwi-spinner text-muted bwi-fw"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="$any(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]="$any(readKeyBtn).loading || webAuthnListening || !keyIdAvailable"
|
||||
#readKeyBtn
|
||||
[appApiAction]="challengePromise"
|
||||
>
|
||||
{{ "readKey" | i18n }}
|
||||
</button>
|
||||
<ng-container *ngIf="$any(readKeyBtn).loading">
|
||||
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!$any(readKeyBtn).loading">
|
||||
<ng-container *ngIf="webAuthnListening">
|
||||
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
|
||||
{{ "twoFactorU2fWaiting" | i18n }}...
|
||||
</ng-container>
|
||||
<ng-container *ngIf="webAuthnResponse">
|
||||
<i class="bwi bwi-check-circle text-success" aria-hidden="true"></i>
|
||||
{{ "twoFactorU2fClickSave" | i18n }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="webAuthnError">
|
||||
<i class="bwi bwi-exclamation-triangle 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="bwi bwi-spinner bwi-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"
|
||||
[disabled]="$any(disableBtn).loading"
|
||||
(click)="disable()"
|
||||
*ngIf="enabled"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-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,180 +0,0 @@
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secret-verification.request";
|
||||
import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/models/request/update-two-factor-web-authn-delete.request";
|
||||
import { UpdateTwoFactorWebAuthnRequest } from "@bitwarden/common/models/request/update-two-factor-web-authn.request";
|
||||
import {
|
||||
ChallengeResponse,
|
||||
TwoFactorWebAuthnResponse,
|
||||
} from "@bitwarden/common/models/response/two-factor-web-authn.response";
|
||||
import { AuthResponse } from "@bitwarden/common/types/authResponse";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
interface Key {
|
||||
id: number;
|
||||
name: string;
|
||||
configured: boolean;
|
||||
migrated?: boolean;
|
||||
removePromise: Promise<TwoFactorWebAuthnResponse> | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-webauthn",
|
||||
templateUrl: "two-factor-webauthn.component.html",
|
||||
})
|
||||
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
|
||||
type = TwoFactorProviderType.WebAuthn;
|
||||
name: string;
|
||||
keys: Key[];
|
||||
keyIdAvailable: number = null;
|
||||
keysConfiguredCount = 0;
|
||||
webAuthnError: boolean;
|
||||
webAuthnListening: boolean;
|
||||
webAuthnResponse: PublicKeyCredential;
|
||||
challengePromise: Promise<ChallengeResponse>;
|
||||
formPromise: Promise<TwoFactorWebAuthnResponse>;
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private ngZone: NgZone,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService
|
||||
) {
|
||||
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorWebAuthnResponse>) {
|
||||
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;
|
||||
|
||||
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: Key) {
|
||||
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
|
||||
return;
|
||||
}
|
||||
const name = key.name != null ? key.name : this.i18nService.t("webAuthnkeyX", key.id as any);
|
||||
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) {
|
||||
// eslint-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;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
// eslint-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 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;
|
||||
}
|
||||
}
|
||||
this.enabled = response.enabled;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="2faYubiKeyTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>YubiKey</small>
|
||||
</h1>
|
||||
<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($any($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="bwi bwi-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="bwi bwi-minus-circle bwi-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="bwi bwi-spinner bwi-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]="$any(disableBtn).loading"
|
||||
(click)="disable()"
|
||||
*ngIf="enabled"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-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,95 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { UpdateTwoFactorYubioOtpRequest } from "@bitwarden/common/models/request/update-two-factor-yubio-otp.request";
|
||||
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/models/response/two-factor-yubi-key.response";
|
||||
import { AuthResponse } from "@bitwarden/common/types/authResponse";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
interface Key {
|
||||
key: string;
|
||||
existingKey: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-yubikey",
|
||||
templateUrl: "two-factor-yubikey.component.html",
|
||||
})
|
||||
export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent {
|
||||
type = TwoFactorProviderType.Yubikey;
|
||||
keys: Key[];
|
||||
nfc = false;
|
||||
|
||||
formPromise: Promise<TwoFactorYubiKeyResponse>;
|
||||
disablePromise: Promise<unknown>;
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService
|
||||
) {
|
||||
super(apiService, i18nService, platformUtilsService, logService, userVerificationService);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorYubiKeyResponse>) {
|
||||
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: Key) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-500 tw-bg-background">
|
||||
<div class="tw-bg-warning-500 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
||||
<i class="bwi bwi-envelope bwi-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }}
|
||||
</div>
|
||||
<div class="tw-p-5">
|
||||
<p>{{ "verifyEmailDesc" | i18n }}</p>
|
||||
<button id="sendBtn" bitButton type="button" block [bitAction]="send">
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-verify-email",
|
||||
templateUrl: "verify-email.component.html",
|
||||
})
|
||||
export class VerifyEmailComponent {
|
||||
actionPromise: Promise<unknown>;
|
||||
|
||||
@Output() onVerified = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private tokenService: TokenService
|
||||
) {}
|
||||
|
||||
async verifyEmail(): Promise<void> {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
if (await this.tokenService.getEmailVerified()) {
|
||||
this.onVerified.emit(true);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified"));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.apiService.postAccountVerifyEmail();
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("checkInboxForVerification")
|
||||
);
|
||||
}
|
||||
|
||||
send = async () => {
|
||||
await this.verifyEmail();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user