[PM-13251] Password History (#11618)
* 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
This commit is contained in:
parent
496bc74b51
commit
97bf459424
|
@ -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 { 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 { 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 { 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 { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
|
||||||
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
||||||
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||||
|
@ -259,12 +260,11 @@ const routes: Routes = [
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "view-cipher" } satisfies RouteDataProperties,
|
data: { state: "view-cipher" } satisfies RouteDataProperties,
|
||||||
}),
|
}),
|
||||||
{
|
...extensionRefreshSwap(PasswordHistoryComponent, PasswordHistoryV2Component, {
|
||||||
path: "cipher-password-history",
|
path: "cipher-password-history",
|
||||||
component: PasswordHistoryComponent,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "cipher-password-history" } satisfies RouteDataProperties,
|
data: { state: "cipher-password-history" } satisfies RouteDataProperties,
|
||||||
},
|
}),
|
||||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||||
path: "add-cipher",
|
path: "add-cipher",
|
||||||
canActivate: [authGuard, debounceNavigationGuard()],
|
canActivate: [authGuard, debounceNavigationGuard()],
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,34 +3,7 @@
|
||||||
{{ "passwordHistory" | i18n }}
|
{{ "passwordHistory" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
<ng-container bitDialogContent>
|
<ng-container bitDialogContent>
|
||||||
<div *ngIf="history && history.length">
|
<vault-password-history-view [cipherId]="cipherId" />
|
||||||
<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>
|
|
||||||
</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">
|
||||||
|
|
|
@ -1,22 +1,11 @@
|
||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { OnInit, Inject, Component } from "@angular/core";
|
import { Inject, Component } from "@angular/core";
|
||||||
import { firstValueFrom, map } from "rxjs";
|
|
||||||
|
|
||||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
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 { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
|
||||||
import {
|
import { AsyncActionsModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||||
AsyncActionsModule,
|
import { PasswordHistoryViewComponent } from "@bitwarden/vault";
|
||||||
DialogModule,
|
|
||||||
DialogService,
|
|
||||||
ToastService,
|
|
||||||
ItemModule,
|
|
||||||
} from "@bitwarden/components";
|
|
||||||
|
|
||||||
import { SharedModule } from "../../shared/shared.module";
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
|
|
||||||
|
@ -34,9 +23,15 @@ export interface ViewPasswordHistoryDialogParams {
|
||||||
selector: "app-vault-password-history",
|
selector: "app-vault-password-history",
|
||||||
templateUrl: "password-history.component.html",
|
templateUrl: "password-history.component.html",
|
||||||
standalone: true,
|
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.
|
* The ID of the cipher to display the password history for.
|
||||||
*/
|
*/
|
||||||
|
@ -50,22 +45,10 @@ export class PasswordHistoryComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The constructor for the password history dialog component.
|
* The constructor for the password history dialog component.
|
||||||
* @param params The parameters passed to the password history dialog.
|
* @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.
|
* @param dialogRef The dialog reference - used to close the dialog.
|
||||||
**/
|
**/
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) public params: ViewPasswordHistoryDialogParams,
|
@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>,
|
private dialogRef: DialogRef<PasswordHistoryComponent>,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
@ -74,44 +57,6 @@ export class PasswordHistoryComponent implements OnInit {
|
||||||
this.cipherId = params.cipherId;
|
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.
|
* Closes the password history dialog.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -663,6 +663,9 @@
|
||||||
"message": "Copy password",
|
"message": "Copy password",
|
||||||
"description": "Copy password to clipboard"
|
"description": "Copy password to clipboard"
|
||||||
},
|
},
|
||||||
|
"passwordCopied": {
|
||||||
|
"message": "Password copied"
|
||||||
|
},
|
||||||
"copyUsername": {
|
"copyUsername": {
|
||||||
"message": "Copy username",
|
"message": "Copy username",
|
||||||
"description": "Copy username to clipboard"
|
"description": "Copy username to clipboard"
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
*ngIf="cipher.hasPasswordHistory && isLogin"
|
*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()"
|
(click)="viewPasswordHistory()"
|
||||||
bitTypography="body2"
|
bitTypography="body2"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { By } from "@angular/platform-browser";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { ColorPasswordModule, ItemModule, ToastService } from "@bitwarden/components";
|
||||||
|
import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component";
|
||||||
|
|
||||||
|
import { PasswordHistoryViewComponent } from "./password-history-view.component";
|
||||||
|
|
||||||
|
describe("PasswordHistoryViewComponent", () => {
|
||||||
|
let component: PasswordHistoryViewComponent;
|
||||||
|
let fixture: ComponentFixture<PasswordHistoryViewComponent>;
|
||||||
|
|
||||||
|
const mockCipher = {
|
||||||
|
id: "122-333-444",
|
||||||
|
type: CipherType.Login,
|
||||||
|
organizationId: "222-444-555",
|
||||||
|
} as CipherView;
|
||||||
|
|
||||||
|
const copyToClipboard = jest.fn();
|
||||||
|
const showToast = jest.fn();
|
||||||
|
const activeAccount$ = new BehaviorSubject<{ id: string }>({ id: "666-444-444" });
|
||||||
|
const mockCipherService = {
|
||||||
|
get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }),
|
||||||
|
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCipherService.get.mockClear();
|
||||||
|
mockCipherService.getKeyForCipherKeyDecryption.mockClear();
|
||||||
|
copyToClipboard.mockClear();
|
||||||
|
showToast.mockClear();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ItemModule, ColorPasswordModule, JslibModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: WINDOW, useValue: window },
|
||||||
|
{ provide: CipherService, useValue: mockCipherService },
|
||||||
|
{ provide: PlatformUtilsService, useValue: { copyToClipboard } },
|
||||||
|
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||||
|
{ provide: ToastService, useValue: { showToast } },
|
||||||
|
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PasswordHistoryViewComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders no history text when history does not exist", () => {
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).toBe("noPasswordsInList");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("history", () => {
|
||||||
|
const password1 = { password: "bad-password-1", lastUsedDate: new Date("09/13/2004") };
|
||||||
|
const password2 = { password: "bad-password-2", lastUsedDate: new Date("02/01/2004") };
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCipher.passwordHistory = [password1, password2];
|
||||||
|
|
||||||
|
mockCipherService.get.mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) });
|
||||||
|
await component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders all passwords", () => {
|
||||||
|
const passwords = fixture.debugElement.queryAll(By.directive(ColorPasswordComponent));
|
||||||
|
|
||||||
|
expect(passwords.map((password) => password.componentInstance.password)).toEqual([
|
||||||
|
"bad-password-1",
|
||||||
|
"bad-password-2",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies a password", () => {
|
||||||
|
const copyButton = fixture.debugElement.query(By.css("button"));
|
||||||
|
|
||||||
|
copyButton.nativeElement.click();
|
||||||
|
|
||||||
|
expect(copyToClipboard).toHaveBeenCalledWith("bad-password-1", { window: window });
|
||||||
|
expect(showToast).toHaveBeenCalledWith({
|
||||||
|
message: "passwordCopied",
|
||||||
|
title: "",
|
||||||
|
variant: "info",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { OnInit, Inject, Component, Input } from "@angular/core";
|
||||||
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
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 {
|
||||||
|
ToastService,
|
||||||
|
ItemModule,
|
||||||
|
ColorPasswordModule,
|
||||||
|
IconButtonModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "vault-password-history-view",
|
||||||
|
templateUrl: "./password-history-view.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ItemModule, ColorPasswordModule, IconButtonModule, JslibModule],
|
||||||
|
})
|
||||||
|
export class PasswordHistoryViewComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The ID of the cipher to display the password history for.
|
||||||
|
*/
|
||||||
|
@Input({ required: true }) cipherId: CipherId;
|
||||||
|
|
||||||
|
/** The password history for the cipher. */
|
||||||
|
history: PasswordHistoryView[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(WINDOW) private win: Window,
|
||||||
|
protected cipherService: CipherService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected accountService: AccountService,
|
||||||
|
protected toastService: ToastService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Copies a password to the clipboard. */
|
||||||
|
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("passwordCopied"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,5 +12,6 @@ export {
|
||||||
} from "./components/assign-collections.component";
|
} from "./components/assign-collections.component";
|
||||||
|
|
||||||
export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component";
|
export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component";
|
||||||
|
export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component";
|
||||||
|
|
||||||
export * as VaultIcons from "./icons";
|
export * as VaultIcons from "./icons";
|
||||||
|
|
Loading…
Reference in New Issue