Merge branch 'main' into autofill/pm-8833-implement-on-page-autofill-menu-for-password-generation
This commit is contained in:
commit
a96b8d69b5
|
@ -3,7 +3,9 @@ import { Component, importProvidersFrom } from "@angular/core";
|
|||
import { RouterModule } from "@angular/router";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import {
|
||||
AvatarModule,
|
||||
BadgeModule,
|
||||
|
@ -318,6 +320,30 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PolicyService,
|
||||
useFactory: () => {
|
||||
return {
|
||||
policyAppliesToActiveUser$: () => {
|
||||
return {
|
||||
pipe: () => ({
|
||||
subscribe: () => ({}),
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SendService,
|
||||
useFactory: () => {
|
||||
return {
|
||||
sends$: () => {
|
||||
return { pipe: () => ({}) };
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
|
|
|
@ -1,9 +1,41 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { filter, map, switchMap } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { LinkModule } from "@bitwarden/components";
|
||||
|
||||
const allNavButtons = [
|
||||
{
|
||||
label: "Vault",
|
||||
page: "/tabs/vault",
|
||||
iconKey: "lock",
|
||||
iconKeyActive: "lock-f",
|
||||
},
|
||||
{
|
||||
label: "Generator",
|
||||
page: "/tabs/generator",
|
||||
iconKey: "generate",
|
||||
iconKeyActive: "generate-f",
|
||||
},
|
||||
{
|
||||
label: "Send",
|
||||
page: "/tabs/send",
|
||||
iconKey: "send",
|
||||
iconKeyActive: "send-f",
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
page: "/tabs/settings",
|
||||
iconKey: "cog",
|
||||
iconKeyActive: "cog-f",
|
||||
},
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: "popup-tab-navigation",
|
||||
templateUrl: "popup-tab-navigation.component.html",
|
||||
|
@ -14,30 +46,23 @@ import { LinkModule } from "@bitwarden/components";
|
|||
},
|
||||
})
|
||||
export class PopupTabNavigationComponent {
|
||||
navButtons = [
|
||||
{
|
||||
label: "Vault",
|
||||
page: "/tabs/vault",
|
||||
iconKey: "lock",
|
||||
iconKeyActive: "lock-f",
|
||||
},
|
||||
{
|
||||
label: "Generator",
|
||||
page: "/tabs/generator",
|
||||
iconKey: "generate",
|
||||
iconKeyActive: "generate-f",
|
||||
},
|
||||
{
|
||||
label: "Send",
|
||||
page: "/tabs/send",
|
||||
iconKey: "send",
|
||||
iconKeyActive: "send-f",
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
page: "/tabs/settings",
|
||||
iconKey: "cog",
|
||||
iconKeyActive: "cog-f",
|
||||
},
|
||||
];
|
||||
navButtons = allNavButtons;
|
||||
constructor(
|
||||
private policyService: PolicyService,
|
||||
private sendService: SendService,
|
||||
) {
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.DisableSend)
|
||||
.pipe(
|
||||
filter((policyAppliesToActiveUser) => policyAppliesToActiveUser),
|
||||
switchMap(() => this.sendService.sends$),
|
||||
map((sends) => sends.length > 1),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((hasSends) => {
|
||||
this.navButtons = hasSends
|
||||
? allNavButtons
|
||||
: allNavButtons.filter((b) => b.page !== "/tabs/send");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="'send' | i18n">
|
||||
<ng-container slot="end">
|
||||
<tools-new-send-dropdown></tools-new-send-dropdown>
|
||||
|
||||
<tools-new-send-dropdown *ngIf="!sendsDisabled"></tools-new-send-dropdown>
|
||||
<app-pop-out></app-pop-out>
|
||||
<app-current-account></app-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<div slot="above-scroll-area" class="tw-p-4">
|
||||
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
|
||||
{{ "sendDisabledWarning" | i18n }}
|
||||
</bit-callout>
|
||||
<ng-container *ngIf="!sendsDisabled">
|
||||
<tools-send-search></tools-send-search>
|
||||
<app-send-list-filters></app-send-list-filters>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="listState === sendState.Empty"
|
||||
|
@ -15,7 +23,7 @@
|
|||
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
|
||||
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
|
||||
<tools-new-send-dropdown slot="button"></tools-new-send-dropdown>
|
||||
<tools-new-send-dropdown *ngIf="!sendsDisabled" slot="button"></tools-new-send-dropdown>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
|
||||
|
@ -31,9 +39,4 @@
|
|||
</div>
|
||||
<app-send-list-items-container [headerText]="title | i18n" [sends]="sends$ | async" />
|
||||
</ng-container>
|
||||
|
||||
<div slot="above-scroll-area" class="tw-p-4" *ngIf="listState !== sendState.Empty">
|
||||
<tools-send-search></tools-send-search>
|
||||
<app-send-list-filters></app-send-list-filters>
|
||||
</div>
|
||||
</popup-page>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { of, BehaviorSubject } from "rxjs";
|
|||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
|
@ -46,6 +47,7 @@ describe("SendV2Component", () => {
|
|||
let sendListFiltersServiceFilters$: BehaviorSubject<{ sendType: SendType | null }>;
|
||||
let sendItemsServiceEmptyList$: BehaviorSubject<boolean>;
|
||||
let sendItemsServiceNoFilteredResults$: BehaviorSubject<boolean>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
sendListFiltersServiceFilters$ = new BehaviorSubject({ sendType: null });
|
||||
|
@ -60,6 +62,9 @@ describe("SendV2Component", () => {
|
|||
latestSearchText$: of(""),
|
||||
});
|
||||
|
||||
policyService = mock<PolicyService>();
|
||||
policyService.policyAppliesToActiveUser$.mockReturnValue(of(true)); // Return `true` by default
|
||||
|
||||
sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder());
|
||||
|
||||
sendListFiltersService.filters$ = sendListFiltersServiceFilters$;
|
||||
|
@ -104,6 +109,7 @@ describe("SendV2Component", () => {
|
|||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: SendListFiltersService, useValue: sendListFiltersService },
|
||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
|
|
@ -5,8 +5,10 @@ import { RouterLink } from "@angular/router";
|
|||
import { combineLatest } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
import { ButtonModule, CalloutModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
import {
|
||||
NoSendsIcon,
|
||||
NewSendDropdownComponent,
|
||||
|
@ -31,6 +33,7 @@ export enum SendState {
|
|||
templateUrl: "send-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CalloutModule,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopOutComponent,
|
||||
|
@ -61,9 +64,12 @@ export class SendV2Component implements OnInit, OnDestroy {
|
|||
|
||||
protected noResultsIcon = Icons.NoResults;
|
||||
|
||||
protected sendsDisabled = false;
|
||||
|
||||
constructor(
|
||||
protected sendItemsService: SendItemsService,
|
||||
protected sendListFiltersService: SendListFiltersService,
|
||||
private policyService: PolicyService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.sendItemsService.emptyList$,
|
||||
|
@ -90,6 +96,13 @@ export class SendV2Component implements OnInit, OnDestroy {
|
|||
|
||||
this.listState = null;
|
||||
});
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.DisableSend)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((sendsDisabled) => {
|
||||
this.sendsDisabled = sendsDisabled;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
|
|
@ -38,7 +38,7 @@ export class MoreFromBitwardenPageV2Component {
|
|||
private organizationService: OrganizationService,
|
||||
) {
|
||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||
this.familySponsorshipAvailable$ = this.organizationService.canManageSponsorships$;
|
||||
this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
|
||||
}
|
||||
|
||||
async openFreeBitwardenFamiliesPage() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 } });
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
|
@ -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 },
|
||||
],
|
||||
})
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 } });
|
||||
}
|
||||
}
|
|
@ -117,6 +117,10 @@ export abstract class OrganizationService {
|
|||
* Emits true if the user can create or manage a Free Bitwarden Families sponsorship.
|
||||
*/
|
||||
canManageSponsorships$: Observable<boolean>;
|
||||
/**
|
||||
* Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available.
|
||||
*/
|
||||
familySponsorshipAvailable$: Observable<boolean>;
|
||||
hasOrganizations: () => Promise<boolean>;
|
||||
get$: (id: string) => Observable<Organization | undefined>;
|
||||
get: (id: string) => Promise<Organization>;
|
||||
|
|
|
@ -88,6 +88,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
|
|||
mapToBooleanHasAnyOrganizations(),
|
||||
);
|
||||
|
||||
familySponsorshipAvailable$ = this.organizations$.pipe(
|
||||
map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)),
|
||||
);
|
||||
|
||||
async hasOrganizations(): Promise<boolean> {
|
||||
return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations()));
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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 }}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
"@storybook/manager-api": "8.2.9",
|
||||
"@storybook/theming": "8.2.9",
|
||||
"@types/argon2-browser": "1.18.4",
|
||||
"@types/chrome": "0.0.270",
|
||||
"@types/chrome": "0.0.272",
|
||||
"@types/firefox-webext-browser": "111.0.5",
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.12",
|
||||
|
@ -8901,9 +8901,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/chrome": {
|
||||
"version": "0.0.270",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.270.tgz",
|
||||
"integrity": "sha512-ADvkowV7YnJfycZZxL2brluZ6STGW+9oKG37B422UePf2PCXuFA/XdERI0T18wtuWPx0tmFeZqq6MOXVk1IC+Q==",
|
||||
"version": "0.0.272",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.272.tgz",
|
||||
"integrity": "sha512-9cxDmmgyhXV8gsZvlRjqaDizNjIjbV0spsR0fIEaQUoHtbl9D8VkTOLyONgiBKK+guR38x5eMO3E3avUYOXwcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
"@storybook/manager-api": "8.2.9",
|
||||
"@storybook/theming": "8.2.9",
|
||||
"@types/argon2-browser": "1.18.4",
|
||||
"@types/chrome": "0.0.270",
|
||||
"@types/chrome": "0.0.272",
|
||||
"@types/firefox-webext-browser": "111.0.5",
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.12",
|
||||
|
|
Loading…
Reference in New Issue