mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43:33 +00:00
Merge branch 'main' into auth/pm-8111/browser-refresh-login-component
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2024.9.2",
|
||||
"version": "2024.10.0",
|
||||
"scripts": {
|
||||
"build": "cross-env MANIFEST_VERSION=3 webpack",
|
||||
"build:mv2": "webpack",
|
||||
|
||||
@@ -283,7 +283,7 @@ export default class MainBackground {
|
||||
folderService: InternalFolderServiceAbstraction;
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
|
||||
collectionService: CollectionServiceAbstraction;
|
||||
vaultTimeoutService: VaultTimeoutService;
|
||||
vaultTimeoutService?: VaultTimeoutService;
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction;
|
||||
syncService: SyncService;
|
||||
@@ -842,24 +842,26 @@ export default class MainBackground {
|
||||
|
||||
this.vaultSettingsService = new VaultSettingsService(this.stateProvider);
|
||||
|
||||
this.vaultTimeoutService = new VaultTimeoutService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cipherService,
|
||||
this.folderService,
|
||||
this.collectionService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.searchService,
|
||||
this.stateService,
|
||||
this.authService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.stateEventRunnerService,
|
||||
this.taskSchedulerService,
|
||||
this.logService,
|
||||
lockedCallback,
|
||||
logoutCallback,
|
||||
);
|
||||
if (!this.popupOnlyContext) {
|
||||
this.vaultTimeoutService = new VaultTimeoutService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cipherService,
|
||||
this.folderService,
|
||||
this.collectionService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.searchService,
|
||||
this.stateService,
|
||||
this.authService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.stateEventRunnerService,
|
||||
this.taskSchedulerService,
|
||||
this.logService,
|
||||
lockedCallback,
|
||||
logoutCallback,
|
||||
);
|
||||
}
|
||||
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
|
||||
|
||||
this.sendStateProvider = new SendStateProvider(this.stateProvider);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "2024.9.2",
|
||||
"version": "2024.10.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "2024.9.2",
|
||||
"version": "2024.10.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -56,7 +56,7 @@ export class PopupTabNavigationComponent {
|
||||
.pipe(
|
||||
filter((policyAppliesToActiveUser) => policyAppliesToActiveUser),
|
||||
switchMap(() => this.sendService.sends$),
|
||||
map((sends) => sends.length > 1),
|
||||
map((sends) => sends.length > 0),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((hasSends) => {
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
</bit-section-header>
|
||||
<bit-item-group>
|
||||
<bit-item *ngFor="let cipher of ciphers">
|
||||
<a
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||
(click)="onViewCipher(cipher)"
|
||||
>
|
||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
</a>
|
||||
</button>
|
||||
<ng-container slot="end" *ngIf="cipher.edit">
|
||||
<bit-item-action>
|
||||
<button
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "12.0.1",
|
||||
"argon2": "0.40.1",
|
||||
"big-integer": "1.6.51",
|
||||
"big-integer": "1.6.52",
|
||||
"browser-hrtime": "1.1.8",
|
||||
"chalk": "4.1.2",
|
||||
"commander": "11.1.0",
|
||||
|
||||
10
apps/desktop/desktop_native/napi/index.d.ts
vendored
10
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -3,7 +3,7 @@
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
export namespace passwords {
|
||||
export declare namespace passwords {
|
||||
/** Fetch the stored password from the keychain. */
|
||||
export function getPassword(service: string, account: string): Promise<string>
|
||||
/** Fetch the stored password from the keychain that was stored with Keytar. */
|
||||
@@ -14,7 +14,7 @@ export namespace passwords {
|
||||
export function deletePassword(service: string, account: string): Promise<void>
|
||||
export function isAvailable(): Promise<boolean>
|
||||
}
|
||||
export namespace biometrics {
|
||||
export declare namespace biometrics {
|
||||
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function available(): Promise<boolean>
|
||||
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
|
||||
@@ -38,16 +38,16 @@ export namespace biometrics {
|
||||
ivB64: string
|
||||
}
|
||||
}
|
||||
export namespace clipboards {
|
||||
export declare namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
}
|
||||
export namespace processisolations {
|
||||
export declare namespace processisolations {
|
||||
export function disableCoredumps(): Promise<void>
|
||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||
export function disableMemoryAccess(): Promise<void>
|
||||
}
|
||||
export namespace powermonitors {
|
||||
export declare namespace powermonitors {
|
||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||
export function isLockMonitorAvailable(): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"author": "",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.16.2"
|
||||
"@napi-rs/cli": "2.18.4"
|
||||
},
|
||||
"napi": {
|
||||
"name": "desktop_napi",
|
||||
|
||||
7
apps/desktop/src/package-lock.json
generated
7
apps/desktop/src/package-lock.json
generated
@@ -21,6 +21,13 @@
|
||||
"@napi-rs/cli": "2.16.2"
|
||||
}
|
||||
},
|
||||
"../desktop_native/napi": {
|
||||
"version": "0.1.0",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.18.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@bitwarden/desktop-napi": {
|
||||
"resolved": "../desktop_native/napi",
|
||||
"link": true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.9.2",
|
||||
"version": "2024.10.0",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
@@ -184,7 +185,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: EnvironmentService,
|
||||
useClass: WebEnvironmentService,
|
||||
deps: [WINDOW, StateProvider, AccountService],
|
||||
deps: [WINDOW, StateProvider, AccountService, Router],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BiometricsService,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Router } from "@angular/router";
|
||||
import { ReplaySubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -23,6 +24,7 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
|
||||
private win: Window,
|
||||
stateProvider: StateProvider,
|
||||
accountService: AccountService,
|
||||
private router: Router,
|
||||
) {
|
||||
super(stateProvider, accountService);
|
||||
|
||||
@@ -47,9 +49,34 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
|
||||
this.environment$ = subject.asObservable();
|
||||
}
|
||||
|
||||
// Web cannot set environment
|
||||
async setEnvironment(region: Region, urls?: Urls): Promise<Urls> {
|
||||
return;
|
||||
// Web setting env means navigating to a new location
|
||||
setEnvironment(region: Region, urls?: Urls): Promise<Urls> {
|
||||
if (region === Region.SelfHosted) {
|
||||
throw new Error("setEnvironment does not work in web for self-hosted.");
|
||||
}
|
||||
|
||||
const currentDomain = Utils.getDomain(this.win.location.href);
|
||||
const currentRegion = this.availableRegions().find(
|
||||
(r) => Utils.getDomain(r.urls.webVault) === currentDomain,
|
||||
);
|
||||
|
||||
if (currentRegion.key === region) {
|
||||
// They have selected the current region, nothing to do
|
||||
return Promise.resolve(currentRegion.urls);
|
||||
}
|
||||
|
||||
const chosenRegion = this.availableRegions().find((r) => r.key === region);
|
||||
|
||||
if (chosenRegion == null) {
|
||||
throw new Error("The selected region is not known as an available region.");
|
||||
}
|
||||
|
||||
// Preserve the current in app route + params in the new location
|
||||
const routeAndParams = `/#${this.router.url}`;
|
||||
this.win.location.href = chosenRegion.urls.webVault + routeAndParams;
|
||||
|
||||
// This return shouldn't matter as we are about to leave the current window
|
||||
return Promise.resolve(chosenRegion.urls);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<bit-dialog dialogSize="large" background="alt" [loading]="performingInitialLoad">
|
||||
<span bitDialogTitle aria-live="polite">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div bitDialogContent #dialogContent>
|
||||
<app-cipher-view
|
||||
*ngIf="showCipherView"
|
||||
[cipher]="cipher"
|
||||
[collections]="collections"
|
||||
></app-cipher-view>
|
||||
<vault-cipher-form
|
||||
*ngIf="loadForm"
|
||||
formId="cipherForm"
|
||||
[config]="formConfig"
|
||||
[submitBtn]="submitBtn"
|
||||
(formReady)="onFormReady()"
|
||||
(cipherSaved)="onCipherSaved($event)"
|
||||
>
|
||||
<bit-item slot="attachment-button">
|
||||
<button bit-item-content type="button" (click)="openAttachmentsDialog()">
|
||||
<p class="tw-m-0">
|
||||
{{ "attachments" | i18n }}
|
||||
<span
|
||||
*ngIf="!(canAccessAttachments$ | async)"
|
||||
bitBadge
|
||||
variant="success"
|
||||
class="tw-ml-2"
|
||||
>
|
||||
{{ "premium" | i18n }}
|
||||
</span>
|
||||
</p>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
</vault-cipher-form>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<ng-container *ngIf="showCipherView">
|
||||
<button
|
||||
bitButton
|
||||
[bitAction]="switchToEdit"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
[disabled]="disableEdit"
|
||||
>
|
||||
{{ "edit" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<button
|
||||
bitButton
|
||||
type="submit"
|
||||
form="cipherForm"
|
||||
buttonType="primary"
|
||||
#submitBtn
|
||||
[hidden]="showCipherView"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
(click)="cancel()"
|
||||
*ngIf="!showCipherView"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="tw-ml-auto">
|
||||
<button
|
||||
bitIconButton="bwi-trash"
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
[appA11yTitle]="'delete' | i18n"
|
||||
[bitAction]="delete"
|
||||
[disabled]="!canDelete"
|
||||
></button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,436 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.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";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
ItemModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CipherAttachmentsComponent,
|
||||
CipherFormConfig,
|
||||
CipherFormGenerationService,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import {
|
||||
AttachmentDialogCloseResult,
|
||||
AttachmentDialogResult,
|
||||
AttachmentsV2Component,
|
||||
} from "../../individual-vault/attachments-v2.component";
|
||||
import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service";
|
||||
import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service";
|
||||
import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service";
|
||||
|
||||
export type VaultItemDialogMode = "view" | "form";
|
||||
|
||||
export interface VaultItemDialogParams {
|
||||
/**
|
||||
* The mode of the dialog.
|
||||
* - `view` is for viewing an existing cipher.
|
||||
* - `form` is for editing or creating a new cipher.
|
||||
*/
|
||||
mode: VaultItemDialogMode;
|
||||
|
||||
/**
|
||||
* The configuration object for the dialog and form.
|
||||
*/
|
||||
formConfig: CipherFormConfig;
|
||||
|
||||
/**
|
||||
* If true, the "edit" button will be disabled in the dialog.
|
||||
*/
|
||||
disableForm?: boolean;
|
||||
}
|
||||
|
||||
export enum VaultItemDialogResult {
|
||||
/**
|
||||
* A cipher was saved (created or updated).
|
||||
*/
|
||||
Saved = "saved",
|
||||
|
||||
/**
|
||||
* A cipher was deleted.
|
||||
*/
|
||||
Deleted = "deleted",
|
||||
|
||||
/**
|
||||
* The dialog was closed to navigate the user the premium upgrade page.
|
||||
*/
|
||||
PremiumUpgrade = "premiumUpgrade",
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-item-dialog",
|
||||
templateUrl: "vault-item-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
ButtonModule,
|
||||
CipherViewComponent,
|
||||
DialogModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CipherFormModule,
|
||||
CipherAttachmentsComponent,
|
||||
AsyncActionsModule,
|
||||
ItemModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService },
|
||||
],
|
||||
})
|
||||
export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Reference to the dialog content element. Used to scroll to the top of the dialog when switching modes.
|
||||
* @protected
|
||||
*/
|
||||
@ViewChild("dialogContent")
|
||||
protected dialogContent: ElementRef<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Tracks if the cipher was ever modified while the dialog was open. Used to ensure the dialog emits the correct result
|
||||
* in case of closing with the X button or ESC key.
|
||||
* @private
|
||||
*/
|
||||
private _cipherModified: boolean = false;
|
||||
|
||||
/**
|
||||
* The original mode of the form when the dialog is first opened.
|
||||
* Used to determine if the form should switch to edit mode after successfully creating a new cipher.
|
||||
* @private
|
||||
*/
|
||||
private _originalFormMode = this.params.formConfig.mode;
|
||||
|
||||
/**
|
||||
* Subject to emit when the form is ready to be displayed.
|
||||
* @private
|
||||
*/
|
||||
private _formReadySubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Tracks if the dialog is performing the initial load. Used to display a spinner while loading.
|
||||
* @private
|
||||
*/
|
||||
protected performingInitialLoad: boolean = true;
|
||||
|
||||
/**
|
||||
* The title of the dialog. Updates based on the dialog mode and cipher type.
|
||||
* @protected
|
||||
*/
|
||||
protected title: string;
|
||||
|
||||
/**
|
||||
* The current cipher being viewed. Undefined if creating a new cipher.
|
||||
* @protected
|
||||
*/
|
||||
protected cipher?: CipherView;
|
||||
|
||||
/**
|
||||
* The organization the current cipher belongs to. Undefined if creating a new cipher.
|
||||
* @protected
|
||||
*/
|
||||
protected organization?: Organization;
|
||||
|
||||
/**
|
||||
* The collections the current cipher is assigned to. Undefined if creating a new cipher.
|
||||
* @protected
|
||||
*/
|
||||
protected collections?: CollectionView[];
|
||||
|
||||
/**
|
||||
* Flag to indicate if the user has access to attachments via a premium subscription.
|
||||
* @protected
|
||||
*/
|
||||
protected canAccessAttachments$ = this.billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||
|
||||
protected get loadingForm() {
|
||||
return this.loadForm && !this.formReady;
|
||||
}
|
||||
|
||||
protected get disableEdit() {
|
||||
return this.params.disableForm;
|
||||
}
|
||||
|
||||
protected get canDelete() {
|
||||
return this.cipher?.edit ?? false;
|
||||
}
|
||||
|
||||
protected get showCipherView() {
|
||||
return this.cipher != undefined && (this.params.mode === "view" || this.loadingForm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to initialize/attach the form component.
|
||||
*/
|
||||
protected loadForm = this.params.mode === "form";
|
||||
|
||||
/**
|
||||
* Flag to indicate the form is ready to be displayed.
|
||||
*/
|
||||
protected formReady = false;
|
||||
|
||||
protected formConfig: CipherFormConfig = this.params.formConfig;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: VaultItemDialogParams,
|
||||
private dialogRef: DialogRef<VaultItemDialogResult>,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private cipherService: CipherService,
|
||||
private accountService: AccountService,
|
||||
private router: Router,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.cipher = await this.getDecryptedCipherView(this.formConfig);
|
||||
|
||||
if (this.cipher) {
|
||||
this.collections = this.formConfig.collections.filter((c) =>
|
||||
this.cipher.collectionIds?.includes(c.id),
|
||||
);
|
||||
this.organization = this.formConfig.organizations.find(
|
||||
(o) => o.id === this.cipher.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
this.performingInitialLoad = false;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// If the cipher was modified, be sure we emit the saved result in case the dialog was closed with the X button or ESC key.
|
||||
if (this._cipherModified) {
|
||||
this.dialogRef.close(VaultItemDialogResult.Saved);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the CipherFormComponent when the cipher is saved successfully.
|
||||
* @param cipherView - The newly saved cipher.
|
||||
*/
|
||||
protected async onCipherSaved(cipherView: CipherView) {
|
||||
// We successfully saved the cipher, update the dialog state and switch to view mode.
|
||||
this.cipher = cipherView;
|
||||
this.collections = this.formConfig.collections.filter((c) =>
|
||||
cipherView.collectionIds?.includes(c.id),
|
||||
);
|
||||
|
||||
// If the cipher was newly created (via add/clone), switch the form to edit for subsequent edits.
|
||||
if (this._originalFormMode === "add" || this._originalFormMode === "clone") {
|
||||
this.formConfig.mode = "edit";
|
||||
}
|
||||
this.formConfig.originalCipher = await this.cipherService.get(cipherView.id);
|
||||
this._cipherModified = true;
|
||||
await this.changeMode("view");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the CipherFormComponent when the form is ready to be displayed.
|
||||
*/
|
||||
protected onFormReady() {
|
||||
this.formReady = true;
|
||||
this._formReadySubject.next();
|
||||
}
|
||||
|
||||
delete = async () => {
|
||||
if (!this.cipher) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteItem" },
|
||||
content: {
|
||||
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
|
||||
},
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.deleteCipher();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("success"),
|
||||
message: this.i18nService.t(
|
||||
this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem",
|
||||
),
|
||||
});
|
||||
this.messagingService.send(
|
||||
this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher",
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this._cipherModified = false;
|
||||
this.dialogRef.close(VaultItemDialogResult.Deleted);
|
||||
};
|
||||
|
||||
openAttachmentsDialog = async () => {
|
||||
const dialogRef = this.dialogService.open<AttachmentDialogCloseResult, { cipherId: CipherId }>(
|
||||
AttachmentsV2Component,
|
||||
{
|
||||
data: {
|
||||
cipherId: this.formConfig.originalCipher?.id as CipherId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (
|
||||
result.action === AttachmentDialogResult.Removed ||
|
||||
result.action === AttachmentDialogResult.Uploaded
|
||||
) {
|
||||
this._cipherModified = true;
|
||||
}
|
||||
};
|
||||
|
||||
switchToEdit = async () => {
|
||||
if (!this.cipher) {
|
||||
return;
|
||||
}
|
||||
await this.changeMode("form");
|
||||
};
|
||||
|
||||
cancel = async () => {
|
||||
// We're in View mode, or we don't have a cipher, close the dialog.
|
||||
if (this.params.mode === "view" || this.cipher == null) {
|
||||
this.dialogRef.close(this._cipherModified ? VaultItemDialogResult.Saved : undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// We're in Form mode, and we have a cipher, switch back to View mode.
|
||||
await this.changeMode("view");
|
||||
};
|
||||
|
||||
private async getDecryptedCipherView(config: CipherFormConfig) {
|
||||
if (config.originalCipher == null) {
|
||||
return;
|
||||
}
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
return await config.originalCipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId),
|
||||
);
|
||||
}
|
||||
|
||||
private updateTitle() {
|
||||
let partOne: string;
|
||||
|
||||
if (this.params.mode === "view") {
|
||||
partOne = "viewItemType";
|
||||
} else if (this.formConfig.mode === "edit" || this.formConfig.mode === "partial-edit") {
|
||||
partOne = "editItemHeader";
|
||||
} else {
|
||||
partOne = "newItemHeader";
|
||||
}
|
||||
|
||||
const type = this.cipher?.type ?? this.formConfig.cipherType ?? CipherType.Login;
|
||||
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase());
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase());
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase());
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the mode of the dialog. When switching to Form mode, the form is initialized first then displayed once ready.
|
||||
* @param mode
|
||||
* @private
|
||||
*/
|
||||
private async changeMode(mode: VaultItemDialogMode) {
|
||||
this.formReady = false;
|
||||
|
||||
if (mode == "form") {
|
||||
this.loadForm = true;
|
||||
// Wait for the formReadySubject to emit before continuing.
|
||||
// This helps prevent flashing an empty dialog while the form is initializing.
|
||||
await firstValueFrom(this._formReadySubject);
|
||||
} else {
|
||||
this.loadForm = false;
|
||||
}
|
||||
|
||||
this.params.mode = mode;
|
||||
this.updateTitle();
|
||||
// Scroll to the top of the dialog content when switching modes.
|
||||
this.dialogContent.nativeElement.parentElement.scrollTop = 0;
|
||||
|
||||
// Update the URL query params to reflect the new mode.
|
||||
await this.router.navigate([], {
|
||||
queryParams: {
|
||||
action: mode === "form" ? "edit" : "view",
|
||||
itemId: this.cipher?.id,
|
||||
},
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to delete cipher.
|
||||
*/
|
||||
private async deleteCipher(): Promise<void> {
|
||||
const asAdmin = this.organization?.canEditAllCiphers;
|
||||
if (this.cipher.isDeleted) {
|
||||
await this.cipherService.deleteWithServer(this.cipher.id, asAdmin);
|
||||
} else {
|
||||
await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the VaultItemDialog.
|
||||
* @param dialogService
|
||||
* @param params
|
||||
*/
|
||||
static open(dialogService: DialogService, params: VaultItemDialogParams) {
|
||||
return dialogService.open<VaultItemDialogResult, VaultItemDialogParams>(
|
||||
VaultItemDialogComponent,
|
||||
{
|
||||
data: params,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,12 +106,7 @@
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="showAttachments || !vaultBulkManagementActionEnabled"
|
||||
type="button"
|
||||
(click)="attachments()"
|
||||
>
|
||||
<button bitMenuItem *ngIf="showAttachments" type="button" (click)="attachments()">
|
||||
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||
{{ "attachments" | i18n }}
|
||||
</button>
|
||||
@@ -119,26 +114,6 @@
|
||||
<i class="bwi bwi-fw bwi-files" aria-hidden="true"></i>
|
||||
{{ "clone" | i18n }}
|
||||
</button>
|
||||
<!-- This option will be phased out in future releases -->
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="!cipher.organizationId && !cipher.isDeleted && !vaultBulkManagementActionEnabled"
|
||||
type="button"
|
||||
(click)="moveToOrganization()"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-arrow-circle-right" aria-hidden="true"></i>
|
||||
{{ "moveToOrganization" | i18n }}
|
||||
</button>
|
||||
<!-- This option will be phased out in future releases -->
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="cipher.organizationId && !cipher.isDeleted && !vaultBulkManagementActionEnabled"
|
||||
type="button"
|
||||
(click)="editCollections()"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||
{{ "collections" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="showAssignToCollections"
|
||||
@@ -156,12 +131,7 @@
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="canEditCipher || !vaultBulkManagementActionEnabled"
|
||||
(click)="deleteCipher()"
|
||||
type="button"
|
||||
>
|
||||
<button bitMenuItem *ngIf="canEditCipher" (click)="deleteCipher()" type="button">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
|
||||
@@ -35,7 +35,6 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
@Input() collections: CollectionView[];
|
||||
@Input() viewingOrgVault: boolean;
|
||||
@Input() canEditCipher: boolean;
|
||||
@Input() vaultBulkManagementActionEnabled: boolean;
|
||||
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||
|
||||
@@ -100,17 +99,15 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected get disableMenu() {
|
||||
return (
|
||||
!(
|
||||
this.isNotDeletedLoginCipher ||
|
||||
this.showCopyPassword ||
|
||||
this.showCopyTotp ||
|
||||
this.showLaunchUri ||
|
||||
this.showAttachments ||
|
||||
this.showClone ||
|
||||
this.canEditCipher ||
|
||||
this.cipher.isDeleted
|
||||
) && this.vaultBulkManagementActionEnabled
|
||||
return !(
|
||||
this.isNotDeletedLoginCipher ||
|
||||
this.showCopyPassword ||
|
||||
this.showCopyTotp ||
|
||||
this.showLaunchUri ||
|
||||
this.showAttachments ||
|
||||
this.showClone ||
|
||||
this.canEditCipher ||
|
||||
this.cipher.isDeleted
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,14 +119,6 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
this.onEvent.emit({ type: "clone", item: this.cipher });
|
||||
}
|
||||
|
||||
protected moveToOrganization() {
|
||||
this.onEvent.emit({ type: "moveToOrganization", items: [this.cipher] });
|
||||
}
|
||||
|
||||
protected editCollections() {
|
||||
this.onEvent.emit({ type: "viewCipherCollections", item: this.cipher });
|
||||
}
|
||||
|
||||
protected events() {
|
||||
this.onEvent.emit({ type: "viewEvents", item: this.cipher });
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { VaultItem } from "./vault-item";
|
||||
|
||||
export type VaultItemEvent =
|
||||
| { type: "viewAttachments"; item: CipherView }
|
||||
| { type: "viewCipherCollections"; item: CipherView }
|
||||
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
|
||||
| { type: "viewCollectionAccess"; item: CollectionView; readonly: boolean }
|
||||
| { type: "viewEvents"; item: CipherView }
|
||||
@@ -15,5 +14,4 @@ export type VaultItemEvent =
|
||||
| { type: "delete"; items: VaultItem[] }
|
||||
| { type: "copyField"; item: CipherView; field: "username" | "password" | "totp" }
|
||||
| { type: "moveToFolder"; items: CipherView[] }
|
||||
| { type: "moveToOrganization"; items: CipherView[] }
|
||||
| { type: "assignToCollections"; items: CipherView[] };
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<bit-menu #headerMenu>
|
||||
<button *ngIf="bulkMoveAllowed" type="button" bitMenuItem (click)="bulkMoveToFolder()">
|
||||
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||
{{ (vaultBulkManagementActionEnabled ? "addToFolder" : "moveSelected") | i18n }}
|
||||
{{ "addToFolder" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="showAdminActions && showBulkEditCollectionAccess"
|
||||
@@ -60,21 +60,12 @@
|
||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||
{{ "assignToCollections" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="bulkMoveAllowed && !vaultBulkManagementActionEnabled"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkMoveToOrganization()"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-arrow-circle-right" aria-hidden="true"></i>
|
||||
{{ "moveSelectedToOrg" | i18n }}
|
||||
</button>
|
||||
<button *ngIf="showBulkTrashOptions" type="button" bitMenuItem (click)="bulkRestore()">
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restoreSelected" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="deleteAllowed || showBulkTrashOptions"
|
||||
*ngIf="showDelete() || showBulkTrashOptions"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkDelete()"
|
||||
@@ -131,8 +122,7 @@
|
||||
[organizations]="allOrganizations"
|
||||
[collections]="allCollections"
|
||||
[checked]="selection.isSelected(item)"
|
||||
[canEditCipher]="canEditCipher(item.cipher) && vaultBulkManagementActionEnabled"
|
||||
[vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled"
|
||||
[canEditCipher]="canEditCipher(item.cipher)"
|
||||
(checkedToggled)="selection.toggle(item)"
|
||||
(onEvent)="event($event)"
|
||||
></tr>
|
||||
|
||||
@@ -46,7 +46,6 @@ export class VaultItemsComponent {
|
||||
@Input() viewingOrgVault: boolean;
|
||||
@Input() addAccessStatus: number;
|
||||
@Input() addAccessToggle: boolean;
|
||||
@Input() vaultBulkManagementActionEnabled = false;
|
||||
|
||||
private _ciphers?: CipherView[] = [];
|
||||
@Input() get ciphers(): CipherView[] {
|
||||
@@ -93,23 +92,13 @@ export class VaultItemsComponent {
|
||||
}
|
||||
|
||||
get disableMenu() {
|
||||
return (
|
||||
this.vaultBulkManagementActionEnabled &&
|
||||
!this.bulkMoveAllowed &&
|
||||
!this.showAssignToCollections() &&
|
||||
!this.showDelete()
|
||||
);
|
||||
return !this.bulkMoveAllowed && !this.showAssignToCollections() && !this.showDelete();
|
||||
}
|
||||
|
||||
get bulkAssignToCollectionsAllowed() {
|
||||
return this.showBulkAddToCollections && this.ciphers.length > 0;
|
||||
}
|
||||
|
||||
// Use new bulk management delete if vaultBulkManagementActionEnabled feature flag is enabled
|
||||
get deleteAllowed() {
|
||||
return this.vaultBulkManagementActionEnabled ? this.showDelete() : true;
|
||||
}
|
||||
|
||||
protected canEditCollection(collection: CollectionView): boolean {
|
||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
if (collection.id === Unassigned) {
|
||||
@@ -156,15 +145,6 @@ export class VaultItemsComponent {
|
||||
});
|
||||
}
|
||||
|
||||
protected bulkMoveToOrganization() {
|
||||
this.event({
|
||||
type: "moveToOrganization",
|
||||
items: this.selection.selected
|
||||
.filter((item) => item.cipher !== undefined)
|
||||
.map((item) => item.cipher),
|
||||
});
|
||||
}
|
||||
|
||||
protected bulkRestore() {
|
||||
this.event({
|
||||
type: "restore",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<bit-dialog dialogSize="default">
|
||||
<span bitDialogTitle>
|
||||
{{ title }}
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<vault-cipher-form-generator
|
||||
[type]="params.type"
|
||||
(valueGenerated)="onValueGenerated($event)"
|
||||
></vault-cipher-form-generator>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="selectValue()"
|
||||
data-testid="select-button"
|
||||
>
|
||||
{{ selectButtonText }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,126 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import {
|
||||
WebVaultGeneratorDialogAction,
|
||||
WebVaultGeneratorDialogComponent,
|
||||
WebVaultGeneratorDialogParams,
|
||||
} from "./web-generator-dialog.component";
|
||||
|
||||
describe("WebVaultGeneratorDialogComponent", () => {
|
||||
let component: WebVaultGeneratorDialogComponent;
|
||||
let fixture: ComponentFixture<WebVaultGeneratorDialogComponent>;
|
||||
|
||||
let dialogRef: MockProxy<DialogRef<any>>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let passwordOptionsSubject: BehaviorSubject<any>;
|
||||
let usernameOptionsSubject: BehaviorSubject<any>;
|
||||
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let mockUsernameGenerationService: MockProxy<UsernameGenerationServiceAbstraction>;
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRef = mock<DialogRef<any>>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
passwordOptionsSubject = new BehaviorSubject([{ type: "password" }]);
|
||||
usernameOptionsSubject = new BehaviorSubject([{ type: "username" }]);
|
||||
|
||||
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
mockPasswordGenerationService.getOptions$.mockReturnValue(
|
||||
passwordOptionsSubject.asObservable(),
|
||||
);
|
||||
|
||||
mockUsernameGenerationService = mock<UsernameGenerationServiceAbstraction>();
|
||||
mockUsernameGenerationService.getOptions$.mockReturnValue(
|
||||
usernameOptionsSubject.asObservable(),
|
||||
);
|
||||
|
||||
const mockDialogData: WebVaultGeneratorDialogParams = { type: "password" };
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, WebVaultGeneratorDialogComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: DialogRef,
|
||||
useValue: dialogRef,
|
||||
},
|
||||
{
|
||||
provide: DIALOG_DATA,
|
||||
useValue: mockDialogData,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mockI18nService,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: mock<PlatformUtilsService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useValue: mockPasswordGenerationService,
|
||||
},
|
||||
{
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useValue: mockUsernameGenerationService,
|
||||
},
|
||||
{
|
||||
provide: CipherFormGeneratorComponent,
|
||||
useValue: {
|
||||
passwordOptions$: passwordOptionsSubject.asObservable(),
|
||||
usernameOptions$: usernameOptionsSubject.asObservable(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WebVaultGeneratorDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("initializes without errors", () => {
|
||||
fixture.detectChanges();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("closes the dialog with 'canceled' result when close is called", () => {
|
||||
const closeSpy = jest.spyOn(dialogRef, "close");
|
||||
|
||||
(component as any).close();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledWith({
|
||||
action: WebVaultGeneratorDialogAction.Canceled,
|
||||
});
|
||||
});
|
||||
|
||||
it("closes the dialog with 'selected' result when selectValue is called", () => {
|
||||
const closeSpy = jest.spyOn(dialogRef, "close");
|
||||
const generatedValue = "generated-value";
|
||||
component.onValueGenerated(generatedValue);
|
||||
|
||||
(component as any).selectValue();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledWith({
|
||||
action: WebVaultGeneratorDialogAction.Selected,
|
||||
generatedValue: generatedValue,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates generatedValue when onValueGenerated is called", () => {
|
||||
const generatedValue = "new-generated-value";
|
||||
component.onValueGenerated(generatedValue);
|
||||
|
||||
expect((component as any).generatedValue).toBe(generatedValue);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
export interface WebVaultGeneratorDialogParams {
|
||||
type: "password" | "username";
|
||||
}
|
||||
|
||||
export interface WebVaultGeneratorDialogResult {
|
||||
action: WebVaultGeneratorDialogAction;
|
||||
generatedValue?: string;
|
||||
}
|
||||
|
||||
export enum WebVaultGeneratorDialogAction {
|
||||
Selected = "selected",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "web-vault-generator-dialog",
|
||||
templateUrl: "./web-generator-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule],
|
||||
})
|
||||
export class WebVaultGeneratorDialogComponent {
|
||||
protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");
|
||||
protected selectButtonText = this.i18nService.t(
|
||||
this.isPassword ? "useThisPassword" : "useThisUsername",
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether the dialog is generating a password/passphrase. If false, it is generating a username.
|
||||
* @protected
|
||||
*/
|
||||
protected get isPassword() {
|
||||
return this.params.type === "password";
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently generated value.
|
||||
* @protected
|
||||
*/
|
||||
protected generatedValue: string = "";
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: WebVaultGeneratorDialogParams,
|
||||
private dialogRef: DialogRef<WebVaultGeneratorDialogResult>,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Close the dialog without selecting a value.
|
||||
*/
|
||||
protected close = () => {
|
||||
this.dialogRef.close({ action: WebVaultGeneratorDialogAction.Canceled });
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the dialog and select the currently generated value.
|
||||
*/
|
||||
protected selectValue = () => {
|
||||
this.dialogRef.close({
|
||||
action: WebVaultGeneratorDialogAction.Selected,
|
||||
generatedValue: this.generatedValue,
|
||||
});
|
||||
};
|
||||
|
||||
onValueGenerated(value: string) {
|
||||
this.generatedValue = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the vault generator dialog.
|
||||
*/
|
||||
static open(dialogService: DialogService, config: DialogConfig<WebVaultGeneratorDialogParams>) {
|
||||
return dialogService.open<WebVaultGeneratorDialogResult, WebVaultGeneratorDialogParams>(
|
||||
WebVaultGeneratorDialogComponent,
|
||||
{
|
||||
...config,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,8 @@ import {
|
||||
CipherFormModule,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { WebCipherFormGenerationService } from "../../../../../../libs/vault/src/cipher-form/services/web-cipher-form-generation.service";
|
||||
import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service";
|
||||
|
||||
import { AttachmentsV2Component } from "./attachments-v2.component";
|
||||
|
||||
@@ -48,13 +47,13 @@ export interface AddEditCipherDialogCloseResult {
|
||||
|
||||
/**
|
||||
* Component for viewing a cipher, presented in a dialog.
|
||||
* @deprecated Use the VaultItemDialogComponent instead.
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-vault-add-edit-v2",
|
||||
templateUrl: "add-edit-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CipherViewComponent,
|
||||
CommonModule,
|
||||
AsyncActionsModule,
|
||||
DialogModule,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<bit-dialog dialogSize="default">
|
||||
<bit-dialog dialogSize="default" background="alt">
|
||||
<span bitDialogTitle>
|
||||
{{ "attachments" | i18n }}
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="small">
|
||||
<span bitDialogTitle>
|
||||
{{ ((vaultBulkManagementActionEnabled$ | async) ? "addToFolder" : "moveSelected") | i18n }}
|
||||
{{ "addToFolder" | i18n }}
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
<p>{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}</p>
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
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";
|
||||
@@ -47,10 +45,6 @@ export class BulkMoveDialogComponent implements OnInit {
|
||||
});
|
||||
folders$: Observable<FolderView[]>;
|
||||
|
||||
protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.VaultBulkManagementAction,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkMoveDialogParams,
|
||||
private dialogRef: DialogRef<BulkMoveDialogResult>,
|
||||
@@ -59,7 +53,6 @@ export class BulkMoveDialogComponent implements OnInit {
|
||||
private i18nService: I18nService,
|
||||
private folderService: FolderService,
|
||||
private formBuilder: FormBuilder,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.cipherIds = params.cipherIds ?? [];
|
||||
}
|
||||
|
||||
@@ -54,9 +54,8 @@
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="false"
|
||||
[showAdminActions]="false"
|
||||
[showBulkAddToCollections]="vaultBulkManagementActionEnabled$ | async"
|
||||
[showBulkAddToCollections]="true"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled$ | async"
|
||||
>
|
||||
</app-vault-items>
|
||||
<div
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -63,6 +64,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
CipherFormConfig,
|
||||
CollectionAssignmentResult,
|
||||
DefaultCipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
@@ -75,16 +77,16 @@ import {
|
||||
CollectionDialogTabType,
|
||||
openCollectionDialog,
|
||||
} from "../components/collection-dialog";
|
||||
import {
|
||||
VaultItemDialogComponent,
|
||||
VaultItemDialogMode,
|
||||
VaultItemDialogResult,
|
||||
} from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||
import { VaultItem } from "../components/vault-items/vault-item";
|
||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||
|
||||
import {
|
||||
AddEditCipherDialogCloseResult,
|
||||
AddEditCipherDialogResult,
|
||||
openAddEditCipherDialog,
|
||||
} from "./add-edit-v2.component";
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import {
|
||||
AttachmentDialogCloseResult,
|
||||
@@ -100,16 +102,7 @@ import {
|
||||
BulkMoveDialogResult,
|
||||
openBulkMoveDialog,
|
||||
} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component";
|
||||
import {
|
||||
BulkShareDialogResult,
|
||||
openBulkShareDialog,
|
||||
} from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component";
|
||||
import {
|
||||
CollectionsDialogResult,
|
||||
openIndividualVaultCollectionsDialog,
|
||||
} from "./collections.component";
|
||||
import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component";
|
||||
import { ShareComponent } from "./share.component";
|
||||
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||
@@ -125,11 +118,6 @@ import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/v
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
|
||||
import {
|
||||
openViewCipherDialog,
|
||||
ViewCipherDialogCloseResult,
|
||||
ViewCipherDialogResult,
|
||||
} from "./view.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
const SearchTextDebounceInterval = 200;
|
||||
@@ -183,14 +171,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
||||
protected canCreateCollections = false;
|
||||
protected currentSearchText$: Observable<string>;
|
||||
protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.VaultBulkManagementAction,
|
||||
);
|
||||
private searchText$ = new Subject<string>();
|
||||
private refresh$ = new BehaviorSubject<void>(null);
|
||||
private destroy$ = new Subject<void>();
|
||||
private extensionRefreshEnabled: boolean;
|
||||
|
||||
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -364,12 +351,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
firstSetup$
|
||||
.pipe(
|
||||
switchMap(() => this.route.queryParams),
|
||||
// Only process the queryParams if the dialog is not open (only when extension refresh is enabled)
|
||||
filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled),
|
||||
switchMap(async (params) => {
|
||||
const cipherId = getCipherIdFromParams(params);
|
||||
|
||||
if (cipherId) {
|
||||
if (await this.cipherService.get(cipherId)) {
|
||||
if (params.action === "view") {
|
||||
let action = params.action;
|
||||
// Default to "view" if extension refresh is enabled
|
||||
if (action == null && this.extensionRefreshEnabled) {
|
||||
action = "view";
|
||||
}
|
||||
|
||||
if (action === "view") {
|
||||
await this.viewCipherById(cipherId);
|
||||
} else {
|
||||
await this.editCipherId(cipherId);
|
||||
@@ -458,9 +453,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
case "viewAttachments":
|
||||
await this.editCipherAttachments(event.item);
|
||||
break;
|
||||
case "viewCipherCollections":
|
||||
await this.editCipherCollections(event.item);
|
||||
break;
|
||||
case "clone":
|
||||
await this.cloneCipher(event.item);
|
||||
break;
|
||||
@@ -477,13 +469,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
case "moveToFolder":
|
||||
await this.bulkMove(event.items);
|
||||
break;
|
||||
case "moveToOrganization":
|
||||
if (event.items.length === 1) {
|
||||
await this.shareCipher(event.items[0]);
|
||||
} else {
|
||||
await this.bulkShare(event.items);
|
||||
}
|
||||
break;
|
||||
case "copyField":
|
||||
await this.copy(event.item, event.field);
|
||||
break;
|
||||
@@ -548,7 +533,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
async editCipherAttachments(cipher: CipherView) {
|
||||
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||
this.go({ cipherId: null, itemId: null });
|
||||
await this.go({ cipherId: null, itemId: null });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -566,9 +551,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const canEditAttachments = await this.canEditAttachments(cipher);
|
||||
const vaultBulkManagementActionEnabled = await firstValueFrom(
|
||||
this.vaultBulkManagementActionEnabled$,
|
||||
);
|
||||
|
||||
let madeAttachmentChanges = false;
|
||||
|
||||
@@ -594,7 +576,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.attachmentsModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher.id;
|
||||
comp.viewOnly = !canEditAttachments && vaultBulkManagementActionEnabled;
|
||||
comp.viewOnly = !canEditAttachments;
|
||||
comp.onUploadedAttachment
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => (madeAttachmentChanges = true));
|
||||
@@ -615,39 +597,27 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
async shareCipher(cipher: CipherView) {
|
||||
if (cipher.organizationId != null) {
|
||||
// You cannot move ciphers between organizations
|
||||
this.showMissingPermissionsError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||
this.go({ cipherId: null, itemId: null });
|
||||
return;
|
||||
}
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
ShareComponent,
|
||||
this.shareModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher.id;
|
||||
comp.onSharedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
modal.close();
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async editCipherCollections(cipher: CipherView) {
|
||||
const dialog = openIndividualVaultCollectionsDialog(this.dialogService, {
|
||||
data: { cipherId: cipher.id },
|
||||
/**
|
||||
* Open the combined view / edit dialog for a cipher.
|
||||
* @param mode - Starting mode of the dialog.
|
||||
* @param formConfig - Configuration for the form when editing/adding a cipher.
|
||||
*/
|
||||
async openVaultItemDialog(mode: VaultItemDialogMode, formConfig: CipherFormConfig) {
|
||||
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
|
||||
mode,
|
||||
formConfig,
|
||||
});
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
if (result === CollectionsDialogResult.Saved) {
|
||||
const result = await lastValueFrom(this.vaultItemDialogRef.closed);
|
||||
this.vaultItemDialogRef = undefined;
|
||||
|
||||
// If the dialog was closed by deleting the cipher, refresh the vault.
|
||||
if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
// Clear the query params when the dialog closes
|
||||
await this.go({ cipherId: null, itemId: null, action: null });
|
||||
}
|
||||
|
||||
async addCipher(cipherType?: CipherType) {
|
||||
@@ -703,23 +673,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
folderId: this.activeFilter.folderId,
|
||||
};
|
||||
|
||||
// Open the dialog.
|
||||
const dialogRef = openAddEditCipherDialog(this.dialogService, {
|
||||
data: cipherFormConfig,
|
||||
});
|
||||
|
||||
// Wait for the dialog to close.
|
||||
const result: AddEditCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
// Refresh the vault to show the new cipher.
|
||||
if (result?.action === AddEditCipherDialogResult.Added) {
|
||||
this.refresh();
|
||||
this.go({ itemId: result.id, action: "view" });
|
||||
return;
|
||||
}
|
||||
|
||||
// If the dialog was closed by any other action navigate back to the vault.
|
||||
this.go({ cipherId: null, itemId: null, action: null });
|
||||
await this.openVaultItemDialog("form", cipherFormConfig);
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView, cloneMode?: boolean) {
|
||||
@@ -735,7 +689,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||
) {
|
||||
// didn't pass password prompt, so don't open add / edit modal
|
||||
this.go({ cipherId: null, itemId: null, action: null });
|
||||
await this.go({ cipherId: null, itemId: null, action: null });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -767,14 +721,14 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
modal.onClosedPromise().then(() => {
|
||||
this.go({ cipherId: null, itemId: null, action: null });
|
||||
void this.go({ cipherId: null, itemId: null, action: null });
|
||||
});
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a cipher using the new AddEditCipherDialogV2 component.
|
||||
* Edit a cipher using the new VaultItemDialog.
|
||||
*
|
||||
* @param cipher
|
||||
* @param cloneMode
|
||||
@@ -786,31 +740,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
cipher.type,
|
||||
);
|
||||
|
||||
const dialogRef = openAddEditCipherDialog(this.dialogService, {
|
||||
data: cipherFormConfig,
|
||||
});
|
||||
|
||||
const result: AddEditCipherDialogCloseResult = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
/**
|
||||
* Refresh the vault if the dialog was closed by adding, editing, or deleting a cipher.
|
||||
*/
|
||||
if (result?.action === AddEditCipherDialogResult.Edited) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* View the cipher if the dialog was closed by editing the cipher.
|
||||
*/
|
||||
if (result?.action === AddEditCipherDialogResult.Edited) {
|
||||
this.go({ itemId: cipher.id, action: "view" });
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the vault if the dialog was closed by any other action.
|
||||
*/
|
||||
this.go({ cipherId: null, itemId: null, action: null });
|
||||
await this.openVaultItemDialog("form", cipherFormConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -837,39 +767,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||
) {
|
||||
// Didn't pass password prompt, so don't open add / edit modal.
|
||||
this.go({ cipherId: null, itemId: null });
|
||||
await this.go({ cipherId: null, itemId: null, action: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
// Decrypt the cipher.
|
||||
const cipherView = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
|
||||
cipher.edit ? "edit" : "partial-edit",
|
||||
cipher.id as CipherId,
|
||||
cipher.type,
|
||||
);
|
||||
|
||||
// Open the dialog.
|
||||
const dialogRef = openViewCipherDialog(this.dialogService, {
|
||||
data: { cipher: cipherView },
|
||||
});
|
||||
|
||||
// Wait for the dialog to close.
|
||||
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
// If the dialog was closed by clicking the edit button, navigate to open the edit dialog.
|
||||
if (result?.action === ViewCipherDialogResult.Edited) {
|
||||
this.go({ itemId: cipherView.id, action: "edit" });
|
||||
return;
|
||||
}
|
||||
|
||||
// If the dialog was closed by deleting the cipher, refresh the vault.
|
||||
if (result?.action === ViewCipherDialogResult.Deleted) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
// Clear the query params when the view dialog closes
|
||||
this.go({ cipherId: null, itemId: null, action: null });
|
||||
await this.openVaultItemDialog("view", cipherFormConfig);
|
||||
}
|
||||
|
||||
async addCollection() {
|
||||
@@ -1018,7 +926,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const component = await this.editCipher(cipher, true);
|
||||
component.cloneMode = true;
|
||||
|
||||
if (component != null) {
|
||||
component.cloneMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
async restore(c: CipherView): Promise<boolean> {
|
||||
@@ -1255,34 +1166,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async bulkShare(ciphers: CipherView[]) {
|
||||
if (!(await this.repromptCipher(ciphers))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ciphers.some((c) => c.organizationId != null)) {
|
||||
// You cannot move ciphers between organizations
|
||||
this.showMissingPermissionsError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ciphers.length === 0) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("nothingSelected"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = openBulkShareDialog(this.dialogService, { data: { ciphers } });
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === BulkShareDialogResult.Shared) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected deleteCipherWithServer(id: string, permanent: boolean) {
|
||||
return permanent
|
||||
? this.cipherService.deleteWithServer(id)
|
||||
@@ -1308,7 +1191,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
return organization.canEditAllCiphers;
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
private async go(queryParams: any = null) {
|
||||
if (queryParams == null) {
|
||||
queryParams = {
|
||||
favorites: this.activeFilter.isFavorites || null,
|
||||
@@ -1319,7 +1202,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
}
|
||||
|
||||
void this.router.navigate([], {
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: "merge",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnInit, EventEmitter } from "@angular/core";
|
||||
import { Component, EventEmitter, Inject, OnInit } from "@angular/core";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -52,6 +52,7 @@ export interface ViewCipherDialogCloseResult {
|
||||
|
||||
/**
|
||||
* Component for viewing a cipher, presented in a dialog.
|
||||
* @deprecated Use the VaultItemDialogComponent instead.
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-vault-view",
|
||||
|
||||
@@ -598,9 +598,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
case "viewAttachments":
|
||||
await this.editCipherAttachments(event.item);
|
||||
break;
|
||||
case "viewCipherCollections":
|
||||
await this.editCipherCollections(event.item);
|
||||
break;
|
||||
case "clone":
|
||||
await this.cloneCipher(event.item);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { WebVaultGeneratorDialogComponent } from "../components/web-generator-dialog/web-generator-dialog.component";
|
||||
|
||||
import { WebCipherFormGenerationService } from "./web-cipher-form-generation.service";
|
||||
|
||||
describe("WebCipherFormGenerationService", () => {
|
||||
let service: WebCipherFormGenerationService;
|
||||
let dialogService: jest.Mocked<DialogService>;
|
||||
let closed = of({});
|
||||
const close = jest.fn();
|
||||
const dialogRef = {
|
||||
close,
|
||||
get closed() {
|
||||
return closed;
|
||||
},
|
||||
} as unknown as DialogRef<unknown, unknown>;
|
||||
|
||||
beforeEach(() => {
|
||||
dialogService = mock<DialogService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
WebCipherFormGenerationService,
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(WebCipherFormGenerationService);
|
||||
});
|
||||
|
||||
it("creates without error", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("generatePassword", () => {
|
||||
it("opens the password generator dialog and returns the generated value", async () => {
|
||||
const generatedValue = "generated-password";
|
||||
closed = of({ action: "generated", generatedValue });
|
||||
dialogService.open.mockReturnValue(dialogRef);
|
||||
|
||||
const result = await service.generatePassword();
|
||||
|
||||
expect(dialogService.open).toHaveBeenCalledWith(WebVaultGeneratorDialogComponent, {
|
||||
data: { type: "password" },
|
||||
});
|
||||
expect(result).toBe(generatedValue);
|
||||
});
|
||||
|
||||
it("returns null if the dialog is canceled", async () => {
|
||||
closed = of({ action: "canceled" });
|
||||
dialogService.open.mockReturnValue(dialogRef);
|
||||
|
||||
const result = await service.generatePassword();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateUsername", () => {
|
||||
it("opens the username generator dialog and returns the generated value", async () => {
|
||||
const generatedValue = "generated-username";
|
||||
closed = of({ action: "generated", generatedValue });
|
||||
dialogService.open.mockReturnValue(dialogRef);
|
||||
|
||||
const result = await service.generateUsername();
|
||||
|
||||
expect(dialogService.open).toHaveBeenCalledWith(WebVaultGeneratorDialogComponent, {
|
||||
data: { type: "username" },
|
||||
});
|
||||
expect(result).toBe(generatedValue);
|
||||
});
|
||||
|
||||
it("returns null if the dialog is canceled", async () => {
|
||||
closed = of({ action: "canceled" });
|
||||
dialogService.open.mockReturnValue(dialogRef);
|
||||
|
||||
const result = await service.generateUsername();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormGenerationService } from "@bitwarden/vault";
|
||||
|
||||
import { WebVaultGeneratorDialogComponent } from "../components/web-generator-dialog/web-generator-dialog.component";
|
||||
|
||||
@Injectable()
|
||||
export class WebCipherFormGenerationService implements CipherFormGenerationService {
|
||||
private dialogService = inject(DialogService);
|
||||
|
||||
async generatePassword(): Promise<string> {
|
||||
const dialogRef = WebVaultGeneratorDialogComponent.open(this.dialogService, {
|
||||
data: { type: "password" },
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (result == null || result.action === "canceled") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.generatedValue;
|
||||
}
|
||||
|
||||
async generateUsername(): Promise<string> {
|
||||
const dialogRef = WebVaultGeneratorDialogComponent.open(this.dialogService, {
|
||||
data: { type: "username" },
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (result == null || result.action === "canceled") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.generatedValue;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { of, lastValueFrom } from "rxjs";
|
||||
import { lastValueFrom, of } from "rxjs";
|
||||
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
ViewCipherDialogCloseResult,
|
||||
ViewCipherDialogResult,
|
||||
} from "../individual-vault/view.component";
|
||||
import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||
|
||||
import { WebVaultPremiumUpgradePromptService } from "./web-premium-upgrade-prompt.service";
|
||||
|
||||
@@ -17,7 +14,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
||||
let service: WebVaultPremiumUpgradePromptService;
|
||||
let dialogServiceMock: jest.Mocked<DialogService>;
|
||||
let routerMock: jest.Mocked<Router>;
|
||||
let dialogRefMock: jest.Mocked<DialogRef<ViewCipherDialogCloseResult>>;
|
||||
let dialogRefMock: jest.Mocked<DialogRef<VaultItemDialogResult>>;
|
||||
|
||||
beforeEach(() => {
|
||||
dialogServiceMock = {
|
||||
@@ -30,7 +27,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
||||
|
||||
dialogRefMock = {
|
||||
close: jest.fn(),
|
||||
} as unknown as jest.Mocked<DialogRef<ViewCipherDialogCloseResult>>;
|
||||
} as unknown as jest.Mocked<DialogRef<VaultItemDialogResult>>;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -62,9 +59,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
||||
"billing",
|
||||
"subscription",
|
||||
]);
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||
action: ViewCipherDialogResult.PremiumUpgrade,
|
||||
});
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith(VaultItemDialogResult.PremiumUpgrade);
|
||||
});
|
||||
|
||||
it("prompts for premium upgrade and navigates to premium subscription if organizationId is not provided", async () => {
|
||||
@@ -79,9 +74,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
||||
type: "success",
|
||||
});
|
||||
expect(routerMock.navigate).toHaveBeenCalledWith(["settings/subscription/premium"]);
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||
action: ViewCipherDialogResult.PremiumUpgrade,
|
||||
});
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith(VaultItemDialogResult.PremiumUpgrade);
|
||||
});
|
||||
|
||||
it("does not navigate or close dialog if upgrade is no action is taken", async () => {
|
||||
|
||||
@@ -6,10 +6,7 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
ViewCipherDialogCloseResult,
|
||||
ViewCipherDialogResult,
|
||||
} from "../individual-vault/view.component";
|
||||
import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||
|
||||
/**
|
||||
* This service is used to prompt the user to upgrade to premium.
|
||||
@@ -19,7 +16,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private router: Router,
|
||||
private dialog: DialogRef<ViewCipherDialogCloseResult>,
|
||||
private dialog: DialogRef<VaultItemDialogResult>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -51,7 +48,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt
|
||||
}
|
||||
|
||||
if (upgradeConfirmed) {
|
||||
this.dialog.close({ action: ViewCipherDialogResult.PremiumUpgrade });
|
||||
this.dialog.close(VaultItemDialogResult.PremiumUpgrade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,10 @@ function redirectToDuoFrameless(redirectUrl: string) {
|
||||
|
||||
if (
|
||||
validateUrl.protocol !== "https:" ||
|
||||
!validateUrl.hostname.endsWith("duosecurity.com") ||
|
||||
!validateUrl.hostname.endsWith("duofederal.com")
|
||||
!(
|
||||
validateUrl.hostname.endsWith("duosecurity.com") ||
|
||||
validateUrl.hostname.endsWith("duofederal.com")
|
||||
)
|
||||
) {
|
||||
throw new Error("Invalid redirect URL");
|
||||
}
|
||||
|
||||
@@ -9277,5 +9277,14 @@
|
||||
},
|
||||
"editAccess": {
|
||||
"message": "Edit access"
|
||||
},
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximum file size is 500 MB"
|
||||
},
|
||||
"permanentlyDeleteAttachmentConfirmation": {
|
||||
"message": "Are you sure you want to permanently delete this attachment?"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user