diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index a02713c53f..107447c50a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -14,6 +14,7 @@ import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.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 { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -30,20 +31,19 @@ import { import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; -import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; -import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; -import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service"; -import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; +import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service"; + +import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component"; +import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service"; @Component({ selector: "app-view-v2", templateUrl: "view-v2.component.html", standalone: true, - providers: [ - { provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService }, - ], imports: [ CommonModule, SearchModule, @@ -58,6 +58,10 @@ import { VaultPopupAutofillService } from "../../../services/vault-popup-autofil AsyncActionsModule, PopOutComponent, ], + providers: [ + { provide: ViewPasswordHistoryService, useClass: BrowserViewPasswordHistoryService }, + { provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService }, + ], }) export class ViewV2Component { headerText: string; diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts new file mode 100644 index 0000000000..ded4686477 --- /dev/null +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts @@ -0,0 +1,28 @@ +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { BrowserViewPasswordHistoryService } from "./browser-view-password-history.service"; + +describe("BrowserViewPasswordHistoryService", () => { + let service: BrowserViewPasswordHistoryService; + let router: MockProxy; + + beforeEach(async () => { + router = mock(); + await TestBed.configureTestingModule({ + providers: [BrowserViewPasswordHistoryService, { provide: Router, useValue: router }], + }).compileComponents(); + + service = TestBed.inject(BrowserViewPasswordHistoryService); + }); + + describe("viewPasswordHistory", () => { + it("navigates to the password history screen", async () => { + await service.viewPasswordHistory("test"); + expect(router.navigate).toHaveBeenCalledWith(["/cipher-password-history"], { + queryParams: { cipherId: "test" }, + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts new file mode 100644 index 0000000000..6b57b0b625 --- /dev/null +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts @@ -0,0 +1,18 @@ +import { inject } from "@angular/core"; +import { Router } from "@angular/router"; + +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; + +/** + * This class handles the premium upgrade process for the browser extension. + */ +export class BrowserViewPasswordHistoryService implements ViewPasswordHistoryService { + private router = inject(Router); + + /** + * Navigates to the password history screen. + */ + async viewPasswordHistory(cipherId: string) { + await this.router.navigate(["/cipher-password-history"], { queryParams: { cipherId } }); + } +} diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.html b/apps/web/src/app/vault/individual-vault/password-history.component.html new file mode 100644 index 0000000000..bae10d85aa --- /dev/null +++ b/apps/web/src/app/vault/individual-vault/password-history.component.html @@ -0,0 +1,40 @@ + + + {{ "passwordHistory" | i18n }} + + +
+ +
+ +
{{ h.lastUsedDate | date: "medium" }}
+
+ + + + + +
+
+
+

{{ "noPasswordsInList" | i18n }}

+
+
+ + + +
diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.ts b/apps/web/src/app/vault/individual-vault/password-history.component.ts new file mode 100644 index 0000000000..21a1b01e58 --- /dev/null +++ b/apps/web/src/app/vault/individual-vault/password-history.component.ts @@ -0,0 +1,131 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { OnInit, Inject, Component } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { + AsyncActionsModule, + DialogModule, + DialogService, + ToastService, + ItemModule, +} from "@bitwarden/components"; + +import { SharedModule } from "../../shared/shared.module"; + +/** + * The parameters for the password history dialog. + */ +export interface ViewPasswordHistoryDialogParams { + cipherId: CipherId; +} + +/** + * A dialog component that displays the password history for a cipher. + */ +@Component({ + selector: "app-vault-password-history", + templateUrl: "password-history.component.html", + standalone: true, + imports: [CommonModule, AsyncActionsModule, DialogModule, ItemModule, SharedModule], +}) +export class PasswordHistoryComponent implements OnInit { + /** + * The ID of the cipher to display the password history for. + */ + cipherId: CipherId; + + /** + * The password history for the cipher. + */ + history: PasswordHistoryView[] = []; + + /** + * The constructor for the password history dialog component. + * @param params The parameters passed to the password history dialog. + * @param cipherService The cipher service - used to get the cipher to display the password history for. + * @param platformUtilsService The platform utils service - used to copy passwords to the clipboard. + * @param i18nService The i18n service - used to translate strings. + * @param accountService The account service - used to get the active account to decrypt the cipher. + * @param win The window object - used to copy passwords to the clipboard. + * @param toastService The toast service - used to display feedback to the user when a password is copied. + * @param dialogRef The dialog reference - used to close the dialog. + **/ + constructor( + @Inject(DIALOG_DATA) public params: ViewPasswordHistoryDialogParams, + protected cipherService: CipherService, + protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, + protected accountService: AccountService, + @Inject(WINDOW) private win: Window, + protected toastService: ToastService, + private dialogRef: DialogRef, + ) { + /** + * Set the cipher ID from the parameters. + */ + this.cipherId = params.cipherId; + } + + async ngOnInit() { + await this.init(); + } + + /** + * Copies a password to the clipboard. + * @param password The password to copy. + */ + copy(password: string) { + const copyOptions = this.win != null ? { window: this.win } : undefined; + this.platformUtilsService.copyToClipboard(password, copyOptions); + this.toastService.showToast({ + variant: "info", + title: "", + message: this.i18nService.t("valueCopied", this.i18nService.t("password")), + }); + } + + /** + * Initializes the password history dialog component. + */ + 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 || !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; + } + + /** + * Closes the password history dialog. + */ + close() { + this.dialogRef.close(); + } +} + +/** + * Strongly typed wrapper around the dialog service to open the password history dialog. + */ +export function openPasswordHistoryDialog( + dialogService: DialogService, + config: DialogConfig, +) { + return dialogService.open(PasswordHistoryComponent, config); +} diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index 67a0223c73..ead52d805a 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -22,6 +23,7 @@ import { PremiumUpgradePromptService } from "../../../../../../libs/common/src/v import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component"; import { SharedModule } from "../../shared/shared.module"; import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service"; +import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service"; export interface ViewCipherDialogParams { cipher: CipherView; @@ -57,6 +59,7 @@ export interface ViewCipherDialogCloseResult { standalone: true, imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule], providers: [ + { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, ], }) diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts b/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts new file mode 100644 index 0000000000..2c5fb82c53 --- /dev/null +++ b/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts @@ -0,0 +1,45 @@ +import { Overlay } from "@angular/cdk/overlay"; +import { TestBed } from "@angular/core/testing"; + +import { CipherId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; + +import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; + +import { WebViewPasswordHistoryService } from "./web-view-password-history.service"; + +jest.mock("../individual-vault/password-history.component", () => ({ + openPasswordHistoryDialog: jest.fn(), +})); + +describe("WebViewPasswordHistoryService", () => { + let service: WebViewPasswordHistoryService; + let dialogService: DialogService; + + beforeEach(async () => { + const mockDialogService = { + open: jest.fn(), + }; + + await TestBed.configureTestingModule({ + providers: [ + WebViewPasswordHistoryService, + { provide: DialogService, useValue: mockDialogService }, + Overlay, + ], + }).compileComponents(); + + service = TestBed.inject(WebViewPasswordHistoryService); + dialogService = TestBed.inject(DialogService); + }); + + describe("viewPasswordHistory", () => { + it("calls openPasswordHistoryDialog with the correct parameters", async () => { + const mockCipherId = "cipher-id" as CipherId; + await service.viewPasswordHistory(mockCipherId); + expect(openPasswordHistoryDialog).toHaveBeenCalledWith(dialogService, { + data: { cipherId: mockCipherId }, + }); + }); + }); +}); diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.ts b/apps/web/src/app/vault/services/web-view-password-history.service.ts new file mode 100644 index 0000000000..cbdc3928e6 --- /dev/null +++ b/apps/web/src/app/vault/services/web-view-password-history.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@angular/core"; + +import { CipherId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; + +import { ViewPasswordHistoryService } from "../../../../../../libs/common/src/vault/abstractions/view-password-history.service"; +import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; + +/** + * This service is used to display the password history dialog in the web vault. + */ +@Injectable() +export class WebViewPasswordHistoryService implements ViewPasswordHistoryService { + constructor(private dialogService: DialogService) {} + + /** + * Opens the password history dialog for the given cipher ID. + * @param cipherId The ID of the cipher to view the password history for. + */ + async viewPasswordHistory(cipherId: CipherId) { + openPasswordHistoryDialog(this.dialogService, { data: { cipherId } }); + } +} diff --git a/libs/common/src/vault/abstractions/view-password-history.service.ts b/libs/common/src/vault/abstractions/view-password-history.service.ts new file mode 100644 index 0000000000..d9b1306eac --- /dev/null +++ b/libs/common/src/vault/abstractions/view-password-history.service.ts @@ -0,0 +1,8 @@ +import { CipherId } from "../../types/guid"; + +/** + * The ViewPasswordHistoryService is responsible for displaying the password history for a cipher. + */ +export abstract class ViewPasswordHistoryService { + abstract viewPasswordHistory(cipherId?: CipherId): Promise; +} diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.html b/libs/vault/src/cipher-view/item-history/item-history-v2.component.html index a03639dee6..d48666ad83 100644 --- a/libs/vault/src/cipher-view/item-history/item-history-v2.component.html +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.html @@ -27,10 +27,8 @@

{{ "passwordHistory" | i18n }} diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts index 55c8b90da1..4a37c9a491 100644 --- a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts @@ -3,6 +3,8 @@ import { Component, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; 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 { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -31,7 +33,16 @@ import { export class ItemHistoryV2Component { @Input() cipher: CipherView; + constructor(private viewPasswordHistoryService: ViewPasswordHistoryService) {} + get isLogin() { return this.cipher.type === CipherType.Login; } + + /** + * View the password history for the cipher. + */ + async viewPasswordHistory() { + await this.viewPasswordHistoryService.viewPasswordHistory(this.cipher?.id as CipherId); + } }