1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-08 05:47:50 +02:00

PM-11390: [Defect] View Login - Clicking Password History opens Edit Item window behind View Login window (#11119)

* Add password dialog component.

* Properly direct to browser password history screen.

* Add padding to history items.

* Update test to correct password history route.

* Remove unneeded provider.

* Use relative path for SharedModule.
This commit is contained in:
Alec Rippberger 2024-09-25 09:45:13 -05:00 committed by GitHub
parent 57c5c46cf7
commit 742900a663
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 320 additions and 11 deletions

View File

@ -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;

View File

@ -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<Router>;
beforeEach(async () => {
router = mock<Router>();
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" },
});
});
});
});

View File

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

View File

@ -0,0 +1,40 @@
<bit-dialog dialogSize="small" background="alt">
<span bitDialogTitle>
{{ "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>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton (click)="close()" buttonType="primary" type="button">
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@ -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<PasswordHistoryComponent>,
) {
/**
* 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<ViewPasswordHistoryDialogParams>,
) {
return dialogService.open(PasswordHistoryComponent, config);
}

View File

@ -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 },
],
})

View File

@ -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 },
});
});
});
});

View File

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

View File

@ -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<void>;
}

View File

@ -27,10 +27,8 @@
</p>
<a
*ngIf="cipher.hasPasswordHistory && isLogin"
bitLink
class="tw-font-bold tw-no-underline"
routerLink="/cipher-password-history"
[queryParams]="{ cipherId: cipher.id }"
class="tw-font-bold tw-no-underline tw-cursor-pointer"
(click)="viewPasswordHistory()"
bitTypography="body2"
>
{{ "passwordHistory" | i18n }}

View File

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