[PM-5054] migrate emergency access to CL (#7850)

Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com>
This commit is contained in:
Will Martin 2024-03-08 09:15:07 -05:00 committed by GitHub
parent 551e43031e
commit c09b446e63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 671 additions and 675 deletions

View File

@ -1,52 +1,37 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle"> <form [formGroup]="confirmForm" [bitSubmit]="submit">
<div class="modal-dialog modal-dialog-scrollable" role="document"> <bit-dialog dialogSize="large" [loading]="loading">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <span bitDialogTitle>
<div class="modal-header"> {{ "confirmUser" | i18n }}
<h1 class="modal-title" id="confirmUserTitle"> <small class="tw-text-muted">{{ params.name }}</small>
{{ "confirmUser" | i18n }} </span>
<small class="text-muted" *ngIf="name">{{ name }}</small> <div bitDialogContent>
</h1> <p bitTypography="body1">
<button {{ "fingerprintEnsureIntegrityVerify" | i18n }}
type="button" <a
class="close" bitLink
data-dismiss="modal" href="https://bitwarden.com/help/fingerprint-phrase/"
appA11yTitle="{{ 'close' | i18n }}" target="_blank"
rel="noreferrer"
> >
<span aria-hidden="true">&times;</span> {{ "learnMore" | i18n }}</a
</button> >
</div> </p>
<div class="modal-body"> <p bitTypography="body1">
<p> <code>{{ fingerprint }}</code>
{{ "fingerprintEnsureIntegrityVerify" | i18n }} </p>
<a href="https://bitwarden.com/help/fingerprint-phrase/" target="_blank" rel="noreferrer">
{{ "learnMore" | i18n }}</a <bit-form-control>
> <input type="checkbox" bitCheckbox formControlName="dontAskAgain" />
</p> <bit-label> {{ "dontAskFingerprintAgain" | i18n }}</bit-label>
<p> </bit-form-control>
<code>{{ fingerprint }}</code> </div>
</p> <div bitDialogFooter>
<div class="form-check"> <button type="submit" buttonType="primary" bitButton bitFormButton>
<input <span>{{ "confirm" | i18n }}</span>
class="form-check-input" </button>
type="checkbox" <button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
id="dontAskAgain" {{ "cancel" | i18n }}
name="DontAskAgain" </button>
[(ngModel)]="dontAskAgain" </div>
/> </bit-dialog>
<label class="form-check-label" for="dontAskAgain"> </form>
{{ "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>

View File

@ -1,39 +1,52 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, OnInit, Inject } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService } from "@bitwarden/components";
export enum EmergencyAccessConfirmDialogResult {
Confirmed = "confirmed",
}
type EmergencyAccessConfirmDialogData = {
/** display name of the account requesting emergency access */
name: string;
/** identifies the account requesting emergency access */
userId: string;
/** traces a unique emergency request */
emergencyAccessId: string;
};
@Component({ @Component({
selector: "emergency-access-confirm", selector: "emergency-access-confirm",
templateUrl: "emergency-access-confirm.component.html", templateUrl: "emergency-access-confirm.component.html",
}) })
export class EmergencyAccessConfirmComponent implements OnInit { 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; loading = true;
fingerprint: string; fingerprint: string;
confirmForm = this.formBuilder.group({
dontAskAgain: [false],
});
constructor( constructor(
@Inject(DIALOG_DATA) protected params: EmergencyAccessConfirmDialogData,
private formBuilder: FormBuilder,
private apiService: ApiService, private apiService: ApiService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private stateService: StateService, private stateService: StateService,
private logService: LogService, private logService: LogService,
private dialogRef: DialogRef<EmergencyAccessConfirmDialogResult>,
) {} ) {}
async ngOnInit() { async ngOnInit() {
try { try {
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId); const publicKeyResponse = await this.apiService.getUserPublicKey(this.params.userId);
if (publicKeyResponse != null) { if (publicKeyResponse != null) {
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const fingerprint = await this.cryptoService.getFingerprint(this.userId, publicKey); const fingerprint = await this.cryptoService.getFingerprint(this.params.userId, publicKey);
if (fingerprint != null) { if (fingerprint != null) {
this.fingerprint = fingerprint.join("-"); this.fingerprint = fingerprint.join("-");
} }
@ -44,19 +57,33 @@ export class EmergencyAccessConfirmComponent implements OnInit {
this.loading = false; this.loading = false;
} }
async submit() { submit = async () => {
if (this.loading) { if (this.loading) {
return; return;
} }
if (this.dontAskAgain) { if (this.confirmForm.get("dontAskAgain").value) {
await this.stateService.setAutoConfirmFingerprints(true); await this.stateService.setAutoConfirmFingerprints(true);
} }
try { try {
this.onConfirmed.emit(); this.dialogRef.close(EmergencyAccessConfirmDialogResult.Confirmed);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
};
/**
* Strongly typed helper to open a EmergencyAccessConfirmComponent
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
static open(
dialogService: DialogService,
config: DialogConfig<EmergencyAccessConfirmDialogData>,
) {
return dialogService.open<EmergencyAccessConfirmDialogResult>(
EmergencyAccessConfirmComponent,
config,
);
} }
} }

View File

@ -1,142 +1,68 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle"> <form [formGroup]="addEditForm" [bitSubmit]="submit">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document"> <bit-dialog dialogSize="large" [loading]="loading">
<form <span bitDialogTitle>
class="modal-content" <app-premium-badge *ngIf="readOnly"></app-premium-badge>
#form {{ title }}
(ngSubmit)="submit()" <small class="tw-text-muted" *ngIf="params.name">{{ params.name }}</small>
[appApiAction]="formPromise" </span>
ngNativeValidate <ng-container bitDialogContent>
> <ng-container *ngIf="!editMode">
<div class="modal-header"> <p bitTypography="body1">{{ "inviteEmergencyContactDesc" | i18n }}</p>
<h1 class="modal-title" id="userAddEditTitle"> <bit-form-field>
<app-premium-badge *ngIf="readOnly"></app-premium-badge> <bit-label>{{ "email" | i18n }}</bit-label>
{{ title }} <input bitInput formControlName="email" />
<small class="text-muted" *ngIf="name">{{ name }}</small> </bit-form-field>
</h1> </ng-container>
<button <bit-radio-group formControlName="emergencyAccessType" [block]="true">
type="button" <bit-label>
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</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 }} {{ "userAccess" | i18n }}
<a <a
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
bitLink
linkType="primary"
appA11yTitle="{{ 'learnMore' | i18n }}" appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/emergency-access/#user-access" href="https://bitwarden.com/help/emergency-access/#user-access"
> >
<i class="bwi bwi-question-circle" aria-hidden="true"></i> <i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a> </a>
</h3> </bit-label>
<div class="form-check mt-2 form-check-block"> <bit-radio-button id="emergencyTypeView" [value]="emergencyAccessType.View">
<input <bit-label>{{ "view" | i18n }}</bit-label>
class="form-check-input" <bit-hint>{{ "viewDesc" | i18n }}</bit-hint>
type="radio" </bit-radio-button>
name="userType"
id="emergencyTypeView" <bit-radio-button id="emergencyTypeTakeover" [value]="emergencyAccessType.Takeover">
[value]="emergencyAccessType.View" <bit-label>{{ "takeover" | i18n }}</bit-label>
[(ngModel)]="type" <bit-hint>{{ "takeoverDesc" | i18n }}</bit-hint>
/> </bit-radio-button>
<label class="form-check-label" for="emergencyTypeView"> </bit-radio-group>
{{ "view" | i18n }}
<small>{{ "viewDesc" | i18n }}</small> <bit-form-field class="tw-w-1/2 tw-relative tw-px-2.5">
</label> <bit-label>{{ "waitTime" | i18n }}</bit-label>
</div> <bit-select formControlName="waitTime">
<div class="form-check mt-2 form-check-block"> <bit-option *ngFor="let o of waitTimes" [value]="o.value" [label]="o.name"></bit-option>
<input </bit-select>
class="form-check-input" <bit-hint class="tw-text-sm">{{ "waitTimeDesc" | i18n }}</bit-hint>
type="radio" </bit-form-field>
name="userType" </ng-container>
id="emergencyTypeTakeover" <ng-container bitDialogFooter>
[value]="emergencyAccessType.Takeover" <button type="submit" buttonType="primary" bitButton bitFormButton [disabled]="readOnly">
[(ngModel)]="type" {{ "save" | i18n }}
[disabled]="readOnly" </button>
/> <button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
<label class="form-check-label" for="emergencyTypeTakeover"> {{ "cancel" | i18n }}
{{ "takeover" | i18n }} </button>
<small>{{ "takeoverDesc" | i18n }}</small> <button
</label> type="button"
</div> bitFormButton
<div class="form-group col-6 mt-4"> class="tw-ml-auto"
<label for="waitTime">{{ "waitTime" | i18n }}</label> bitIconButton="bwi-trash"
<select buttonType="danger"
id="waitTime" [bitAction]="delete"
name="waitTime" *ngIf="editMode"
[(ngModel)]="waitTime" appA11yTitle="{{ 'delete' | i18n }}"
class="form-control" ></button>
[disabled]="readOnly" </ng-container>
> </bit-dialog>
<option *ngFor="let o of waitTimes" [ngValue]="o.value">{{ o.name }}</option> </form>
</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>

View File

@ -1,45 +1,59 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessService } from "../../emergency-access";
import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type"; import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type";
export type EmergencyAccessAddEditDialogData = {
/** display name of the account requesting emergency access */
name: string;
/** traces a unique emergency request */
emergencyAccessId: string;
/** A boolean indicating whether the emergency access request is in read-only mode (true for view-only, false for editing). */
readOnly: boolean;
};
export enum EmergencyAccessAddEditDialogResult {
Saved = "saved",
Canceled = "canceled",
Deleted = "deleted",
}
@Component({ @Component({
selector: "emergency-access-add-edit", selector: "emergency-access-add-edit",
templateUrl: "emergency-access-add-edit.component.html", templateUrl: "emergency-access-add-edit.component.html",
}) })
export class EmergencyAccessAddEditComponent implements OnInit { export class EmergencyAccessAddEditComponent implements OnInit {
@Input() name: string;
@Input() emergencyAccessId: string;
@Output() onSaved = new EventEmitter();
@Output() onDeleted = new EventEmitter();
loading = true; loading = true;
readOnly = false; readOnly = false;
editMode = false; editMode = false;
title: string; title: string;
email: string;
type: EmergencyAccessType = EmergencyAccessType.View; type: EmergencyAccessType = EmergencyAccessType.View;
formPromise: Promise<any>;
emergencyAccessType = EmergencyAccessType; emergencyAccessType = EmergencyAccessType;
waitTimes: { name: string; value: number }[]; waitTimes: { name: string; value: number }[];
waitTime: number;
addEditForm = this.formBuilder.group({
email: ["", [Validators.email, Validators.required]],
emergencyAccessType: [this.emergencyAccessType.View],
waitTime: [{ value: null, disabled: this.readOnly }, [Validators.required]],
});
constructor( constructor(
@Inject(DIALOG_DATA) protected params: EmergencyAccessAddEditDialogData,
private formBuilder: FormBuilder,
private emergencyAccessService: EmergencyAccessService, private emergencyAccessService: EmergencyAccessService,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private logService: LogService, private logService: LogService,
private dialogRef: DialogRef<EmergencyAccessAddEditDialogResult>,
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.editMode = this.loading = this.emergencyAccessId != null; this.editMode = this.loading = this.params.emergencyAccessId != null;
this.waitTimes = [ this.waitTimes = [
{ name: this.i18nService.t("oneDay"), value: 1 }, { name: this.i18nService.t("oneDay"), value: 1 },
{ name: this.i18nService.t("days", "2"), value: 2 }, { name: this.i18nService.t("days", "2"), value: 2 },
@ -50,46 +64,72 @@ export class EmergencyAccessAddEditComponent implements OnInit {
]; ];
if (this.editMode) { if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t("editEmergencyContact"); this.title = this.i18nService.t("editEmergencyContact");
try { try {
const emergencyAccess = await this.emergencyAccessService.getEmergencyAccess( const emergencyAccess = await this.emergencyAccessService.getEmergencyAccess(
this.emergencyAccessId, this.params.emergencyAccessId,
); );
this.type = emergencyAccess.type; this.addEditForm.patchValue({
this.waitTime = emergencyAccess.waitTimeDays; email: emergencyAccess.email,
waitTime: emergencyAccess.waitTimeDays,
emergencyAccessType: emergencyAccess.type,
});
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
} else { } else {
this.title = this.i18nService.t("inviteEmergencyContact"); this.title = this.i18nService.t("inviteEmergencyContact");
this.waitTime = this.waitTimes[2].value; this.addEditForm.patchValue({ waitTime: this.waitTimes[2].value });
} }
this.loading = false; this.loading = false;
} }
async submit() { submit = async () => {
if (this.addEditForm.invalid) {
this.addEditForm.markAllAsTouched();
return;
}
try { try {
if (this.editMode) { if (this.editMode) {
await this.emergencyAccessService.update(this.emergencyAccessId, this.type, this.waitTime); await this.emergencyAccessService.update(
this.params.emergencyAccessId,
this.addEditForm.value.emergencyAccessType,
this.addEditForm.value.waitTime,
);
} else { } else {
await this.emergencyAccessService.invite(this.email, this.type, this.waitTime); await this.emergencyAccessService.invite(
this.addEditForm.value.email,
this.addEditForm.value.emergencyAccessType,
this.addEditForm.value.waitTime,
);
} }
await this.formPromise;
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",
null, null,
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name), this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.params.name),
); );
this.onSaved.emit(); this.dialogRef.close(EmergencyAccessAddEditDialogResult.Saved);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
} };
async delete() { delete = async () => {
this.onDeleted.emit(); this.dialogRef.close(EmergencyAccessAddEditDialogResult.Deleted);
} };
/**
* Strongly typed helper to open a EmergencyAccessAddEditComponent
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
static open = (
dialogService: DialogService,
config: DialogConfig<EmergencyAccessAddEditDialogData>,
) => {
return dialogService.open<EmergencyAccessAddEditDialogResult>(
EmergencyAccessAddEditComponent,
config,
);
};
} }

View File

@ -1,264 +1,276 @@
<app-header></app-header> <app-header></app-header>
<bit-container> <bit-container>
<p> <bit-section>
{{ "emergencyAccessDesc" | i18n }} <p bitTypography="body1">
<a href="https://bitwarden.com/help/emergency-access/" target="_blank" rel="noreferrer"> <span class="tw-text-main">{{ "emergencyAccessDesc" | i18n }}</span>
{{ "learnMore" | i18n }}. <a
</a> bitLink
</p> href="https://bitwarden.com/help/emergency-access/"
target="_blank"
<p *ngIf="isOrganizationOwner"> rel="noreferrer"
<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> {{ "learnMore" | i18n }}.
{{ "addEmergencyContact" | i18n }} </a>
</button> </p>
<bit-callout *ngIf="isOrganizationOwner" type="warning" title="{{ 'warning' | i18n }}">{{
"emergencyAccessOwnerWarning" | i18n
}}</bit-callout>
</bit-section>
<bit-section>
<div class="tw-flex tw-items-center tw-gap-2 tw-mb-2">
<h2 bitTypography="h2" noMargin class="tw-mb-0">
{{ "trustedEmergencyContacts" | i18n }}
</h2>
<app-premium-badge></app-premium-badge>
<div class="tw-ml-auto tw-flex">
<button
type="button"
bitButton
buttonType="primary"
[bitAction]="invite"
[disabled]="!canAccessPremium"
>
<i aria-hidden="true" class="bwi bwi-plus bwi-fw"></i>
{{ "addEmergencyContact" | i18n }}
</button>
</div>
</div> </div>
</div> <bit-table *ngIf="trustedContacts && trustedContacts.length">
<ng-container header>
<tr>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "accessLevel" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "options" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let c of trustedContacts; let i = index">
<td bitCell class="tw-flex tw-items-center tw-gap-4">
<bit-avatar
[text]="c | userName"
[id]="c.granteeId"
[color]="c.avatarColor"
size="small"
></bit-avatar>
<span>
<a bitLink href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
<span
bitBadge
variant="secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{
"emergencyAccessRecoveryApproved" | i18n
}}</span>
<table <small class="tw-text-muted tw-block" *ngIf="c.name">{{ c.name }}</small>
class="table table-hover table-list mb-0" </span>
*ngIf="trustedContacts && trustedContacts.length" </td>
> <td bitCell>
<tbody> <span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<tr *ngFor="let c of trustedContacts; let i = index"> <span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
<td width="30"> "takeover" | i18n
<bit-avatar }}</span>
[text]="c | userName" </td>
[id]="c.granteeId" <td bitCell class="tw-text-right">
[color]="c.avatarColor"
size="small"
></bit-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
<span
bitBadge
variant="secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
variant="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 <button
[bitMenuTriggerFor]="trustedContactOptions"
type="button" type="button"
bitMenuItem appA11yTitle="{{ 'options' | i18n }}"
*ngIf="c.status === emergencyAccessStatusType.Invited" bitIconButton="bwi-ellipsis-v"
(click)="reinvite(c)" buttonType="main"
> ></button>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i> <bit-menu #trustedContactOptions>
{{ "resendInvitation" | i18n }} <button
</button> type="button"
<button bitMenuItem
type="button" *ngIf="c.status === emergencyAccessStatusType.Invited"
bitMenuItem (click)="reinvite(c)"
*ngIf="c.status === emergencyAccessStatusType.Accepted" >
(click)="confirm(c)" <i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
> {{ "resendInvitation" | i18n }}
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i> </button>
{{ "confirm" | i18n }} <button
</button> type="button"
<button bitMenuItem
type="button" *ngIf="c.status === emergencyAccessStatusType.Accepted"
bitMenuItem (click)="confirm(c)"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated" >
(click)="approve(c)" <i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
> {{ "confirm" | i18n }}
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i> </button>
{{ "approve" | i18n }} <button
</button> type="button"
<button bitMenuItem
type="button" *ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
bitMenuItem (click)="approve(c)"
*ngIf=" >
c.status === emergencyAccessStatusType.RecoveryInitiated || <i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
c.status === emergencyAccessStatusType.RecoveryApproved {{ "approve" | i18n }}
" </button>
(click)="reject(c)" <button
> type="button"
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> bitMenuItem
{{ "reject" | i18n }} *ngIf="
</button> c.status === emergencyAccessStatusType.RecoveryInitiated ||
<button type="button" bitMenuItem (click)="remove(c)"> c.status === emergencyAccessStatusType.RecoveryApproved
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> "
{{ "remove" | i18n }} (click)="reject(c)"
</button> >
</bit-menu> <i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
</td> {{ "reject" | i18n }}
</tr> </button>
</tbody> <button type="button" bitMenuItem (click)="remove(c)">
</table> <i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
<ng-container *ngIf="!trustedContacts || !trustedContacts.length"> </button>
<p *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p> </bit-menu>
<ng-container *ngIf="!loaded"> </td>
<i </tr>
class="bwi bwi-spinner bwi-spin text-muted" </ng-template>
title="{{ 'loading' | i18n }}" </bit-table>
aria-hidden="true" <ng-container *ngIf="!trustedContacts || !trustedContacts.length">
></i> <p bitTypography="body1" class="tw-mt-2" *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p>
<span class="sr-only">{{ "loading" | i18n }}</span> <ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container> </ng-container>
</ng-container> </bit-section>
<div class="page-header spaced-header"> <bit-section>
<h2>{{ "designatedEmergencyContacts" | i18n }}</h2> <h2 bitTypography="h2">{{ "designatedEmergencyContacts" | i18n }}</h2>
</div>
<table <bit-table *ngIf="grantedContacts && grantedContacts.length">
class="table table-hover table-list mb-0" <ng-container header>
*ngIf="grantedContacts && grantedContacts.length" <tr>
> <th bitCell>{{ "name" | i18n }}</th>
<tbody> <th bitCell>{{ "accessLevel" | i18n }}</th>
<tr *ngFor="let c of grantedContacts; let i = index"> <th bitCell class="tw-text-right">{{ "options" | i18n }}</th>
<td width="30"> </tr>
<bit-avatar </ng-container>
[text]="c | userName" <ng-template body>
[id]="c.grantorId" <tr bitRow *ngFor="let c of grantedContacts; let i = index">
[color]="c.avatarColor" <td bitCell class="tw-flex tw-items-center tw-gap-4">
size="small" <bit-avatar
></bit-avatar> [text]="c | userName"
</td> [id]="c.grantorId"
<td> [color]="c.avatarColor"
<span>{{ c.email }}</span> size="small"
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{ ></bit-avatar>
"invited" | i18n <span>
}}</span> <span>{{ c.email }}</span>
<span <span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{
bitBadge "invited" | i18n
variant="warning" }}</span>
*ngIf="c.status === emergencyAccessStatusType.Accepted" <span
>{{ "accepted" | i18n }}</span bitBadge
> variant="warning"
<span *ngIf="c.status === emergencyAccessStatusType.Accepted"
bitBadge >{{ "accepted" | i18n }}</span
variant="warning" >
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated" <span
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span bitBadge
> variant="warning"
<span *ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
bitBadge >{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
variant="success" >
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved" <span
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span bitBadge
> variant="success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
>
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span> <small class="tw-text-muted tw-block" *ngIf="c.name">{{ c.name }}</small>
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{ </span>
"takeover" | i18n </td>
}}</span> <td bitCell>
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small> <span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
</td> "takeover" | i18n
<td class="table-list-options"> }}</span>
<button </td>
[bitMenuTriggerFor]="grantedContactOptions" <td bitCell class="tw-text-right">
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 <button
[bitMenuTriggerFor]="grantedContactOptions"
type="button" type="button"
bitMenuItem appA11yTitle="{{ 'options' | i18n }}"
*ngIf="c.status === emergencyAccessStatusType.Confirmed" bitIconButton="bwi-ellipsis-v"
(click)="requestAccess(c)" buttonType="main"
> ></button>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i> <bit-menu #grantedContactOptions>
{{ "requestAccess" | i18n }} <button
</button> type="button"
<button bitMenuItem
type="button" *ngIf="c.status === emergencyAccessStatusType.Confirmed"
bitMenuItem (click)="requestAccess(c)"
*ngIf=" >
c.status === emergencyAccessStatusType.RecoveryApproved && <i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
c.type === emergencyAccessType.Takeover {{ "requestAccess" | i18n }}
" </button>
(click)="takeover(c)" <button
> type="button"
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i> bitMenuItem
{{ "takeover" | i18n }} *ngIf="
</button> c.status === emergencyAccessStatusType.RecoveryApproved &&
<button c.type === emergencyAccessType.Takeover
type="button" "
bitMenuItem (click)="takeover(c)"
*ngIf=" >
c.status === emergencyAccessStatusType.RecoveryApproved && <i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
c.type === emergencyAccessType.View {{ "takeover" | i18n }}
" </button>
[routerLink]="c.id" <button
> type="button"
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i> bitMenuItem
{{ "view" | i18n }} *ngIf="
</button> c.status === emergencyAccessStatusType.RecoveryApproved &&
<button type="button" bitMenuItem (click)="remove(c)"> c.type === emergencyAccessType.View
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> "
{{ "remove" | i18n }} [routerLink]="c.id"
</button> >
</bit-menu> <i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
</td> {{ "view" | i18n }}
</tr> </button>
</tbody> <button type="button" bitMenuItem (click)="remove(c)">
</table> <i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
<ng-container *ngIf="!grantedContacts || !grantedContacts.length"> </button>
<p *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p> </bit-menu>
<ng-container *ngIf="!loaded"> </td>
<i </tr>
class="bwi bwi-spinner bwi-spin text-muted" </ng-template>
title="{{ 'loading' | i18n }}" </bit-table>
aria-hidden="true" <ng-container *ngIf="!grantedContacts || !grantedContacts.length">
></i> <p bitTypography="body1" *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p>
<span class="sr-only">{{ "loading" | i18n }}</span> <ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container> </ng-container>
</ng-container> </bit-section>
</bit-container> </bit-container>
<ng-template #addEdit></ng-template> <ng-template #addEdit></ng-template>

View File

@ -1,7 +1,7 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { lastValueFrom } from "rxjs";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -18,9 +18,18 @@ import {
GrantorEmergencyAccess, GrantorEmergencyAccess,
} from "../../emergency-access/models/emergency-access"; } from "../../emergency-access/models/emergency-access";
import { EmergencyAccessConfirmComponent } from "./confirm/emergency-access-confirm.component"; import {
import { EmergencyAccessAddEditComponent } from "./emergency-access-add-edit.component"; EmergencyAccessConfirmComponent,
import { EmergencyAccessTakeoverComponent } from "./takeover/emergency-access-takeover.component"; EmergencyAccessConfirmDialogResult,
} from "./confirm/emergency-access-confirm.component";
import {
EmergencyAccessAddEditComponent,
EmergencyAccessAddEditDialogResult,
} from "./emergency-access-add-edit.component";
import {
EmergencyAccessTakeoverComponent,
EmergencyAccessTakeoverResultType,
} from "./takeover/emergency-access-takeover.component";
@Component({ @Component({
selector: "emergency-access", selector: "emergency-access",
@ -46,7 +55,6 @@ export class EmergencyAccessComponent implements OnInit {
constructor( constructor(
private emergencyAccessService: EmergencyAccessService, private emergencyAccessService: EmergencyAccessService,
private i18nService: I18nService, private i18nService: I18nService,
private modalService: ModalService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService, private messagingService: MessagingService,
private userNamePipe: UserNamePipe, private userNamePipe: UserNamePipe,
@ -78,37 +86,26 @@ export class EmergencyAccessComponent implements OnInit {
} }
} }
async edit(details: GranteeEmergencyAccess) { edit = async (details: GranteeEmergencyAccess) => {
const [modal] = await this.modalService.openViewRef( const dialogRef = EmergencyAccessAddEditComponent.open(this.dialogService, {
EmergencyAccessAddEditComponent, data: {
this.addEditModalRef, name: this.userNamePipe.transform(details),
(comp) => { emergencyAccessId: details?.id,
comp.name = this.userNamePipe.transform(details); readOnly: !this.canAccessPremium,
comp.emergencyAccessId = details?.id;
comp.readOnly = !this.canAccessPremium;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onSaved.subscribe(() => {
modal.close();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onDeleted.subscribe(() => {
modal.close();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.remove(details);
});
}, },
); });
}
invite() { const result = await lastValueFrom(dialogRef.closed);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. if (result === EmergencyAccessAddEditDialogResult.Saved) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises await this.load();
this.edit(null); } else if (result === EmergencyAccessAddEditDialogResult.Deleted) {
} await this.remove(details);
}
};
invite = async () => {
await this.edit(null);
};
async reinvite(contact: GranteeEmergencyAccess) { async reinvite(contact: GranteeEmergencyAccess) {
if (this.actionPromise != null) { if (this.actionPromise != null) {
@ -135,29 +132,23 @@ export class EmergencyAccessComponent implements OnInit {
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints(); const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
if (autoConfirm == null || !autoConfirm) { if (autoConfirm == null || !autoConfirm) {
const [modal] = await this.modalService.openViewRef( const dialogRef = EmergencyAccessConfirmComponent.open(this.dialogService, {
EmergencyAccessConfirmComponent, data: {
this.confirmModalRef, name: this.userNamePipe.transform(contact),
(comp) => { emergencyAccessId: contact.id,
comp.name = this.userNamePipe.transform(contact); userId: contact?.granteeId,
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.emergencyAccessService.confirm(contact.id, contact.granteeId);
await comp.formPromise;
updateUser();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)),
);
});
}, },
); });
const result = await lastValueFrom(dialogRef.closed);
if (result === EmergencyAccessConfirmDialogResult.Confirmed) {
await this.emergencyAccessService.confirm(contact.id, contact.granteeId);
updateUser();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)),
);
}
return; return;
} }
@ -267,27 +258,23 @@ export class EmergencyAccessComponent implements OnInit {
); );
} }
async takeover(details: GrantorEmergencyAccess) { takeover = async (details: GrantorEmergencyAccess) => {
const [modal] = await this.modalService.openViewRef( const dialogRef = EmergencyAccessTakeoverComponent.open(this.dialogService, {
EmergencyAccessTakeoverComponent, data: {
this.takeoverModalRef, name: this.userNamePipe.transform(details),
(comp) => { email: details.email,
comp.name = this.userNamePipe.transform(details); emergencyAccessId: details.id ?? null,
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)),
);
});
}, },
); });
} const result = await lastValueFrom(dialogRef.closed);
if (result === EmergencyAccessTakeoverResultType.Done) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)),
);
}
};
private removeGrantee(details: GranteeEmergencyAccess) { private removeGrantee(details: GranteeEmergencyAccess) {
const index = this.trustedContacts.indexOf(details); const index = this.trustedContacts.indexOf(details);

View File

@ -1,79 +1,54 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle"> <form [formGroup]="takeoverForm" [bitSubmit]="submit">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document"> <bit-dialog dialogSize="large">
<form <span bitDialogTitle>
class="modal-content" {{ "takeover" | i18n }}
#form <small class="tw-text-muted" *ngIf="params.name">{{ params.name }}</small>
(ngSubmit)="submit()" </span>
[appApiAction]="formPromise" <div bitDialogContent>
ngNativeValidate <app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
> <auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
<div class="modal-header"> </auth-password-callout>
<h1 class="modal-title" id="userAddEditTitle"> <div class="tw-w-full tw-flex tw-gap-4">
{{ "takeover" | i18n }} <div class="tw-relative tw-flex-1">
<small class="text-muted" *ngIf="name">{{ name }}</small> <bit-form-field disableMargin class="tw-mb-2">
</h1> <bit-label>{{ "newMasterPass" | i18n }}</bit-label>
<button <input
type="button" bitInput
class="close" type="password"
data-dismiss="modal" autocomplete="new-password"
appA11yTitle="{{ 'close' | i18n }}" formControlName="masterPassword"
> />
<span aria-hidden="true">&times;</span> <button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
</button> </bit-form-field>
</div> <app-password-strength
<div class="modal-body"> [password]="takeoverForm.value.masterPassword"
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout> [email]="email"
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions"> [showText]="true"
</auth-password-callout> (passwordStrengthResult)="getStrengthResult($event)"
<div class="row"> >
<div class="col-6"> </app-password-strength>
<div class="form-group"> </div>
<label for="masterPassword">{{ "newMasterPass" | i18n }}</label> <div class="tw-relative tw-flex-1">
<input <bit-form-field disableMargin class="tw-mb-2">
id="masterPassword" <bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
type="password" <input
name="NewMasterPasswordHash" bitInput
class="form-control mb-1" type="password"
[(ngModel)]="masterPassword" autocomplete="new-password"
required formControlName="masterPasswordRetype"
appInputVerbatim />
autocomplete="new-password" <button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
/> </bit-form-field>
<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> </div>
<div class="modal-footer"> </div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <div bitDialogFooter>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <button type="submit" bitButton bitFormButton buttonType="primary">
<span>{{ "save" | i18n }}</span> {{ "save" | i18n }}
</button> </button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> <button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }} {{ "cancel" | i18n }}
</button> </button>
</div> </div>
</form> </bit-dialog>
</div> </form>
</div>

View File

@ -1,4 +1,6 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, OnDestroy, OnInit, Inject, Input } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { takeUntil } from "rxjs"; import { takeUntil } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
@ -15,6 +17,17 @@ import { DialogService } from "@bitwarden/components";
import { EmergencyAccessService } from "../../../emergency-access"; import { EmergencyAccessService } from "../../../emergency-access";
export enum EmergencyAccessTakeoverResultType {
Done = "done",
}
type EmergencyAccessTakeoverDialogData = {
/** display name of the account requesting emergency access takeover */
name: string;
/** email of the account requesting emergency access takeover */
email: string;
/** traces a unique emergency request */
emergencyAccessId: string;
};
@Component({ @Component({
selector: "emergency-access-takeover", selector: "emergency-access-takeover",
templateUrl: "emergency-access-takeover.component.html", templateUrl: "emergency-access-takeover.component.html",
@ -24,16 +37,16 @@ export class EmergencyAccessTakeoverComponent
extends ChangePasswordComponent extends ChangePasswordComponent
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
@Output() onDone = new EventEmitter();
@Input() emergencyAccessId: string;
@Input() name: string;
@Input() email: string;
@Input() kdf: KdfType; @Input() kdf: KdfType;
@Input() kdfIterations: number; @Input() kdfIterations: number;
takeoverForm = this.formBuilder.group({
formPromise: Promise<any>; masterPassword: ["", [Validators.required]],
masterPasswordRetype: ["", [Validators.required]],
});
constructor( constructor(
@Inject(DIALOG_DATA) protected params: EmergencyAccessTakeoverDialogData,
private formBuilder: FormBuilder,
i18nService: I18nService, i18nService: I18nService,
cryptoService: CryptoService, cryptoService: CryptoService,
messagingService: MessagingService, messagingService: MessagingService,
@ -44,6 +57,7 @@ export class EmergencyAccessTakeoverComponent
private emergencyAccessService: EmergencyAccessService, private emergencyAccessService: EmergencyAccessService,
private logService: LogService, private logService: LogService,
dialogService: DialogService, dialogService: DialogService,
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
) { ) {
super( super(
i18nService, i18nService,
@ -58,7 +72,9 @@ export class EmergencyAccessTakeoverComponent
} }
async ngOnInit() { async ngOnInit() {
const policies = await this.emergencyAccessService.getGrantorPolicies(this.emergencyAccessId); const policies = await this.emergencyAccessService.getGrantorPolicies(
this.params.emergencyAccessId,
);
this.policyService this.policyService
.masterPasswordPolicyOptions$(policies) .masterPasswordPolicyOptions$(policies)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
@ -70,18 +86,23 @@ export class EmergencyAccessTakeoverComponent
super.ngOnDestroy(); super.ngOnDestroy();
} }
async submit() { submit = async () => {
if (this.takeoverForm.invalid) {
this.takeoverForm.markAllAsTouched();
return;
}
this.masterPassword = this.takeoverForm.get("masterPassword").value;
this.masterPasswordRetype = this.takeoverForm.get("masterPasswordRetype").value;
if (!(await this.strongPassword())) { if (!(await this.strongPassword())) {
return; return;
} }
try { try {
await this.emergencyAccessService.takeover( await this.emergencyAccessService.takeover(
this.emergencyAccessId, this.params.emergencyAccessId,
this.masterPassword, this.masterPassword,
this.email, this.params.email,
); );
this.onDone.emit();
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
@ -90,5 +111,20 @@ export class EmergencyAccessTakeoverComponent
this.i18nService.t("unexpectedError"), this.i18nService.t("unexpectedError"),
); );
} }
} this.dialogRef.close(EmergencyAccessTakeoverResultType.Done);
};
/**
* Strongly typed helper to open a EmergencyAccessTakeoverComponent
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
static open = (
dialogService: DialogService,
config: DialogConfig<EmergencyAccessTakeoverDialogData>,
) => {
return dialogService.open<EmergencyAccessTakeoverResultType>(
EmergencyAccessTakeoverComponent,
config,
);
};
} }

View File

@ -1,71 +1,76 @@
<div class="page-header"> <h1 bitTypography="h1">{{ "vault" | i18n }}</h1>
<h1>{{ "vault" | i18n }}</h1>
</div> <div class="tw-mt-6">
<div class="mt-4">
<ng-container *ngIf="ciphers.length"> <ng-container *ngIf="ciphers.length">
<table class="table table-hover table-list table-ciphers"> <bit-table>
<tbody> <ng-template body>
<tr *ngFor="let c of ciphers"> <tr bitRow *ngFor="let currentCipher of ciphers">
<td class="table-list-icon"> <td bitCell>
<app-vault-icon [cipher]="c"></app-vault-icon> <app-vault-icon [cipher]="currentCipher"></app-vault-icon>
</td> </td>
<td class="reduced-lh wrap"> <td bitCell class="tw-w-full">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{ <a
c.name bitLink
}}</a> href="#"
<ng-container *ngIf="c.organizationId"> appStopClick
(click)="selectCipher(currentCipher)"
title="{{ 'editItem' | i18n }}"
>{{ currentCipher.name }}</a
>
<ng-container *ngIf="currentCipher.organizationId">
<i <i
class="bwi bwi-collection" class="bwi bwi-collection"
appStopProp appStopProp
title="{{ 'shared' | i18n }}" title="{{ 'shared' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "shared" | i18n }}</span> <span class="tw-sr-only">{{ "shared" | i18n }}</span>
</ng-container> </ng-container>
<ng-container *ngIf="c.hasAttachments"> <ng-container *ngIf="currentCipher.hasAttachments">
<i <i
class="bwi bwi-paperclip" class="bwi bwi-paperclip"
appStopProp appStopProp
title="{{ 'attachments' | i18n }}" title="{{ 'attachments' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "attachments" | i18n }}</span> <span class="tw-sr-only">{{ "attachments" | i18n }}</span>
</ng-container> </ng-container>
<br /> <br />
<small>{{ c.subTitle }}</small> <small class="tw-text-xs">{{ currentCipher.subTitle }}</small>
</td> </td>
<td class="table-list-options"> <td bitCell>
<div class="dropdown" appListDropdown *ngIf="c.hasAttachments"> <div *ngIf="currentCipher.hasAttachments">
<button <button
class="btn btn-outline-secondary dropdown-toggle" [bitMenuTriggerFor]="optionsMenu"
type="button" type="button"
id="dropdownMenuButton" buttonType="main"
data-toggle="dropdown" bitIconButton="bwi-ellipsis-v"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}" appA11yTitle="{{ 'options' | i18n }}"
> ></button>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i> <bit-menu #optionsMenu>
</button> <button
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton"> type="button"
<a class="dropdown-item" href="#" appStopClick (click)="viewAttachments(c)"> bitMenuItem
appStopClick
(click)="viewAttachments(currentCipher)"
>
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i> <i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
{{ "attachments" | i18n }} {{ "attachments" | i18n }}
</a> </button>
</div> </bit-menu>
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </ng-template>
</table> </bit-table>
</ng-container> </ng-container>
<ng-container *ngIf="!loaded"> <ng-container *ngIf="!loaded">
<i <i
class="bwi bwi-spinner bwi-spin text-muted" class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
</div> </div>
<ng-template #cipherAddEdit></ng-template> <ng-template #cipherAddEdit></ng-template>

View File

@ -578,6 +578,9 @@
"access": { "access": {
"message": "Access" "message": "Access"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },