mirror of
https://github.com/bitwarden/browser.git
synced 2024-10-04 05:08:06 +02:00
Merge branch 'main' into autofill/pm-6426-create-alarms-manager-and-update-usage-of-long-lived-timeouts-rework
This commit is contained in:
commit
644b1aa104
10
angular.json
10
angular.json
@ -142,7 +142,15 @@
|
|||||||
"configDir": ".storybook",
|
"configDir": ".storybook",
|
||||||
"browserTarget": "components:build",
|
"browserTarget": "components:build",
|
||||||
"compodoc": true,
|
"compodoc": true,
|
||||||
"compodocArgs": ["-p", "./tsconfig.json", "-e", "json", "-d", "."],
|
"compodocArgs": [
|
||||||
|
"-p",
|
||||||
|
"./tsconfig.json",
|
||||||
|
"-e",
|
||||||
|
"json",
|
||||||
|
"-d",
|
||||||
|
".",
|
||||||
|
"--disableRoutesGraph"
|
||||||
|
],
|
||||||
"port": 6006
|
"port": 6006
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -374,12 +374,21 @@
|
|||||||
"other": {
|
"other": {
|
||||||
"message": "Other"
|
"message": "Other"
|
||||||
},
|
},
|
||||||
|
"unlockMethods": {
|
||||||
|
"message": "Unlock options"
|
||||||
|
},
|
||||||
"unlockMethodNeededToChangeTimeoutActionDesc": {
|
"unlockMethodNeededToChangeTimeoutActionDesc": {
|
||||||
"message": "Set up an unlock method to change your vault timeout action."
|
"message": "Set up an unlock method to change your vault timeout action."
|
||||||
},
|
},
|
||||||
"unlockMethodNeeded": {
|
"unlockMethodNeeded": {
|
||||||
"message": "Set up an unlock method in Settings"
|
"message": "Set up an unlock method in Settings"
|
||||||
},
|
},
|
||||||
|
"sessionTimeoutHeader": {
|
||||||
|
"message": "Session timeout"
|
||||||
|
},
|
||||||
|
"otherOptions": {
|
||||||
|
"message": "Other options"
|
||||||
|
},
|
||||||
"rateExtension": {
|
"rateExtension": {
|
||||||
"message": "Rate the extension"
|
"message": "Rate the extension"
|
||||||
},
|
},
|
||||||
@ -3023,6 +3032,12 @@
|
|||||||
"adminConsole": {
|
"adminConsole": {
|
||||||
"message": "Admin Console"
|
"message": "Admin Console"
|
||||||
},
|
},
|
||||||
|
"accountSecurity": {
|
||||||
|
"message": "Account security"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"message": "Appearance"
|
||||||
|
},
|
||||||
"errorAssigningTargetCollection": {
|
"errorAssigningTargetCollection": {
|
||||||
"message": "Error assigning target collection."
|
"message": "Error assigning target collection."
|
||||||
},
|
},
|
||||||
|
@ -143,15 +143,17 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
try {
|
try {
|
||||||
success = await super.unlockBiometric();
|
success = await super.unlockBiometric();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = BiometricErrors[e as BiometricErrorTypes];
|
const error = BiometricErrors[e?.message as BiometricErrorTypes];
|
||||||
|
|
||||||
if (error == null) {
|
if (error == null) {
|
||||||
this.logService.error("Unknown error: " + e);
|
this.logService.error("Unknown error: " + e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.biometricError = this.i18nService.t(error.description);
|
this.biometricError = this.i18nService.t(error.description);
|
||||||
|
} finally {
|
||||||
|
this.pendingBiometric = false;
|
||||||
}
|
}
|
||||||
this.pendingBiometric = false;
|
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
<app-header>
|
||||||
|
<div class="left">
|
||||||
|
<button type="button" routerLink="/tabs/settings">
|
||||||
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ "back" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1 class="center">
|
||||||
|
<span class="title">{{ "accountSecurity" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="right">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
<main tabindex="-1" [formGroup]="form">
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "unlockMethods" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
|
||||||
|
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
|
||||||
|
<input id="biometric" type="checkbox" formControlName="biometric" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="box-content-row box-content-row-checkbox"
|
||||||
|
appBoxRow
|
||||||
|
*ngIf="supportsBiometric && this.form.value.biometric"
|
||||||
|
>
|
||||||
|
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="autoBiometricsPrompt"
|
||||||
|
type="checkbox"
|
||||||
|
(change)="updateAutoBiometricsPrompt()"
|
||||||
|
formControlName="enableAutoBiometricsPrompt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
|
||||||
|
<input id="pin" type="checkbox" formControlName="pin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "sessionTimeoutHeader" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
|
||||||
|
<span *ngIf="policy.timeout && policy.action">
|
||||||
|
{{
|
||||||
|
"vaultTimeoutPolicyWithActionInEffect"
|
||||||
|
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="policy.timeout && !policy.action">
|
||||||
|
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!policy.timeout && policy.action">
|
||||||
|
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
|
||||||
|
</span>
|
||||||
|
</app-callout>
|
||||||
|
<app-vault-timeout-input
|
||||||
|
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||||
|
[formControl]="form.controls.vaultTimeout"
|
||||||
|
ngDefaultControl
|
||||||
|
>
|
||||||
|
</app-vault-timeout-input>
|
||||||
|
<div class="box-content-row display-block" appBoxRow>
|
||||||
|
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
|
||||||
|
<select
|
||||||
|
id="vaultTimeoutAction"
|
||||||
|
name="VaultTimeoutActions"
|
||||||
|
formControlName="vaultTimeoutAction"
|
||||||
|
>
|
||||||
|
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
|
||||||
|
{{ action | i18n }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
|
||||||
|
id="unlockMethodHelp"
|
||||||
|
class="box-footer"
|
||||||
|
>
|
||||||
|
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "otherOptions" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="fingerprint()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="twoStep()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="changePassword()"
|
||||||
|
*ngIf="showChangeMasterPass"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="
|
||||||
|
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
|
||||||
|
"
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="lock()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "lockNow" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="!accountSwitcherEnabled"
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="logOut()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "logOut" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
@ -1,6 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
@ -23,7 +22,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
|||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { DeviceType } from "@bitwarden/common/enums";
|
|
||||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
@ -34,35 +32,20 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
|||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
||||||
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
import { enableAccountSwitching } from "../../platform/flags";
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
import { SetPinComponent } from "../components/set-pin.component";
|
||||||
|
|
||||||
import { AboutComponent } from "./about.component";
|
|
||||||
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||||
|
|
||||||
const RateUrls = {
|
|
||||||
[DeviceType.ChromeExtension]:
|
|
||||||
"https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
|
|
||||||
[DeviceType.FirefoxExtension]:
|
|
||||||
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews",
|
|
||||||
[DeviceType.OperaExtension]:
|
|
||||||
"https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container",
|
|
||||||
[DeviceType.EdgeExtension]:
|
|
||||||
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
|
|
||||||
[DeviceType.VivaldiExtension]:
|
|
||||||
"https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
|
|
||||||
[DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147",
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-settings",
|
selector: "auth-account-security",
|
||||||
templateUrl: "settings.component.html",
|
templateUrl: "account-security.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class SettingsComponent implements OnInit {
|
export class AccountSecurityComponent implements OnInit {
|
||||||
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||||
|
|
||||||
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
|
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
|
||||||
@ -95,7 +78,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
private vaultTimeoutService: VaultTimeoutService,
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
public messagingService: MessagingService,
|
public messagingService: MessagingService,
|
||||||
private router: Router,
|
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
@ -425,23 +407,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async lock() {
|
|
||||||
await this.vaultTimeoutService.lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
async logOut() {
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
|
||||||
title: { key: "logOut" },
|
|
||||||
content: { key: "logOutConfirmation" },
|
|
||||||
type: "info",
|
|
||||||
});
|
|
||||||
|
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
if (confirmed) {
|
|
||||||
this.messagingService.send("logout", { userId: userId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async changePassword() {
|
async changePassword() {
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "continueToWebApp" },
|
title: { key: "continueToWebApp" },
|
||||||
@ -468,44 +433,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async share() {
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
|
||||||
title: { key: "learnOrg" },
|
|
||||||
content: { key: "learnOrgConfirmation" },
|
|
||||||
type: "info",
|
|
||||||
});
|
|
||||||
if (confirmed) {
|
|
||||||
// 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
|
|
||||||
BrowserApi.createNewTab("https://bitwarden.com/help/about-organizations/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async webVault() {
|
|
||||||
const env = await firstValueFrom(this.environmentService.environment$);
|
|
||||||
const url = env.getWebVaultUrl();
|
|
||||||
await BrowserApi.createNewTab(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async import() {
|
|
||||||
await this.router.navigate(["/import"]);
|
|
||||||
if (await BrowserApi.isPopupOpen()) {
|
|
||||||
// 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
|
|
||||||
BrowserPopupUtils.openCurrentPagePopout(window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export() {
|
|
||||||
// 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
|
|
||||||
this.router.navigate(["/export"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
about() {
|
|
||||||
this.dialogService.open(AboutComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fingerprint() {
|
async fingerprint() {
|
||||||
const fingerprint = await this.cryptoService.getFingerprint(
|
const fingerprint = await this.cryptoService.getFingerprint(
|
||||||
await this.stateService.getUserId(),
|
await this.stateService.getUserId(),
|
||||||
@ -518,11 +445,21 @@ export class SettingsComponent implements OnInit {
|
|||||||
return firstValueFrom(dialogRef.closed);
|
return firstValueFrom(dialogRef.closed);
|
||||||
}
|
}
|
||||||
|
|
||||||
rate() {
|
async lock() {
|
||||||
const deviceType = this.platformUtilsService.getDevice();
|
await this.vaultTimeoutService.lock();
|
||||||
// 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
|
|
||||||
BrowserApi.createNewTab((RateUrls as any)[deviceType]);
|
async logOut() {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "logOut" },
|
||||||
|
content: { key: "logOutConfirmation" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
if (confirmed) {
|
||||||
|
this.messagingService.send("logout", { userId: userId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
@ -84,7 +84,6 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar
|
|||||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
@ -249,10 +248,9 @@ export default class MainBackground {
|
|||||||
messagingService: MessageSender;
|
messagingService: MessageSender;
|
||||||
storageService: BrowserLocalStorageService;
|
storageService: BrowserLocalStorageService;
|
||||||
secureStorageService: AbstractStorageService;
|
secureStorageService: AbstractStorageService;
|
||||||
memoryStorageService: AbstractMemoryStorageService;
|
memoryStorageService: AbstractStorageService;
|
||||||
memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService;
|
memoryStorageForStateProviders: AbstractStorageService & ObservableStorageService;
|
||||||
largeObjectMemoryStorageForStateProviders: AbstractMemoryStorageService &
|
largeObjectMemoryStorageForStateProviders: AbstractStorageService & ObservableStorageService;
|
||||||
ObservableStorageService;
|
|
||||||
i18nService: I18nServiceAbstraction;
|
i18nService: I18nServiceAbstraction;
|
||||||
platformUtilsService: PlatformUtilsServiceAbstraction;
|
platformUtilsService: PlatformUtilsServiceAbstraction;
|
||||||
logService: LogServiceAbstraction;
|
logService: LogServiceAbstraction;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
@ -66,9 +65,9 @@ export function sessionStorageServiceFactory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function memoryStorageServiceFactory(
|
export function memoryStorageServiceFactory(
|
||||||
cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices,
|
cache: { memoryStorageService?: AbstractStorageService } & CachedServices,
|
||||||
opts: MemoryStorageServiceInitOptions,
|
opts: MemoryStorageServiceInitOptions,
|
||||||
): Promise<AbstractMemoryStorageService> {
|
): Promise<AbstractStorageService> {
|
||||||
return factory(cache, "memoryStorageService", opts, async () => {
|
return factory(cache, "memoryStorageService", opts, async () => {
|
||||||
if (BrowserApi.isManifestVersion(3)) {
|
if (BrowserApi.isManifestVersion(3)) {
|
||||||
return new LocalBackedSessionStorageService(
|
return new LocalBackedSessionStorageService(
|
||||||
@ -97,10 +96,10 @@ export function memoryStorageServiceFactory(
|
|||||||
|
|
||||||
export function observableMemoryStorageServiceFactory(
|
export function observableMemoryStorageServiceFactory(
|
||||||
cache: {
|
cache: {
|
||||||
memoryStorageService?: AbstractMemoryStorageService & ObservableStorageService;
|
memoryStorageService?: AbstractStorageService & ObservableStorageService;
|
||||||
} & CachedServices,
|
} & CachedServices,
|
||||||
opts: MemoryStorageServiceInitOptions,
|
opts: MemoryStorageServiceInitOptions,
|
||||||
): Promise<AbstractMemoryStorageService & ObservableStorageService> {
|
): Promise<AbstractStorageService & ObservableStorageService> {
|
||||||
return factory(cache, "memoryStorageService", opts, async () => {
|
return factory(cache, "memoryStorageService", opts, async () => {
|
||||||
return new BackgroundMemoryStorageService();
|
return new BackgroundMemoryStorageService();
|
||||||
});
|
});
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
|
||||||
|
|
||||||
import { DefaultBrowserStateService } from "../../services/default-browser-state.service";
|
|
||||||
|
|
||||||
import { browserSession } from "./browser-session.decorator";
|
|
||||||
import { SessionStorable } from "./session-storable";
|
|
||||||
import { sessionSync } from "./session-sync.decorator";
|
|
||||||
|
|
||||||
// browserSession initializes SessionSyncers for each sessionSync decorated property
|
|
||||||
// We don't want to test SessionSyncers, so we'll mock them
|
|
||||||
jest.mock("./session-syncer");
|
|
||||||
|
|
||||||
describe("browserSession decorator", () => {
|
|
||||||
it("should throw if neither StateService nor MemoryStorageService is a constructor argument", () => {
|
|
||||||
@browserSession
|
|
||||||
class TestClass {}
|
|
||||||
expect(() => {
|
|
||||||
new TestClass();
|
|
||||||
}).toThrowError(
|
|
||||||
"Cannot decorate TestClass with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create if StateService is a constructor argument", () => {
|
|
||||||
const stateService = Object.create(DefaultBrowserStateService.prototype, {
|
|
||||||
memoryStorageService: {
|
|
||||||
value: Object.create(MemoryStorageService.prototype, {
|
|
||||||
type: { value: MemoryStorageService.TYPE },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@browserSession
|
|
||||||
class TestClass {
|
|
||||||
constructor(private stateService: DefaultBrowserStateService) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(new TestClass(stateService)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create if MemoryStorageService is a constructor argument", () => {
|
|
||||||
const memoryStorageService = Object.create(MemoryStorageService.prototype, {
|
|
||||||
type: { value: MemoryStorageService.TYPE },
|
|
||||||
});
|
|
||||||
|
|
||||||
@browserSession
|
|
||||||
class TestClass {
|
|
||||||
constructor(private memoryStorageService: AbstractMemoryStorageService) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(new TestClass(memoryStorageService)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("interaction with @sessionSync decorator", () => {
|
|
||||||
let memoryStorageService: MemoryStorageService;
|
|
||||||
|
|
||||||
@browserSession
|
|
||||||
class TestClass {
|
|
||||||
@sessionSync({ initializer: (s: string) => s })
|
|
||||||
private behaviorSubject = new BehaviorSubject("");
|
|
||||||
|
|
||||||
constructor(private memoryStorageService: MemoryStorageService) {}
|
|
||||||
|
|
||||||
fromJSON(json: any) {
|
|
||||||
this.behaviorSubject.next(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
memoryStorageService = Object.create(MemoryStorageService.prototype, {
|
|
||||||
type: { value: MemoryStorageService.TYPE },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a session syncer", () => {
|
|
||||||
const testClass = new TestClass(memoryStorageService) as any as SessionStorable;
|
|
||||||
expect(testClass.__sessionSyncers.length).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should initialize the session syncer", () => {
|
|
||||||
const testClass = new TestClass(memoryStorageService) as any as SessionStorable;
|
|
||||||
expect(testClass.__sessionSyncers[0].init).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,75 +0,0 @@
|
|||||||
import { Constructor } from "type-fest";
|
|
||||||
|
|
||||||
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
|
|
||||||
import { SessionStorable } from "./session-storable";
|
|
||||||
import { SessionSyncer } from "./session-syncer";
|
|
||||||
import { SyncedItemMetadata } from "./sync-item-metadata";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark the class as syncing state across the browser session. This decorator finds rxjs BehaviorSubject properties
|
|
||||||
* marked with @sessionSync and syncs these values across the browser session.
|
|
||||||
*
|
|
||||||
* @param constructor
|
|
||||||
* @returns A new constructor that extends the original one to add session syncing.
|
|
||||||
*/
|
|
||||||
export function browserSession<TCtor extends Constructor<any>>(constructor: TCtor) {
|
|
||||||
return class extends constructor implements SessionStorable {
|
|
||||||
__syncedItemMetadata: SyncedItemMetadata[];
|
|
||||||
__sessionSyncers: SessionSyncer[];
|
|
||||||
|
|
||||||
constructor(...args: any[]) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
// Require state service to be injected
|
|
||||||
const storageService: AbstractMemoryStorageService = this.findStorageService(
|
|
||||||
[this as any].concat(args),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.__syncedItemMetadata == null || !(this.__syncedItemMetadata instanceof Array)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.__sessionSyncers = this.__syncedItemMetadata.map((metadata) =>
|
|
||||||
this.buildSyncer(metadata, storageService),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildSyncer(metadata: SyncedItemMetadata, storageSerice: AbstractMemoryStorageService) {
|
|
||||||
const syncer = new SessionSyncer(
|
|
||||||
(this as any)[metadata.propertyKey],
|
|
||||||
storageSerice,
|
|
||||||
metadata,
|
|
||||||
);
|
|
||||||
// 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
|
|
||||||
syncer.init();
|
|
||||||
return syncer;
|
|
||||||
}
|
|
||||||
|
|
||||||
findStorageService(args: any[]): AbstractMemoryStorageService {
|
|
||||||
const storageService = args.find(this.isMemoryStorageService);
|
|
||||||
|
|
||||||
if (storageService) {
|
|
||||||
return storageService;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateService = args.find(
|
|
||||||
(arg) =>
|
|
||||||
arg?.memoryStorageService != null &&
|
|
||||||
this.isMemoryStorageService(arg.memoryStorageService),
|
|
||||||
);
|
|
||||||
if (stateService) {
|
|
||||||
return stateService.memoryStorageService;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`Cannot decorate ${constructor.name} with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isMemoryStorageService(arg: any): arg is AbstractMemoryStorageService {
|
|
||||||
return arg.type != null && arg.type === AbstractMemoryStorageService.TYPE;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
export { browserSession } from "./browser-session.decorator";
|
|
||||||
export { sessionSync } from "./session-sync.decorator";
|
|
@ -1,7 +0,0 @@
|
|||||||
import { SessionSyncer } from "./session-syncer";
|
|
||||||
import { SyncedItemMetadata } from "./sync-item-metadata";
|
|
||||||
|
|
||||||
export interface SessionStorable {
|
|
||||||
__syncedItemMetadata: SyncedItemMetadata[];
|
|
||||||
__sessionSyncers: SessionSyncer[];
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { sessionSync } from "./session-sync.decorator";
|
|
||||||
|
|
||||||
describe("sessionSync decorator", () => {
|
|
||||||
const initializer = (s: string) => "test";
|
|
||||||
class TestClass {
|
|
||||||
@sessionSync({ initializer: initializer })
|
|
||||||
private testProperty = new BehaviorSubject("");
|
|
||||||
@sessionSync({ initializer: initializer, initializeAs: "array" })
|
|
||||||
private secondTestProperty = new BehaviorSubject("");
|
|
||||||
|
|
||||||
complete() {
|
|
||||||
this.testProperty.complete();
|
|
||||||
this.secondTestProperty.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should add __syncedItemKeys to prototype", () => {
|
|
||||||
const testClass = new TestClass();
|
|
||||||
expect((testClass as any).__syncedItemMetadata).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
propertyKey: "testProperty",
|
|
||||||
sessionKey: "testProperty_0",
|
|
||||||
initializer: initializer,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
propertyKey: "secondTestProperty",
|
|
||||||
sessionKey: "secondTestProperty_1",
|
|
||||||
initializer: initializer,
|
|
||||||
initializeAs: "array",
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
testClass.complete();
|
|
||||||
});
|
|
||||||
|
|
||||||
class TestClass2 {
|
|
||||||
@sessionSync({ initializer: initializer })
|
|
||||||
private testProperty = new BehaviorSubject("");
|
|
||||||
|
|
||||||
complete() {
|
|
||||||
this.testProperty.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should maintain sessionKey index count for other test classes", () => {
|
|
||||||
const testClass = new TestClass2();
|
|
||||||
expect((testClass as any).__syncedItemMetadata).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
propertyKey: "testProperty",
|
|
||||||
sessionKey: "testProperty_2",
|
|
||||||
initializer: initializer,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
testClass.complete();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,54 +0,0 @@
|
|||||||
import { Jsonify } from "type-fest";
|
|
||||||
|
|
||||||
import { SessionStorable } from "./session-storable";
|
|
||||||
import { InitializeOptions } from "./sync-item-metadata";
|
|
||||||
|
|
||||||
class BuildOptions<T, TJson = Jsonify<T>> {
|
|
||||||
initializer?: (keyValuePair: TJson) => T;
|
|
||||||
initializeAs?: InitializeOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to ensure uniqueness for each synced observable
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A decorator used to indicate the BehaviorSubject should be synced for this browser session across all contexts.
|
|
||||||
*
|
|
||||||
* >**Note** This decorator does nothing if the enclosing class is not decorated with @browserSession.
|
|
||||||
*
|
|
||||||
* >**Note** The Behavior subject must be initialized with a default or in the constructor of the class. If it is not, an error will be thrown.
|
|
||||||
*
|
|
||||||
* >**!!Warning!!** If the property is overwritten at any time, the new value will not be synced across the browser session.
|
|
||||||
*
|
|
||||||
* @param buildOptions
|
|
||||||
* Builders for the value, requires either a constructor (ctor) for your BehaviorSubject type or an
|
|
||||||
* initializer function that takes a key value pair representation of the BehaviorSubject data
|
|
||||||
* and returns your instantiated BehaviorSubject value. `initializeAs can optionally be used to indicate
|
|
||||||
* the provided initializer function should be used to build an array of values. For example,
|
|
||||||
* ```ts
|
|
||||||
* \@sessionSync({ initializer: Foo.fromJSON, initializeAs: 'array' })
|
|
||||||
* ```
|
|
||||||
* is equivalent to
|
|
||||||
* ```
|
|
||||||
* \@sessionSync({ initializer: (obj: any[]) => obj.map((f) => Foo.fromJSON })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @returns decorator function
|
|
||||||
*/
|
|
||||||
export function sessionSync<T>(buildOptions: BuildOptions<T>) {
|
|
||||||
return (prototype: unknown, propertyKey: string) => {
|
|
||||||
// Force prototype into SessionStorable and implement it.
|
|
||||||
const p = prototype as SessionStorable;
|
|
||||||
|
|
||||||
if (p.__syncedItemMetadata == null) {
|
|
||||||
p.__syncedItemMetadata = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
p.__syncedItemMetadata.push({
|
|
||||||
propertyKey,
|
|
||||||
sessionKey: `${propertyKey}_${index++}`,
|
|
||||||
initializer: buildOptions.initializer,
|
|
||||||
initializeAs: buildOptions.initializeAs ?? "object",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,301 +0,0 @@
|
|||||||
import { awaitAsync } from "@bitwarden/common/../spec/utils";
|
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
|
||||||
import { BehaviorSubject, ReplaySubject } from "rxjs";
|
|
||||||
|
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
|
||||||
|
|
||||||
import { BrowserApi } from "../../browser/browser-api";
|
|
||||||
|
|
||||||
import { SessionSyncer } from "./session-syncer";
|
|
||||||
import { SyncedItemMetadata } from "./sync-item-metadata";
|
|
||||||
|
|
||||||
describe("session syncer", () => {
|
|
||||||
const propertyKey = "behaviorSubject";
|
|
||||||
const sessionKey = "Test__" + propertyKey;
|
|
||||||
const metaData: SyncedItemMetadata = {
|
|
||||||
propertyKey,
|
|
||||||
sessionKey,
|
|
||||||
initializer: (s: string) => s,
|
|
||||||
initializeAs: "object",
|
|
||||||
};
|
|
||||||
let storageService: MockProxy<MemoryStorageService>;
|
|
||||||
let sut: SessionSyncer;
|
|
||||||
let behaviorSubject: BehaviorSubject<string>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
behaviorSubject = new BehaviorSubject<string>("");
|
|
||||||
jest.spyOn(chrome.runtime, "getManifest").mockReturnValue({
|
|
||||||
name: "bitwarden-test",
|
|
||||||
version: "0.0.0",
|
|
||||||
manifest_version: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
storageService = mock();
|
|
||||||
storageService.has.mockResolvedValue(false);
|
|
||||||
sut = new SessionSyncer(behaviorSubject, storageService, metaData);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
|
|
||||||
behaviorSubject.complete();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("constructor", () => {
|
|
||||||
it("should throw if subject is not an instance of Subject", () => {
|
|
||||||
expect(() => {
|
|
||||||
new SessionSyncer({} as any, storageService, null);
|
|
||||||
}).toThrowError("subject must inherit from Subject");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create if either ctor or initializer is provided", () => {
|
|
||||||
expect(
|
|
||||||
new SessionSyncer(behaviorSubject, storageService, {
|
|
||||||
propertyKey,
|
|
||||||
sessionKey,
|
|
||||||
initializeAs: "object",
|
|
||||||
initializer: () => null,
|
|
||||||
}),
|
|
||||||
).toBeDefined();
|
|
||||||
expect(
|
|
||||||
new SessionSyncer(behaviorSubject, storageService, {
|
|
||||||
propertyKey,
|
|
||||||
sessionKey,
|
|
||||||
initializer: (s: any) => s,
|
|
||||||
initializeAs: "object",
|
|
||||||
}),
|
|
||||||
).toBeDefined();
|
|
||||||
});
|
|
||||||
it("should throw if neither ctor or initializer is provided", () => {
|
|
||||||
expect(() => {
|
|
||||||
new SessionSyncer(behaviorSubject, storageService, {
|
|
||||||
propertyKey,
|
|
||||||
sessionKey,
|
|
||||||
initializeAs: "object",
|
|
||||||
initializer: null,
|
|
||||||
});
|
|
||||||
}).toThrowError("initializer must be provided");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("init", () => {
|
|
||||||
it("should ignore all updates currently in a ReplaySubject's buffer", () => {
|
|
||||||
const replaySubject = new ReplaySubject<string>(Infinity);
|
|
||||||
replaySubject.next("1");
|
|
||||||
replaySubject.next("2");
|
|
||||||
replaySubject.next("3");
|
|
||||||
sut = new SessionSyncer(replaySubject, storageService, metaData);
|
|
||||||
// block observing the subject
|
|
||||||
jest.spyOn(sut as any, "observe").mockImplementation();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
sut.init();
|
|
||||||
|
|
||||||
expect(sut["ignoreNUpdates"]).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should ignore BehaviorSubject's initial value", () => {
|
|
||||||
const behaviorSubject = new BehaviorSubject<string>("initial");
|
|
||||||
sut = new SessionSyncer(behaviorSubject, storageService, metaData);
|
|
||||||
// block observing the subject
|
|
||||||
jest.spyOn(sut as any, "observe").mockImplementation();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
sut.init();
|
|
||||||
|
|
||||||
expect(sut["ignoreNUpdates"]).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should grab an initial value from storage if it exists", async () => {
|
|
||||||
storageService.has.mockResolvedValue(true);
|
|
||||||
//Block a call to update
|
|
||||||
const updateSpy = jest.spyOn(sut as any, "updateFromMemory").mockImplementation();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
sut.init();
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(updateSpy).toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not grab an initial value from storage if it does not exist", async () => {
|
|
||||||
storageService.has.mockResolvedValue(false);
|
|
||||||
//Block a call to update
|
|
||||||
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
sut.init();
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(updateSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("a value is emitted on the observable", () => {
|
|
||||||
let sendMessageSpy: jest.SpyInstance;
|
|
||||||
const value = "test";
|
|
||||||
const serializedValue = JSON.stringify(value);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
|
|
||||||
|
|
||||||
// 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
|
|
||||||
sut.init();
|
|
||||||
|
|
||||||
behaviorSubject.next(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update sessionSyncers in other contexts", async () => {
|
|
||||||
// await finishing of fire-and-forget operation
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledWith(`${sessionKey}_update`, {
|
|
||||||
id: sut.id,
|
|
||||||
serializedValue,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("A message is received", () => {
|
|
||||||
let nextSpy: jest.SpyInstance;
|
|
||||||
let sendMessageSpy: jest.SpyInstance;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
nextSpy = jest.spyOn(behaviorSubject, "next");
|
|
||||||
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
|
|
||||||
|
|
||||||
// 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
|
|
||||||
sut.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should ignore messages with the wrong command", async () => {
|
|
||||||
await sut.updateFromMessage({ command: "wrong_command", id: sut.id });
|
|
||||||
|
|
||||||
expect(storageService.getBypassCache).not.toHaveBeenCalled();
|
|
||||||
expect(nextSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should ignore messages from itself", async () => {
|
|
||||||
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id });
|
|
||||||
|
|
||||||
expect(storageService.getBypassCache).not.toHaveBeenCalled();
|
|
||||||
expect(nextSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update from message on emit from another instance", async () => {
|
|
||||||
const builder = jest.fn();
|
|
||||||
jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder);
|
|
||||||
const value = "test";
|
|
||||||
const serializedValue = JSON.stringify(value);
|
|
||||||
builder.mockReturnValue(value);
|
|
||||||
|
|
||||||
// Expect no circular messaging
|
|
||||||
await awaitAsync();
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
await sut.updateFromMessage({
|
|
||||||
command: `${sessionKey}_update`,
|
|
||||||
id: "different_id",
|
|
||||||
serializedValue,
|
|
||||||
});
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(storageService.getBypassCache).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
expect(nextSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(nextSpy).toHaveBeenCalledWith(value);
|
|
||||||
expect(behaviorSubject.value).toBe(value);
|
|
||||||
|
|
||||||
// Expect no circular messaging
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("memory storage", () => {
|
|
||||||
const value = "test";
|
|
||||||
const serializedValue = JSON.stringify(value);
|
|
||||||
let saveSpy: jest.SpyInstance;
|
|
||||||
const builder = jest.fn().mockReturnValue(value);
|
|
||||||
const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
|
||||||
const isBackgroundPageSpy = jest.spyOn(BrowserApi, "isBackgroundPage");
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder);
|
|
||||||
saveSpy = jest.spyOn(storageService, "save");
|
|
||||||
|
|
||||||
// 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
|
|
||||||
sut.init();
|
|
||||||
await awaitAsync();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should always store on observed next for manifest version 3", async () => {
|
|
||||||
manifestVersionSpy.mockReturnValue(3);
|
|
||||||
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
||||||
behaviorSubject.next(value);
|
|
||||||
await awaitAsync();
|
|
||||||
behaviorSubject.next(value);
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(saveSpy).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not store on message receive for manifest version 3", async () => {
|
|
||||||
manifestVersionSpy.mockReturnValue(3);
|
|
||||||
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
||||||
await sut.updateFromMessage({
|
|
||||||
command: `${sessionKey}_update`,
|
|
||||||
id: "different_id",
|
|
||||||
serializedValue,
|
|
||||||
});
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(saveSpy).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should store on message receive for manifest version 2 for background page only", async () => {
|
|
||||||
manifestVersionSpy.mockReturnValue(2);
|
|
||||||
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
||||||
await sut.updateFromMessage({
|
|
||||||
command: `${sessionKey}_update`,
|
|
||||||
id: "different_id",
|
|
||||||
serializedValue,
|
|
||||||
});
|
|
||||||
await awaitAsync();
|
|
||||||
await sut.updateFromMessage({
|
|
||||||
command: `${sessionKey}_update`,
|
|
||||||
id: "different_id",
|
|
||||||
serializedValue,
|
|
||||||
});
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(saveSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should store on observed next for manifest version 2 for background page only", async () => {
|
|
||||||
manifestVersionSpy.mockReturnValue(2);
|
|
||||||
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
||||||
behaviorSubject.next(value);
|
|
||||||
await awaitAsync();
|
|
||||||
behaviorSubject.next(value);
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(saveSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,125 +0,0 @@
|
|||||||
import { BehaviorSubject, concatMap, ReplaySubject, skip, Subject, Subscription } from "rxjs";
|
|
||||||
|
|
||||||
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
||||||
|
|
||||||
import { BrowserApi } from "../../browser/browser-api";
|
|
||||||
|
|
||||||
import { SyncedItemMetadata } from "./sync-item-metadata";
|
|
||||||
|
|
||||||
export class SessionSyncer {
|
|
||||||
subscription: Subscription;
|
|
||||||
id = Utils.newGuid();
|
|
||||||
|
|
||||||
// ignore initial values
|
|
||||||
private ignoreNUpdates = 0;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private subject: Subject<any>,
|
|
||||||
private memoryStorageService: AbstractMemoryStorageService,
|
|
||||||
private metaData: SyncedItemMetadata,
|
|
||||||
) {
|
|
||||||
if (!(subject instanceof Subject)) {
|
|
||||||
throw new Error("subject must inherit from Subject");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metaData.initializer == null) {
|
|
||||||
throw new Error("initializer must be provided");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
switch (this.subject.constructor) {
|
|
||||||
case ReplaySubject:
|
|
||||||
// ignore all updates currently in the buffer
|
|
||||||
this.ignoreNUpdates = (this.subject as any)._buffer.length;
|
|
||||||
break;
|
|
||||||
case BehaviorSubject:
|
|
||||||
this.ignoreNUpdates = 1;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.observe();
|
|
||||||
// must be synchronous
|
|
||||||
const hasInSessionMemory = await this.memoryStorageService.has(this.metaData.sessionKey);
|
|
||||||
if (hasInSessionMemory) {
|
|
||||||
await this.updateFromMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listenForUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async observe() {
|
|
||||||
const stream = this.subject.pipe(skip(this.ignoreNUpdates));
|
|
||||||
this.ignoreNUpdates = 0;
|
|
||||||
|
|
||||||
// This may be a memory leak.
|
|
||||||
// There is no good time to unsubscribe from this observable. Hopefully Manifest V3 clears memory from temporary
|
|
||||||
// contexts. If so, this is handled by destruction of the context.
|
|
||||||
this.subscription = stream
|
|
||||||
.pipe(
|
|
||||||
concatMap(async (next) => {
|
|
||||||
if (this.ignoreNUpdates > 0) {
|
|
||||||
this.ignoreNUpdates -= 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.updateSession(next);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
private listenForUpdates() {
|
|
||||||
// This is an unawaited promise, but it will be executed asynchronously in the background.
|
|
||||||
BrowserApi.messageListener(this.updateMessageCommand, (message) => {
|
|
||||||
// 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
|
|
||||||
this.updateFromMessage(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateFromMessage(message: any) {
|
|
||||||
if (message.command != this.updateMessageCommand || message.id === this.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.update(message.serializedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateFromMemory() {
|
|
||||||
const value = await this.memoryStorageService.getBypassCache(this.metaData.sessionKey);
|
|
||||||
await this.update(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(serializedValue: any) {
|
|
||||||
if (!serializedValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unBuiltValue = JSON.parse(serializedValue);
|
|
||||||
if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) {
|
|
||||||
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
|
|
||||||
}
|
|
||||||
const builder = SyncedItemMetadata.builder(this.metaData);
|
|
||||||
const value = builder(unBuiltValue);
|
|
||||||
this.ignoreNUpdates = 1;
|
|
||||||
this.subject.next(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateSession(value: any) {
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializedValue = JSON.stringify(value);
|
|
||||||
if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) {
|
|
||||||
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
|
|
||||||
}
|
|
||||||
await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id, serializedValue });
|
|
||||||
}
|
|
||||||
|
|
||||||
private get updateMessageCommand() {
|
|
||||||
return `${this.metaData.sessionKey}_update`;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
export type InitializeOptions = "array" | "record" | "object";
|
|
||||||
|
|
||||||
export class SyncedItemMetadata {
|
|
||||||
propertyKey: string;
|
|
||||||
sessionKey: string;
|
|
||||||
initializer: (keyValuePair: any) => any;
|
|
||||||
initializeAs: InitializeOptions;
|
|
||||||
|
|
||||||
static builder(metadata: SyncedItemMetadata): (o: any) => any {
|
|
||||||
const itemBuilder = metadata.initializer;
|
|
||||||
if (metadata.initializeAs === "array") {
|
|
||||||
return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o));
|
|
||||||
} else if (metadata.initializeAs === "record") {
|
|
||||||
return (keyValuePair: any) => {
|
|
||||||
const record: Record<any, any> = {};
|
|
||||||
for (const key in keyValuePair) {
|
|
||||||
record[key] = itemBuilder(keyValuePair[key]);
|
|
||||||
}
|
|
||||||
return record;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return (keyValuePair: any) => itemBuilder(keyValuePair);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
import { SyncedItemMetadata } from "./sync-item-metadata";
|
|
||||||
|
|
||||||
describe("builder", () => {
|
|
||||||
const propertyKey = "propertyKey";
|
|
||||||
const key = "key";
|
|
||||||
const initializer = (s: any) => "used initializer";
|
|
||||||
|
|
||||||
it("should use initializer", () => {
|
|
||||||
const metadata: SyncedItemMetadata = {
|
|
||||||
propertyKey,
|
|
||||||
sessionKey: key,
|
|
||||||
initializer,
|
|
||||||
initializeAs: "object",
|
|
||||||
};
|
|
||||||
const builder = SyncedItemMetadata.builder(metadata);
|
|
||||||
expect(builder({})).toBe("used initializer");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should honor initialize as array", () => {
|
|
||||||
const metadata: SyncedItemMetadata = {
|
|
||||||
propertyKey,
|
|
||||||
sessionKey: key,
|
|
||||||
initializer: initializer,
|
|
||||||
initializeAs: "array",
|
|
||||||
};
|
|
||||||
const builder = SyncedItemMetadata.builder(metadata);
|
|
||||||
expect(builder([{}])).toBeInstanceOf(Array);
|
|
||||||
expect(builder([{}])[0]).toBe("used initializer");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should honor initialize as record", () => {
|
|
||||||
const metadata: SyncedItemMetadata = {
|
|
||||||
propertyKey,
|
|
||||||
sessionKey: key,
|
|
||||||
initializer: initializer,
|
|
||||||
initializeAs: "record",
|
|
||||||
};
|
|
||||||
const builder = SyncedItemMetadata.builder(metadata);
|
|
||||||
expect(builder({ key: "" })).toBeInstanceOf(Object);
|
|
||||||
expect(builder({ key: "" })).toStrictEqual({ key: "used initializer" });
|
|
||||||
});
|
|
||||||
});
|
|
@ -315,13 +315,13 @@ export default {
|
|||||||
importProvidersFrom(
|
importProvidersFrom(
|
||||||
RouterModule.forRoot(
|
RouterModule.forRoot(
|
||||||
[
|
[
|
||||||
{ path: "", redirectTo: "vault", pathMatch: "full" },
|
{ path: "", redirectTo: "tabs/vault", pathMatch: "full" },
|
||||||
{ path: "vault", component: MockVaultPageComponent },
|
{ path: "tabs/vault", component: MockVaultPageComponent },
|
||||||
{ path: "generator", component: MockGeneratorPageComponent },
|
{ path: "tabs/generator", component: MockGeneratorPageComponent },
|
||||||
{ path: "send", component: MockSendPageComponent },
|
{ path: "tabs/send", component: MockSendPageComponent },
|
||||||
{ path: "settings", component: MockSettingsPageComponent },
|
{ path: "tabs/settings", component: MockSettingsPageComponent },
|
||||||
// in case you are coming from a story that also uses the router
|
// in case you are coming from a story that also uses the router
|
||||||
{ path: "**", redirectTo: "vault" },
|
{ path: "**", redirectTo: "tabs/vault" },
|
||||||
],
|
],
|
||||||
{ useHash: true },
|
{ useHash: true },
|
||||||
),
|
),
|
||||||
|
@ -1,16 +1,7 @@
|
|||||||
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
|
|
||||||
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
|
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
|
||||||
|
|
||||||
export default class BrowserMemoryStorageService
|
export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
|
||||||
extends AbstractChromeStorageService
|
|
||||||
implements AbstractMemoryStorageService
|
|
||||||
{
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(chrome.storage.session);
|
super(chrome.storage.session);
|
||||||
}
|
}
|
||||||
type = "MemoryStorageService" as const;
|
|
||||||
getBypassCache<T>(key: string): Promise<T> {
|
|
||||||
return this.get(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { State } from "@bitwarden/common/platform/models/domain/state";
|
import { State } from "@bitwarden/common/platform/models/domain/state";
|
||||||
@ -18,9 +15,6 @@ import { Account } from "../../models/account";
|
|||||||
|
|
||||||
import { DefaultBrowserStateService } from "./default-browser-state.service";
|
import { DefaultBrowserStateService } from "./default-browser-state.service";
|
||||||
|
|
||||||
// disable session syncing to just test class
|
|
||||||
jest.mock("../decorators/session-sync-observable/");
|
|
||||||
|
|
||||||
describe("Browser State Service", () => {
|
describe("Browser State Service", () => {
|
||||||
let secureStorageService: MockProxy<AbstractStorageService>;
|
let secureStorageService: MockProxy<AbstractStorageService>;
|
||||||
let diskStorageService: MockProxy<AbstractStorageService>;
|
let diskStorageService: MockProxy<AbstractStorageService>;
|
||||||
@ -56,7 +50,7 @@ describe("Browser State Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("state methods", () => {
|
describe("state methods", () => {
|
||||||
let memoryStorageService: MockProxy<AbstractMemoryStorageService>;
|
let memoryStorageService: MockProxy<AbstractStorageService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
memoryStorageService = mock();
|
memoryStorageService = mock();
|
||||||
|
@ -2,10 +2,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
AbstractStorageService,
|
|
||||||
AbstractMemoryStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
@ -25,7 +22,7 @@ export class DefaultBrowserStateService
|
|||||||
constructor(
|
constructor(
|
||||||
storageService: AbstractStorageService,
|
storageService: AbstractStorageService,
|
||||||
secureStorageService: AbstractStorageService,
|
secureStorageService: AbstractStorageService,
|
||||||
memoryStorageService: AbstractMemoryStorageService,
|
memoryStorageService: AbstractStorageService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
stateFactory: StateFactory<GlobalState, Account>,
|
stateFactory: StateFactory<GlobalState, Account>,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
@ -59,24 +59,12 @@ describe("LocalBackedSessionStorage", () => {
|
|||||||
await sut.get("test");
|
await sut.get("test");
|
||||||
expect(sut["cache"]["test"]).toEqual("decrypted");
|
expect(sut["cache"]["test"]).toEqual("decrypted");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("getBypassCache", () => {
|
|
||||||
it("ignores cached values", async () => {
|
|
||||||
sut["cache"]["test"] = "cached";
|
|
||||||
const encrypted = makeEncString("encrypted");
|
|
||||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
|
||||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
|
||||||
const result = await sut.getBypassCache("test");
|
|
||||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
|
|
||||||
expect(result).toEqual("decrypted");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a decrypted value when one is stored in local storage", async () => {
|
it("returns a decrypted value when one is stored in local storage", async () => {
|
||||||
const encrypted = makeEncString("encrypted");
|
const encrypted = makeEncString("encrypted");
|
||||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||||
const result = await sut.getBypassCache("test");
|
const result = await sut.get("test");
|
||||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
|
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
|
||||||
expect(result).toEqual("decrypted");
|
expect(result).toEqual("decrypted");
|
||||||
});
|
});
|
||||||
@ -85,19 +73,9 @@ describe("LocalBackedSessionStorage", () => {
|
|||||||
const encrypted = makeEncString("encrypted");
|
const encrypted = makeEncString("encrypted");
|
||||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||||
await sut.getBypassCache("test");
|
await sut.get("test");
|
||||||
expect(sut["cache"]["test"]).toEqual("decrypted");
|
expect(sut["cache"]["test"]).toEqual("decrypted");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deserializes when a deserializer is provided", async () => {
|
|
||||||
const encrypted = makeEncString("encrypted");
|
|
||||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
|
||||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
|
||||||
const deserializer = jest.fn().mockReturnValue("deserialized");
|
|
||||||
const result = await sut.getBypassCache("test", { deserializer });
|
|
||||||
expect(deserializer).toHaveBeenCalledWith("decrypted");
|
|
||||||
expect(result).toEqual("deserialized");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("has", () => {
|
describe("has", () => {
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
|
||||||
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
StorageUpdate,
|
StorageUpdate,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
|
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
@ -20,7 +18,7 @@ import { MemoryStoragePortMessage } from "../storage/port-messages";
|
|||||||
import { portName } from "../storage/port-name";
|
import { portName } from "../storage/port-name";
|
||||||
|
|
||||||
export class LocalBackedSessionStorageService
|
export class LocalBackedSessionStorageService
|
||||||
extends AbstractMemoryStorageService
|
extends AbstractStorageService
|
||||||
implements ObservableStorageService
|
implements ObservableStorageService
|
||||||
{
|
{
|
||||||
private ports: Set<chrome.runtime.Port> = new Set([]);
|
private ports: Set<chrome.runtime.Port> = new Set([]);
|
||||||
@ -65,20 +63,12 @@ export class LocalBackedSessionStorageService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
|
async get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||||
if (this.cache[key] !== undefined) {
|
if (this.cache[key] !== undefined) {
|
||||||
return this.cache[key] as T;
|
return this.cache[key] as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.getBypassCache(key, options);
|
const value = await this.getLocalSessionValue(await this.sessionKey.get(), key);
|
||||||
}
|
|
||||||
|
|
||||||
async getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
|
|
||||||
let value = await this.getLocalSessionValue(await this.sessionKey.get(), key);
|
|
||||||
|
|
||||||
if (options?.deserializer != null) {
|
|
||||||
value = options.deserializer(value as Jsonify<T>);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cache[key] = value;
|
this.cache[key] = value;
|
||||||
return value as T;
|
return value as T;
|
||||||
@ -159,7 +149,6 @@ export class LocalBackedSessionStorageService
|
|||||||
|
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
case "get":
|
case "get":
|
||||||
case "getBypassCache":
|
|
||||||
case "has": {
|
case "has": {
|
||||||
result = await this[message.action](message.key);
|
result = await this[message.action](message.key);
|
||||||
break;
|
break;
|
||||||
|
@ -51,7 +51,6 @@ export class BackgroundMemoryStorageService extends MemoryStorageService {
|
|||||||
|
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
case "get":
|
case "get":
|
||||||
case "getBypassCache":
|
|
||||||
case "has": {
|
case "has": {
|
||||||
result = await this[message.action](message.key);
|
result = await this[message.action](message.key);
|
||||||
break;
|
break;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Observable, Subject, filter, firstValueFrom, map } from "rxjs";
|
import { Observable, Subject, filter, firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
AbstractStorageService,
|
||||||
StorageUpdate,
|
StorageUpdate,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@ -11,7 +11,7 @@ import { fromChromeEvent } from "../browser/from-chrome-event";
|
|||||||
import { MemoryStoragePortMessage } from "./port-messages";
|
import { MemoryStoragePortMessage } from "./port-messages";
|
||||||
import { portName } from "./port-name";
|
import { portName } from "./port-name";
|
||||||
|
|
||||||
export class ForegroundMemoryStorageService extends AbstractMemoryStorageService {
|
export class ForegroundMemoryStorageService extends AbstractStorageService {
|
||||||
private _port: chrome.runtime.Port;
|
private _port: chrome.runtime.Port;
|
||||||
private _backgroundResponses$: Observable<MemoryStoragePortMessage>;
|
private _backgroundResponses$: Observable<MemoryStoragePortMessage>;
|
||||||
private updatesSubject = new Subject<StorageUpdate>();
|
private updatesSubject = new Subject<StorageUpdate>();
|
||||||
@ -59,9 +59,6 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService
|
|||||||
async get<T>(key: string): Promise<T> {
|
async get<T>(key: string): Promise<T> {
|
||||||
return await this.delegateToBackground<T>("get", key);
|
return await this.delegateToBackground<T>("get", key);
|
||||||
}
|
}
|
||||||
async getBypassCache<T>(key: string): Promise<T> {
|
|
||||||
return await this.delegateToBackground<T>("getBypassCache", key);
|
|
||||||
}
|
|
||||||
async has(key: string): Promise<boolean> {
|
async has(key: string): Promise<boolean> {
|
||||||
return await this.delegateToBackground<boolean>("has", key);
|
return await this.delegateToBackground<boolean>("has", key);
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,9 @@ describe("foreground background memory storage interaction", () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each(["has", "get", "getBypassCache"])(
|
test.each(["has", "get"])(
|
||||||
"background should respond with the correct value for %s",
|
"background should respond with the correct value for %s",
|
||||||
async (action: "get" | "has" | "getBypassCache") => {
|
async (action: "get" | "has") => {
|
||||||
const key = "key";
|
const key = "key";
|
||||||
const value = "value";
|
const value = "value";
|
||||||
background[action] = jest.fn().mockResolvedValue(value);
|
background[action] = jest.fn().mockResolvedValue(value);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
AbstractStorageService,
|
||||||
StorageUpdate,
|
StorageUpdate,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ type MemoryStoragePortMessage = {
|
|||||||
data: string | string[] | StorageUpdate;
|
data: string | string[] | StorageUpdate;
|
||||||
originator: "foreground" | "background";
|
originator: "foreground" | "background";
|
||||||
action?:
|
action?:
|
||||||
| keyof Pick<AbstractMemoryStorageService, "get" | "getBypassCache" | "has" | "save" | "remove">
|
| keyof Pick<AbstractStorageService, "get" | "has" | "save" | "remove">
|
||||||
| "subject_update"
|
| "subject_update"
|
||||||
| "initialization";
|
| "initialization";
|
||||||
};
|
};
|
||||||
|
@ -174,20 +174,27 @@ export const routerTransition = trigger("routerTransition", [
|
|||||||
transition("clone-cipher => attachments, clone-cipher => collections", inSlideLeft),
|
transition("clone-cipher => attachments, clone-cipher => collections", inSlideLeft),
|
||||||
transition("attachments => clone-cipher, collections => clone-cipher", outSlideRight),
|
transition("attachments => clone-cipher, collections => clone-cipher", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => import", inSlideLeft),
|
transition("tabs => account-security", inSlideLeft),
|
||||||
transition("import => tabs", outSlideRight),
|
transition("account-security => tabs", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => export", inSlideLeft),
|
// Vault settings
|
||||||
transition("export => tabs", outSlideRight),
|
transition("tabs => vault-settings", inSlideLeft),
|
||||||
|
transition("vault-settings => tabs", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => folders", inSlideLeft),
|
transition("vault-settings => import", inSlideLeft),
|
||||||
transition("folders => tabs", outSlideRight),
|
transition("import => vault-settings", outSlideRight),
|
||||||
|
|
||||||
|
transition("vault-settings => export", inSlideLeft),
|
||||||
|
transition("export => vault-settings", outSlideRight),
|
||||||
|
|
||||||
|
transition("vault-settings => folders", inSlideLeft),
|
||||||
|
transition("folders => vault-settings", outSlideRight),
|
||||||
|
|
||||||
transition("folders => edit-folder, folders => add-folder", inSlideUp),
|
transition("folders => edit-folder, folders => add-folder", inSlideUp),
|
||||||
transition("edit-folder => folders, add-folder => folders", outSlideDown),
|
transition("edit-folder => folders, add-folder => folders", outSlideDown),
|
||||||
|
|
||||||
transition("tabs => sync", inSlideLeft),
|
transition("vault-settings => sync", inSlideLeft),
|
||||||
transition("sync => tabs", outSlideRight),
|
transition("sync => vault-settings", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => excluded-domains", inSlideLeft),
|
transition("tabs => excluded-domains", inSlideLeft),
|
||||||
transition("excluded-domains => tabs", outSlideRight),
|
transition("excluded-domains => tabs", outSlideRight),
|
||||||
@ -195,6 +202,10 @@ export const routerTransition = trigger("routerTransition", [
|
|||||||
transition("tabs => options", inSlideLeft),
|
transition("tabs => options", inSlideLeft),
|
||||||
transition("options => tabs", outSlideRight),
|
transition("options => tabs", outSlideRight),
|
||||||
|
|
||||||
|
// Appearance settings
|
||||||
|
transition("tabs => appearance", inSlideLeft),
|
||||||
|
transition("appearance => tabs", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => premium", inSlideLeft),
|
transition("tabs => premium", inSlideLeft),
|
||||||
transition("premium => tabs", outSlideRight),
|
transition("premium => tabs", outSlideRight),
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import { LoginComponent } from "../auth/popup/login.component";
|
|||||||
import { RegisterComponent } from "../auth/popup/register.component";
|
import { RegisterComponent } from "../auth/popup/register.component";
|
||||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||||
|
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||||
import { SsoComponent } from "../auth/popup/sso.component";
|
import { SsoComponent } from "../auth/popup/sso.component";
|
||||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
@ -35,6 +36,7 @@ import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.compo
|
|||||||
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
||||||
import { ExportComponent } from "../tools/popup/settings/export.component";
|
import { ExportComponent } from "../tools/popup/settings/export.component";
|
||||||
import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component";
|
import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component";
|
||||||
|
import { SettingsComponent } from "../tools/popup/settings/settings.component";
|
||||||
import { Fido2Component } from "../vault/popup/components/fido2/fido2.component";
|
import { Fido2Component } from "../vault/popup/components/fido2/fido2.component";
|
||||||
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
||||||
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
|
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
|
||||||
@ -44,17 +46,19 @@ import { PasswordHistoryComponent } from "../vault/popup/components/vault/passwo
|
|||||||
import { ShareComponent } from "../vault/popup/components/vault/share.component";
|
import { ShareComponent } from "../vault/popup/components/vault/share.component";
|
||||||
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
||||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||||
|
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
|
||||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||||
|
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||||
|
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||||
|
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||||
|
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
||||||
|
|
||||||
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
|
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
|
||||||
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
||||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
||||||
import { FoldersComponent } from "./settings/folders.component";
|
|
||||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||||
import { OptionsComponent } from "./settings/options.component";
|
import { OptionsComponent } from "./settings/options.component";
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
|
||||||
import { SyncComponent } from "./settings/sync.component";
|
|
||||||
import { TabsV2Component } from "./tabs-v2.component";
|
import { TabsV2Component } from "./tabs-v2.component";
|
||||||
import { TabsComponent } from "./tabs.component";
|
import { TabsComponent } from "./tabs.component";
|
||||||
|
|
||||||
@ -246,6 +250,18 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "autofill" },
|
data: { state: "autofill" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "account-security",
|
||||||
|
component: AccountSecurityComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: { state: "account-security" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "vault-settings",
|
||||||
|
component: VaultSettingsComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: { state: "vault-settings" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "folders",
|
path: "folders",
|
||||||
component: FoldersComponent,
|
component: FoldersComponent,
|
||||||
@ -288,6 +304,12 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "options" },
|
data: { state: "options" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "appearance",
|
||||||
|
component: AppearanceComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: { state: "appearance" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "clone-cipher",
|
path: "clone-cipher",
|
||||||
component: AddEditComponent,
|
component: AddEditComponent,
|
||||||
@ -341,12 +363,11 @@ const routes: Routes = [
|
|||||||
data: { state: "tabs_current" },
|
data: { state: "tabs_current" },
|
||||||
runGuardsAndResolvers: "always",
|
runGuardsAndResolvers: "always",
|
||||||
},
|
},
|
||||||
{
|
...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, {
|
||||||
path: "vault",
|
path: "vault",
|
||||||
component: VaultFilterComponent,
|
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "tabs_vault" },
|
data: { state: "tabs_vault" },
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
path: "generator",
|
path: "generator",
|
||||||
component: GeneratorComponent,
|
component: GeneratorComponent,
|
||||||
|
@ -30,6 +30,8 @@ import { LoginComponent } from "../auth/popup/login.component";
|
|||||||
import { RegisterComponent } from "../auth/popup/register.component";
|
import { RegisterComponent } from "../auth/popup/register.component";
|
||||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||||
|
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||||
|
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
|
||||||
import { SsoComponent } from "../auth/popup/sso.component";
|
import { SsoComponent } from "../auth/popup/sso.component";
|
||||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
@ -49,6 +51,7 @@ import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.componen
|
|||||||
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
|
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
|
||||||
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
||||||
import { ExportComponent } from "../tools/popup/settings/export.component";
|
import { ExportComponent } from "../tools/popup/settings/export.component";
|
||||||
|
import { SettingsComponent } from "../tools/popup/settings/settings.component";
|
||||||
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
|
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
|
||||||
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
|
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
|
||||||
import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component";
|
import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component";
|
||||||
@ -64,9 +67,14 @@ import { ShareComponent } from "../vault/popup/components/vault/share.component"
|
|||||||
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
||||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||||
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
|
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
|
||||||
|
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
|
||||||
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
|
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
|
||||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||||
|
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||||
|
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||||
|
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||||
|
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
||||||
|
|
||||||
import { AppRoutingModule } from "./app-routing.module";
|
import { AppRoutingModule } from "./app-routing.module";
|
||||||
import { AppComponent } from "./app.component";
|
import { AppComponent } from "./app.component";
|
||||||
@ -74,12 +82,8 @@ import { PopOutComponent } from "./components/pop-out.component";
|
|||||||
import { UserVerificationComponent } from "./components/user-verification.component";
|
import { UserVerificationComponent } from "./components/user-verification.component";
|
||||||
import { ServicesModule } from "./services/services.module";
|
import { ServicesModule } from "./services/services.module";
|
||||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
||||||
import { FoldersComponent } from "./settings/folders.component";
|
|
||||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||||
import { OptionsComponent } from "./settings/options.component";
|
import { OptionsComponent } from "./settings/options.component";
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
|
||||||
import { SyncComponent } from "./settings/sync.component";
|
|
||||||
import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component";
|
|
||||||
import { TabsV2Component } from "./tabs-v2.component";
|
import { TabsV2Component } from "./tabs-v2.component";
|
||||||
import { TabsComponent } from "./tabs.component";
|
import { TabsComponent } from "./tabs.component";
|
||||||
|
|
||||||
@ -145,6 +149,7 @@ import "../platform/popup/locales";
|
|||||||
LoginViaAuthRequestComponent,
|
LoginViaAuthRequestComponent,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponent,
|
||||||
OptionsComponent,
|
OptionsComponent,
|
||||||
|
AppearanceComponent,
|
||||||
GeneratorComponent,
|
GeneratorComponent,
|
||||||
PasswordGeneratorHistoryComponent,
|
PasswordGeneratorHistoryComponent,
|
||||||
PasswordHistoryComponent,
|
PasswordHistoryComponent,
|
||||||
@ -156,7 +161,9 @@ import "../platform/popup/locales";
|
|||||||
SendListComponent,
|
SendListComponent,
|
||||||
SendTypeComponent,
|
SendTypeComponent,
|
||||||
SetPasswordComponent,
|
SetPasswordComponent,
|
||||||
|
AccountSecurityComponent,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
|
VaultSettingsComponent,
|
||||||
ShareComponent,
|
ShareComponent,
|
||||||
SsoComponent,
|
SsoComponent,
|
||||||
SyncComponent,
|
SyncComponent,
|
||||||
@ -177,6 +184,7 @@ import "../platform/popup/locales";
|
|||||||
EnvironmentSelectorComponent,
|
EnvironmentSelectorComponent,
|
||||||
CurrentAccountComponent,
|
CurrentAccountComponent,
|
||||||
AccountSwitcherComponent,
|
AccountSwitcherComponent,
|
||||||
|
VaultV2Component,
|
||||||
],
|
],
|
||||||
providers: [CurrencyPipe, DatePipe],
|
providers: [CurrencyPipe, DatePipe],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
@ -424,6 +424,10 @@ img,
|
|||||||
.modal-title,
|
.modal-title,
|
||||||
.overlay-container {
|
.overlay-container {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
&.user-select {
|
||||||
|
user-select: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app-about .modal-body > *,
|
app-about .modal-body > *,
|
||||||
|
@ -59,7 +59,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
@ -413,7 +412,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE,
|
provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
regularMemoryStorageService: AbstractMemoryStorageService & ObservableStorageService,
|
regularMemoryStorageService: AbstractStorageService & ObservableStorageService,
|
||||||
) => {
|
) => {
|
||||||
if (BrowserApi.isManifestVersion(2)) {
|
if (BrowserApi.isManifestVersion(2)) {
|
||||||
return regularMemoryStorageService;
|
return regularMemoryStorageService;
|
||||||
@ -441,7 +440,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: AbstractStorageService,
|
storageService: AbstractStorageService,
|
||||||
secureStorageService: AbstractStorageService,
|
secureStorageService: AbstractStorageService,
|
||||||
memoryStorageService: AbstractMemoryStorageService,
|
memoryStorageService: AbstractStorageService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
accountService: AccountServiceAbstraction,
|
accountService: AccountServiceAbstraction,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
|
@ -192,56 +192,5 @@
|
|||||||
{{ "showIdentitiesCurrentTabDesc" | i18n }}
|
{{ "showIdentitiesCurrentTabDesc" | i18n }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="favicon"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="faviconHelp"
|
|
||||||
(change)="updateFavicon()"
|
|
||||||
[(ngModel)]="enableFavicon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="faviconHelp" class="box-footer">
|
|
||||||
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="badge"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="badgeHelp"
|
|
||||||
(change)="updateBadgeCounter()"
|
|
||||||
[(ngModel)]="enableBadgeCounter"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row" appBoxRow>
|
|
||||||
<label for="theme">{{ "theme" | i18n }}</label>
|
|
||||||
<select
|
|
||||||
id="theme"
|
|
||||||
name="Theme"
|
|
||||||
aria-describedby="themeHelp"
|
|
||||||
[(ngModel)]="theme"
|
|
||||||
(change)="saveTheme()"
|
|
||||||
>
|
|
||||||
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="themeHelp" class="box-footer">
|
|
||||||
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</main>
|
</main>
|
||||||
|
@ -2,7 +2,6 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
||||||
@ -12,8 +11,6 @@ import {
|
|||||||
} from "@bitwarden/common/models/domain/domain-service";
|
} from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
|
||||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
|
|
||||||
import { enableAccountSwitching } from "../../platform/flags";
|
import { enableAccountSwitching } from "../../platform/flags";
|
||||||
@ -23,8 +20,6 @@ import { enableAccountSwitching } from "../../platform/flags";
|
|||||||
templateUrl: "options.component.html",
|
templateUrl: "options.component.html",
|
||||||
})
|
})
|
||||||
export class OptionsComponent implements OnInit {
|
export class OptionsComponent implements OnInit {
|
||||||
enableFavicon = false;
|
|
||||||
enableBadgeCounter = true;
|
|
||||||
enableAutoFillOnPageLoad = false;
|
enableAutoFillOnPageLoad = false;
|
||||||
autoFillOnPageLoadDefault = false;
|
autoFillOnPageLoadDefault = false;
|
||||||
autoFillOnPageLoadOptions: any[];
|
autoFillOnPageLoadOptions: any[];
|
||||||
@ -36,8 +31,6 @@ export class OptionsComponent implements OnInit {
|
|||||||
showCardsCurrentTab = false;
|
showCardsCurrentTab = false;
|
||||||
showIdentitiesCurrentTab = false;
|
showIdentitiesCurrentTab = false;
|
||||||
showClearClipboard = true;
|
showClearClipboard = true;
|
||||||
theme: ThemeType;
|
|
||||||
themeOptions: any[];
|
|
||||||
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
||||||
uriMatchOptions: any[];
|
uriMatchOptions: any[];
|
||||||
clearClipboard: ClearClipboardDelaySetting;
|
clearClipboard: ClearClipboardDelaySetting;
|
||||||
@ -52,18 +45,9 @@ export class OptionsComponent implements OnInit {
|
|||||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
private themeStateService: ThemeStateService,
|
|
||||||
private vaultSettingsService: VaultSettingsService,
|
private vaultSettingsService: VaultSettingsService,
|
||||||
) {
|
) {
|
||||||
this.themeOptions = [
|
|
||||||
{ name: i18nService.t("default"), value: ThemeType.System },
|
|
||||||
{ name: i18nService.t("light"), value: ThemeType.Light },
|
|
||||||
{ name: i18nService.t("dark"), value: ThemeType.Dark },
|
|
||||||
{ name: "Nord", value: ThemeType.Nord },
|
|
||||||
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
|
|
||||||
];
|
|
||||||
this.uriMatchOptions = [
|
this.uriMatchOptions = [
|
||||||
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||||
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||||
@ -117,14 +101,8 @@ export class OptionsComponent implements OnInit {
|
|||||||
|
|
||||||
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
|
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
|
||||||
|
|
||||||
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
|
||||||
|
|
||||||
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
|
|
||||||
|
|
||||||
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
|
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
|
||||||
|
|
||||||
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
|
||||||
|
|
||||||
const defaultUriMatch = await firstValueFrom(
|
const defaultUriMatch = await firstValueFrom(
|
||||||
this.domainSettingsService.defaultUriMatchStrategy$,
|
this.domainSettingsService.defaultUriMatchStrategy$,
|
||||||
);
|
);
|
||||||
@ -166,15 +144,6 @@ export class OptionsComponent implements OnInit {
|
|||||||
await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault);
|
await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFavicon() {
|
|
||||||
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateBadgeCounter() {
|
|
||||||
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
|
|
||||||
this.messagingService.send("bgUpdateContextMenu");
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateShowCardsCurrentTab() {
|
async updateShowCardsCurrentTab() {
|
||||||
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
|
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
|
||||||
}
|
}
|
||||||
@ -183,10 +152,6 @@ export class OptionsComponent implements OnInit {
|
|||||||
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
|
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveTheme() {
|
|
||||||
await this.themeStateService.setSelectedTheme(this.theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveClearClipboard() {
|
async saveClearClipboard() {
|
||||||
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
|
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
|
||||||
}
|
}
|
||||||
|
@ -1,264 +0,0 @@
|
|||||||
<app-header>
|
|
||||||
<div class="left">
|
|
||||||
<app-pop-out></app-pop-out>
|
|
||||||
</div>
|
|
||||||
<h1 class="center">
|
|
||||||
<span class="title">{{ "settings" | i18n }}</span>
|
|
||||||
</h1>
|
|
||||||
<div class="right"></div>
|
|
||||||
</app-header>
|
|
||||||
<main tabindex="-1" [formGroup]="form">
|
|
||||||
<div class="box list">
|
|
||||||
<h2 class="box-header">{{ "manage" | i18n }}</h2>
|
|
||||||
<div class="box-content single-line">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
routerLink="/autofill"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "autofill" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
routerLink="/folders"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "folders" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
routerLink="/sync"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "sync" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
routerLink="/excluded-domains"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box list">
|
|
||||||
<h2 class="box-header">{{ "security" | i18n }}</h2>
|
|
||||||
<div class="box-content single-line">
|
|
||||||
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
|
|
||||||
<span *ngIf="policy.timeout && policy.action">
|
|
||||||
{{
|
|
||||||
"vaultTimeoutPolicyWithActionInEffect"
|
|
||||||
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="policy.timeout && !policy.action">
|
|
||||||
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="!policy.timeout && policy.action">
|
|
||||||
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
|
|
||||||
</span>
|
|
||||||
</app-callout>
|
|
||||||
<app-vault-timeout-input
|
|
||||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
|
||||||
[formControl]="form.controls.vaultTimeout"
|
|
||||||
ngDefaultControl
|
|
||||||
>
|
|
||||||
</app-vault-timeout-input>
|
|
||||||
<div class="box-content-row display-block" appBoxRow>
|
|
||||||
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
|
|
||||||
<select
|
|
||||||
id="vaultTimeoutAction"
|
|
||||||
name="VaultTimeoutActions"
|
|
||||||
formControlName="vaultTimeoutAction"
|
|
||||||
>
|
|
||||||
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
|
|
||||||
{{ action | i18n }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
|
|
||||||
id="unlockMethodHelp"
|
|
||||||
class="box-footer"
|
|
||||||
>
|
|
||||||
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
|
|
||||||
</div>
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
|
|
||||||
<input id="pin" type="checkbox" formControlName="pin" />
|
|
||||||
</div>
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
|
|
||||||
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
|
|
||||||
<input id="biometric" type="checkbox" formControlName="biometric" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="box-content-row box-content-row-checkbox"
|
|
||||||
appBoxRow
|
|
||||||
*ngIf="supportsBiometric && this.form.value.biometric"
|
|
||||||
>
|
|
||||||
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="autoBiometricsPrompt"
|
|
||||||
type="checkbox"
|
|
||||||
(change)="updateAutoBiometricsPrompt()"
|
|
||||||
formControlName="enableAutoBiometricsPrompt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
*ngIf="
|
|
||||||
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
|
|
||||||
"
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="lock()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "lockNow" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="twoStep()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box list">
|
|
||||||
<h2 class="box-header">{{ "account" | i18n }}</h2>
|
|
||||||
<div class="box-content single-line">
|
|
||||||
<button type="button" class="box-content-row" routerLink="/premium">
|
|
||||||
<div class="row-main">
|
|
||||||
<div class="icon text-primary">
|
|
||||||
<i class="bwi bwi-fw bwi-lg bwi-star-f" aria-hidden="true"></i>
|
|
||||||
</div>
|
|
||||||
<span class="text text-primary"
|
|
||||||
><b>{{ "premiumMembership" | i18n }}</b></span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="changePassword()"
|
|
||||||
*ngIf="showChangeMasterPass"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="fingerprint()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
*ngIf="!accountSwitcherEnabled"
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="logOut()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "logOut" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box list">
|
|
||||||
<h2 class="box-header">{{ "tools" | i18n }}</h2>
|
|
||||||
<div class="box-content single-line">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="import()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "importItems" | i18n }}</div>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-external-link bwi-lg row-sub-icon bwi-rotate-270 bwi-fw"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="export()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "exportVault" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="webVault()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "bitWebVault" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box list">
|
|
||||||
<h2 class="box-header">{{ "other" | i18n }}</h2>
|
|
||||||
<div class="box-content single-line">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
routerLink="/options"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "options" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="about()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "about" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="share()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "learnOrg" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
routerLink="/help-and-feedback"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "helpFeedback" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-describedby="rateExtensionHelp"
|
|
||||||
class="box-content-row box-content-row-flex text-default"
|
|
||||||
appStopClick
|
|
||||||
(click)="rate()"
|
|
||||||
>
|
|
||||||
<div class="row-main">{{ "rateExtension" | i18n }}</div>
|
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="rateExtensionHelp" class="box-footer">{{ "rateExtensionDesc" | i18n }}</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
@ -5,7 +5,7 @@
|
|||||||
<div bitDialogTitle>Bitwarden</div>
|
<div bitDialogTitle>Bitwarden</div>
|
||||||
<div bitDialogContent>
|
<div bitDialogContent>
|
||||||
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
||||||
<p>{{ "version" | i18n }}: {{ version$ | async }}</p>
|
<p class="user-select">{{ "version" | i18n }}: {{ version$ | async }}</p>
|
||||||
<ng-container *ngIf="data$ | async as data">
|
<ng-container *ngIf="data$ | async as data">
|
||||||
<p *ngIf="data.isCloud">
|
<p *ngIf="data.isCloud">
|
||||||
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="!data.isCloud">
|
<ng-container *ngIf="!data.isCloud">
|
||||||
<ng-container *ngIf="data.serverConfig.server">
|
<ng-container *ngIf="data.serverConfig.server">
|
||||||
<p>
|
<p class="user-select">
|
||||||
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
|
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
|
||||||
{{ data.serverConfig?.version }}
|
{{ data.serverConfig?.version }}
|
||||||
<span *ngIf="!data.serverConfig.isValid()">
|
<span *ngIf="!data.serverConfig.isValid()">
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<p *ngIf="!data.serverConfig.server">
|
<p class="user-select" *ngIf="!data.serverConfig.server">
|
||||||
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
|
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
|
||||||
{{ data.serverConfig?.version }}
|
{{ data.serverConfig?.version }}
|
||||||
<span *ngIf="!data.serverConfig.isValid()">
|
<span *ngIf="!data.serverConfig.isValid()">
|
@ -1,7 +1,7 @@
|
|||||||
<form (ngSubmit)="submit()" [formGroup]="exportForm">
|
<form (ngSubmit)="submit()" [formGroup]="exportForm">
|
||||||
<header>
|
<header>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button type="button" routerLink="/tabs/settings">
|
<button type="button" routerLink="/vault-settings">
|
||||||
<span class="header-icon" aria-hidden="true"><i class="bwi bwi-angle-left"></i></span>
|
<span class="header-icon" aria-hidden="true"><i class="bwi bwi-angle-left"></i></span>
|
||||||
<span>{{ "back" | i18n }}</span>
|
<span>{{ "back" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button type="button" routerLink="/tabs/settings">
|
<button type="button" routerLink="/vault-settings">
|
||||||
<span class="header-icon" aria-hidden="true"><i class="bwi bwi-angle-left"></i></span>
|
<span class="header-icon" aria-hidden="true"><i class="bwi bwi-angle-left"></i></span>
|
||||||
<span>{{ "back" | i18n }}</span>
|
<span>{{ "back" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
136
apps/browser/src/tools/popup/settings/settings.component.html
Normal file
136
apps/browser/src/tools/popup/settings/settings.component.html
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<app-header>
|
||||||
|
<div class="left">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</div>
|
||||||
|
<h1 class="center">
|
||||||
|
<span class="title">{{ "settings" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="right"></div>
|
||||||
|
</app-header>
|
||||||
|
<main tabindex="-1">
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "manage" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/account-security"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "accountSecurity" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/autofill"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "autofill" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/vault-settings"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "vault" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/excluded-domains"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "account" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button type="button" class="box-content-row" routerLink="/premium">
|
||||||
|
<div class="row-main">
|
||||||
|
<div class="icon text-primary">
|
||||||
|
<i class="bwi bwi-fw bwi-lg bwi-star-f" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<span class="text text-primary"
|
||||||
|
><b>{{ "premiumMembership" | i18n }}</b></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "tools" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="webVault()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "bitWebVault" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<h2 class="box-header">{{ "other" | i18n }}</h2>
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/options"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "options" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/appearance"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "appearance" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="about()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "about" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="share()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "learnOrg" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/help-and-feedback"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "helpFeedback" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-describedby="rateExtensionHelp"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="rate()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "rateExtension" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="rateExtensionHelp" class="box-footer">{{ "rateExtensionDesc" | i18n }}</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
101
apps/browser/src/tools/popup/settings/settings.component.ts
Normal file
101
apps/browser/src/tools/popup/settings/settings.component.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom, Subject } from "rxjs";
|
||||||
|
|
||||||
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
|
import { AboutComponent } from "./about/about.component";
|
||||||
|
|
||||||
|
const RateUrls = {
|
||||||
|
[DeviceType.ChromeExtension]:
|
||||||
|
"https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
|
||||||
|
[DeviceType.FirefoxExtension]:
|
||||||
|
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews",
|
||||||
|
[DeviceType.OperaExtension]:
|
||||||
|
"https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container",
|
||||||
|
[DeviceType.EdgeExtension]:
|
||||||
|
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
|
||||||
|
[DeviceType.VivaldiExtension]:
|
||||||
|
"https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
|
||||||
|
[DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147",
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "tools-settings",
|
||||||
|
templateUrl: "settings.component.html",
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
export class SettingsComponent implements OnInit {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
|
public messagingService: MessagingService,
|
||||||
|
private router: Router,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {}
|
||||||
|
|
||||||
|
async share() {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "learnOrg" },
|
||||||
|
content: { key: "learnOrgConfirmation" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
// 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
|
||||||
|
BrowserApi.createNewTab("https://bitwarden.com/help/about-organizations/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async webVault() {
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
const url = env.getWebVaultUrl();
|
||||||
|
await BrowserApi.createNewTab(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async import() {
|
||||||
|
await this.router.navigate(["/import"]);
|
||||||
|
if (await BrowserApi.isPopupOpen()) {
|
||||||
|
// 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
|
||||||
|
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export() {
|
||||||
|
// 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
|
||||||
|
this.router.navigate(["/export"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
about() {
|
||||||
|
this.dialogService.open(AboutComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
rate() {
|
||||||
|
const deviceType = this.platformUtilsService.getDevice();
|
||||||
|
// 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
|
||||||
|
BrowserApi.createNewTab((RateUrls as any)[deviceType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import { first } from "rxjs/operators";
|
|||||||
|
|
||||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
||||||
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 { 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -26,6 +27,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
|
configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
collectionService,
|
collectionService,
|
||||||
@ -34,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
cipherService,
|
cipherService,
|
||||||
organizationService,
|
organizationService,
|
||||||
logService,
|
logService,
|
||||||
|
configService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<h1>Vault V2 Extension Refresh</h1>
|
@ -0,0 +1,13 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault",
|
||||||
|
templateUrl: "vault-v2.component.html",
|
||||||
|
})
|
||||||
|
export class VaultV2Component implements OnInit, OnDestroy {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnInit(): void {}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
<header>
|
||||||
|
<div class="left">
|
||||||
|
<button type="button" routerLink="/tabs/settings">
|
||||||
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ "back" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1 class="center">
|
||||||
|
<span class="title">{{ "appearance" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="right">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main tabindex="-1">
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row" appBoxRow>
|
||||||
|
<label for="theme">{{ "theme" | i18n }}</label>
|
||||||
|
<select
|
||||||
|
id="theme"
|
||||||
|
name="Theme"
|
||||||
|
aria-describedby="themeHelp"
|
||||||
|
[(ngModel)]="theme"
|
||||||
|
(change)="saveTheme()"
|
||||||
|
>
|
||||||
|
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="themeHelp" class="box-footer">
|
||||||
|
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="badge"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="badgeHelp"
|
||||||
|
(change)="updateBadgeCounter()"
|
||||||
|
[(ngModel)]="enableBadgeCounter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="favicon"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="faviconHelp"
|
||||||
|
(change)="updateFavicon()"
|
||||||
|
[(ngModel)]="enableFavicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="faviconHelp" class="box-footer">
|
||||||
|
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
@ -0,0 +1,62 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
|
|
||||||
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "vault-appearance",
|
||||||
|
templateUrl: "appearance.component.html",
|
||||||
|
})
|
||||||
|
export class AppearanceComponent implements OnInit {
|
||||||
|
enableFavicon = false;
|
||||||
|
enableBadgeCounter = true;
|
||||||
|
theme: ThemeType;
|
||||||
|
themeOptions: any[];
|
||||||
|
accountSwitcherEnabled = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
|
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||||
|
i18nService: I18nService,
|
||||||
|
private themeStateService: ThemeStateService,
|
||||||
|
) {
|
||||||
|
this.themeOptions = [
|
||||||
|
{ name: i18nService.t("default"), value: ThemeType.System },
|
||||||
|
{ name: i18nService.t("light"), value: ThemeType.Light },
|
||||||
|
{ name: i18nService.t("dark"), value: ThemeType.Dark },
|
||||||
|
{ name: "Nord", value: ThemeType.Nord },
|
||||||
|
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
|
||||||
|
];
|
||||||
|
|
||||||
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
||||||
|
|
||||||
|
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
|
||||||
|
|
||||||
|
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFavicon() {
|
||||||
|
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBadgeCounter() {
|
||||||
|
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
|
||||||
|
this.messagingService.send("bgUpdateContextMenu");
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveTheme() {
|
||||||
|
await this.themeStateService.setSelectedTheme(this.theme);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button type="button" routerLink="/tabs/settings">
|
<button type="button" routerLink="/vault-settings">
|
||||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
<span>{{ "back" | i18n }}</span>
|
<span>{{ "back" | i18n }}</span>
|
||||||
</button>
|
</button>
|
@ -1,6 +1,6 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button type="button" routerLink="/tabs/settings">
|
<button type="button" routerLink="/vault-settings">
|
||||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
<span>{{ "back" | i18n }}</span>
|
<span>{{ "back" | i18n }}</span>
|
||||||
</button>
|
</button>
|
@ -0,0 +1,56 @@
|
|||||||
|
<app-header>
|
||||||
|
<div class="left">
|
||||||
|
<button type="button" routerLink="/tabs/settings">
|
||||||
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ "back" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1 class="center">
|
||||||
|
<span class="title">{{ "vault" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="right">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
<main tabindex="-1">
|
||||||
|
<div class="box list">
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/folders"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "folders" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
appStopClick
|
||||||
|
(click)="import()"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "importItems" | i18n }}</div>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-external-link bwi-lg row-sub-icon bwi-rotate-270 bwi-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/export"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "exportVault" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/sync"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "sync" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "vault-settings",
|
||||||
|
templateUrl: "vault-settings.component.html",
|
||||||
|
})
|
||||||
|
export class VaultSettingsComponent {
|
||||||
|
constructor(
|
||||||
|
public messagingService: MessagingService,
|
||||||
|
private router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async import() {
|
||||||
|
await this.router.navigate(["/import"]);
|
||||||
|
if (await BrowserApi.isPopupOpen()) {
|
||||||
|
await BrowserPopupUtils.openCurrentPagePopout(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { Component } from "@angular/core";
|
|||||||
|
|
||||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
||||||
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 { 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -20,6 +21,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
|
configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
collectionService,
|
collectionService,
|
||||||
@ -28,6 +30,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
cipherService,
|
cipherService,
|
||||||
organizationService,
|
organizationService,
|
||||||
logService,
|
logService,
|
||||||
|
configService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,7 +165,22 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||||||
newMasterKey: MasterKey,
|
newMasterKey: MasterKey,
|
||||||
newUserKey: [UserKey, EncString],
|
newUserKey: [UserKey, EncString],
|
||||||
) {
|
) {
|
||||||
const masterKey = await this.cryptoService.getOrDeriveMasterKey(this.currentMasterPassword);
|
const masterKey = await this.cryptoService.makeMasterKey(
|
||||||
|
this.currentMasterPassword,
|
||||||
|
await this.stateService.getEmail(),
|
||||||
|
await this.kdfConfigService.getKdfConfig(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
|
if (userKey == null) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("invalidMasterPassword"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const request = new PasswordRequest();
|
const request = new PasswordRequest();
|
||||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||||
this.currentMasterPassword,
|
this.currentMasterPassword,
|
||||||
|
@ -9,10 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
@ -26,7 +23,7 @@ export class StateService extends BaseStateService<GlobalState, Account> {
|
|||||||
constructor(
|
constructor(
|
||||||
storageService: AbstractStorageService,
|
storageService: AbstractStorageService,
|
||||||
@Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService,
|
@Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService,
|
||||||
@Inject(MEMORY_STORAGE) memoryStorageService: AbstractMemoryStorageService,
|
@Inject(MEMORY_STORAGE) memoryStorageService: AbstractStorageService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,
|
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
bitLink
|
bitLink
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
type="button"
|
type="button"
|
||||||
class="tw-w-full tw-truncate tw-text-start tw-leading-snug"
|
class="tw-flex tw-w-full tw-text-start tw-leading-snug"
|
||||||
linkType="secondary"
|
linkType="secondary"
|
||||||
title="{{ 'viewCollectionWithName' | i18n: collection.name }}"
|
title="{{ 'viewCollectionWithName' | i18n: collection.name }}"
|
||||||
[routerLink]="[]"
|
[routerLink]="[]"
|
||||||
@ -28,7 +28,15 @@
|
|||||||
queryParamsHandling="merge"
|
queryParamsHandling="merge"
|
||||||
appStopProp
|
appStopProp
|
||||||
>
|
>
|
||||||
{{ collection.name }}
|
<span class="tw-truncate tw-mr-1">{{ collection.name }}</span>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
*ngIf="collection.addAccess && collection.id !== Unassigned"
|
||||||
|
bitBadge
|
||||||
|
variant="warning"
|
||||||
|
>{{ "addAccess" | i18n }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell [ngClass]="RowHeightClass" *ngIf="showOwner">
|
<td bitCell [ngClass]="RowHeightClass" *ngIf="showOwner">
|
||||||
|
@ -21,6 +21,7 @@ import { RowHeightClass } from "./vault-items.component";
|
|||||||
})
|
})
|
||||||
export class VaultCollectionRowComponent {
|
export class VaultCollectionRowComponent {
|
||||||
protected RowHeightClass = RowHeightClass;
|
protected RowHeightClass = RowHeightClass;
|
||||||
|
protected Unassigned = "unassigned";
|
||||||
|
|
||||||
@Input() disabled: boolean;
|
@Input() disabled: boolean;
|
||||||
@Input() collection: CollectionView;
|
@Input() collection: CollectionView;
|
||||||
|
@ -99,8 +99,12 @@
|
|||||||
(checkedToggled)="selection.toggle(item)"
|
(checkedToggled)="selection.toggle(item)"
|
||||||
(onEvent)="event($event)"
|
(onEvent)="event($event)"
|
||||||
></tr>
|
></tr>
|
||||||
|
<!--
|
||||||
|
addAccessStatus check here so ciphers do not show if user
|
||||||
|
has filtered for collections with addAccess
|
||||||
|
-->
|
||||||
<tr
|
<tr
|
||||||
*ngIf="item.cipher"
|
*ngIf="item.cipher && (!addAccessToggle || (addAccessToggle && addAccessStatus !== 1))"
|
||||||
bitRow
|
bitRow
|
||||||
appVaultCipherRow
|
appVaultCipherRow
|
||||||
alignContent="middle"
|
alignContent="middle"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { SelectionModel } from "@angular/cdk/collections";
|
import { SelectionModel } from "@angular/cdk/collections";
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
@ -45,6 +46,8 @@ export class VaultItemsComponent {
|
|||||||
@Input() showPermissionsColumn = false;
|
@Input() showPermissionsColumn = false;
|
||||||
@Input() viewingOrgVault: boolean;
|
@Input() viewingOrgVault: boolean;
|
||||||
@Input({ required: true }) flexibleCollectionsV1Enabled = false;
|
@Input({ required: true }) flexibleCollectionsV1Enabled = false;
|
||||||
|
@Input() addAccessStatus: number;
|
||||||
|
@Input() addAccessToggle: boolean;
|
||||||
|
|
||||||
private _ciphers?: CipherView[] = [];
|
private _ciphers?: CipherView[] = [];
|
||||||
@Input() get ciphers(): CipherView[] {
|
@Input() get ciphers(): CipherView[] {
|
||||||
@ -101,6 +104,28 @@ export class VaultItemsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||||
|
|
||||||
|
if (this.flexibleCollectionsV1Enabled) {
|
||||||
|
//Custom user without edit access should not see the Edit option unless that user has "Can Manage" access to a collection
|
||||||
|
if (
|
||||||
|
!collection.manage &&
|
||||||
|
organization?.type === OrganizationUserType.Custom &&
|
||||||
|
!organization?.permissions.editAnyCollection
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//Owner/Admin and Custom Users with Edit can see Edit and Access of Orphaned Collections
|
||||||
|
if (
|
||||||
|
collection.addAccess &&
|
||||||
|
collection.id !== Unassigned &&
|
||||||
|
((organization?.type === OrganizationUserType.Custom &&
|
||||||
|
organization?.permissions.editAnyCollection) ||
|
||||||
|
organization.isAdmin ||
|
||||||
|
organization.isOwner)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return collection.canEdit(organization, this.flexibleCollectionsV1Enabled);
|
return collection.canEdit(organization, this.flexibleCollectionsV1Enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +136,32 @@ export class VaultItemsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||||
|
|
||||||
|
if (this.flexibleCollectionsV1Enabled) {
|
||||||
|
//Custom user with only edit access should not see the Delete button for orphaned collections
|
||||||
|
if (
|
||||||
|
collection.addAccess &&
|
||||||
|
organization?.type === OrganizationUserType.Custom &&
|
||||||
|
!organization?.permissions.deleteAnyCollection &&
|
||||||
|
organization?.permissions.editAnyCollection
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner/Admin with no access to a collection will not see Delete
|
||||||
|
if (
|
||||||
|
!collection.assigned &&
|
||||||
|
!collection.addAccess &&
|
||||||
|
(organization.isAdmin || organization.isOwner) &&
|
||||||
|
!(
|
||||||
|
organization?.type === OrganizationUserType.Custom &&
|
||||||
|
organization?.permissions.deleteAnyCollection
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return collection.canDelete(organization);
|
return collection.canDelete(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response";
|
import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
@ -7,6 +8,7 @@ import { CollectionAccessSelectionView } from "../../../admin-console/organizati
|
|||||||
export class CollectionAdminView extends CollectionView {
|
export class CollectionAdminView extends CollectionView {
|
||||||
groups: CollectionAccessSelectionView[] = [];
|
groups: CollectionAccessSelectionView[] = [];
|
||||||
users: CollectionAccessSelectionView[] = [];
|
users: CollectionAccessSelectionView[] = [];
|
||||||
|
addAccess: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag indicating the user has been explicitly assigned to this Collection
|
* Flag indicating the user has been explicitly assigned to this Collection
|
||||||
@ -31,6 +33,33 @@ export class CollectionAdminView extends CollectionView {
|
|||||||
this.assigned = response.assigned;
|
this.assigned = response.assigned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupsCanManage() {
|
||||||
|
if (this.groups.length === 0) {
|
||||||
|
return this.groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnedGroups = this.groups.filter((group) => {
|
||||||
|
if (group.manage) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return returnedGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
usersCanManage(revokedUsers: OrganizationUserUserDetailsResponse[]) {
|
||||||
|
if (this.users.length === 0) {
|
||||||
|
return this.users;
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnedUsers = this.users.filter((user) => {
|
||||||
|
const isRevoked = revokedUsers.some((revoked) => revoked.id === user.id);
|
||||||
|
if (user.manage && !isRevoked) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return returnedUsers;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the current user can edit the collection, including user and group access
|
* Whether the current user can edit the collection, including user and group access
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
@ -56,6 +56,10 @@ export class BulkDeleteDialogComponent {
|
|||||||
FeatureFlag.FlexibleCollectionsV1,
|
FeatureFlag.FlexibleCollectionsV1,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private restrictProviderAccess$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.RestrictProviderAccess,
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
||||||
private dialogRef: DialogRef<BulkDeleteDialogResult>,
|
private dialogRef: DialogRef<BulkDeleteDialogResult>,
|
||||||
@ -81,10 +85,11 @@ export class BulkDeleteDialogComponent {
|
|||||||
const deletePromises: Promise<void>[] = [];
|
const deletePromises: Promise<void>[] = [];
|
||||||
if (this.cipherIds.length) {
|
if (this.cipherIds.length) {
|
||||||
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||||
|
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.organization ||
|
!this.organization ||
|
||||||
!this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled)
|
!this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled, restrictProviderAccess)
|
||||||
) {
|
) {
|
||||||
deletePromises.push(this.deleteCiphers());
|
deletePromises.push(this.deleteCiphers());
|
||||||
} else {
|
} else {
|
||||||
@ -118,7 +123,11 @@ export class BulkDeleteDialogComponent {
|
|||||||
|
|
||||||
private async deleteCiphers(): Promise<any> {
|
private async deleteCiphers(): Promise<any> {
|
||||||
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||||
const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled);
|
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
|
||||||
|
const asAdmin = this.organization?.canEditAllCiphers(
|
||||||
|
flexibleCollectionsV1Enabled,
|
||||||
|
restrictProviderAccess,
|
||||||
|
);
|
||||||
if (this.permanent) {
|
if (this.permanent) {
|
||||||
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin);
|
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin);
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,7 +32,13 @@
|
|||||||
[(ngModel)]="$any(c).checked"
|
[(ngModel)]="$any(c).checked"
|
||||||
name="Collection[{{ i }}].Checked"
|
name="Collection[{{ i }}].Checked"
|
||||||
appStopProp
|
appStopProp
|
||||||
[disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)"
|
[disabled]="
|
||||||
|
!c.canEditItems(
|
||||||
|
this.organization,
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess
|
||||||
|
)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
{{ c.name }}
|
{{ c.name }}
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Component, OnDestroy, Inject } from "@angular/core";
|
import { Component, Inject, OnDestroy } from "@angular/core";
|
||||||
|
|
||||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
||||||
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 { 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -23,6 +24,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
|
|||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
organizationSerivce: OrganizationService,
|
organizationSerivce: OrganizationService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
|
configService: ConfigService,
|
||||||
protected dialogRef: DialogRef,
|
protected dialogRef: DialogRef,
|
||||||
@Inject(DIALOG_DATA) params: CollectionsDialogParams,
|
@Inject(DIALOG_DATA) params: CollectionsDialogParams,
|
||||||
) {
|
) {
|
||||||
@ -33,6 +35,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
|
|||||||
cipherService,
|
cipherService,
|
||||||
organizationSerivce,
|
organizationSerivce,
|
||||||
logService,
|
logService,
|
||||||
|
configService,
|
||||||
);
|
);
|
||||||
this.cipherId = params?.cipherId;
|
this.cipherId = params?.cipherId;
|
||||||
}
|
}
|
||||||
@ -47,7 +50,13 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
|
|||||||
}
|
}
|
||||||
|
|
||||||
check(c: CollectionView, select?: boolean) {
|
check(c: CollectionView, select?: boolean) {
|
||||||
if (!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
!c.canEditItems(
|
||||||
|
this.organization,
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||||
|
@ -82,7 +82,12 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected loadCollections() {
|
protected loadCollections() {
|
||||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
!this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return super.loadCollections();
|
return super.loadCollections();
|
||||||
}
|
}
|
||||||
return Promise.resolve(this.collections);
|
return Promise.resolve(this.collections);
|
||||||
@ -93,7 +98,10 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
const firstCipherCheck = await super.loadCipher();
|
const firstCipherCheck = await super.loadCipher();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
!this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
) &&
|
||||||
firstCipherCheck != null
|
firstCipherCheck != null
|
||||||
) {
|
) {
|
||||||
return firstCipherCheck;
|
return firstCipherCheck;
|
||||||
@ -108,14 +116,24 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected encryptCipher() {
|
protected encryptCipher() {
|
||||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
!this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return super.encryptCipher();
|
return super.encryptCipher();
|
||||||
}
|
}
|
||||||
return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher);
|
return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async deleteCipher() {
|
protected async deleteCipher() {
|
||||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
!this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return super.deleteCipher();
|
return super.deleteCipher();
|
||||||
}
|
}
|
||||||
return this.cipher.isDeleted
|
return this.cipher.isDeleted
|
||||||
|
@ -29,6 +29,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
|||||||
organization: Organization;
|
organization: Organization;
|
||||||
|
|
||||||
private flexibleCollectionsV1Enabled = false;
|
private flexibleCollectionsV1Enabled = false;
|
||||||
|
private restrictProviderAccess = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
@ -62,11 +63,17 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
|||||||
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
||||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||||
);
|
);
|
||||||
|
this.restrictProviderAccess = await firstValueFrom(
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.RestrictProviderAccess),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async reupload(attachment: AttachmentView) {
|
protected async reupload(attachment: AttachmentView) {
|
||||||
if (
|
if (
|
||||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
) &&
|
||||||
this.showFixOldAttachments(attachment)
|
this.showFixOldAttachments(attachment)
|
||||||
) {
|
) {
|
||||||
await super.reuploadCipherAttachment(attachment, true);
|
await super.reuploadCipherAttachment(attachment, true);
|
||||||
@ -74,7 +81,12 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async loadCipher() {
|
protected async loadCipher() {
|
||||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
!this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return await super.loadCipher();
|
return await super.loadCipher();
|
||||||
}
|
}
|
||||||
const response = await this.apiService.getCipherAdmin(this.cipherId);
|
const response = await this.apiService.getCipherAdmin(this.cipherId);
|
||||||
@ -85,12 +97,20 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
|||||||
return this.cipherService.saveAttachmentWithServer(
|
return this.cipherService.saveAttachmentWithServer(
|
||||||
this.cipherDomain,
|
this.cipherDomain,
|
||||||
file,
|
file,
|
||||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled),
|
this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected deleteCipherAttachment(attachmentId: string) {
|
protected deleteCipherAttachment(attachmentId: string) {
|
||||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
!this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return super.deleteCipherAttachment(attachmentId);
|
return super.deleteCipherAttachment(attachmentId);
|
||||||
}
|
}
|
||||||
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
|
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
|
||||||
@ -99,7 +119,10 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
|
|||||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||||
return (
|
return (
|
||||||
attachment.key == null &&
|
attachment.key == null &&
|
||||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
|
this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,12 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1);
|
const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1);
|
||||||
|
const restrictProviderAccess = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.RestrictProviderAccess,
|
||||||
|
);
|
||||||
const org = await this.organizationService.get(this.params.organizationId);
|
const org = await this.organizationService.get(this.params.organizationId);
|
||||||
|
|
||||||
if (org.canEditAllCiphers(v1FCEnabled)) {
|
if (org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess)) {
|
||||||
this.editableItems = this.params.ciphers;
|
this.editableItems = this.params.ciphers;
|
||||||
} else {
|
} else {
|
||||||
this.editableItems = this.params.ciphers.filter((c) => c.edit);
|
this.editableItems = this.params.ciphers.filter((c) => c.edit);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
|
import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
|
||||||
|
|
||||||
@ -22,12 +22,18 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height=
|
|||||||
buttonType="secondary"
|
buttonType="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "viewCollection" | i18n }}
|
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ buttonText | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</bit-no-items>`,
|
</bit-no-items>`,
|
||||||
})
|
})
|
||||||
export class CollectionAccessRestrictedComponent {
|
export class CollectionAccessRestrictedComponent {
|
||||||
protected icon = icon;
|
protected icon = icon;
|
||||||
|
|
||||||
|
@Input() canEditCollection = false;
|
||||||
|
|
||||||
@Output() viewCollectionClicked = new EventEmitter<void>();
|
@Output() viewCollectionClicked = new EventEmitter<void>();
|
||||||
|
|
||||||
|
get buttonText() {
|
||||||
|
return this.canEditCollection ? "editCollection" : "viewCollection";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { Component, Inject } from "@angular/core";
|
|||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
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";
|
||||||
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -35,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
|
configService: ConfigService,
|
||||||
protected dialogRef: DialogRef,
|
protected dialogRef: DialogRef,
|
||||||
@Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams,
|
@Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams,
|
||||||
) {
|
) {
|
||||||
@ -45,6 +47,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
cipherService,
|
cipherService,
|
||||||
organizationService,
|
organizationService,
|
||||||
logService,
|
logService,
|
||||||
|
configService,
|
||||||
dialogRef,
|
dialogRef,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
@ -58,7 +61,10 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
protected async loadCipher() {
|
protected async loadCipher() {
|
||||||
// if cipher is unassigned use apiService. We can see this by looking at this.collectionIds
|
// if cipher is unassigned use apiService. We can see this by looking at this.collectionIds
|
||||||
if (
|
if (
|
||||||
!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
!this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
) &&
|
||||||
this.collectionIds.length !== 0
|
this.collectionIds.length !== 0
|
||||||
) {
|
) {
|
||||||
return await super.loadCipher();
|
return await super.loadCipher();
|
||||||
@ -83,7 +89,10 @@ export class CollectionsComponent extends BaseCollectionsComponent {
|
|||||||
|
|
||||||
protected saveCollections() {
|
protected saveCollections() {
|
||||||
if (
|
if (
|
||||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) ||
|
this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
) ||
|
||||||
this.collectionIds.length === 0
|
this.collectionIds.length === 0
|
||||||
) {
|
) {
|
||||||
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
|
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
|
||||||
|
@ -73,8 +73,16 @@
|
|||||||
</small>
|
</small>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<bit-search
|
||||||
|
*ngIf="organization?.isProviderUser"
|
||||||
|
class="tw-grow"
|
||||||
|
[ngModel]="searchText"
|
||||||
|
(ngModelChange)="onSearchTextChanged($event)"
|
||||||
|
[placeholder]="'searchCollection' | i18n"
|
||||||
|
></bit-search>
|
||||||
|
|
||||||
<div *ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned" class="tw-shrink-0">
|
<div *ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned" class="tw-shrink-0">
|
||||||
<div *ngIf="organization?.canCreateNewCollections" appListDropdown>
|
<div *ngIf="canCreateCipher && canCreateCollection" appListDropdown>
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
@ -97,7 +105,7 @@
|
|||||||
</bit-menu>
|
</bit-menu>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
*ngIf="!organization?.canCreateNewCollections"
|
*ngIf="canCreateCipher && !canCreateCollection"
|
||||||
type="button"
|
type="button"
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
@ -106,5 +114,16 @@
|
|||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
{{ "newItem" | i18n }}
|
{{ "newItem" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="canCreateCollection && !canCreateCipher"
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
(click)="addCollection()"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
{{ "newCollection" | i18n }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</app-header>
|
</app-header>
|
||||||
|
@ -43,6 +43,9 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
/** Currently selected collection */
|
/** Currently selected collection */
|
||||||
@Input() collection?: TreeNode<CollectionAdminView>;
|
@Input() collection?: TreeNode<CollectionAdminView>;
|
||||||
|
|
||||||
|
/** The current search text in the header */
|
||||||
|
@Input() searchText: string;
|
||||||
|
|
||||||
/** Emits an event when the new item button is clicked in the header */
|
/** Emits an event when the new item button is clicked in the header */
|
||||||
@Output() onAddCipher = new EventEmitter<void>();
|
@Output() onAddCipher = new EventEmitter<void>();
|
||||||
|
|
||||||
@ -55,10 +58,14 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
/** Emits an event when the delete collection button is clicked in the header */
|
/** Emits an event when the delete collection button is clicked in the header */
|
||||||
@Output() onDeleteCollection = new EventEmitter<void>();
|
@Output() onDeleteCollection = new EventEmitter<void>();
|
||||||
|
|
||||||
|
/** Emits an event when the search text changes in the header*/
|
||||||
|
@Output() searchTextChanged = new EventEmitter<string>();
|
||||||
|
|
||||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||||
protected organizations$ = this.organizationService.organizations$;
|
protected organizations$ = this.organizationService.organizations$;
|
||||||
|
|
||||||
private flexibleCollectionsV1Enabled = false;
|
private flexibleCollectionsV1Enabled = false;
|
||||||
|
private restrictProviderAccessFlag = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
@ -73,6 +80,9 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
||||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||||
);
|
);
|
||||||
|
this.restrictProviderAccessFlag = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.RestrictProviderAccess,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
@ -197,7 +207,23 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
return this.collection.node.canDelete(this.organization);
|
return this.collection.node.canDelete(this.organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canCreateCollection(): boolean {
|
||||||
|
return this.organization?.canCreateNewCollections;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canCreateCipher(): boolean {
|
||||||
|
if (this.organization?.isProviderUser && this.restrictProviderAccessFlag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
deleteCollection() {
|
deleteCollection() {
|
||||||
this.onDeleteCollection.emit();
|
this.onDeleteCollection.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSearchTextChanged(t: string) {
|
||||||
|
this.searchText = t;
|
||||||
|
this.searchTextChanged.emit(t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,20 @@
|
|||||||
[loading]="refreshing"
|
[loading]="refreshing"
|
||||||
[organization]="organization"
|
[organization]="organization"
|
||||||
[collection]="selectedCollection"
|
[collection]="selectedCollection"
|
||||||
|
[searchText]="currentSearchText$ | async"
|
||||||
(onAddCipher)="addCipher()"
|
(onAddCipher)="addCipher()"
|
||||||
(onAddCollection)="addCollection()"
|
(onAddCollection)="addCollection()"
|
||||||
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
|
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
|
||||||
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||||
|
(searchTextChanged)="filterSearchText($event)"
|
||||||
></app-org-vault-header>
|
></app-org-vault-header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3">
|
<div class="col-3" *ngIf="!organization?.isProviderUser">
|
||||||
<div class="groupings">
|
<div class="groupings">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="inner-content">
|
<div class="inner-content">
|
||||||
<app-organization-vault-filter
|
<app-organization-vault-filter
|
||||||
#vaultFilter
|
|
||||||
[organization]="organization"
|
[organization]="organization"
|
||||||
[activeFilter]="activeFilter"
|
[activeFilter]="activeFilter"
|
||||||
[searchText]="currentSearchText$ | async"
|
[searchText]="currentSearchText$ | async"
|
||||||
@ -25,7 +26,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9">
|
<div [class]="organization?.isProviderUser ? 'col-12' : 'col-9'">
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||||
|
[selected]="addAccessStatus$ | async"
|
||||||
|
(selectedChange)="addAccessToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<bit-toggle [value]="0">
|
||||||
|
{{ "all" | i18n }}
|
||||||
|
</bit-toggle>
|
||||||
|
|
||||||
|
<bit-toggle [value]="1">
|
||||||
|
{{ "addAccess" | i18n }}
|
||||||
|
</bit-toggle>
|
||||||
|
</bit-toggle-group>
|
||||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi bwi-exclamation-triangle">
|
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi bwi-exclamation-triangle">
|
||||||
{{ trashCleanupWarning }}
|
{{ trashCleanupWarning }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
@ -54,6 +69,8 @@
|
|||||||
[showBulkAddToCollections]="organization?.flexibleCollections"
|
[showBulkAddToCollections]="organization?.flexibleCollections"
|
||||||
[viewingOrgVault]="true"
|
[viewingOrgVault]="true"
|
||||||
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
|
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
|
||||||
|
[addAccessStatus]="addAccessStatus$ | async"
|
||||||
|
[addAccessToggle]="showAddAccessToggle"
|
||||||
>
|
>
|
||||||
</app-vault-items>
|
</app-vault-items>
|
||||||
<ng-container *ngIf="!flexibleCollectionsV1Enabled">
|
<ng-container *ngIf="!flexibleCollectionsV1Enabled">
|
||||||
@ -98,8 +115,13 @@
|
|||||||
</bit-no-items>
|
</bit-no-items>
|
||||||
<collection-access-restricted
|
<collection-access-restricted
|
||||||
*ngIf="showCollectionAccessRestricted"
|
*ngIf="showCollectionAccessRestricted"
|
||||||
|
[canEditCollection]="organization.isProviderUser"
|
||||||
(viewCollectionClicked)="
|
(viewCollectionClicked)="
|
||||||
editCollection(selectedCollection.node, CollectionDialogTabType.Info, true)
|
editCollection(
|
||||||
|
selectedCollection.node,
|
||||||
|
CollectionDialogTabType.Info,
|
||||||
|
!organization.isProviderUser
|
||||||
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
</collection-access-restricted>
|
</collection-access-restricted>
|
||||||
|
@ -36,6 +36,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
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 { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
|
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
|
||||||
|
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
@ -97,11 +100,15 @@ import {
|
|||||||
BulkCollectionsDialogResult,
|
BulkCollectionsDialogResult,
|
||||||
} from "./bulk-collections-dialog";
|
} from "./bulk-collections-dialog";
|
||||||
import { openOrgVaultCollectionsDialog } from "./collections.component";
|
import { openOrgVaultCollectionsDialog } from "./collections.component";
|
||||||
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||||
const SearchTextDebounceInterval = 200;
|
const SearchTextDebounceInterval = 200;
|
||||||
|
|
||||||
|
enum AddAccessStatusType {
|
||||||
|
All = 0,
|
||||||
|
AddAccess = 1,
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-org-vault",
|
selector: "app-org-vault",
|
||||||
templateUrl: "vault.component.html",
|
templateUrl: "vault.component.html",
|
||||||
@ -110,8 +117,6 @@ const SearchTextDebounceInterval = 200;
|
|||||||
export class VaultComponent implements OnInit, OnDestroy {
|
export class VaultComponent implements OnInit, OnDestroy {
|
||||||
protected Unassigned = Unassigned;
|
protected Unassigned = Unassigned;
|
||||||
|
|
||||||
@ViewChild("vaultFilter", { static: true })
|
|
||||||
vaultFilterComponent: VaultFilterComponent;
|
|
||||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||||
attachmentsModalRef: ViewContainerRef;
|
attachmentsModalRef: ViewContainerRef;
|
||||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||||
@ -122,6 +127,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
trashCleanupWarning: string = null;
|
trashCleanupWarning: string = null;
|
||||||
activeFilter: VaultFilter = new VaultFilter();
|
activeFilter: VaultFilter = new VaultFilter();
|
||||||
|
|
||||||
|
protected showAddAccessToggle = false;
|
||||||
protected noItemIcon = Icons.Search;
|
protected noItemIcon = Icons.Search;
|
||||||
protected performingInitialLoad = true;
|
protected performingInitialLoad = true;
|
||||||
protected refreshing = false;
|
protected refreshing = false;
|
||||||
@ -142,6 +148,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected showMissingCollectionPermissionMessage: boolean;
|
protected showMissingCollectionPermissionMessage: boolean;
|
||||||
protected showCollectionAccessRestricted: boolean;
|
protected showCollectionAccessRestricted: boolean;
|
||||||
protected currentSearchText$: Observable<string>;
|
protected currentSearchText$: Observable<string>;
|
||||||
|
/**
|
||||||
|
* A list of collections that the user can assign items to and edit those items within.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected editableCollections$: Observable<CollectionView[]>;
|
protected editableCollections$: Observable<CollectionView[]>;
|
||||||
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
|
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
|
||||||
private _flexibleCollectionsV1FlagEnabled: boolean;
|
private _flexibleCollectionsV1FlagEnabled: boolean;
|
||||||
@ -149,10 +159,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected get flexibleCollectionsV1Enabled(): boolean {
|
protected get flexibleCollectionsV1Enabled(): boolean {
|
||||||
return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections;
|
return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections;
|
||||||
}
|
}
|
||||||
|
protected orgRevokedUsers: OrganizationUserUserDetailsResponse[];
|
||||||
|
|
||||||
|
private _restrictProviderAccessFlagEnabled: boolean;
|
||||||
|
protected get restrictProviderAccessEnabled(): boolean {
|
||||||
|
return this._restrictProviderAccessFlagEnabled && this.flexibleCollectionsV1Enabled;
|
||||||
|
}
|
||||||
|
|
||||||
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>();
|
||||||
|
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -181,6 +198,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private totpService: TotpService,
|
private totpService: TotpService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
|
private organizationUserService: OrganizationUserService,
|
||||||
protected configService: ConfigService,
|
protected configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -195,6 +213,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
FeatureFlag.FlexibleCollectionsV1,
|
FeatureFlag.FlexibleCollectionsV1,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this._restrictProviderAccessFlagEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.RestrictProviderAccess,
|
||||||
|
);
|
||||||
|
|
||||||
const filter$ = this.routedVaultFilterService.filter$;
|
const filter$ = this.routedVaultFilterService.filter$;
|
||||||
const organizationId$ = filter$.pipe(
|
const organizationId$ = filter$.pipe(
|
||||||
map((filter) => filter.organizationId),
|
map((filter) => filter.organizationId),
|
||||||
@ -241,6 +263,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe((activeFilter) => {
|
.subscribe((activeFilter) => {
|
||||||
this.activeFilter = activeFilter;
|
this.activeFilter = activeFilter;
|
||||||
|
|
||||||
|
// watch the active filters. Only show toggle when viewing the collections filter
|
||||||
|
if (!this.activeFilter.collectionId) {
|
||||||
|
this.showAddAccessToggle = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.searchText$
|
this.searchText$
|
||||||
@ -280,10 +307,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe(
|
this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe(
|
||||||
map((collections) => {
|
map((collections) => {
|
||||||
// Users that can edit all ciphers can implicitly edit all collections
|
// If restricted, providers can not add items to any collections or edit those items
|
||||||
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (this.organization.isProviderUser && this.restrictProviderAccessEnabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Users that can edit all ciphers can implicitly add to / edit within any collection
|
||||||
|
if (
|
||||||
|
this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return collections;
|
return collections;
|
||||||
}
|
}
|
||||||
|
// The user is only allowed to add/edit items to assigned collections that are not readonly
|
||||||
return collections.filter((c) => c.assigned && !c.readOnly);
|
return collections.filter((c) => c.assigned && !c.readOnly);
|
||||||
}),
|
}),
|
||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
@ -309,12 +346,25 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const allCiphers$ = organization$.pipe(
|
const allCiphers$ = organization$.pipe(
|
||||||
concatMap(async (organization) => {
|
concatMap(async (organization) => {
|
||||||
|
// If user swaps organization reset the addAccessToggle
|
||||||
|
if (!this.showAddAccessToggle || organization) {
|
||||||
|
this.addAccessToggle(0);
|
||||||
|
}
|
||||||
let ciphers;
|
let ciphers;
|
||||||
|
|
||||||
|
if (organization.isProviderUser && this.restrictProviderAccessEnabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (this.flexibleCollectionsV1Enabled) {
|
if (this.flexibleCollectionsV1Enabled) {
|
||||||
// Flexible collections V1 logic.
|
// Flexible collections V1 logic.
|
||||||
// If the user can edit all ciphers for the organization then fetch them ALL.
|
// If the user can edit all ciphers for the organization then fetch them ALL.
|
||||||
if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
)
|
||||||
|
) {
|
||||||
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, only fetch ciphers they have access to (includes unassigned for admins).
|
// Otherwise, only fetch ciphers they have access to (includes unassigned for admins).
|
||||||
@ -322,7 +372,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Pre-flexible collections logic, to be removed after flexible collections is fully released
|
// Pre-flexible collections logic, to be removed after flexible collections is fully released
|
||||||
if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
)
|
||||||
|
) {
|
||||||
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
||||||
} else {
|
} else {
|
||||||
ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||||
@ -348,9 +403,21 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
|
// This will be passed into the usersCanManage call
|
||||||
|
this.orgRevokedUsers = (
|
||||||
|
await this.organizationUserService.getAllUsers(await firstValueFrom(organizationId$))
|
||||||
|
).data.filter((user: OrganizationUserUserDetailsResponse) => {
|
||||||
|
return user.status === -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const collections$ = combineLatest([
|
||||||
|
nestedCollections$,
|
||||||
|
filter$,
|
||||||
|
this.currentSearchText$,
|
||||||
|
this.addAccessStatus$,
|
||||||
|
]).pipe(
|
||||||
filter(([collections, filter]) => collections != undefined && filter != undefined),
|
filter(([collections, filter]) => collections != undefined && filter != undefined),
|
||||||
concatMap(async ([collections, filter, searchText]) => {
|
concatMap(async ([collections, filter, searchText, addAccessStatus]) => {
|
||||||
if (
|
if (
|
||||||
filter.collectionId === Unassigned ||
|
filter.collectionId === Unassigned ||
|
||||||
(filter.collectionId === undefined && filter.type !== undefined)
|
(filter.collectionId === undefined && filter.type !== undefined)
|
||||||
@ -358,26 +425,30 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.showAddAccessToggle = false;
|
||||||
let collectionsToReturn = [];
|
let collectionsToReturn = [];
|
||||||
if (filter.collectionId === undefined || filter.collectionId === All) {
|
if (filter.collectionId === undefined || filter.collectionId === All) {
|
||||||
collectionsToReturn = collections.map((c) => c.node);
|
collectionsToReturn = await this.addAccessCollectionsMap(collections);
|
||||||
} else {
|
} else {
|
||||||
const selectedCollection = ServiceUtils.getTreeNodeObjectFromList(
|
const selectedCollection = ServiceUtils.getTreeNodeObjectFromList(
|
||||||
collections,
|
collections,
|
||||||
filter.collectionId,
|
filter.collectionId,
|
||||||
);
|
);
|
||||||
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
collectionsToReturn = await this.addAccessCollectionsMap(selectedCollection?.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.searchService.isSearchable(searchText)) {
|
if (await this.searchService.isSearchable(searchText)) {
|
||||||
collectionsToReturn = this.searchPipe.transform(
|
collectionsToReturn = this.searchPipe.transform(
|
||||||
collectionsToReturn,
|
collectionsToReturn,
|
||||||
searchText,
|
searchText,
|
||||||
(collection) => collection.name,
|
(collection: CollectionAdminView) => collection.name,
|
||||||
(collection) => collection.id,
|
(collection: CollectionAdminView) => collection.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (addAccessStatus === 1 && this.showAddAccessToggle) {
|
||||||
|
collectionsToReturn = collectionsToReturn.filter((c: any) => c.addAccess);
|
||||||
|
}
|
||||||
return collectionsToReturn;
|
return collectionsToReturn;
|
||||||
}),
|
}),
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
@ -406,9 +477,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
organization$,
|
organization$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([filter, collection, organization]) => {
|
map(([filter, collection, organization]) => {
|
||||||
|
if (organization.isProviderUser && this.restrictProviderAccessEnabled) {
|
||||||
|
return collection != undefined || filter.collectionId === Unassigned;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) ||
|
(filter.collectionId === Unassigned &&
|
||||||
(!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
!organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) ||
|
||||||
|
(!organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
) &&
|
||||||
collection != undefined &&
|
collection != undefined &&
|
||||||
!collection.node.assigned)
|
!collection.node.assigned)
|
||||||
);
|
);
|
||||||
@ -453,7 +532,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
map(([filter, collection, organization]) => {
|
map(([filter, collection, organization]) => {
|
||||||
return (
|
return (
|
||||||
// Filtering by unassigned, show message if not admin
|
// Filtering by unassigned, show message if not admin
|
||||||
(filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) ||
|
(filter.collectionId === Unassigned &&
|
||||||
|
!organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) ||
|
||||||
// Filtering by a collection, so show message if user is not assigned
|
// Filtering by a collection, so show message if user is not assigned
|
||||||
(collection != undefined &&
|
(collection != undefined &&
|
||||||
!collection.node.assigned &&
|
!collection.node.assigned &&
|
||||||
@ -476,7 +556,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (this.flexibleCollectionsV1Enabled) {
|
if (this.flexibleCollectionsV1Enabled) {
|
||||||
canEditCipher =
|
canEditCipher =
|
||||||
organization.canEditAllCiphers(true) ||
|
organization.canEditAllCiphers(true, this.restrictProviderAccessEnabled) ||
|
||||||
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
|
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
|
||||||
} else {
|
} else {
|
||||||
canEditCipher =
|
canEditCipher =
|
||||||
@ -586,6 +666,60 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the list of collections to see if any collection is orphaned
|
||||||
|
// and will receive the addAccess badge / be filterable by the user
|
||||||
|
async addAccessCollectionsMap(collections: TreeNode<CollectionAdminView>[]) {
|
||||||
|
let mappedCollections;
|
||||||
|
const { type, allowAdminAccessToAllCollectionItems, permissions } = this.organization;
|
||||||
|
|
||||||
|
const canEditCiphersCheck =
|
||||||
|
this._flexibleCollectionsV1FlagEnabled &&
|
||||||
|
!this.organization.canEditAllCiphers(
|
||||||
|
this._flexibleCollectionsV1FlagEnabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
// This custom type check will show addAccess badge for
|
||||||
|
// Custom users with canEdit access AND owner/admin manage access setting is OFF
|
||||||
|
const customUserCheck =
|
||||||
|
this._flexibleCollectionsV1FlagEnabled &&
|
||||||
|
!allowAdminAccessToAllCollectionItems &&
|
||||||
|
type === OrganizationUserType.Custom &&
|
||||||
|
permissions.editAnyCollection;
|
||||||
|
|
||||||
|
// If Custom user has Delete Only access they will not see Add Access toggle
|
||||||
|
const customUserOnlyDelete =
|
||||||
|
this.flexibleCollectionsV1Enabled &&
|
||||||
|
type === OrganizationUserType.Custom &&
|
||||||
|
permissions.deleteAnyCollection &&
|
||||||
|
!permissions.editAnyCollection;
|
||||||
|
|
||||||
|
if (!customUserOnlyDelete && (canEditCiphersCheck || customUserCheck)) {
|
||||||
|
mappedCollections = collections.map((c: TreeNode<CollectionAdminView>) => {
|
||||||
|
const groupsCanManage = c.node.groupsCanManage();
|
||||||
|
const usersCanManage = c.node.usersCanManage(this.orgRevokedUsers);
|
||||||
|
if (
|
||||||
|
groupsCanManage.length === 0 &&
|
||||||
|
usersCanManage.length === 0 &&
|
||||||
|
c.node.id !== Unassigned
|
||||||
|
) {
|
||||||
|
c.node.addAccess = true;
|
||||||
|
this.showAddAccessToggle = true;
|
||||||
|
} else {
|
||||||
|
c.node.addAccess = false;
|
||||||
|
}
|
||||||
|
return c.node;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mappedCollections = collections.map((c: TreeNode<CollectionAdminView>) => c.node);
|
||||||
|
}
|
||||||
|
return mappedCollections;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAccessToggle(e: any) {
|
||||||
|
this.addAccessStatus$.next(e);
|
||||||
|
}
|
||||||
|
|
||||||
get loading() {
|
get loading() {
|
||||||
return this.refreshing || this.processingEvent;
|
return this.refreshing || this.processingEvent;
|
||||||
}
|
}
|
||||||
@ -692,13 +826,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
map((c) => {
|
map((c) => {
|
||||||
return c.sort((a, b) => {
|
return c.sort((a, b) => {
|
||||||
if (
|
if (
|
||||||
a.canEditItems(this.organization, true) &&
|
a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) &&
|
||||||
!b.canEditItems(this.organization, true)
|
!b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled)
|
||||||
) {
|
) {
|
||||||
return -1;
|
return -1;
|
||||||
} else if (
|
} else if (
|
||||||
!a.canEditItems(this.organization, true) &&
|
!a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) &&
|
||||||
b.canEditItems(this.organization, true)
|
b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled)
|
||||||
) {
|
) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
@ -714,33 +848,14 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const dialog = openOrgVaultCollectionsDialog(this.dialogService, {
|
const dialog = openOrgVaultCollectionsDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
collectionIds: cipher.collectionIds,
|
collectionIds: cipher.collectionIds,
|
||||||
collections: collections.filter((c) => !c.readOnly && c.id != Unassigned),
|
collections: collections,
|
||||||
organization: this.organization,
|
organization: this.organization,
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
/**
|
|
||||||
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
|
||||||
CollectionsComponent,
|
|
||||||
this.collectionsModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled;
|
|
||||||
comp.collectionIds = cipher.collectionIds;
|
|
||||||
comp.collections = collections;
|
|
||||||
comp.organization = this.organization;
|
|
||||||
comp.cipherId = cipher.id;
|
|
||||||
comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
||||||
modal.close();
|
|
||||||
this.refresh();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
if ((await lastValueFrom(dialog.closed)) == CollectionsDialogResult.Saved) {
|
if ((await lastValueFrom(dialog.closed)) == CollectionsDialogResult.Saved) {
|
||||||
await this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1178,7 +1293,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected deleteCipherWithServer(id: string, permanent: boolean) {
|
protected deleteCipherWithServer(id: string, permanent: boolean) {
|
||||||
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
const asAdmin = this.organization?.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccessEnabled,
|
||||||
|
);
|
||||||
return permanent
|
return permanent
|
||||||
? this.cipherService.deleteWithServer(id, asAdmin)
|
? this.cipherService.deleteWithServer(id, asAdmin)
|
||||||
: this.cipherService.softDeleteWithServer(id, asAdmin);
|
: this.cipherService.softDeleteWithServer(id, asAdmin);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { BreadcrumbsModule, NoItemsModule } from "@bitwarden/components";
|
import { BreadcrumbsModule, NoItemsModule, SearchModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { LooseComponentsModule } from "../../shared/loose-components.module";
|
import { LooseComponentsModule } from "../../shared/loose-components.module";
|
||||||
import { SharedModule } from "../../shared/shared.module";
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
@ -32,6 +32,7 @@ import { VaultComponent } from "./vault.component";
|
|||||||
CollectionDialogModule,
|
CollectionDialogModule,
|
||||||
CollectionAccessRestrictedComponent,
|
CollectionAccessRestrictedComponent,
|
||||||
NoItemsModule,
|
NoItemsModule,
|
||||||
|
SearchModule,
|
||||||
],
|
],
|
||||||
declarations: [VaultComponent, VaultHeaderComponent],
|
declarations: [VaultComponent, VaultHeaderComponent],
|
||||||
exports: [VaultComponent],
|
exports: [VaultComponent],
|
||||||
|
@ -2788,6 +2788,12 @@
|
|||||||
"all": {
|
"all": {
|
||||||
"message": "All"
|
"message": "All"
|
||||||
},
|
},
|
||||||
|
"addAccess": {
|
||||||
|
"message": "Add Access"
|
||||||
|
},
|
||||||
|
"addAccessFilter": {
|
||||||
|
"message": "Add Access Filter"
|
||||||
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"message": "Refresh"
|
"message": "Refresh"
|
||||||
},
|
},
|
||||||
|
@ -62,7 +62,7 @@
|
|||||||
{{ "addOrganization" | i18n }}
|
{{ "addOrganization" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||||
{{ "close" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-dialog>
|
</bit-dialog>
|
||||||
|
@ -2,6 +2,8 @@ import { Directive, EventEmitter, Input, OnInit, Output } 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";
|
||||||
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -23,6 +25,7 @@ export class CollectionsComponent implements OnInit {
|
|||||||
collections: CollectionView[] = [];
|
collections: CollectionView[] = [];
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
flexibleCollectionsV1Enabled: boolean;
|
flexibleCollectionsV1Enabled: boolean;
|
||||||
|
restrictProviderAccess: boolean;
|
||||||
|
|
||||||
protected cipherDomain: Cipher;
|
protected cipherDomain: Cipher;
|
||||||
|
|
||||||
@ -33,9 +36,16 @@ export class CollectionsComponent implements OnInit {
|
|||||||
protected cipherService: CipherService,
|
protected cipherService: CipherService,
|
||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.FlexibleCollectionsV1,
|
||||||
|
);
|
||||||
|
this.restrictProviderAccess = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.RestrictProviderAccess,
|
||||||
|
);
|
||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +72,12 @@ export class CollectionsComponent implements OnInit {
|
|||||||
async submit(): Promise<boolean> {
|
async submit(): Promise<boolean> {
|
||||||
const selectedCollectionIds = this.collections
|
const selectedCollectionIds = this.collections
|
||||||
.filter((c) => {
|
.filter((c) => {
|
||||||
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (
|
||||||
|
this.organization.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return !!(c as any).checked;
|
return !!(c as any).checked;
|
||||||
} else {
|
} else {
|
||||||
return !!(c as any).checked && c.readOnly == null;
|
return !!(c as any).checked && c.readOnly == null;
|
||||||
|
@ -3,7 +3,6 @@ import { Observable, Subject } from "rxjs";
|
|||||||
|
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
@ -24,7 +23,7 @@ export class SafeInjectionToken<T> extends InjectionToken<T> {
|
|||||||
|
|
||||||
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");
|
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");
|
||||||
export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken<
|
export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken<
|
||||||
AbstractMemoryStorageService & ObservableStorageService
|
AbstractStorageService & ObservableStorageService
|
||||||
>("OBSERVABLE_MEMORY_STORAGE");
|
>("OBSERVABLE_MEMORY_STORAGE");
|
||||||
export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken<
|
export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken<
|
||||||
AbstractStorageService & ObservableStorageService
|
AbstractStorageService & ObservableStorageService
|
||||||
@ -32,9 +31,7 @@ export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken<
|
|||||||
export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken<
|
export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken<
|
||||||
AbstractStorageService & ObservableStorageService
|
AbstractStorageService & ObservableStorageService
|
||||||
>("OBSERVABLE_DISK_LOCAL_STORAGE");
|
>("OBSERVABLE_DISK_LOCAL_STORAGE");
|
||||||
export const MEMORY_STORAGE = new SafeInjectionToken<AbstractMemoryStorageService>(
|
export const MEMORY_STORAGE = new SafeInjectionToken<AbstractStorageService>("MEMORY_STORAGE");
|
||||||
"MEMORY_STORAGE",
|
|
||||||
);
|
|
||||||
export const SECURE_STORAGE = new SafeInjectionToken<AbstractStorageService>("SECURE_STORAGE");
|
export const SECURE_STORAGE = new SafeInjectionToken<AbstractStorageService>("SECURE_STORAGE");
|
||||||
export const STATE_FACTORY = new SafeInjectionToken<StateFactory>("STATE_FACTORY");
|
export const STATE_FACTORY = new SafeInjectionToken<StateFactory>("STATE_FACTORY");
|
||||||
export const LOGOUT_CALLBACK = new SafeInjectionToken<
|
export const LOGOUT_CALLBACK = new SafeInjectionToken<
|
||||||
|
@ -91,6 +91,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
private previousCipherId: string;
|
private previousCipherId: string;
|
||||||
|
|
||||||
protected flexibleCollectionsV1Enabled = false;
|
protected flexibleCollectionsV1Enabled = false;
|
||||||
|
protected restrictProviderAccess = false;
|
||||||
|
|
||||||
get fido2CredentialCreationDateValue(): string {
|
get fido2CredentialCreationDateValue(): string {
|
||||||
const dateCreated = this.i18nService.t("dateCreated");
|
const dateCreated = this.i18nService.t("dateCreated");
|
||||||
@ -183,6 +184,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
||||||
FeatureFlag.FlexibleCollectionsV1,
|
FeatureFlag.FlexibleCollectionsV1,
|
||||||
);
|
);
|
||||||
|
this.restrictProviderAccess = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.RestrictProviderAccess,
|
||||||
|
);
|
||||||
|
|
||||||
this.policyService
|
this.policyService
|
||||||
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
||||||
@ -668,11 +672,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
protected saveCipher(cipher: Cipher) {
|
protected saveCipher(cipher: Cipher) {
|
||||||
const isNotClone = this.editMode && !this.cloneMode;
|
const isNotClone = this.editMode && !this.cloneMode;
|
||||||
let orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
let orgAdmin = this.organization?.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
);
|
||||||
|
|
||||||
// if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection
|
// if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection
|
||||||
if (!cipher.collectionIds) {
|
if (!cipher.collectionIds) {
|
||||||
orgAdmin = this.organization?.canEditUnassignedCiphers();
|
orgAdmin = this.organization?.canEditUnassignedCiphers(this.restrictProviderAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.cipher.id == null
|
return this.cipher.id == null
|
||||||
@ -681,14 +688,20 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected deleteCipher() {
|
protected deleteCipher() {
|
||||||
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
const asAdmin = this.organization?.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
);
|
||||||
return this.cipher.isDeleted
|
return this.cipher.isDeleted
|
||||||
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
|
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
|
||||||
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
|
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected restoreCipher() {
|
protected restoreCipher() {
|
||||||
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
const asAdmin = this.organization?.canEditAllCiphers(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
this.restrictProviderAccess,
|
||||||
|
);
|
||||||
return this.cipherService.restoreWithServer(this.cipher.id, asAdmin);
|
return this.cipherService.restoreWithServer(this.cipher.id, asAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,22 +203,32 @@ export class Organization {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
canEditUnassignedCiphers() {
|
canEditUnassignedCiphers(restrictProviderAccessFlagEnabled: boolean) {
|
||||||
// TODO: Update this to exclude Providers if provider access is restricted in AC-1707
|
if (this.isProviderUser) {
|
||||||
|
return !restrictProviderAccessFlagEnabled;
|
||||||
|
}
|
||||||
return this.isAdmin || this.permissions.editAnyCollection;
|
return this.isAdmin || this.permissions.editAnyCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) {
|
canEditAllCiphers(
|
||||||
|
flexibleCollectionsV1Enabled: boolean,
|
||||||
|
restrictProviderAccessFlagEnabled: boolean,
|
||||||
|
) {
|
||||||
// Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers
|
// Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers
|
||||||
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) {
|
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled || !this.flexibleCollections) {
|
||||||
return this.isAdmin || this.permissions.editAnyCollection;
|
return this.isAdmin || this.permissions.editAnyCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isProviderUser) {
|
||||||
|
return !restrictProviderAccessFlagEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
|
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
|
||||||
// Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
|
// Custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
|
||||||
return (
|
return (
|
||||||
this.isProviderUser ||
|
|
||||||
(this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) ||
|
(this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) ||
|
||||||
(this.allowAdminAccessToAllCollectionItems && this.isAdmin)
|
(this.allowAdminAccessToAllCollectionItems &&
|
||||||
|
(this.type === OrganizationUserType.Admin || this.type === OrganizationUserType.Owner))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ export enum FeatureFlag {
|
|||||||
UnassignedItemsBanner = "unassigned-items-banner",
|
UnassignedItemsBanner = "unassigned-items-banner",
|
||||||
EnableDeleteProvider = "AC-1218-delete-provider",
|
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||||
ExtensionRefresh = "extension-refresh",
|
ExtensionRefresh = "extension-refresh",
|
||||||
|
RestrictProviderAccess = "restrict-provider-access",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||||
@ -44,6 +45,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.UnassignedItemsBanner]: FALSE,
|
[FeatureFlag.UnassignedItemsBanner]: FALSE,
|
||||||
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
||||||
[FeatureFlag.ExtensionRefresh]: FALSE,
|
[FeatureFlag.ExtensionRefresh]: FALSE,
|
||||||
|
[FeatureFlag.RestrictProviderAccess]: FALSE,
|
||||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||||
|
|
||||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { MemoryStorageOptions, StorageOptions } from "../models/domain/storage-options";
|
import { StorageOptions } from "../models/domain/storage-options";
|
||||||
|
|
||||||
export type StorageUpdateType = "save" | "remove";
|
export type StorageUpdateType = "save" | "remove";
|
||||||
export type StorageUpdate = {
|
export type StorageUpdate = {
|
||||||
@ -24,12 +24,3 @@ export abstract class AbstractStorageService {
|
|||||||
abstract save<T>(key: string, obj: T, options?: StorageOptions): Promise<void>;
|
abstract save<T>(key: string, obj: T, options?: StorageOptions): Promise<void>;
|
||||||
abstract remove(key: string, options?: StorageOptions): Promise<void>;
|
abstract remove(key: string, options?: StorageOptions): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AbstractMemoryStorageService extends AbstractStorageService {
|
|
||||||
// Used to identify the service in the session sync decorator framework
|
|
||||||
static readonly TYPE = "MemoryStorageService";
|
|
||||||
readonly type = AbstractMemoryStorageService.TYPE;
|
|
||||||
|
|
||||||
abstract get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
|
|
||||||
abstract getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { Jsonify } from "type-fest";
|
|
||||||
|
|
||||||
import { HtmlStorageLocation, StorageLocation } from "../../enums";
|
import { HtmlStorageLocation, StorageLocation } from "../../enums";
|
||||||
|
|
||||||
export type StorageOptions = {
|
export type StorageOptions = {
|
||||||
@ -9,5 +7,3 @@ export type StorageOptions = {
|
|||||||
htmlStorageLocation?: HtmlStorageLocation;
|
htmlStorageLocation?: HtmlStorageLocation;
|
||||||
keySuffix?: string;
|
keySuffix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MemoryStorageOptions<T> = StorageOptions & { deserializer?: (obj: Jsonify<T>) => T };
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
import { AbstractMemoryStorageService, StorageUpdate } from "../abstractions/storage.service";
|
import { AbstractStorageService, StorageUpdate } from "../abstractions/storage.service";
|
||||||
|
|
||||||
export class MemoryStorageService extends AbstractMemoryStorageService {
|
export class MemoryStorageService extends AbstractStorageService {
|
||||||
protected store = new Map<string, unknown>();
|
protected store = new Map<string, unknown>();
|
||||||
private updatesSubject = new Subject<StorageUpdate>();
|
private updatesSubject = new Subject<StorageUpdate>();
|
||||||
|
|
||||||
@ -42,8 +42,4 @@ export class MemoryStorageService extends AbstractMemoryStorageService {
|
|||||||
this.updatesSubject.next({ key, updateType: "remove" });
|
this.updatesSubject.next({ key, updateType: "remove" });
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
getBypassCache<T>(key: string): Promise<T> {
|
|
||||||
return this.get<T>(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,7 @@ import {
|
|||||||
InitOptions,
|
InitOptions,
|
||||||
StateService as StateServiceAbstraction,
|
StateService as StateServiceAbstraction,
|
||||||
} from "../abstractions/state.service";
|
} from "../abstractions/state.service";
|
||||||
import {
|
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
} from "../abstractions/storage.service";
|
|
||||||
import { HtmlStorageLocation, StorageLocation } from "../enums";
|
import { HtmlStorageLocation, StorageLocation } from "../enums";
|
||||||
import { StateFactory } from "../factories/state-factory";
|
import { StateFactory } from "../factories/state-factory";
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
@ -61,7 +58,7 @@ export class StateService<
|
|||||||
constructor(
|
constructor(
|
||||||
protected storageService: AbstractStorageService,
|
protected storageService: AbstractStorageService,
|
||||||
protected secureStorageService: AbstractStorageService,
|
protected secureStorageService: AbstractStorageService,
|
||||||
protected memoryStorageService: AbstractMemoryStorageService,
|
protected memoryStorageService: AbstractStorageService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
@ -1111,9 +1108,10 @@ export class StateService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async state(): Promise<State<TGlobalState, TAccount>> {
|
protected async state(): Promise<State<TGlobalState, TAccount>> {
|
||||||
const state = await this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state, {
|
let state = await this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state);
|
||||||
deserializer: (s) => State.fromJSON(s, this.accountDeserializer),
|
if (this.memoryStorageService.valuesRequireDeserialization) {
|
||||||
});
|
state = State.fromJSON(state, this.accountDeserializer);
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
StorageUpdate,
|
StorageUpdate,
|
||||||
} from "../../abstractions/storage.service";
|
} from "../../abstractions/storage.service";
|
||||||
|
|
||||||
export class MemoryStorageService
|
export class MemoryStorageService
|
||||||
extends AbstractMemoryStorageService
|
extends AbstractStorageService
|
||||||
implements ObservableStorageService
|
implements ObservableStorageService
|
||||||
{
|
{
|
||||||
protected store: Record<string, string> = {};
|
protected store: Record<string, string> = {};
|
||||||
@ -49,8 +49,4 @@ export class MemoryStorageService
|
|||||||
this.updatesSubject.next({ key, updateType: "remove" });
|
this.updatesSubject.next({ key, updateType: "remove" });
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
getBypassCache<T>(key: string): Promise<T> {
|
|
||||||
return this.get<T>(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,7 @@ describe("Collection", () => {
|
|||||||
const view = await collection.decrypt();
|
const view = await collection.decrypt();
|
||||||
|
|
||||||
expect(view).toEqual({
|
expect(view).toEqual({
|
||||||
|
addAccess: false,
|
||||||
externalId: "extId",
|
externalId: "extId",
|
||||||
hidePasswords: false,
|
hidePasswords: false,
|
||||||
id: "id",
|
id: "id",
|
||||||
|
@ -17,6 +17,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
readOnly: boolean = null;
|
readOnly: boolean = null;
|
||||||
hidePasswords: boolean = null;
|
hidePasswords: boolean = null;
|
||||||
manage: boolean = null;
|
manage: boolean = null;
|
||||||
|
addAccess: boolean = false;
|
||||||
assigned: boolean = null;
|
assigned: boolean = null;
|
||||||
|
|
||||||
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
||||||
@ -38,7 +39,11 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canEditItems(org: Organization, v1FlexibleCollections: boolean): boolean {
|
canEditItems(
|
||||||
|
org: Organization,
|
||||||
|
v1FlexibleCollections: boolean,
|
||||||
|
restrictProviderAccess: boolean,
|
||||||
|
): boolean {
|
||||||
if (org != null && org.id !== this.organizationId) {
|
if (org != null && org.id !== this.organizationId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Id of the organization provided does not match the org id of the collection.",
|
"Id of the organization provided does not match the org id of the collection.",
|
||||||
@ -47,7 +52,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
|
|
||||||
if (org?.flexibleCollections) {
|
if (org?.flexibleCollections) {
|
||||||
return (
|
return (
|
||||||
org?.canEditAllCiphers(v1FlexibleCollections) ||
|
org?.canEditAllCiphers(v1FlexibleCollections, restrictProviderAccess) ||
|
||||||
this.manage ||
|
this.manage ||
|
||||||
(this.assigned && !this.readOnly)
|
(this.assigned && !this.readOnly)
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user