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);
+ }
}