Skip to content

Commit

Permalink
[PM-13251] Password History (#11618)
Browse files Browse the repository at this point in the history
* add password history view component in vault lib

* integrate PasswordHistoryView into individual vault

* add password history v2 to browser extension

* update color of password history link

* add check for `cipherId` before rendering password history
  • Loading branch information
nick-livefront authored Oct 18, 2024
1 parent 496bc74 commit 97bf459
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 99 deletions.
6 changes: 3 additions & 3 deletions apps/browser/src/popup/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
Expand Down Expand Up @@ -259,12 +260,11 @@ const routes: Routes = [
canActivate: [authGuard],
data: { state: "view-cipher" } satisfies RouteDataProperties,
}),
{
...extensionRefreshSwap(PasswordHistoryComponent, PasswordHistoryV2Component, {
path: "cipher-password-history",
component: PasswordHistoryComponent,
canActivate: [authGuard],
data: { state: "cipher-password-history" } satisfies RouteDataProperties,
},
}),
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
path: "add-cipher",
canActivate: [authGuard, debounceNavigationGuard()],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<popup-page>
<popup-header slot="header" pageTitle="{{ 'passwordHistory' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
<vault-password-history-view *ngIf="cipherId" [cipherId]="cipherId" />
</popup-page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended";
import { Subject } from "rxjs";

import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";

import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";

import { PasswordHistoryV2Component } from "./vault-password-history-v2.component";

describe("PasswordHistoryV2Component", () => {
let component: PasswordHistoryV2Component;
let fixture: ComponentFixture<PasswordHistoryV2Component>;
const params$ = new Subject();
const back = jest.fn().mockResolvedValue(undefined);

beforeEach(async () => {
back.mockClear();

await TestBed.configureTestingModule({
imports: [PasswordHistoryV2Component],
providers: [
{ provide: WINDOW, useValue: window },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: CipherService, useValue: mock<CipherService>() },
{ provide: AccountService, useValue: mock<AccountService>() },
{ provide: PopupRouterCacheService, useValue: { back } },
{ provide: ActivatedRoute, useValue: { queryParams: params$ } },
{ provide: I18nService, useValue: { t: (key: string) => key } },
],
}).compileComponents();

fixture = TestBed.createComponent(PasswordHistoryV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});

it("sets the cipherId from the params", () => {
params$.next({ cipherId: "444-33-33-1111" });

expect(component["cipherId"]).toBe("444-33-33-1111");
});

it("navigates back when a cipherId is not in the params", () => {
params$.next({});

expect(back).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NgIf } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherId } from "@bitwarden/common/types/guid";

import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.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";

@Component({
standalone: true,
selector: "vault-password-history-v2",
templateUrl: "vault-password-history-v2.component.html",
imports: [
JslibModule,
PopupPageComponent,
PopOutComponent,
PopupHeaderComponent,
PasswordHistoryViewComponent,
NgIf,
],
})
export class PasswordHistoryV2Component implements OnInit {
protected cipherId: CipherId;

constructor(
private browserRouterHistory: PopupRouterCacheService,
private route: ActivatedRoute,
) {}

ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.queryParams.pipe(first()).subscribe((params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
} else {
this.close();
}
});
}

close() {
void this.browserRouterHistory.back();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,7 @@
{{ "passwordHistory" | i18n }}
</span>
<ng-container bitDialogContent>
<div *ngIf="history && history.length">
<bit-item *ngFor="let h of history">
<div class="tw-pl-3 tw-py-2">
<bit-color-password
class="tw-text-base"
[password]="h.password"
[showCount]="false"
></bit-color-password>
<div class="tw-text-sm tw-text-muted">{{ h.lastUsedDate | date: "medium" }}</div>
</div>
<ng-container slot="end">
<bit-item-action>
<button
type="button"
bitIconButton="bwi-clone"
aria-label="Copy"
appStopClick
(click)="copy(h.password)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-item-action>
</ng-container>
</bit-item>
</div>
<div class="no-items" *ngIf="!history || !history.length">
<p>{{ "noPasswordsInList" | i18n }}</p>
</div>
<vault-password-history-view [cipherId]="cipherId" />
</ng-container>
<ng-container bitDialogFooter>
<button bitButton (click)="close()" buttonType="primary" type="button">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
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 { Inject, Component } from "@angular/core";

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 { CipherId } from "@bitwarden/common/types/guid";
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
import {
AsyncActionsModule,
DialogModule,
DialogService,
ToastService,
ItemModule,
} from "@bitwarden/components";
import { AsyncActionsModule, DialogModule, DialogService } from "@bitwarden/components";
import { PasswordHistoryViewComponent } from "@bitwarden/vault";

import { SharedModule } from "../../shared/shared.module";

Expand All @@ -34,9 +23,15 @@ export interface ViewPasswordHistoryDialogParams {
selector: "app-vault-password-history",
templateUrl: "password-history.component.html",
standalone: true,
imports: [CommonModule, AsyncActionsModule, DialogModule, ItemModule, SharedModule],
imports: [
CommonModule,
AsyncActionsModule,
DialogModule,
SharedModule,
PasswordHistoryViewComponent,
],
})
export class PasswordHistoryComponent implements OnInit {
export class PasswordHistoryComponent {
/**
* The ID of the cipher to display the password history for.
*/
Expand All @@ -50,22 +45,10 @@ export class PasswordHistoryComponent implements OnInit {
/**
* 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<PasswordHistoryComponent>,
) {
/**
Expand All @@ -74,44 +57,6 @@ export class PasswordHistoryComponent implements OnInit {
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.
*/
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,9 @@
"message": "Copy password",
"description": "Copy password to clipboard"
},
"passwordCopied": {
"message": "Password copied"
},
"copyUsername": {
"message": "Copy username",
"description": "Copy username to clipboard"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ <h2 bitTypography="h6">{{ "itemHistory" | i18n }}</h2>
</p>
<a
*ngIf="cipher.hasPasswordHistory && isLogin"
class="tw-font-bold tw-no-underline tw-cursor-pointer"
class="tw-font-bold tw-no-underline tw-cursor-pointer tw-text-primary-600"
(click)="viewPasswordHistory()"
bitTypography="body2"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div *ngIf="history && history.length">
<bit-item *ngFor="let h of history">
<div class="tw-pl-3 tw-py-2">
<bit-color-password
class="tw-text-base"
[password]="h.password"
[showCount]="false"
></bit-color-password>
<div class="tw-text-sm tw-text-muted">{{ h.lastUsedDate | date: "medium" }}</div>
</div>
<ng-container slot="end">
<bit-item-action>
<button
type="button"
bitIconButton="bwi-clone"
[appA11yTitle]="'copyPassword' | i18n"
appStopClick
(click)="copy(h.password)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-item-action>
</ng-container>
</bit-item>
</div>
<div class="no-items" *ngIf="!history?.length">
<p>{{ "noPasswordsInList" | i18n }}</p>
</div>
Loading

0 comments on commit 97bf459

Please sign in to comment.