1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[PM-11442] Emergency Cipher Viewing (#12054)

* force viewOnly to be true for emergency access

* add input to hide password history, applicable when the view is used from emergency view

* add extension refresh version of the emergency view dialog

* allow emergency access to view password history

- `ViewPasswordHistoryService` accepts a cipher id or CipherView. When a CipherView is included, the history component no longer has to fetch the cipher.

* remove unused comments

* Add fixme comment for removing non-extension refresh code

* refactor password history component to accept a full cipher view

- Remove the option to pass in only an id
This commit is contained in:
Nick Krantz
2024-12-19 09:42:37 -06:00
committed by GitHub
parent 1d04a0a399
commit 0f3803ac91
18 changed files with 337 additions and 83 deletions

View File

@@ -1,8 +1,8 @@
<popup-page> <popup-page [loading]="!cipher">
<popup-header slot="header" pageTitle="{{ 'passwordHistory' | i18n }}" showBackButton> <popup-header slot="header" pageTitle="{{ 'passwordHistory' | i18n }}" showBackButton>
<ng-container slot="end"> <ng-container slot="end">
<app-pop-out></app-pop-out> <app-pop-out></app-pop-out>
</ng-container> </ng-container>
</popup-header> </popup-header>
<vault-password-history-view *ngIf="cipherId" [cipherId]="cipherId" /> <vault-password-history-view *ngIf="cipher" [cipher]="cipher" />
</popup-page> </popup-page>

View File

@@ -1,27 +1,40 @@
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { Subject } from "rxjs"; import { BehaviorSubject, Subject } from "rxjs";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { PasswordHistoryV2Component } from "./vault-password-history-v2.component"; import { PasswordHistoryV2Component } from "./vault-password-history-v2.component";
describe("PasswordHistoryV2Component", () => { describe("PasswordHistoryV2Component", () => {
let component: PasswordHistoryV2Component;
let fixture: ComponentFixture<PasswordHistoryV2Component>; let fixture: ComponentFixture<PasswordHistoryV2Component>;
const params$ = new Subject(); const params$ = new Subject();
const mockCipherView = {
id: "111-222-333",
name: "cipher one",
} as CipherView;
const mockCipher = {
decrypt: jest.fn().mockResolvedValue(mockCipherView),
} as unknown as Cipher;
const back = jest.fn().mockResolvedValue(undefined); const back = jest.fn().mockResolvedValue(undefined);
const getCipher = jest.fn().mockResolvedValue(mockCipher);
beforeEach(async () => { beforeEach(async () => {
back.mockClear(); back.mockClear();
getCipher.mockClear();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PasswordHistoryV2Component], imports: [PasswordHistoryV2Component],
@@ -29,8 +42,13 @@ describe("PasswordHistoryV2Component", () => {
{ provide: WINDOW, useValue: window }, { provide: WINDOW, useValue: window },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() }, { provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() }, { provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: CipherService, useValue: mock<CipherService>() }, { provide: CipherService, useValue: mock<CipherService>({ get: getCipher }) },
{ provide: AccountService, useValue: mock<AccountService>() }, {
provide: AccountService,
useValue: mock<AccountService>({
activeAccount$: new BehaviorSubject({ id: "acct-1" } as Account),
}),
},
{ provide: PopupRouterCacheService, useValue: { back } }, { provide: PopupRouterCacheService, useValue: { back } },
{ provide: ActivatedRoute, useValue: { queryParams: params$ } }, { provide: ActivatedRoute, useValue: { queryParams: params$ } },
{ provide: I18nService, useValue: { t: (key: string) => key } }, { provide: I18nService, useValue: { t: (key: string) => key } },
@@ -38,19 +56,21 @@ describe("PasswordHistoryV2Component", () => {
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(PasswordHistoryV2Component); fixture = TestBed.createComponent(PasswordHistoryV2Component);
component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it("sets the cipherId from the params", () => { it("loads the cipher from params the cipherId from the params", fakeAsync(() => {
params$.next({ cipherId: "444-33-33-1111" }); params$.next({ cipherId: mockCipherView.id });
expect(component["cipherId"]).toBe("444-33-33-1111"); tick(100);
});
expect(getCipher).toHaveBeenCalledWith(mockCipherView.id);
}));
it("navigates back when a cipherId is not in the params", () => { it("navigates back when a cipherId is not in the params", () => {
params$.next({}); params$.next({});
expect(back).toHaveBeenCalledTimes(1); expect(back).toHaveBeenCalledTimes(1);
expect(getCipher).not.toHaveBeenCalled();
}); });
}); });

View File

@@ -3,10 +3,14 @@
import { NgIf } from "@angular/common"; import { NgIf } from "@angular/common";
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators"; import { firstValueFrom } from "rxjs";
import { first, map } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherId } from "@bitwarden/common/types/guid"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component"; import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
@@ -28,18 +32,20 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach
], ],
}) })
export class PasswordHistoryV2Component implements OnInit { export class PasswordHistoryV2Component implements OnInit {
protected cipherId: CipherId; protected cipher: CipherView;
constructor( constructor(
private browserRouterHistory: PopupRouterCacheService, private browserRouterHistory: PopupRouterCacheService,
private route: ActivatedRoute, private route: ActivatedRoute,
private cipherService: CipherService,
private accountService: AccountService,
) {} ) {}
ngOnInit() { ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.queryParams.pipe(first()).subscribe((params) => { this.route.queryParams.pipe(first()).subscribe((params) => {
if (params.cipherId) { if (params.cipherId) {
this.cipherId = params.cipherId; void this.loadCipher(params.cipherId);
} else { } else {
this.close(); this.close();
} }
@@ -49,4 +55,22 @@ export class PasswordHistoryV2Component implements OnInit {
close() { close() {
void this.browserRouterHistory.back(); void this.browserRouterHistory.back();
} }
/** Load the cipher based on the given Id */
private async loadCipher(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
const activeAccount = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)),
);
if (!activeAccount?.id) {
throw new Error("Active account is not available.");
}
const activeUserId = activeAccount.id as UserId;
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
}
} }

View File

@@ -2,6 +2,8 @@ import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { BrowserViewPasswordHistoryService } from "./browser-view-password-history.service"; import { BrowserViewPasswordHistoryService } from "./browser-view-password-history.service";
describe("BrowserViewPasswordHistoryService", () => { describe("BrowserViewPasswordHistoryService", () => {
@@ -19,9 +21,9 @@ describe("BrowserViewPasswordHistoryService", () => {
describe("viewPasswordHistory", () => { describe("viewPasswordHistory", () => {
it("navigates to the password history screen", async () => { it("navigates to the password history screen", async () => {
await service.viewPasswordHistory("test"); await service.viewPasswordHistory({ id: "cipher-id" } as CipherView);
expect(router.navigate).toHaveBeenCalledWith(["/cipher-password-history"], { expect(router.navigate).toHaveBeenCalledWith(["/cipher-password-history"], {
queryParams: { cipherId: "test" }, queryParams: { cipherId: "cipher-id" },
}); });
}); });
}); });

View File

@@ -4,6 +4,7 @@ import { inject } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
/** /**
* This class handles the premium upgrade process for the browser extension. * This class handles the premium upgrade process for the browser extension.
@@ -14,7 +15,9 @@ export class BrowserViewPasswordHistoryService implements ViewPasswordHistorySer
/** /**
* Navigates to the password history screen. * Navigates to the password history screen.
*/ */
async viewPasswordHistory(cipherId: string) { async viewPasswordHistory(cipher: CipherView) {
await this.router.navigate(["/cipher-password-history"], { queryParams: { cipherId } }); await this.router.navigate(["/cipher-password-history"], {
queryParams: { cipherId: cipher.id },
});
} }
} }

View File

@@ -4,16 +4,22 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault";
import { EmergencyAccessService } from "../../../emergency-access"; import { EmergencyAccessService } from "../../../emergency-access";
import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component"; import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component";
import { EmergencyAddEditCipherComponent } from "./emergency-add-edit-cipher.component"; import { EmergencyAddEditCipherComponent } from "./emergency-add-edit-cipher.component";
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
@Component({ @Component({
selector: "emergency-access-view", selector: "emergency-access-view",
templateUrl: "emergency-access-view.component.html", templateUrl: "emergency-access-view.component.html",
providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }],
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class EmergencyAccessViewComponent implements OnInit { export class EmergencyAccessViewComponent implements OnInit {
@@ -31,6 +37,8 @@ export class EmergencyAccessViewComponent implements OnInit {
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private emergencyAccessService: EmergencyAccessService, private emergencyAccessService: EmergencyAccessService,
private configService: ConfigService,
private dialogService: DialogService,
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -49,6 +57,19 @@ export class EmergencyAccessViewComponent implements OnInit {
} }
async selectCipher(cipher: CipherView) { async selectCipher(cipher: CipherView) {
const browserRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
if (browserRefreshEnabled) {
EmergencyViewDialogComponent.open(this.dialogService, {
cipher,
});
return;
}
// FIXME PM-15385: Remove below dialog service logic once extension refresh is live.
// eslint-disable-next-line // eslint-disable-next-line
const [_, childComponent] = await this.modalService.openViewRef( const [_, childComponent] = await this.modalService.openViewRef(
EmergencyAddEditCipherComponent, EmergencyAddEditCipherComponent,

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { Component } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { CollectionService } from "@bitwarden/admin-console/common"; import { CollectionService } from "@bitwarden/admin-console/common";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -30,7 +30,7 @@ import { AddEditComponent as BaseAddEditComponent } from "../../../../vault/indi
selector: "app-org-vault-add-edit", selector: "app-org-vault-add-edit",
templateUrl: "../../../../vault/individual-vault/add-edit.component.html", templateUrl: "../../../../vault/individual-vault/add-edit.component.html",
}) })
export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implements OnInit {
originalCipher: Cipher = null; originalCipher: Cipher = null;
viewOnly = true; viewOnly = true;
protected override componentName = "app-org-vault-add-edit"; protected override componentName = "app-org-vault-add-edit";
@@ -85,6 +85,14 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
this.title = this.i18nService.t("viewItem"); this.title = this.i18nService.t("viewItem");
} }
async ngOnInit(): Promise<void> {
await super.ngOnInit();
// The base component `ngOnInit` calculates the `viewOnly` property based on cipher properties
// In the case of emergency access, `viewOnly` should always be true, set it manually here after
// the base `ngOnInit` is complete.
this.viewOnly = true;
}
protected async loadCipher() { protected async loadCipher() {
return Promise.resolve(this.originalCipher); return Promise.resolve(this.originalCipher);
} }

View File

@@ -0,0 +1,13 @@
<bit-dialog dialogSize="large" background="alt" #dialog>
<span bitDialogTitle aria-live="polite">
{{ title }}
</span>
<div bitDialogContent #dialogContent>
<app-cipher-view [cipher]="cipher"></app-cipher-view>
</div>
<ng-container bitDialogFooter>
<button bitButton type="button" buttonType="secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,108 @@
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { mock } from "jest-mock-extended";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
describe("EmergencyViewDialogComponent", () => {
let component: EmergencyViewDialogComponent;
let fixture: ComponentFixture<EmergencyViewDialogComponent>;
const open = jest.fn();
const close = jest.fn();
const mockCipher = {
id: "cipher1",
name: "Cipher",
type: CipherType.Login,
login: { uris: [] },
card: {},
} as CipherView;
beforeEach(async () => {
open.mockClear();
close.mockClear();
await TestBed.configureTestingModule({
imports: [EmergencyViewDialogComponent, NoopAnimationsModule],
providers: [
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
{ provide: CollectionService, useValue: mock<CollectionService>() },
{ provide: FolderService, useValue: mock<FolderService>() },
{ provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } },
{ provide: DialogService, useValue: { open } },
{ provide: DialogRef, useValue: { close } },
{ provide: DIALOG_DATA, useValue: { cipher: mockCipher } },
],
}).compileComponents();
fixture = TestBed.createComponent(EmergencyViewDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("creates", () => {
expect(component).toBeTruthy();
});
it("opens dialog", () => {
EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher });
expect(open).toHaveBeenCalled();
});
it("closes the dialog", () => {
EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher });
fixture.detectChanges();
const cancelButton = fixture.debugElement.queryAll(By.css("button")).pop();
cancelButton.nativeElement.click();
expect(close).toHaveBeenCalled();
});
describe("updateTitle", () => {
it("sets login title", () => {
mockCipher.type = CipherType.Login;
component["updateTitle"]();
expect(component["title"]).toBe("viewItemType typelogin");
});
it("sets card title", () => {
mockCipher.type = CipherType.Card;
component["updateTitle"]();
expect(component["title"]).toBe("viewItemType typecard");
});
it("sets identity title", () => {
mockCipher.type = CipherType.Identity;
component["updateTitle"]();
expect(component["title"]).toBe("viewItemType typeidentity");
});
it("sets note title", () => {
mockCipher.type = CipherType.SecureNote;
component["updateTitle"]();
expect(component["title"]).toBe("viewItemType note");
});
});
});

View File

@@ -0,0 +1,90 @@
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
import { CipherViewComponent } from "@bitwarden/vault";
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
export interface EmergencyViewDialogParams {
/** The cipher being viewed. */
cipher: CipherView;
}
/** Stubbed class, premium upgrade is not applicable for emergency viewing */
class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
async promptForPremium() {
return Promise.resolve();
}
}
@Component({
selector: "app-emergency-view-dialog",
templateUrl: "emergency-view-dialog.component.html",
standalone: true,
imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule],
providers: [
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
],
})
export class EmergencyViewDialogComponent {
/**
* The title of the dialog. Updates based on the cipher type.
* @protected
*/
protected title: string;
constructor(
@Inject(DIALOG_DATA) protected params: EmergencyViewDialogParams,
private dialogRef: DialogRef,
private i18nService: I18nService,
) {
this.updateTitle();
}
get cipher(): CipherView {
return this.params.cipher;
}
cancel = () => {
this.dialogRef.close();
};
private updateTitle() {
const partOne = "viewItemType";
const type = this.cipher.type;
switch (type) {
case CipherType.Login:
this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase());
break;
case CipherType.Card:
this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase());
break;
case CipherType.Identity:
this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase());
break;
case CipherType.SecureNote:
this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase());
break;
}
}
/**
* Opens the EmergencyViewDialog.
*/
static open(dialogService: DialogService, params: EmergencyViewDialogParams) {
return dialogService.open<EmergencyViewDialogParams>(EmergencyViewDialogComponent, {
data: params,
});
}
}

View File

@@ -3,7 +3,7 @@
{{ "passwordHistory" | i18n }} {{ "passwordHistory" | i18n }}
</span> </span>
<ng-container bitDialogContent> <ng-container bitDialogContent>
<vault-password-history-view [cipherId]="cipherId" /> <vault-password-history-view [cipher]="cipher" />
</ng-container> </ng-container>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button bitButton (click)="close()" buttonType="primary" type="button"> <button bitButton (click)="close()" buttonType="primary" type="button">

View File

@@ -4,8 +4,7 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Inject, Component } from "@angular/core"; import { Inject, Component } from "@angular/core";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
import { AsyncActionsModule, DialogModule, DialogService } from "@bitwarden/components"; import { AsyncActionsModule, DialogModule, DialogService } from "@bitwarden/components";
import { PasswordHistoryViewComponent } from "@bitwarden/vault"; import { PasswordHistoryViewComponent } from "@bitwarden/vault";
@@ -15,7 +14,7 @@ import { SharedModule } from "../../shared/shared.module";
* The parameters for the password history dialog. * The parameters for the password history dialog.
*/ */
export interface ViewPasswordHistoryDialogParams { export interface ViewPasswordHistoryDialogParams {
cipherId: CipherId; cipher: CipherView;
} }
/** /**
@@ -35,14 +34,9 @@ export interface ViewPasswordHistoryDialogParams {
}) })
export class PasswordHistoryComponent { export class PasswordHistoryComponent {
/** /**
* The ID of the cipher to display the password history for. * The cipher to display the password history for.
*/ */
cipherId: CipherId; cipher: CipherView;
/**
* The password history for the cipher.
*/
history: PasswordHistoryView[] = [];
/** /**
* The constructor for the password history dialog component. * The constructor for the password history dialog component.
@@ -54,9 +48,9 @@ export class PasswordHistoryComponent {
private dialogRef: DialogRef<PasswordHistoryComponent>, private dialogRef: DialogRef<PasswordHistoryComponent>,
) { ) {
/** /**
* Set the cipher ID from the parameters. * Set the cipher from the parameters.
*/ */
this.cipherId = params.cipherId; this.cipher = params.cipher;
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { Overlay } from "@angular/cdk/overlay"; import { Overlay } from "@angular/cdk/overlay";
import { TestBed } from "@angular/core/testing"; import { TestBed } from "@angular/core/testing";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; import { openPasswordHistoryDialog } from "../individual-vault/password-history.component";
@@ -35,10 +35,10 @@ describe("WebViewPasswordHistoryService", () => {
describe("viewPasswordHistory", () => { describe("viewPasswordHistory", () => {
it("calls openPasswordHistoryDialog with the correct parameters", async () => { it("calls openPasswordHistoryDialog with the correct parameters", async () => {
const mockCipherId = "cipher-id" as CipherId; const mockCipher = { id: "cipher-id" } as CipherView;
await service.viewPasswordHistory(mockCipherId); await service.viewPasswordHistory(mockCipher);
expect(openPasswordHistoryDialog).toHaveBeenCalledWith(dialogService, { expect(openPasswordHistoryDialog).toHaveBeenCalledWith(dialogService, {
data: { cipherId: mockCipherId }, data: { cipher: mockCipher },
}); });
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { ViewPasswordHistoryService } from "../../../../../../libs/common/src/vault/abstractions/view-password-history.service"; import { ViewPasswordHistoryService } from "../../../../../../libs/common/src/vault/abstractions/view-password-history.service";
@@ -17,7 +17,7 @@ export class WebViewPasswordHistoryService implements ViewPasswordHistoryService
* Opens the password history dialog for the given cipher ID. * Opens the password history dialog for the given cipher ID.
* @param cipherId The ID of the cipher to view the password history for. * @param cipherId The ID of the cipher to view the password history for.
*/ */
async viewPasswordHistory(cipherId: CipherId) { async viewPasswordHistory(cipher: CipherView) {
openPasswordHistoryDialog(this.dialogService, { data: { cipherId } }); openPasswordHistoryDialog(this.dialogService, { data: { cipher } });
} }
} }

View File

@@ -1,8 +1,8 @@
import { CipherId } from "../../types/guid"; import { CipherView } from "../models/view/cipher.view";
/** /**
* The ViewPasswordHistoryService is responsible for displaying the password history for a cipher. * The ViewPasswordHistoryService is responsible for displaying the password history for a cipher.
*/ */
export abstract class ViewPasswordHistoryService { export abstract class ViewPasswordHistoryService {
abstract viewPasswordHistory(cipherId?: CipherId): Promise<void>; abstract viewPasswordHistory(cipher: CipherView): Promise<void>;
} }

View File

@@ -5,7 +5,6 @@ import { Component, Input } from "@angular/core";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherId } from "@bitwarden/common/types/guid";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -45,6 +44,6 @@ export class ItemHistoryV2Component {
* View the password history for the cipher. * View the password history for the cipher.
*/ */
async viewPasswordHistory() { async viewPasswordHistory() {
await this.viewPasswordHistoryService.viewPasswordHistory(this.cipher?.id as CipherId); await this.viewPasswordHistoryService.viewPasswordHistory(this.cipher);
} }
} }

View File

@@ -46,6 +46,7 @@ describe("PasswordHistoryViewComponent", () => {
fixture = TestBed.createComponent(PasswordHistoryViewComponent); fixture = TestBed.createComponent(PasswordHistoryViewComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.cipher = mockCipher;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -60,8 +61,8 @@ describe("PasswordHistoryViewComponent", () => {
beforeEach(async () => { beforeEach(async () => {
mockCipher.passwordHistory = [password1, password2]; mockCipher.passwordHistory = [password1, password2];
mockCipherService.get.mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }); component.cipher = mockCipher;
await component.ngOnInit(); component.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -2,13 +2,9 @@
// @ts-strict-ignore // @ts-strict-ignore
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { OnInit, Component, Input } from "@angular/core"; import { OnInit, Component, Input } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
import { ItemModule, ColorPasswordModule, IconButtonModule } from "@bitwarden/components"; import { ItemModule, ColorPasswordModule, IconButtonModule } from "@bitwarden/components";
@@ -20,39 +16,14 @@ import { ItemModule, ColorPasswordModule, IconButtonModule } from "@bitwarden/co
}) })
export class PasswordHistoryViewComponent implements OnInit { export class PasswordHistoryViewComponent implements OnInit {
/** /**
* The ID of the cipher to display the password history for. * Optional cipher view. When included `cipherId` is ignored.
*/ */
@Input({ required: true }) cipherId: CipherId; @Input({ required: true }) cipher: CipherView;
/** The password history for the cipher. */ /** The password history for the cipher. */
history: PasswordHistoryView[] = []; history: PasswordHistoryView[] = [];
constructor( ngOnInit() {
protected cipherService: CipherService, this.history = this.cipher.passwordHistory == null ? [] : this.cipher.passwordHistory;
protected i18nService: I18nService,
protected accountService: AccountService,
) {}
async ngOnInit() {
await this.init();
}
/** Retrieve the password history for the given cipher */
protected async init() {
const cipher = await this.cipherService.get(this.cipherId);
const activeAccount = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)),
);
if (!activeAccount?.id) {
throw new Error("Active account is not available.");
}
const activeUserId = activeAccount.id as UserId;
const decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
} }
} }