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",
|
"name": "@bitwarden/browser",
|
||||||
"version": "2024.9.2",
|
"version": "2024.10.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env MANIFEST_VERSION=3 webpack",
|
"build": "cross-env MANIFEST_VERSION=3 webpack",
|
||||||
"build:mv2": "webpack",
|
"build:mv2": "webpack",
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ export default class MainBackground {
|
|||||||
folderService: InternalFolderServiceAbstraction;
|
folderService: InternalFolderServiceAbstraction;
|
||||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
|
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
|
||||||
collectionService: CollectionServiceAbstraction;
|
collectionService: CollectionServiceAbstraction;
|
||||||
vaultTimeoutService: VaultTimeoutService;
|
vaultTimeoutService?: VaultTimeoutService;
|
||||||
vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
|
vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
|
||||||
passwordGenerationService: PasswordGenerationServiceAbstraction;
|
passwordGenerationService: PasswordGenerationServiceAbstraction;
|
||||||
syncService: SyncService;
|
syncService: SyncService;
|
||||||
@@ -842,24 +842,26 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.vaultSettingsService = new VaultSettingsService(this.stateProvider);
|
this.vaultSettingsService = new VaultSettingsService(this.stateProvider);
|
||||||
|
|
||||||
this.vaultTimeoutService = new VaultTimeoutService(
|
if (!this.popupOnlyContext) {
|
||||||
this.accountService,
|
this.vaultTimeoutService = new VaultTimeoutService(
|
||||||
this.masterPasswordService,
|
this.accountService,
|
||||||
this.cipherService,
|
this.masterPasswordService,
|
||||||
this.folderService,
|
this.cipherService,
|
||||||
this.collectionService,
|
this.folderService,
|
||||||
this.platformUtilsService,
|
this.collectionService,
|
||||||
this.messagingService,
|
this.platformUtilsService,
|
||||||
this.searchService,
|
this.messagingService,
|
||||||
this.stateService,
|
this.searchService,
|
||||||
this.authService,
|
this.stateService,
|
||||||
this.vaultTimeoutSettingsService,
|
this.authService,
|
||||||
this.stateEventRunnerService,
|
this.vaultTimeoutSettingsService,
|
||||||
this.taskSchedulerService,
|
this.stateEventRunnerService,
|
||||||
this.logService,
|
this.taskSchedulerService,
|
||||||
lockedCallback,
|
this.logService,
|
||||||
logoutCallback,
|
lockedCallback,
|
||||||
);
|
logoutCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
|
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
|
||||||
|
|
||||||
this.sendStateProvider = new SendStateProvider(this.stateProvider);
|
this.sendStateProvider = new SendStateProvider(this.stateProvider);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "2024.9.2",
|
"version": "2024.10.0",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Bitwarden Inc.",
|
"author": "Bitwarden Inc.",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"minimum_chrome_version": "102.0",
|
"minimum_chrome_version": "102.0",
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "2024.9.2",
|
"version": "2024.10.0",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Bitwarden Inc.",
|
"author": "Bitwarden Inc.",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class PopupTabNavigationComponent {
|
|||||||
.pipe(
|
.pipe(
|
||||||
filter((policyAppliesToActiveUser) => policyAppliesToActiveUser),
|
filter((policyAppliesToActiveUser) => policyAppliesToActiveUser),
|
||||||
switchMap(() => this.sendService.sends$),
|
switchMap(() => this.sendService.sends$),
|
||||||
map((sends) => sends.length > 1),
|
map((sends) => sends.length > 0),
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
)
|
)
|
||||||
.subscribe((hasSends) => {
|
.subscribe((hasSends) => {
|
||||||
|
|||||||
@@ -7,14 +7,15 @@
|
|||||||
</bit-section-header>
|
</bit-section-header>
|
||||||
<bit-item-group>
|
<bit-item-group>
|
||||||
<bit-item *ngFor="let cipher of ciphers">
|
<bit-item *ngFor="let cipher of ciphers">
|
||||||
<a
|
<button
|
||||||
bit-item-content
|
bit-item-content
|
||||||
|
type="button"
|
||||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||||
(click)="onViewCipher(cipher)"
|
(click)="onViewCipher(cipher)"
|
||||||
>
|
>
|
||||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||||
</a>
|
</button>
|
||||||
<ng-container slot="end" *ngIf="cipher.edit">
|
<ng-container slot="end" *ngIf="cipher.edit">
|
||||||
<bit-item-action>
|
<bit-item-action>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"@koa/multer": "3.0.2",
|
"@koa/multer": "3.0.2",
|
||||||
"@koa/router": "12.0.1",
|
"@koa/router": "12.0.1",
|
||||||
"argon2": "0.40.1",
|
"argon2": "0.40.1",
|
||||||
"big-integer": "1.6.51",
|
"big-integer": "1.6.52",
|
||||||
"browser-hrtime": "1.1.8",
|
"browser-hrtime": "1.1.8",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"commander": "11.1.0",
|
"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 */
|
/* auto-generated by NAPI-RS */
|
||||||
|
|
||||||
export namespace passwords {
|
export declare namespace passwords {
|
||||||
/** Fetch the stored password from the keychain. */
|
/** Fetch the stored password from the keychain. */
|
||||||
export function getPassword(service: string, account: string): Promise<string>
|
export function getPassword(service: string, account: string): Promise<string>
|
||||||
/** Fetch the stored password from the keychain that was stored with Keytar. */
|
/** 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 deletePassword(service: string, account: string): Promise<void>
|
||||||
export function isAvailable(): Promise<boolean>
|
export function isAvailable(): Promise<boolean>
|
||||||
}
|
}
|
||||||
export namespace biometrics {
|
export declare namespace biometrics {
|
||||||
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
|
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
|
||||||
export function available(): Promise<boolean>
|
export function available(): Promise<boolean>
|
||||||
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
|
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
|
ivB64: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export namespace clipboards {
|
export declare namespace clipboards {
|
||||||
export function read(): Promise<string>
|
export function read(): Promise<string>
|
||||||
export function write(text: string, password: boolean): Promise<void>
|
export function write(text: string, password: boolean): Promise<void>
|
||||||
}
|
}
|
||||||
export namespace processisolations {
|
export declare namespace processisolations {
|
||||||
export function disableCoredumps(): Promise<void>
|
export function disableCoredumps(): Promise<void>
|
||||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||||
export function disableMemoryAccess(): Promise<void>
|
export function disableMemoryAccess(): Promise<void>
|
||||||
}
|
}
|
||||||
export namespace powermonitors {
|
export declare namespace powermonitors {
|
||||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||||
export function isLockMonitorAvailable(): Promise<boolean>
|
export function isLockMonitorAvailable(): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "2.16.2"
|
"@napi-rs/cli": "2.18.4"
|
||||||
},
|
},
|
||||||
"napi": {
|
"napi": {
|
||||||
"name": "desktop_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"
|
"@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": {
|
"node_modules/@bitwarden/desktop-napi": {
|
||||||
"resolved": "../desktop_native/napi",
|
"resolved": "../desktop_native/napi",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitwarden/web-vault",
|
"name": "@bitwarden/web-vault",
|
||||||
"version": "2024.9.2",
|
"version": "2024.10.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:oss": "webpack",
|
"build:oss": "webpack",
|
||||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
|
import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CollectionAdminService,
|
CollectionAdminService,
|
||||||
@@ -184,7 +185,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: EnvironmentService,
|
provide: EnvironmentService,
|
||||||
useClass: WebEnvironmentService,
|
useClass: WebEnvironmentService,
|
||||||
deps: [WINDOW, StateProvider, AccountService],
|
deps: [WINDOW, StateProvider, AccountService, Router],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: BiometricsService,
|
provide: BiometricsService,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Router } from "@angular/router";
|
||||||
import { ReplaySubject } from "rxjs";
|
import { ReplaySubject } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -23,6 +24,7 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
|
|||||||
private win: Window,
|
private win: Window,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
private router: Router,
|
||||||
) {
|
) {
|
||||||
super(stateProvider, accountService);
|
super(stateProvider, accountService);
|
||||||
|
|
||||||
@@ -47,9 +49,34 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
|
|||||||
this.environment$ = subject.asObservable();
|
this.environment$ = subject.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web cannot set environment
|
// Web setting env means navigating to a new location
|
||||||
async setEnvironment(region: Region, urls?: Urls): Promise<Urls> {
|
setEnvironment(region: Region, urls?: Urls): Promise<Urls> {
|
||||||
return;
|
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>
|
</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<button
|
<button bitMenuItem *ngIf="showAttachments" type="button" (click)="attachments()">
|
||||||
bitMenuItem
|
|
||||||
*ngIf="showAttachments || !vaultBulkManagementActionEnabled"
|
|
||||||
type="button"
|
|
||||||
(click)="attachments()"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||||
{{ "attachments" | i18n }}
|
{{ "attachments" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
@@ -119,26 +114,6 @@
|
|||||||
<i class="bwi bwi-fw bwi-files" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-files" aria-hidden="true"></i>
|
||||||
{{ "clone" | i18n }}
|
{{ "clone" | i18n }}
|
||||||
</button>
|
</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
|
<button
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
*ngIf="showAssignToCollections"
|
*ngIf="showAssignToCollections"
|
||||||
@@ -156,12 +131,7 @@
|
|||||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||||
{{ "restore" | i18n }}
|
{{ "restore" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button bitMenuItem *ngIf="canEditCipher" (click)="deleteCipher()" type="button">
|
||||||
bitMenuItem
|
|
||||||
*ngIf="canEditCipher || !vaultBulkManagementActionEnabled"
|
|
||||||
(click)="deleteCipher()"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export class VaultCipherRowComponent implements OnInit {
|
|||||||
@Input() collections: CollectionView[];
|
@Input() collections: CollectionView[];
|
||||||
@Input() viewingOrgVault: boolean;
|
@Input() viewingOrgVault: boolean;
|
||||||
@Input() canEditCipher: boolean;
|
@Input() canEditCipher: boolean;
|
||||||
@Input() vaultBulkManagementActionEnabled: boolean;
|
|
||||||
|
|
||||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||||
|
|
||||||
@@ -100,17 +99,15 @@ export class VaultCipherRowComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected get disableMenu() {
|
protected get disableMenu() {
|
||||||
return (
|
return !(
|
||||||
!(
|
this.isNotDeletedLoginCipher ||
|
||||||
this.isNotDeletedLoginCipher ||
|
this.showCopyPassword ||
|
||||||
this.showCopyPassword ||
|
this.showCopyTotp ||
|
||||||
this.showCopyTotp ||
|
this.showLaunchUri ||
|
||||||
this.showLaunchUri ||
|
this.showAttachments ||
|
||||||
this.showAttachments ||
|
this.showClone ||
|
||||||
this.showClone ||
|
this.canEditCipher ||
|
||||||
this.canEditCipher ||
|
this.cipher.isDeleted
|
||||||
this.cipher.isDeleted
|
|
||||||
) && this.vaultBulkManagementActionEnabled
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,14 +119,6 @@ export class VaultCipherRowComponent implements OnInit {
|
|||||||
this.onEvent.emit({ type: "clone", item: this.cipher });
|
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() {
|
protected events() {
|
||||||
this.onEvent.emit({ type: "viewEvents", item: this.cipher });
|
this.onEvent.emit({ type: "viewEvents", item: this.cipher });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { VaultItem } from "./vault-item";
|
|||||||
|
|
||||||
export type VaultItemEvent =
|
export type VaultItemEvent =
|
||||||
| { type: "viewAttachments"; item: CipherView }
|
| { type: "viewAttachments"; item: CipherView }
|
||||||
| { type: "viewCipherCollections"; item: CipherView }
|
|
||||||
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
|
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
|
||||||
| { type: "viewCollectionAccess"; item: CollectionView; readonly: boolean }
|
| { type: "viewCollectionAccess"; item: CollectionView; readonly: boolean }
|
||||||
| { type: "viewEvents"; item: CipherView }
|
| { type: "viewEvents"; item: CipherView }
|
||||||
@@ -15,5 +14,4 @@ export type VaultItemEvent =
|
|||||||
| { type: "delete"; items: VaultItem[] }
|
| { type: "delete"; items: VaultItem[] }
|
||||||
| { type: "copyField"; item: CipherView; field: "username" | "password" | "totp" }
|
| { type: "copyField"; item: CipherView; field: "username" | "password" | "totp" }
|
||||||
| { type: "moveToFolder"; items: CipherView[] }
|
| { type: "moveToFolder"; items: CipherView[] }
|
||||||
| { type: "moveToOrganization"; items: CipherView[] }
|
|
||||||
| { type: "assignToCollections"; items: CipherView[] };
|
| { type: "assignToCollections"; items: CipherView[] };
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<bit-menu #headerMenu>
|
<bit-menu #headerMenu>
|
||||||
<button *ngIf="bulkMoveAllowed" type="button" bitMenuItem (click)="bulkMoveToFolder()">
|
<button *ngIf="bulkMoveAllowed" type="button" bitMenuItem (click)="bulkMoveToFolder()">
|
||||||
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||||
{{ (vaultBulkManagementActionEnabled ? "addToFolder" : "moveSelected") | i18n }}
|
{{ "addToFolder" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="showAdminActions && showBulkEditCollectionAccess"
|
*ngIf="showAdminActions && showBulkEditCollectionAccess"
|
||||||
@@ -60,21 +60,12 @@
|
|||||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||||
{{ "assignToCollections" | i18n }}
|
{{ "assignToCollections" | i18n }}
|
||||||
</button>
|
</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()">
|
<button *ngIf="showBulkTrashOptions" type="button" bitMenuItem (click)="bulkRestore()">
|
||||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||||
{{ "restoreSelected" | i18n }}
|
{{ "restoreSelected" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="deleteAllowed || showBulkTrashOptions"
|
*ngIf="showDelete() || showBulkTrashOptions"
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="bulkDelete()"
|
(click)="bulkDelete()"
|
||||||
@@ -131,8 +122,7 @@
|
|||||||
[organizations]="allOrganizations"
|
[organizations]="allOrganizations"
|
||||||
[collections]="allCollections"
|
[collections]="allCollections"
|
||||||
[checked]="selection.isSelected(item)"
|
[checked]="selection.isSelected(item)"
|
||||||
[canEditCipher]="canEditCipher(item.cipher) && vaultBulkManagementActionEnabled"
|
[canEditCipher]="canEditCipher(item.cipher)"
|
||||||
[vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled"
|
|
||||||
(checkedToggled)="selection.toggle(item)"
|
(checkedToggled)="selection.toggle(item)"
|
||||||
(onEvent)="event($event)"
|
(onEvent)="event($event)"
|
||||||
></tr>
|
></tr>
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export class VaultItemsComponent {
|
|||||||
@Input() viewingOrgVault: boolean;
|
@Input() viewingOrgVault: boolean;
|
||||||
@Input() addAccessStatus: number;
|
@Input() addAccessStatus: number;
|
||||||
@Input() addAccessToggle: boolean;
|
@Input() addAccessToggle: boolean;
|
||||||
@Input() vaultBulkManagementActionEnabled = false;
|
|
||||||
|
|
||||||
private _ciphers?: CipherView[] = [];
|
private _ciphers?: CipherView[] = [];
|
||||||
@Input() get ciphers(): CipherView[] {
|
@Input() get ciphers(): CipherView[] {
|
||||||
@@ -93,23 +92,13 @@ export class VaultItemsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get disableMenu() {
|
get disableMenu() {
|
||||||
return (
|
return !this.bulkMoveAllowed && !this.showAssignToCollections() && !this.showDelete();
|
||||||
this.vaultBulkManagementActionEnabled &&
|
|
||||||
!this.bulkMoveAllowed &&
|
|
||||||
!this.showAssignToCollections() &&
|
|
||||||
!this.showDelete()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get bulkAssignToCollectionsAllowed() {
|
get bulkAssignToCollectionsAllowed() {
|
||||||
return this.showBulkAddToCollections && this.ciphers.length > 0;
|
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 {
|
protected canEditCollection(collection: CollectionView): boolean {
|
||||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||||
if (collection.id === 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() {
|
protected bulkRestore() {
|
||||||
this.event({
|
this.event({
|
||||||
type: "restore",
|
type: "restore",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
@@ -6,15 +6,16 @@ import { BehaviorSubject } from "rxjs";
|
|||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import {
|
||||||
|
PasswordGenerationServiceAbstraction,
|
||||||
import { UsernameGenerationServiceAbstraction } from "../../../../../../libs/tools/generator/extensions/legacy/src/username-generation.service.abstraction";
|
UsernameGenerationServiceAbstraction,
|
||||||
import { CipherFormGeneratorComponent } from "../cipher-generator/cipher-form-generator.component";
|
} from "@bitwarden/generator-legacy";
|
||||||
|
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
WebVaultGeneratorDialogAction,
|
||||||
WebVaultGeneratorDialogComponent,
|
WebVaultGeneratorDialogComponent,
|
||||||
WebVaultGeneratorDialogParams,
|
WebVaultGeneratorDialogParams,
|
||||||
WebVaultGeneratorDialogAction,
|
|
||||||
} from "./web-generator-dialog.component";
|
} from "./web-generator-dialog.component";
|
||||||
|
|
||||||
describe("WebVaultGeneratorDialogComponent", () => {
|
describe("WebVaultGeneratorDialogComponent", () => {
|
||||||
@@ -3,11 +3,9 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ButtonModule, DialogService } from "@bitwarden/components";
|
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { DialogModule } from "../../../../../../libs/components/src/dialog";
|
|
||||||
|
|
||||||
export interface WebVaultGeneratorDialogParams {
|
export interface WebVaultGeneratorDialogParams {
|
||||||
type: "password" | "username";
|
type: "password" | "username";
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,8 @@ import {
|
|||||||
CipherFormModule,
|
CipherFormModule,
|
||||||
} from "@bitwarden/vault";
|
} 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 { SharedModule } from "../../shared/shared.module";
|
||||||
|
import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service";
|
||||||
|
|
||||||
import { AttachmentsV2Component } from "./attachments-v2.component";
|
import { AttachmentsV2Component } from "./attachments-v2.component";
|
||||||
|
|
||||||
@@ -48,13 +47,13 @@ export interface AddEditCipherDialogCloseResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for viewing a cipher, presented in a dialog.
|
* Component for viewing a cipher, presented in a dialog.
|
||||||
|
* @deprecated Use the VaultItemDialogComponent instead.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault-add-edit-v2",
|
selector: "app-vault-add-edit-v2",
|
||||||
templateUrl: "add-edit-v2.component.html",
|
templateUrl: "add-edit-v2.component.html",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CipherViewComponent,
|
|
||||||
CommonModule,
|
CommonModule,
|
||||||
AsyncActionsModule,
|
AsyncActionsModule,
|
||||||
DialogModule,
|
DialogModule,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<bit-dialog dialogSize="default">
|
<bit-dialog dialogSize="default" background="alt">
|
||||||
<span bitDialogTitle>
|
<span bitDialogTitle>
|
||||||
{{ "attachments" | i18n }}
|
{{ "attachments" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<bit-dialog dialogSize="small">
|
<bit-dialog dialogSize="small">
|
||||||
<span bitDialogTitle>
|
<span bitDialogTitle>
|
||||||
{{ ((vaultBulkManagementActionEnabled$ | async) ? "addToFolder" : "moveSelected") | i18n }}
|
{{ "addToFolder" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
<span bitDialogContent>
|
<span bitDialogContent>
|
||||||
<p>{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}</p>
|
<p>{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}</p>
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { Component, Inject, OnInit } from "@angular/core";
|
|||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { firstValueFrom, Observable } from "rxjs";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -47,10 +45,6 @@ export class BulkMoveDialogComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
folders$: Observable<FolderView[]>;
|
folders$: Observable<FolderView[]>;
|
||||||
|
|
||||||
protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.VaultBulkManagementAction,
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) params: BulkMoveDialogParams,
|
@Inject(DIALOG_DATA) params: BulkMoveDialogParams,
|
||||||
private dialogRef: DialogRef<BulkMoveDialogResult>,
|
private dialogRef: DialogRef<BulkMoveDialogResult>,
|
||||||
@@ -59,7 +53,6 @@ export class BulkMoveDialogComponent implements OnInit {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private folderService: FolderService,
|
private folderService: FolderService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
) {
|
||||||
this.cipherIds = params.cipherIds ?? [];
|
this.cipherIds = params.cipherIds ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,8 @@
|
|||||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||||
[useEvents]="false"
|
[useEvents]="false"
|
||||||
[showAdminActions]="false"
|
[showAdminActions]="false"
|
||||||
[showBulkAddToCollections]="vaultBulkManagementActionEnabled$ | async"
|
[showBulkAddToCollections]="true"
|
||||||
(onEvent)="onVaultItemsEvent($event)"
|
(onEvent)="onVaultItemsEvent($event)"
|
||||||
[vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled$ | async"
|
|
||||||
>
|
>
|
||||||
</app-vault-items>
|
</app-vault-items>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
@@ -63,6 +64,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
|||||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||||
import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||||
import {
|
import {
|
||||||
|
CipherFormConfig,
|
||||||
CollectionAssignmentResult,
|
CollectionAssignmentResult,
|
||||||
DefaultCipherFormConfigService,
|
DefaultCipherFormConfigService,
|
||||||
PasswordRepromptService,
|
PasswordRepromptService,
|
||||||
@@ -75,16 +77,16 @@ import {
|
|||||||
CollectionDialogTabType,
|
CollectionDialogTabType,
|
||||||
openCollectionDialog,
|
openCollectionDialog,
|
||||||
} from "../components/collection-dialog";
|
} 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 { VaultItem } from "../components/vault-items/vault-item";
|
||||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||||
|
|
||||||
import {
|
|
||||||
AddEditCipherDialogCloseResult,
|
|
||||||
AddEditCipherDialogResult,
|
|
||||||
openAddEditCipherDialog,
|
|
||||||
} from "./add-edit-v2.component";
|
|
||||||
import { AddEditComponent } from "./add-edit.component";
|
import { AddEditComponent } from "./add-edit.component";
|
||||||
import {
|
import {
|
||||||
AttachmentDialogCloseResult,
|
AttachmentDialogCloseResult,
|
||||||
@@ -100,16 +102,7 @@ import {
|
|||||||
BulkMoveDialogResult,
|
BulkMoveDialogResult,
|
||||||
openBulkMoveDialog,
|
openBulkMoveDialog,
|
||||||
} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component";
|
} 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 { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component";
|
||||||
import { ShareComponent } from "./share.component";
|
|
||||||
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
||||||
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
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 { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||||
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
|
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
|
||||||
import {
|
|
||||||
openViewCipherDialog,
|
|
||||||
ViewCipherDialogCloseResult,
|
|
||||||
ViewCipherDialogResult,
|
|
||||||
} from "./view.component";
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "VaultComponent";
|
const BroadcasterSubscriptionId = "VaultComponent";
|
||||||
const SearchTextDebounceInterval = 200;
|
const SearchTextDebounceInterval = 200;
|
||||||
@@ -183,14 +171,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
||||||
protected canCreateCollections = false;
|
protected canCreateCollections = false;
|
||||||
protected currentSearchText$: Observable<string>;
|
protected currentSearchText$: Observable<string>;
|
||||||
protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.VaultBulkManagementAction,
|
|
||||||
);
|
|
||||||
private searchText$ = new Subject<string>();
|
private searchText$ = new Subject<string>();
|
||||||
private refresh$ = new BehaviorSubject<void>(null);
|
private refresh$ = new BehaviorSubject<void>(null);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private extensionRefreshEnabled: boolean;
|
private extensionRefreshEnabled: boolean;
|
||||||
|
|
||||||
|
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -364,12 +351,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
firstSetup$
|
firstSetup$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => this.route.queryParams),
|
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) => {
|
switchMap(async (params) => {
|
||||||
const cipherId = getCipherIdFromParams(params);
|
const cipherId = getCipherIdFromParams(params);
|
||||||
|
|
||||||
if (cipherId) {
|
if (cipherId) {
|
||||||
if (await this.cipherService.get(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);
|
await this.viewCipherById(cipherId);
|
||||||
} else {
|
} else {
|
||||||
await this.editCipherId(cipherId);
|
await this.editCipherId(cipherId);
|
||||||
@@ -458,9 +453,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
case "viewAttachments":
|
case "viewAttachments":
|
||||||
await this.editCipherAttachments(event.item);
|
await this.editCipherAttachments(event.item);
|
||||||
break;
|
break;
|
||||||
case "viewCipherCollections":
|
|
||||||
await this.editCipherCollections(event.item);
|
|
||||||
break;
|
|
||||||
case "clone":
|
case "clone":
|
||||||
await this.cloneCipher(event.item);
|
await this.cloneCipher(event.item);
|
||||||
break;
|
break;
|
||||||
@@ -477,13 +469,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
case "moveToFolder":
|
case "moveToFolder":
|
||||||
await this.bulkMove(event.items);
|
await this.bulkMove(event.items);
|
||||||
break;
|
break;
|
||||||
case "moveToOrganization":
|
|
||||||
if (event.items.length === 1) {
|
|
||||||
await this.shareCipher(event.items[0]);
|
|
||||||
} else {
|
|
||||||
await this.bulkShare(event.items);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "copyField":
|
case "copyField":
|
||||||
await this.copy(event.item, event.field);
|
await this.copy(event.item, event.field);
|
||||||
break;
|
break;
|
||||||
@@ -548,7 +533,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
async editCipherAttachments(cipher: CipherView) {
|
async editCipherAttachments(cipher: CipherView) {
|
||||||
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||||
this.go({ cipherId: null, itemId: null });
|
await this.go({ cipherId: null, itemId: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,9 +551,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canEditAttachments = await this.canEditAttachments(cipher);
|
const canEditAttachments = await this.canEditAttachments(cipher);
|
||||||
const vaultBulkManagementActionEnabled = await firstValueFrom(
|
|
||||||
this.vaultBulkManagementActionEnabled$,
|
|
||||||
);
|
|
||||||
|
|
||||||
let madeAttachmentChanges = false;
|
let madeAttachmentChanges = false;
|
||||||
|
|
||||||
@@ -594,7 +576,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.attachmentsModalRef,
|
this.attachmentsModalRef,
|
||||||
(comp) => {
|
(comp) => {
|
||||||
comp.cipherId = cipher.id;
|
comp.cipherId = cipher.id;
|
||||||
comp.viewOnly = !canEditAttachments && vaultBulkManagementActionEnabled;
|
comp.viewOnly = !canEditAttachments;
|
||||||
comp.onUploadedAttachment
|
comp.onUploadedAttachment
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe(() => (madeAttachmentChanges = true));
|
.subscribe(() => (madeAttachmentChanges = true));
|
||||||
@@ -615,39 +597,27 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async shareCipher(cipher: CipherView) {
|
/**
|
||||||
if (cipher.organizationId != null) {
|
* Open the combined view / edit dialog for a cipher.
|
||||||
// You cannot move ciphers between organizations
|
* @param mode - Starting mode of the dialog.
|
||||||
this.showMissingPermissionsError();
|
* @param formConfig - Configuration for the form when editing/adding a cipher.
|
||||||
return;
|
*/
|
||||||
}
|
async openVaultItemDialog(mode: VaultItemDialogMode, formConfig: CipherFormConfig) {
|
||||||
|
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
|
||||||
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
mode,
|
||||||
this.go({ cipherId: null, itemId: null });
|
formConfig,
|
||||||
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 },
|
|
||||||
});
|
});
|
||||||
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();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the query params when the dialog closes
|
||||||
|
await this.go({ cipherId: null, itemId: null, action: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
async addCipher(cipherType?: CipherType) {
|
async addCipher(cipherType?: CipherType) {
|
||||||
@@ -703,23 +673,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
folderId: this.activeFilter.folderId,
|
folderId: this.activeFilter.folderId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Open the dialog.
|
await this.openVaultItemDialog("form", cipherFormConfig);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCipher(cipher: CipherView, cloneMode?: boolean) {
|
async editCipher(cipher: CipherView, cloneMode?: boolean) {
|
||||||
@@ -735,7 +689,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||||
) {
|
) {
|
||||||
// didn't pass password prompt, so don't open add / edit modal
|
// 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;
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
modal.onClosedPromise().then(() => {
|
modal.onClosedPromise().then(() => {
|
||||||
this.go({ cipherId: null, itemId: null, action: null });
|
void this.go({ cipherId: null, itemId: null, action: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
return childComponent;
|
return childComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit a cipher using the new AddEditCipherDialogV2 component.
|
* Edit a cipher using the new VaultItemDialog.
|
||||||
*
|
*
|
||||||
* @param cipher
|
* @param cipher
|
||||||
* @param cloneMode
|
* @param cloneMode
|
||||||
@@ -786,31 +740,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
cipher.type,
|
cipher.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dialogRef = openAddEditCipherDialog(this.dialogService, {
|
await this.openVaultItemDialog("form", cipherFormConfig);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -837,39 +767,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||||
) {
|
) {
|
||||||
// Didn't pass password prompt, so don't open add / edit modal.
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeUserId = await firstValueFrom(
|
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
cipher.edit ? "edit" : "partial-edit",
|
||||||
);
|
cipher.id as CipherId,
|
||||||
// Decrypt the cipher.
|
cipher.type,
|
||||||
const cipherView = await cipher.decrypt(
|
|
||||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Open the dialog.
|
await this.openVaultItemDialog("view", cipherFormConfig);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addCollection() {
|
async addCollection() {
|
||||||
@@ -1018,7 +926,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const component = await this.editCipher(cipher, true);
|
const component = await this.editCipher(cipher, true);
|
||||||
component.cloneMode = true;
|
|
||||||
|
if (component != null) {
|
||||||
|
component.cloneMode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(c: CipherView): Promise<boolean> {
|
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) {
|
protected deleteCipherWithServer(id: string, permanent: boolean) {
|
||||||
return permanent
|
return permanent
|
||||||
? this.cipherService.deleteWithServer(id)
|
? this.cipherService.deleteWithServer(id)
|
||||||
@@ -1308,7 +1191,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return organization.canEditAllCiphers;
|
return organization.canEditAllCiphers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private go(queryParams: any = null) {
|
private async go(queryParams: any = null) {
|
||||||
if (queryParams == null) {
|
if (queryParams == null) {
|
||||||
queryParams = {
|
queryParams = {
|
||||||
favorites: this.activeFilter.isFavorites || null,
|
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,
|
relativeTo: this.route,
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
queryParamsHandling: "merge",
|
queryParamsHandling: "merge",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { 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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
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.
|
* Component for viewing a cipher, presented in a dialog.
|
||||||
|
* @deprecated Use the VaultItemDialogComponent instead.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault-view",
|
selector: "app-vault-view",
|
||||||
|
|||||||
@@ -598,9 +598,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
case "viewAttachments":
|
case "viewAttachments":
|
||||||
await this.editCipherAttachments(event.item);
|
await this.editCipherAttachments(event.item);
|
||||||
break;
|
break;
|
||||||
case "viewCipherCollections":
|
|
||||||
await this.editCipherCollections(event.item);
|
|
||||||
break;
|
|
||||||
case "clone":
|
case "clone":
|
||||||
await this.cloneCipher(event.item);
|
await this.cloneCipher(event.item);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { DialogRef } from "@angular/cdk/dialog";
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
import { TestBed } from "@angular/core/testing";
|
import { TestBed } from "@angular/core/testing";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { of, lastValueFrom } from "rxjs";
|
import { lastValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import {
|
import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||||
ViewCipherDialogCloseResult,
|
|
||||||
ViewCipherDialogResult,
|
|
||||||
} from "../individual-vault/view.component";
|
|
||||||
|
|
||||||
import { WebVaultPremiumUpgradePromptService } from "./web-premium-upgrade-prompt.service";
|
import { WebVaultPremiumUpgradePromptService } from "./web-premium-upgrade-prompt.service";
|
||||||
|
|
||||||
@@ -17,7 +14,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
|||||||
let service: WebVaultPremiumUpgradePromptService;
|
let service: WebVaultPremiumUpgradePromptService;
|
||||||
let dialogServiceMock: jest.Mocked<DialogService>;
|
let dialogServiceMock: jest.Mocked<DialogService>;
|
||||||
let routerMock: jest.Mocked<Router>;
|
let routerMock: jest.Mocked<Router>;
|
||||||
let dialogRefMock: jest.Mocked<DialogRef<ViewCipherDialogCloseResult>>;
|
let dialogRefMock: jest.Mocked<DialogRef<VaultItemDialogResult>>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dialogServiceMock = {
|
dialogServiceMock = {
|
||||||
@@ -30,7 +27,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
|||||||
|
|
||||||
dialogRefMock = {
|
dialogRefMock = {
|
||||||
close: jest.fn(),
|
close: jest.fn(),
|
||||||
} as unknown as jest.Mocked<DialogRef<ViewCipherDialogCloseResult>>;
|
} as unknown as jest.Mocked<DialogRef<VaultItemDialogResult>>;
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -62,9 +59,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
|||||||
"billing",
|
"billing",
|
||||||
"subscription",
|
"subscription",
|
||||||
]);
|
]);
|
||||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
expect(dialogRefMock.close).toHaveBeenCalledWith(VaultItemDialogResult.PremiumUpgrade);
|
||||||
action: ViewCipherDialogResult.PremiumUpgrade,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prompts for premium upgrade and navigates to premium subscription if organizationId is not provided", async () => {
|
it("prompts for premium upgrade and navigates to premium subscription if organizationId is not provided", async () => {
|
||||||
@@ -79,9 +74,7 @@ describe("WebVaultPremiumUpgradePromptService", () => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
expect(routerMock.navigate).toHaveBeenCalledWith(["settings/subscription/premium"]);
|
expect(routerMock.navigate).toHaveBeenCalledWith(["settings/subscription/premium"]);
|
||||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
expect(dialogRefMock.close).toHaveBeenCalledWith(VaultItemDialogResult.PremiumUpgrade);
|
||||||
action: ViewCipherDialogResult.PremiumUpgrade,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not navigate or close dialog if upgrade is no action is taken", async () => {
|
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 { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import {
|
import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||||
ViewCipherDialogCloseResult,
|
|
||||||
ViewCipherDialogResult,
|
|
||||||
} from "../individual-vault/view.component";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service is used to prompt the user to upgrade to premium.
|
* This service is used to prompt the user to upgrade to premium.
|
||||||
@@ -19,7 +16,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt
|
|||||||
constructor(
|
constructor(
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private dialog: DialogRef<ViewCipherDialogCloseResult>,
|
private dialog: DialogRef<VaultItemDialogResult>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +48,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (upgradeConfirmed) {
|
if (upgradeConfirmed) {
|
||||||
this.dialog.close({ action: ViewCipherDialogResult.PremiumUpgrade });
|
this.dialog.close(VaultItemDialogResult.PremiumUpgrade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,10 @@ function redirectToDuoFrameless(redirectUrl: string) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
validateUrl.protocol !== "https:" ||
|
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");
|
throw new Error("Invalid redirect URL");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9277,5 +9277,14 @@
|
|||||||
},
|
},
|
||||||
"editAccess": {
|
"editAccess": {
|
||||||
"message": "Edit access"
|
"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?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
@@ -22,7 +23,7 @@ export default {
|
|||||||
component: AnonLayoutComponent,
|
component: AnonLayoutComponent,
|
||||||
decorators: [
|
decorators: [
|
||||||
moduleMetadata({
|
moduleMetadata({
|
||||||
imports: [ButtonModule],
|
imports: [ButtonModule, RouterModule],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: PlatformUtilsService,
|
provide: PlatformUtilsService,
|
||||||
@@ -46,6 +47,10 @@ export default {
|
|||||||
}).asObservable(),
|
}).asObservable(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: { queryParams: of({}) },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -66,7 +71,7 @@ export const WithPrimaryContent: Story = {
|
|||||||
template:
|
template:
|
||||||
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
||||||
`
|
`
|
||||||
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname">
|
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" >
|
||||||
<div>
|
<div>
|
||||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||||
@@ -83,7 +88,7 @@ export const WithSecondaryContent: Story = {
|
|||||||
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
|
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
|
||||||
// Notice that slot="secondary" is requred to project any secondary content.
|
// Notice that slot="secondary" is requred to project any secondary content.
|
||||||
`
|
`
|
||||||
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname">
|
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" >
|
||||||
<div>
|
<div>
|
||||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||||
@@ -104,7 +109,7 @@ export const WithLongContent: Story = {
|
|||||||
template:
|
template:
|
||||||
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
|
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
|
||||||
`
|
`
|
||||||
<auth-anon-layout title="Page Title lorem ipsum dolor consectetur sit amet expedita quod est" subtitle="Subtitle here Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?" [showReadonlyHostname]="showReadonlyHostname">
|
<auth-anon-layout title="Page Title lorem ipsum dolor consectetur sit amet expedita quod est" subtitle="Subtitle here Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" >
|
||||||
<div>
|
<div>
|
||||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.</div>
|
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.</div>
|
||||||
@@ -126,7 +131,7 @@ export const WithThinPrimaryContent: Story = {
|
|||||||
template:
|
template:
|
||||||
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
|
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
|
||||||
`
|
`
|
||||||
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname">
|
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="hideLogo" >
|
||||||
<div class="text-center">Lorem ipsum</div>
|
<div class="text-center">Lorem ipsum</div>
|
||||||
|
|
||||||
<div slot="secondary" class="text-center">
|
<div slot="secondary" class="text-center">
|
||||||
@@ -160,7 +165,7 @@ export const HideLogo: Story = {
|
|||||||
template:
|
template:
|
||||||
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
||||||
`
|
`
|
||||||
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="true">
|
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="true" >
|
||||||
<div>
|
<div>
|
||||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||||
@@ -176,7 +181,7 @@ export const HideFooter: Story = {
|
|||||||
template:
|
template:
|
||||||
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
||||||
`
|
`
|
||||||
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideFooter]="true">
|
<auth-anon-layout [title]="title" [subtitle]="subtitle" [showReadonlyHostname]="showReadonlyHostname" [hideFooter]="true" [hideLogo]="hideLogo" >
|
||||||
<div>
|
<div>
|
||||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export enum FeatureFlag {
|
|||||||
EnableTimeThreshold = "PM-5864-dollar-threshold",
|
EnableTimeThreshold = "PM-5864-dollar-threshold",
|
||||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||||
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
||||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
|
||||||
AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page",
|
AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page",
|
||||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||||
@@ -64,7 +63,6 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.EnableTimeThreshold]: FALSE,
|
[FeatureFlag.EnableTimeThreshold]: FALSE,
|
||||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
|
||||||
[FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE,
|
[FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE,
|
||||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { CollectionId, UserId } from "../../types/guid";
|
import { CollectionId, OrganizationId, UserId } from "../../types/guid";
|
||||||
|
import { OrgKey } from "../../types/key";
|
||||||
import { CollectionData } from "../models/data/collection.data";
|
import { CollectionData } from "../models/data/collection.data";
|
||||||
import { Collection } from "../models/domain/collection";
|
import { Collection } from "../models/domain/collection";
|
||||||
import { TreeNode } from "../models/domain/tree-node";
|
import { TreeNode } from "../models/domain/tree-node";
|
||||||
@@ -13,9 +14,13 @@ export abstract class CollectionService {
|
|||||||
encrypt: (model: CollectionView) => Promise<Collection>;
|
encrypt: (model: CollectionView) => Promise<Collection>;
|
||||||
decryptedCollectionViews$: (ids: CollectionId[]) => Observable<CollectionView[]>;
|
decryptedCollectionViews$: (ids: CollectionId[]) => Observable<CollectionView[]>;
|
||||||
/**
|
/**
|
||||||
* @deprecated This method will soon be made private, use `decryptedCollectionViews$` instead.
|
* @deprecated This method will soon be made private
|
||||||
|
* See PM-12375
|
||||||
*/
|
*/
|
||||||
decryptMany: (collections: Collection[]) => Promise<CollectionView[]>;
|
decryptMany: (
|
||||||
|
collections: Collection[],
|
||||||
|
orgKeys?: Record<OrganizationId, OrgKey>,
|
||||||
|
) => Promise<CollectionView[]>;
|
||||||
get: (id: string) => Promise<Collection>;
|
get: (id: string) => Promise<Collection>;
|
||||||
getAll: () => Promise<Collection[]>;
|
getAll: () => Promise<Collection[]>;
|
||||||
getAllDecrypted: () => Promise<CollectionView[]>;
|
getAllDecrypted: () => Promise<CollectionView[]>;
|
||||||
|
|||||||
135
libs/common/src/vault/services/collection.service.spec.ts
Normal file
135
libs/common/src/vault/services/collection.service.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FakeStateProvider,
|
||||||
|
makeEncString,
|
||||||
|
makeSymmetricCryptoKey,
|
||||||
|
mockAccountServiceWith,
|
||||||
|
} from "../../../spec";
|
||||||
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||||
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
|
import { ContainerService } from "../../platform/services/container.service";
|
||||||
|
import { CollectionId, OrganizationId, UserId } from "../../types/guid";
|
||||||
|
import { OrgKey } from "../../types/key";
|
||||||
|
import { CollectionData } from "../models/data/collection.data";
|
||||||
|
|
||||||
|
import { CollectionService, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.service";
|
||||||
|
|
||||||
|
describe("CollectionService", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete (window as any).bitwardenContainerService;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decryptedCollections$", () => {
|
||||||
|
it("emits decrypted collections from state", async () => {
|
||||||
|
// Arrange test collections
|
||||||
|
const org1 = Utils.newGuid() as OrganizationId;
|
||||||
|
const org2 = Utils.newGuid() as OrganizationId;
|
||||||
|
|
||||||
|
const collection1 = collectionDataFactory(org1);
|
||||||
|
const collection2 = collectionDataFactory(org2);
|
||||||
|
|
||||||
|
// Arrange state provider
|
||||||
|
const fakeStateProvider = mockStateProvider();
|
||||||
|
await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, {
|
||||||
|
[collection1.id]: collection1,
|
||||||
|
[collection2.id]: collection2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Arrange cryptoService - orgKeys and mock decryption
|
||||||
|
const cryptoService = mockCryptoService();
|
||||||
|
cryptoService.orgKeys$.mockReturnValue(
|
||||||
|
of({
|
||||||
|
[org1]: makeSymmetricCryptoKey<OrgKey>(),
|
||||||
|
[org2]: makeSymmetricCryptoKey<OrgKey>(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectionService = new CollectionService(
|
||||||
|
cryptoService,
|
||||||
|
mock<EncryptService>(),
|
||||||
|
mockI18nService(),
|
||||||
|
fakeStateProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(collectionService.decryptedCollections$);
|
||||||
|
expect(result.length).toBe(2);
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
id: collection1.id,
|
||||||
|
name: "DECRYPTED_STRING",
|
||||||
|
});
|
||||||
|
expect(result[1]).toMatchObject({
|
||||||
|
id: collection2.id,
|
||||||
|
name: "DECRYPTED_STRING",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles null collection state", async () => {
|
||||||
|
// Arrange test collections
|
||||||
|
const org1 = Utils.newGuid() as OrganizationId;
|
||||||
|
const org2 = Utils.newGuid() as OrganizationId;
|
||||||
|
|
||||||
|
// Arrange state provider
|
||||||
|
const fakeStateProvider = mockStateProvider();
|
||||||
|
await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, null);
|
||||||
|
|
||||||
|
// Arrange cryptoService - orgKeys and mock decryption
|
||||||
|
const cryptoService = mockCryptoService();
|
||||||
|
cryptoService.orgKeys$.mockReturnValue(
|
||||||
|
of({
|
||||||
|
[org1]: makeSymmetricCryptoKey<OrgKey>(),
|
||||||
|
[org2]: makeSymmetricCryptoKey<OrgKey>(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectionService = new CollectionService(
|
||||||
|
cryptoService,
|
||||||
|
mock<EncryptService>(),
|
||||||
|
mockI18nService(),
|
||||||
|
fakeStateProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decryptedCollections = await firstValueFrom(collectionService.decryptedCollections$);
|
||||||
|
expect(decryptedCollections.length).toBe(0);
|
||||||
|
|
||||||
|
const encryptedCollections = await firstValueFrom(collectionService.encryptedCollections$);
|
||||||
|
expect(encryptedCollections.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockI18nService = () => {
|
||||||
|
const i18nService = mock<I18nService>();
|
||||||
|
i18nService.collator = null; // this is a mock only, avoid use of this object
|
||||||
|
return i18nService;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockStateProvider = () => {
|
||||||
|
const userId = Utils.newGuid() as UserId;
|
||||||
|
return new FakeStateProvider(mockAccountServiceWith(userId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCryptoService = () => {
|
||||||
|
const cryptoService = mock<CryptoService>();
|
||||||
|
const encryptService = mock<EncryptService>();
|
||||||
|
encryptService.decryptToUtf8
|
||||||
|
.calledWith(expect.any(EncString), expect.anything())
|
||||||
|
.mockResolvedValue("DECRYPTED_STRING");
|
||||||
|
|
||||||
|
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
|
||||||
|
|
||||||
|
return cryptoService;
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectionDataFactory = (orgId: OrganizationId) => {
|
||||||
|
const collection = new CollectionData({} as any);
|
||||||
|
collection.id = Utils.newGuid() as CollectionId;
|
||||||
|
collection.organizationId = orgId;
|
||||||
|
collection.name = makeEncString("ENC_STRING").encryptedString;
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { firstValueFrom, map, Observable } from "rxjs";
|
import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "../../platform/state";
|
} from "../../platform/state";
|
||||||
import { CollectionId, OrganizationId, UserId } from "../../types/guid";
|
import { CollectionId, OrganizationId, UserId } from "../../types/guid";
|
||||||
|
import { OrgKey } from "../../types/key";
|
||||||
import { CollectionService as CollectionServiceAbstraction } from "../../vault/abstractions/collection.service";
|
import { CollectionService as CollectionServiceAbstraction } from "../../vault/abstractions/collection.service";
|
||||||
import { CollectionData } from "../models/data/collection.data";
|
import { CollectionData } from "../models/data/collection.data";
|
||||||
import { Collection } from "../models/domain/collection";
|
import { Collection } from "../models/domain/collection";
|
||||||
@@ -22,7 +23,7 @@ import { TreeNode } from "../models/domain/tree-node";
|
|||||||
import { CollectionView } from "../models/view/collection.view";
|
import { CollectionView } from "../models/view/collection.view";
|
||||||
import { ServiceUtils } from "../service-utils";
|
import { ServiceUtils } from "../service-utils";
|
||||||
|
|
||||||
const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<CollectionData, CollectionId>(
|
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<CollectionData, CollectionId>(
|
||||||
COLLECTION_DATA,
|
COLLECTION_DATA,
|
||||||
"collections",
|
"collections",
|
||||||
{
|
{
|
||||||
@@ -31,19 +32,19 @@ const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<CollectionData, C
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const DECRYPTED_COLLECTION_DATA_KEY = DeriveDefinition.from<
|
const DECRYPTED_COLLECTION_DATA_KEY = new DeriveDefinition<
|
||||||
Record<CollectionId, CollectionData>,
|
[Record<CollectionId, CollectionData>, Record<OrganizationId, OrgKey>],
|
||||||
CollectionView[],
|
CollectionView[],
|
||||||
{ collectionService: CollectionService }
|
{ collectionService: CollectionService }
|
||||||
>(ENCRYPTED_COLLECTION_DATA_KEY, {
|
>(COLLECTION_DATA, "decryptedCollections", {
|
||||||
deserializer: (obj) => obj.map((collection) => CollectionView.fromJSON(collection)),
|
deserializer: (obj) => obj.map((collection) => CollectionView.fromJSON(collection)),
|
||||||
derive: async (collections: Record<CollectionId, CollectionData>, { collectionService }) => {
|
derive: async ([collections, orgKeys], { collectionService }) => {
|
||||||
const data: Collection[] = [];
|
if (collections == null) {
|
||||||
for (const id in collections ?? {}) {
|
return [];
|
||||||
const collectionId = id as CollectionId;
|
|
||||||
data.push(new Collection(collections[collectionId]));
|
|
||||||
}
|
}
|
||||||
return await collectionService.decryptMany(data);
|
|
||||||
|
const data = Object.values(collections).map((c) => new Collection(c));
|
||||||
|
return await collectionService.decryptMany(data, orgKeys);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,18 +69,25 @@ export class CollectionService implements CollectionServiceAbstraction {
|
|||||||
protected stateProvider: StateProvider,
|
protected stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
this.encryptedCollectionDataState = this.stateProvider.getActive(ENCRYPTED_COLLECTION_DATA_KEY);
|
this.encryptedCollectionDataState = this.stateProvider.getActive(ENCRYPTED_COLLECTION_DATA_KEY);
|
||||||
|
|
||||||
this.encryptedCollections$ = this.encryptedCollectionDataState.state$.pipe(
|
this.encryptedCollections$ = this.encryptedCollectionDataState.state$.pipe(
|
||||||
map((collections) => {
|
map((collections) => {
|
||||||
const response: Collection[] = [];
|
if (collections == null) {
|
||||||
for (const id in collections ?? {}) {
|
return [];
|
||||||
response.push(new Collection(collections[id as CollectionId]));
|
|
||||||
}
|
}
|
||||||
return response;
|
|
||||||
|
return Object.values(collections).map((c) => new Collection(c));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const encryptedCollectionsWithKeys = this.encryptedCollectionDataState.combinedState$.pipe(
|
||||||
|
switchMap(([userId, collectionData]) =>
|
||||||
|
combineLatest([of(collectionData), this.cryptoService.orgKeys$(userId)]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
this.decryptedCollectionDataState = this.stateProvider.getDerived(
|
this.decryptedCollectionDataState = this.stateProvider.getDerived(
|
||||||
this.encryptedCollectionDataState.state$,
|
encryptedCollectionsWithKeys,
|
||||||
DECRYPTED_COLLECTION_DATA_KEY,
|
DECRYPTED_COLLECTION_DATA_KEY,
|
||||||
{ collectionService: this },
|
{ collectionService: this },
|
||||||
);
|
);
|
||||||
@@ -108,19 +116,24 @@ export class CollectionService implements CollectionServiceAbstraction {
|
|||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptMany(collections: Collection[]): Promise<CollectionView[]> {
|
// TODO: this should be private and orgKeys should be required.
|
||||||
if (collections == null) {
|
// See https://bitwarden.atlassian.net/browse/PM-12375
|
||||||
|
async decryptMany(
|
||||||
|
collections: Collection[],
|
||||||
|
orgKeys?: Record<OrganizationId, OrgKey>,
|
||||||
|
): Promise<CollectionView[]> {
|
||||||
|
if (collections == null || collections.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const decCollections: CollectionView[] = [];
|
const decCollections: CollectionView[] = [];
|
||||||
|
|
||||||
const organizationKeys = await firstValueFrom(this.cryptoService.activeUserOrgKeys$);
|
orgKeys ??= await firstValueFrom(this.cryptoService.activeUserOrgKeys$);
|
||||||
|
|
||||||
const promises: Promise<any>[] = [];
|
const promises: Promise<any>[] = [];
|
||||||
collections.forEach((collection) => {
|
collections.forEach((collection) => {
|
||||||
promises.push(
|
promises.push(
|
||||||
collection
|
collection
|
||||||
.decrypt(organizationKeys[collection.organizationId as OrganizationId])
|
.decrypt(orgKeys[collection.organizationId as OrganizationId])
|
||||||
.then((c) => decCollections.push(c)),
|
.then((c) => decCollections.push(c)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
352
libs/tools/generator/core/src/rx.spec.ts
Normal file
352
libs/tools/generator/core/src/rx.spec.ts
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
import { EmptyError, Subject, tap } from "rxjs";
|
||||||
|
|
||||||
|
import { anyComplete, on, ready } from "./rx";
|
||||||
|
|
||||||
|
describe("anyComplete", () => {
|
||||||
|
it("emits true when its input completes", () => {
|
||||||
|
const input$ = new Subject<void>();
|
||||||
|
|
||||||
|
const emissions: boolean[] = [];
|
||||||
|
anyComplete(input$).subscribe((e) => emissions.push(e));
|
||||||
|
input$.complete();
|
||||||
|
|
||||||
|
expect(emissions).toEqual([true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when its input is already complete", () => {
|
||||||
|
const input = new Subject<void>();
|
||||||
|
input.complete();
|
||||||
|
|
||||||
|
let completed = false;
|
||||||
|
anyComplete(input).subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
expect(completed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when any input completes", () => {
|
||||||
|
const input$ = new Subject<void>();
|
||||||
|
const completing$ = new Subject<void>();
|
||||||
|
|
||||||
|
let completed = false;
|
||||||
|
anyComplete([input$, completing$]).subscribe({ complete: () => (completed = true) });
|
||||||
|
completing$.complete();
|
||||||
|
|
||||||
|
expect(completed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores emissions", () => {
|
||||||
|
const input$ = new Subject<number>();
|
||||||
|
|
||||||
|
const emissions: boolean[] = [];
|
||||||
|
anyComplete(input$).subscribe((e) => emissions.push(e));
|
||||||
|
input$.next(1);
|
||||||
|
input$.next(2);
|
||||||
|
input$.complete();
|
||||||
|
|
||||||
|
expect(emissions).toEqual([true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards errors", () => {
|
||||||
|
const input$ = new Subject<void>();
|
||||||
|
const expected = { some: "error" };
|
||||||
|
|
||||||
|
let error = null;
|
||||||
|
anyComplete(input$).subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
input$.error(expected);
|
||||||
|
|
||||||
|
expect(error).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ready", () => {
|
||||||
|
it("connects when subscribed", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
let connected = false;
|
||||||
|
const source$ = new Subject<number>().pipe(tap({ subscribe: () => (connected = true) }));
|
||||||
|
|
||||||
|
// precondition: ready$ should be cold
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
expect(connected).toBe(false);
|
||||||
|
|
||||||
|
ready$.subscribe();
|
||||||
|
|
||||||
|
expect(connected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses source emissions until its watch emits", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
const results: number[] = [];
|
||||||
|
ready$.subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
// precondition: no emissions
|
||||||
|
source$.next(1);
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
|
||||||
|
watch$.next();
|
||||||
|
|
||||||
|
expect(results).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses source emissions until all watches emit", () => {
|
||||||
|
const watchA$ = new Subject<void>();
|
||||||
|
const watchB$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready([watchA$, watchB$]));
|
||||||
|
const results: number[] = [];
|
||||||
|
ready$.subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
// preconditions: no emissions
|
||||||
|
source$.next(1);
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
watchA$.next();
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
|
||||||
|
watchB$.next();
|
||||||
|
|
||||||
|
expect(results).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits the last source emission when its watch emits", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
const results: number[] = [];
|
||||||
|
ready$.subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
// precondition: no emissions
|
||||||
|
source$.next(1);
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
|
||||||
|
source$.next(2);
|
||||||
|
watch$.next();
|
||||||
|
|
||||||
|
expect(results).toEqual([2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits all source emissions after its watch emits", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
const results: number[] = [];
|
||||||
|
ready$.subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
watch$.next();
|
||||||
|
source$.next(1);
|
||||||
|
source$.next(2);
|
||||||
|
|
||||||
|
expect(results).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores repeated watch emissions", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
const results: number[] = [];
|
||||||
|
ready$.subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
watch$.next();
|
||||||
|
source$.next(1);
|
||||||
|
watch$.next();
|
||||||
|
source$.next(2);
|
||||||
|
watch$.next();
|
||||||
|
|
||||||
|
expect(results).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when its source completes", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
let completed = false;
|
||||||
|
ready$.subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
source$.complete();
|
||||||
|
|
||||||
|
expect(completed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when its source errors", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
const expected = { some: "error" };
|
||||||
|
let error = null;
|
||||||
|
ready$.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
source$.error(expected);
|
||||||
|
|
||||||
|
expect(error).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when its watch errors", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
const expected = { some: "error" };
|
||||||
|
let error = null;
|
||||||
|
ready$.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
watch$.error(expected);
|
||||||
|
|
||||||
|
expect(error).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when its watch completes before emitting", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const ready$ = source$.pipe(ready(watch$));
|
||||||
|
let error = null;
|
||||||
|
ready$.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
watch$.complete();
|
||||||
|
|
||||||
|
expect(error).toBeInstanceOf(EmptyError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("on", () => {
|
||||||
|
it("connects when subscribed", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
let connected = false;
|
||||||
|
const source$ = new Subject<number>().pipe(tap({ subscribe: () => (connected = true) }));
|
||||||
|
|
||||||
|
// precondition: on$ should be cold
|
||||||
|
const on$ = source$.pipe(on(watch$));
|
||||||
|
expect(connected).toBeFalsy();
|
||||||
|
|
||||||
|
on$.subscribe();
|
||||||
|
|
||||||
|
expect(connected).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses source emissions until `on` emits", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const results: number[] = [];
|
||||||
|
source$.pipe(on(watch$)).subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
// precondition: on$ should be cold
|
||||||
|
source$.next(1);
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
|
||||||
|
watch$.next();
|
||||||
|
|
||||||
|
expect(results).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("repeats source emissions when `on` emits", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const results: number[] = [];
|
||||||
|
source$.pipe(on(watch$)).subscribe((n) => results.push(n));
|
||||||
|
source$.next(1);
|
||||||
|
|
||||||
|
watch$.next();
|
||||||
|
watch$.next();
|
||||||
|
|
||||||
|
expect(results).toEqual([1, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates source emissions when `on` emits", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const results: number[] = [];
|
||||||
|
source$.pipe(on(watch$)).subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
source$.next(1);
|
||||||
|
watch$.next();
|
||||||
|
source$.next(2);
|
||||||
|
watch$.next();
|
||||||
|
|
||||||
|
expect(results).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a value when `on` emits before the source is ready", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const results: number[] = [];
|
||||||
|
source$.pipe(on(watch$)).subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
watch$.next();
|
||||||
|
source$.next(1);
|
||||||
|
|
||||||
|
expect(results).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores repeated `on` emissions before the source is ready", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const results: number[] = [];
|
||||||
|
source$.pipe(on(watch$)).subscribe((n) => results.push(n));
|
||||||
|
|
||||||
|
watch$.next();
|
||||||
|
watch$.next();
|
||||||
|
source$.next(1);
|
||||||
|
|
||||||
|
expect(results).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits only the latest source emission when `on` emits", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const results: number[] = [];
|
||||||
|
source$.pipe(on(watch$)).subscribe((n) => results.push(n));
|
||||||
|
source$.next(1);
|
||||||
|
|
||||||
|
watch$.next();
|
||||||
|
|
||||||
|
source$.next(2);
|
||||||
|
source$.next(3);
|
||||||
|
watch$.next();
|
||||||
|
|
||||||
|
expect(results).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when its source completes", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
let complete: boolean = false;
|
||||||
|
source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) });
|
||||||
|
|
||||||
|
source$.complete();
|
||||||
|
|
||||||
|
expect(complete).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when its watch completes", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
let complete: boolean = false;
|
||||||
|
source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) });
|
||||||
|
|
||||||
|
watch$.complete();
|
||||||
|
|
||||||
|
expect(complete).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when its source errors", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const expected = { some: "error" };
|
||||||
|
let error = null;
|
||||||
|
source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
source$.error(expected);
|
||||||
|
|
||||||
|
expect(error).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when its watch errors", () => {
|
||||||
|
const watch$ = new Subject<void>();
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const expected = { some: "error" };
|
||||||
|
let error = null;
|
||||||
|
source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
watch$.error(expected);
|
||||||
|
|
||||||
|
expect(error).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,18 @@
|
|||||||
import { map, pipe } from "rxjs";
|
import {
|
||||||
|
concat,
|
||||||
|
concatMap,
|
||||||
|
connect,
|
||||||
|
endWith,
|
||||||
|
first,
|
||||||
|
ignoreElements,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
pipe,
|
||||||
|
race,
|
||||||
|
ReplaySubject,
|
||||||
|
takeUntil,
|
||||||
|
zip,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { reduceCollection, distinctIfShallowMatch } from "@bitwarden/common/tools/rx";
|
import { reduceCollection, distinctIfShallowMatch } from "@bitwarden/common/tools/rx";
|
||||||
|
|
||||||
@@ -37,3 +51,86 @@ export function newDefaultEvaluator<Target>() {
|
|||||||
return pipe(map((_) => new DefaultPolicyEvaluator<Target>()));
|
return pipe(map((_) => new DefaultPolicyEvaluator<Target>()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create an observable that, once subscribed, emits `true` then completes when
|
||||||
|
* any input completes. If an input is already complete when the subscription
|
||||||
|
* occurs, it emits immediately.
|
||||||
|
* @param watch$ the observable(s) to watch for completion; if an array is passed,
|
||||||
|
* null and undefined members are ignored. If `watch$` is empty, `anyComplete`
|
||||||
|
* will never complete.
|
||||||
|
* @returns An observable that emits `true` when any of its inputs
|
||||||
|
* complete. The observable forwards the first error from its input.
|
||||||
|
* @remarks This method is particularly useful in combination with `takeUntil` and
|
||||||
|
* streams that are not guaranteed to complete on their own.
|
||||||
|
*/
|
||||||
|
export function anyComplete(watch$: Observable<any> | Observable<any>[]): Observable<any> {
|
||||||
|
if (Array.isArray(watch$)) {
|
||||||
|
const completes$ = watch$
|
||||||
|
.filter((w$) => !!w$)
|
||||||
|
.map((w$) => w$.pipe(ignoreElements(), endWith(true)));
|
||||||
|
const completed$ = race(completes$);
|
||||||
|
return completed$;
|
||||||
|
} else {
|
||||||
|
return watch$.pipe(ignoreElements(), endWith(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an observable that delays the input stream until all watches have
|
||||||
|
* emitted a value. The watched values are not included in the source stream.
|
||||||
|
* The last emission from the source is output when all the watches have
|
||||||
|
* emitted at least once.
|
||||||
|
* @param watch$ the observable(s) to watch for readiness. If `watch$` is empty,
|
||||||
|
* `ready` will never emit.
|
||||||
|
* @returns An observable that emits when the source stream emits. The observable
|
||||||
|
* errors if one of its watches completes before emitting. It also errors if one
|
||||||
|
* of its watches errors.
|
||||||
|
*/
|
||||||
|
export function ready<T>(watch$: Observable<any> | Observable<any>[]) {
|
||||||
|
const watching$ = Array.isArray(watch$) ? watch$ : [watch$];
|
||||||
|
return pipe(
|
||||||
|
connect<T, Observable<T>>((source$) => {
|
||||||
|
// this subscription is safe because `source$` connects only after there
|
||||||
|
// is an external subscriber.
|
||||||
|
const source = new ReplaySubject<T>(1);
|
||||||
|
source$.subscribe(source);
|
||||||
|
|
||||||
|
// `concat` is subscribed immediately after it's returned, at which point
|
||||||
|
// `zip` blocks until all items in `watching$` are ready. If that occurs
|
||||||
|
// after `source$` is hot, then the replay subject sends the last-captured
|
||||||
|
// emission through immediately. Otherwise, `ready` waits for the next
|
||||||
|
// emission
|
||||||
|
return concat(zip(watching$).pipe(first(), ignoreElements()), source).pipe(
|
||||||
|
takeUntil(anyComplete(source)),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an observable that emits the latest value of the source stream
|
||||||
|
* when `watch$` emits. If `watch$` emits before the stream emits, then
|
||||||
|
* an emission occurs as soon as a value becomes ready.
|
||||||
|
* @param watch$ the observable that triggers emissions
|
||||||
|
* @returns An observable that emits when `watch$` emits. The observable
|
||||||
|
* errors if its source stream errors. It also errors if `on` errors. It
|
||||||
|
* completes if its watch completes.
|
||||||
|
*
|
||||||
|
* @remarks This works like `audit`, but it repeats emissions when
|
||||||
|
* watch$ fires.
|
||||||
|
*/
|
||||||
|
export function on<T>(watch$: Observable<any>) {
|
||||||
|
return pipe(
|
||||||
|
connect<T, Observable<T>>((source$) => {
|
||||||
|
const source = new ReplaySubject<T>(1);
|
||||||
|
source$.subscribe(source);
|
||||||
|
|
||||||
|
return watch$
|
||||||
|
.pipe(
|
||||||
|
ready(source),
|
||||||
|
concatMap(() => source.pipe(first())),
|
||||||
|
)
|
||||||
|
.pipe(takeUntil(anyComplete(source)));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<bit-section [formGroup]="sendTextDetailsForm" disableMargin>
|
<bit-section [formGroup]="sendTextDetailsForm" disableMargin>
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "sendTypeTextToShare" | i18n }}</bit-label>
|
<bit-label>{{ "sendTypeTextToShare" | i18n }}</bit-label>
|
||||||
<textarea bitInput id="text" rows="6" formControlName="text"></textarea>
|
<textarea bitInput id="text" rows="3" formControlName="text"></textarea>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<bit-form-control>
|
<bit-form-control>
|
||||||
<input bitCheckbox type="checkbox" formControlName="hidden" />
|
<input bitCheckbox type="checkbox" formControlName="hidden" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NgIf } from "@angular/common";
|
import { NgIf } from "@angular/common";
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||||
@@ -101,6 +103,10 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
*/
|
*/
|
||||||
@Output() cipherSaved = new EventEmitter<CipherView>();
|
@Output() cipherSaved = new EventEmitter<CipherView>();
|
||||||
|
|
||||||
|
private formReadySubject = new Subject<void>();
|
||||||
|
|
||||||
|
@Output() formReady = this.formReadySubject.asObservable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The original cipher being edited or cloned. Null for add mode.
|
* The original cipher being edited or cloned. Null for add mode.
|
||||||
*/
|
*/
|
||||||
@@ -173,9 +179,13 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
|
// Force change detection so that all child components are destroyed and re-created
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
this.updatedCipherView = new CipherView();
|
this.updatedCipherView = new CipherView();
|
||||||
this.originalCipherView = null;
|
this.originalCipherView = null;
|
||||||
this.cipherForm.reset();
|
this.cipherForm = this.formBuilder.group<CipherForm>({});
|
||||||
|
|
||||||
if (this.config == null) {
|
if (this.config == null) {
|
||||||
return;
|
return;
|
||||||
@@ -207,6 +217,7 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.formReadySubject.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -214,6 +225,7 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
|||||||
private addEditFormService: CipherFormService,
|
private addEditFormService: CipherFormService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
import { Component, Input, OnChanges, OnDestroy } from "@angular/core";
|
||||||
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
@@ -44,7 +44,7 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
|
|||||||
AutofillOptionsViewComponent,
|
AutofillOptionsViewComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CipherViewComponent implements OnInit, OnDestroy {
|
export class CipherViewComponent implements OnChanges, OnDestroy {
|
||||||
@Input({ required: true }) cipher: CipherView;
|
@Input({ required: true }) cipher: CipherView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +63,11 @@ export class CipherViewComponent implements OnInit, OnDestroy {
|
|||||||
private folderService: FolderService,
|
private folderService: FolderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnChanges() {
|
||||||
|
if (this.cipher == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.loadCipherData();
|
await this.loadCipherData();
|
||||||
|
|
||||||
this.cardIsExpired = isCardExpired(this.cipher.card);
|
this.cardIsExpired = isCardExpired(this.cipher.card);
|
||||||
|
|||||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -32,7 +32,7 @@
|
|||||||
"@ng-select/ng-select": "11.2.0",
|
"@ng-select/ng-select": "11.2.0",
|
||||||
"argon2": "0.40.1",
|
"argon2": "0.40.1",
|
||||||
"argon2-browser": "1.18.0",
|
"argon2-browser": "1.18.0",
|
||||||
"big-integer": "1.6.51",
|
"big-integer": "1.6.52",
|
||||||
"bootstrap": "4.6.0",
|
"bootstrap": "4.6.0",
|
||||||
"braintree-web-drop-in": "1.43.0",
|
"braintree-web-drop-in": "1.43.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
},
|
},
|
||||||
"apps/browser": {
|
"apps/browser": {
|
||||||
"name": "@bitwarden/browser",
|
"name": "@bitwarden/browser",
|
||||||
"version": "2024.9.2"
|
"version": "2024.10.0"
|
||||||
},
|
},
|
||||||
"apps/cli": {
|
"apps/cli": {
|
||||||
"name": "@bitwarden/cli",
|
"name": "@bitwarden/cli",
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
"@koa/multer": "3.0.2",
|
"@koa/multer": "3.0.2",
|
||||||
"@koa/router": "12.0.1",
|
"@koa/router": "12.0.1",
|
||||||
"argon2": "0.40.1",
|
"argon2": "0.40.1",
|
||||||
"big-integer": "1.6.51",
|
"big-integer": "1.6.52",
|
||||||
"browser-hrtime": "1.1.8",
|
"browser-hrtime": "1.1.8",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"commander": "11.1.0",
|
"commander": "11.1.0",
|
||||||
@@ -242,12 +242,12 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "2.16.2"
|
"@napi-rs/cli": "2.18.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@bitwarden/web-vault",
|
"name": "@bitwarden/web-vault",
|
||||||
"version": "2024.9.2"
|
"version": "2024.10.0"
|
||||||
},
|
},
|
||||||
"libs/admin-console": {
|
"libs/admin-console": {
|
||||||
"name": "@bitwarden/admin-console",
|
"name": "@bitwarden/admin-console",
|
||||||
@@ -7051,9 +7051,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/cli": {
|
"node_modules/@napi-rs/cli": {
|
||||||
"version": "2.16.2",
|
"version": "2.18.4",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.4.tgz",
|
||||||
"integrity": "sha512-U2aZfnr0s9KkXpZlYC0l5WxWCXL7vJUNpCnWMwq3T9GG9rhYAAUM9CTZsi1Z+0iR2LcHbfq9EfMgoqnuTyUjfg==",
|
"integrity": "sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -12852,9 +12852,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/big-integer": {
|
"node_modules/big-integer": {
|
||||||
"version": "1.6.51",
|
"version": "1.6.52",
|
||||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
|
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||||
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
|
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
|
|||||||
@@ -165,7 +165,7 @@
|
|||||||
"@ng-select/ng-select": "11.2.0",
|
"@ng-select/ng-select": "11.2.0",
|
||||||
"argon2": "0.40.1",
|
"argon2": "0.40.1",
|
||||||
"argon2-browser": "1.18.0",
|
"argon2-browser": "1.18.0",
|
||||||
"big-integer": "1.6.51",
|
"big-integer": "1.6.52",
|
||||||
"bootstrap": "4.6.0",
|
"bootstrap": "4.6.0",
|
||||||
"braintree-web-drop-in": "1.43.0",
|
"braintree-web-drop-in": "1.43.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user