Sorry, this page isn't available.
diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts
index d0a4376556a..145d7666273 100644
--- a/apps/web/src/app/auth/login/login.component.ts
+++ b/apps/web/src/app/auth/login/login.component.ts
@@ -28,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
+import { UserId } from "@bitwarden/common/types/guid";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { flagEnabled } from "../../../utils/flags";
@@ -129,7 +130,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
}
}
- async goAfterLogIn() {
+ async goAfterLogIn(userId: UserId) {
const masterPassword = this.formGroup.value.masterPassword;
// Check master password against policy
@@ -150,7 +151,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
) {
const policiesData: { [id: string]: PolicyData } = {};
this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p)));
- await this.policyService.replace(policiesData);
+ await this.policyService.replace(policiesData, userId);
await this.router.navigate(["update-password"]);
return;
}
diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html
index e1f5431c45b..8b5ef867cca 100644
--- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html
+++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html
@@ -30,6 +30,8 @@
bitIconButton="bwi-clone"
bitSuffix
type="button"
+ showToast
+ [valueLabel]="'billingSyncKey' | i18n"
[appCopyClick]="clientSecret"
[appA11yTitle]="'copyValue' | i18n"
>
diff --git a/apps/web/src/app/core/web-platform-utils.service.ts b/apps/web/src/app/core/web-platform-utils.service.ts
index dceaaf51d15..dbd0ef593d6 100644
--- a/apps/web/src/app/core/web-platform-utils.service.ts
+++ b/apps/web/src/app/core/web-platform-utils.service.ts
@@ -186,20 +186,6 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
throw new Error("Cannot read from clipboard on web.");
}
- supportsBiometric() {
- return Promise.resolve(false);
- }
-
- authenticateBiometric() {
- return Promise.resolve(false);
- }
-
- biometricsNeedsSetup: () => Promise;
- biometricsSupportsAutoSetup(): Promise {
- throw new Error("Method not implemented.");
- }
- biometricsSetup: () => Promise;
-
supportsSecureStorage() {
return false;
}
diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts
index e1ba6389abf..96de2585532 100644
--- a/apps/web/src/app/settings/preferences.component.ts
+++ b/apps/web/src/app/settings/preferences.component.ts
@@ -167,7 +167,10 @@ export class PreferencesComponent implements OnInit, OnDestroy {
);
return;
}
- const values = this.form.value;
+
+ // must get raw value b/c the vault timeout action is disabled when a policy is applied
+ // which removes the timeout action property and value from the normal form.value.
+ const values = this.form.getRawValue();
const activeAcct = await firstValueFrom(this.accountService.activeAccount$);
diff --git a/apps/web/src/images/logo-white.svg b/apps/web/src/images/logo-white.svg
new file mode 100644
index 00000000000..d9ffdd8e339
--- /dev/null
+++ b/apps/web/src/images/logo-white.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/apps/web/src/index.html b/apps/web/src/index.html
index c3a2c03ed97..ce1a955b88c 100644
--- a/apps/web/src/index.html
+++ b/apps/web/src/index.html
@@ -5,7 +5,7 @@
- Bitwarden Web Vault
+ Bitwarden Web vault
@@ -15,16 +15,14 @@
-
-
-
![Bitwarden]()
-
-
-
+
+
![Bitwarden]()
+
+
diff --git a/apps/web/src/scss/callouts.scss b/apps/web/src/scss/callouts.scss
deleted file mode 100644
index da28d607161..00000000000
--- a/apps/web/src/scss/callouts.scss
+++ /dev/null
@@ -1,79 +0,0 @@
-.callout {
- border-left-width: 5px !important;
- border-radius: $card-inner-border-radius;
- margin-bottom: $alert-margin-bottom;
- padding: $alert-padding-y $alert-padding-x;
- @include themify($themes) {
- background-color: themed("calloutBackground");
- border: 1px solid themed("borderColor");
- color: themed("calloutColor");
- }
-
- .callout-heading {
- margin-top: 0;
- }
-
- h3.callout-heading {
- font-weight: bold;
- text-transform: uppercase;
- }
-
- &.callout-primary {
- @include themify($themes) {
- border-left-color: themed("primary");
- }
- .callout-heading {
- @include themify($themes) {
- color: themed("primary");
- }
- }
- }
-
- &.callout-info {
- @include themify($themes) {
- border-left-color: themed("info");
- }
-
- .callout-heading {
- @include themify($themes) {
- color: themed("info");
- }
- }
- }
-
- &.callout-danger {
- @include themify($themes) {
- border-left-color: themed("danger");
- }
-
- .callout-heading {
- @include themify($themes) {
- color: themed("danger");
- }
- }
- }
-
- &.callout-success {
- @include themify($themes) {
- border-left-color: themed("success");
- }
-
- .callout-heading {
- @include themify($themes) {
- color: themed("success");
- }
- }
- }
-
- &.callout-warning {
- @include themify($themes) {
- border-left-color: themed("warning");
- }
-
- .callout-heading {
- @include themify($themes) {
- color: themed("warning");
- }
- }
- }
-}
diff --git a/apps/web/src/scss/styles.scss b/apps/web/src/scss/styles.scss
index 8fbea200a96..d17181615ca 100644
--- a/apps/web/src/scss/styles.scss
+++ b/apps/web/src/scss/styles.scss
@@ -45,7 +45,6 @@
@import "./base";
@import "./buttons";
-@import "./callouts";
@import "./cards";
@import "./forms";
@import "./modals";
diff --git a/apps/web/src/scss/tailwind.css b/apps/web/src/scss/tailwind.css
index 9c64be63080..1ac7b154011 100644
--- a/apps/web/src/scss/tailwind.css
+++ b/apps/web/src/scss/tailwind.css
@@ -5,9 +5,14 @@
@import "../../../../libs/components/src/tw-theme.css";
/*
- * Duplicated styling from Angular components.
+ * Web specific global styling.
*
- * For use in non Angular pages like the 404 and connectors.
+ * Be mindful of what is added here. Generally use Tailwind classes directly in Angular components.
+ *
+ * Some valid scenarios for adding styles here:
+ *
+ * - Duplicated styling for CL components used in non Angular pages like connectors and 404.
+ * - Shared styles like Logo.
*/
@layer components {
.tw-h1 {
@@ -24,4 +29,35 @@
@apply tw-bg-transparent tw-border-text-muted hover:tw-bg-text-muted hover:tw-border-text-muted hover:!tw-text-contrast disabled:tw-bg-transparent disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60 disabled:tw-cursor-not-allowed;
@apply tw-text-muted !important;
}
+
+ /**
+ * Loading page
+ */
+ body.layout_frontend {
+ /* We apply the background color here since body classes are dynamically added and removed */
+ @apply tw-bg-background-alt !important;
+
+ /* Spinner requires fixed height and width to appear centered */
+ .spinner-container {
+ @apply tw-fixed tw-inset-2/4 -tw-translate-x-1/2 -tw-translate-y-1/2;
+
+ height: 42px;
+ width: 42px;
+ }
+ }
+
+ /**
+ * Logo, used both in loading and on "frontend" pages.
+ */
+ img.new-logo-themed {
+ @apply tw-block;
+
+ width: 128px;
+ }
+ .theme_light img.new-logo-themed {
+ content: url("../images/logo.svg");
+ }
+ .theme_dark img.new-logo-themed {
+ content: url("../images/logo-white.svg");
+ }
}
diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts
index 50eded416b2..400dcfd8891 100644
--- a/libs/angular/src/auth/components/lock.component.ts
+++ b/libs/angular/src/auth/components/lock.component.ts
@@ -30,6 +30,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
+import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
@@ -84,6 +85,7 @@ export class LockComponent implements OnInit, OnDestroy {
protected userVerificationService: UserVerificationService,
protected pinService: PinServiceAbstraction,
protected biometricStateService: BiometricStateService,
+ protected biometricsService: BiometricsService,
protected accountService: AccountService,
protected authService: AuthService,
protected kdfConfigService: KdfConfigService,
@@ -146,6 +148,13 @@ export class LockComponent implements OnInit, OnDestroy {
return !!userKey;
}
+ async isBiometricUnlockAvailable(): Promise
{
+ if (!(await this.biometricsService.supportsBiometric())) {
+ return false;
+ }
+ return this.biometricsService.isBiometricUnlockAvailable();
+ }
+
togglePassword() {
this.showPassword = !this.showPassword;
const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword");
@@ -327,7 +336,7 @@ export class LockComponent implements OnInit, OnDestroy {
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
- this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
+ this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.biometricLock =
(await this.vaultTimeoutSettingsService.isBiometricLockSet()) &&
((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) ||
diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts
index 057d67b1527..40880b514aa 100644
--- a/libs/angular/src/auth/components/login.component.ts
+++ b/libs/angular/src/auth/components/login.component.ts
@@ -23,6 +23,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { UserId } from "@bitwarden/common/types/guid";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
@@ -39,7 +40,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
showPassword = false;
formPromise: Promise;
onSuccessfulLogin: () => Promise;
- onSuccessfulLoginNavigate: () => Promise;
+ onSuccessfulLoginNavigate: (userId: UserId) => Promise;
onSuccessfulLoginTwoFactorNavigate: () => Promise;
onSuccessfulLoginForceResetNavigate: () => Promise;
showLoginWithDevice: boolean;
@@ -185,7 +186,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
if (this.onSuccessfulLoginNavigate != null) {
// 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.onSuccessfulLoginNavigate();
+ this.onSuccessfulLoginNavigate(response.userId);
} else {
this.loginEmailService.clearValues();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
diff --git a/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html
index 7add3f6d35d..f7e57acb7f8 100644
--- a/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html
+++ b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html
@@ -141,7 +141,7 @@
-
+
{{ "makeSureEnoughCredit" | i18n }}
diff --git a/libs/angular/src/components/callout.component.html b/libs/angular/src/components/callout.component.html
index a049d5cb722..7e352fa0ced 100644
--- a/libs/angular/src/components/callout.component.html
+++ b/libs/angular/src/components/callout.component.html
@@ -1,14 +1,5 @@
-
-
-
- {{ title }}
-
-
+
+
{{ enforcedPolicyMessage }}
-
+
diff --git a/libs/angular/src/components/callout.component.ts b/libs/angular/src/components/callout.component.ts
index c595beec196..2fd0878654d 100644
--- a/libs/angular/src/components/callout.component.ts
+++ b/libs/angular/src/components/callout.component.ts
@@ -2,16 +2,19 @@ import { Component, Input, OnInit } from "@angular/core";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { CalloutTypes } from "@bitwarden/components";
+/**
+ * @deprecated use the CL's `CalloutComponent` instead
+ */
@Component({
selector: "app-callout",
templateUrl: "callout.component.html",
})
-export class CalloutComponent implements OnInit {
- @Input() type = "info";
+export class DeprecatedCalloutComponent implements OnInit {
+ @Input() type: CalloutTypes = "info";
@Input() icon: string;
@Input() title: string;
- @Input() clickable: boolean;
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
@Input() enforcedPolicyMessage: string;
@Input() useAlertRole = false;
@@ -26,34 +29,6 @@ export class CalloutComponent implements OnInit {
if (this.enforcedPolicyMessage === undefined) {
this.enforcedPolicyMessage = this.i18nService.t("masterPasswordPolicyInEffect");
}
-
- if (this.type === "warning" || this.type === "danger") {
- if (this.type === "danger") {
- this.calloutStyle = "danger";
- }
- if (this.title === undefined) {
- this.title = this.i18nService.t("warning");
- }
- if (this.icon === undefined) {
- this.icon = "bwi-exclamation-triangle";
- }
- } else if (this.type === "error") {
- this.calloutStyle = "danger";
- if (this.title === undefined) {
- this.title = this.i18nService.t("error");
- }
- if (this.icon === undefined) {
- this.icon = "bwi-error";
- }
- } else if (this.type === "tip") {
- this.calloutStyle = "success";
- if (this.title === undefined) {
- this.title = this.i18nService.t("tip");
- }
- if (this.icon === undefined) {
- this.icon = "bwi-lightbulb";
- }
- }
}
getPasswordScoreAlertDisplay() {
diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts
index da8a4dd4181..755d52c0e77 100644
--- a/libs/angular/src/jslib.module.ts
+++ b/libs/angular/src/jslib.module.ts
@@ -14,6 +14,7 @@ import {
AsyncActionsModule,
AutofocusDirective,
ButtonModule,
+ CalloutModule,
CheckboxModule,
DialogModule,
FormFieldModule,
@@ -29,7 +30,7 @@ import {
} from "@bitwarden/components";
import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component";
-import { CalloutComponent } from "./components/callout.component";
+import { DeprecatedCalloutComponent } from "./components/callout.component";
import { A11yInvalidDirective } from "./directives/a11y-invalid.directive";
import { A11yTitleDirective } from "./directives/a11y-title.directive";
import { ApiActionDirective } from "./directives/api-action.directive";
@@ -72,6 +73,7 @@ import { IconComponent } from "./vault/components/icon.component";
FormFieldModule,
SelectModule,
ButtonModule,
+ CalloutModule,
CheckboxModule,
DialogModule,
TypographyModule,
@@ -88,7 +90,7 @@ import { IconComponent } from "./vault/components/icon.component";
ApiActionDirective,
AutofocusDirective,
BoxRowDirective,
- CalloutComponent,
+ DeprecatedCalloutComponent,
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
@@ -125,7 +127,7 @@ import { IconComponent } from "./vault/components/icon.component";
AutofocusDirective,
ToastModule,
BoxRowDirective,
- CalloutComponent,
+ DeprecatedCalloutComponent,
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
diff --git a/libs/angular/src/platform/abstractions/view-cache.service.ts b/libs/angular/src/platform/abstractions/view-cache.service.ts
new file mode 100644
index 00000000000..0ee09afb812
--- /dev/null
+++ b/libs/angular/src/platform/abstractions/view-cache.service.ts
@@ -0,0 +1,83 @@
+import { Injector, WritableSignal } from "@angular/core";
+import type { FormGroup } from "@angular/forms";
+import type { Jsonify, JsonValue } from "type-fest";
+
+type Deserializer
= {
+ /**
+ * A function to use to safely convert your type from json to your expected type.
+ *
+ * @param jsonValue The JSON object representation of your state.
+ * @returns The fully typed version of your state.
+ */
+ readonly deserializer?: (jsonValue: Jsonify) => T;
+};
+
+type BaseCacheOptions = {
+ /** A unique key for saving the cached value to state */
+ key: string;
+
+ /** An optional injector. Required if the method is called outside of an injection context. */
+ injector?: Injector;
+} & (T extends JsonValue ? Deserializer : Required>);
+
+export type SignalCacheOptions = BaseCacheOptions & {
+ /** The initial value for the signal. */
+ initialValue: T;
+};
+
+/** Extract the value type from a FormGroup */
+type FormValue = TFormGroup["value"];
+
+export type FormCacheOptions = BaseCacheOptions<
+ FormValue
+> & {
+ control: TFormGroup;
+};
+
+/**
+ * Cache for temporary component state
+ *
+ * #### Implementations
+ * - browser extension popup: used to persist UI between popup open and close
+ * - all other clients: noop
+ */
+export abstract class ViewCacheService {
+ /**
+ * Create a signal from a previously cached value. Whenever the signal is updated, the new value is saved to the cache.
+ *
+ * Non browser extension implementations are noop and return a normal signal.
+ *
+ * @returns the created signal
+ *
+ * @example
+ * ```ts
+ * const mySignal = this.viewCacheService.signal({
+ * key: "popup-search-text"
+ * initialValue: ""
+ * });
+ * ```
+ */
+ abstract signal(options: SignalCacheOptions): WritableSignal;
+
+ /**
+ * - Initialize a form from a cached value
+ * - Save form value to cache when it changes
+ * - The form is marked dirty if the restored value is not `undefined`.
+ *
+ * Non browser extension implementations are noop and return the original form group.
+ *
+ * @example
+ * ```ts
+ * this.loginDetailsForm = this.viewCacheService.formGroup({
+ * key: "vault-login-details-form",
+ * control: this.formBuilder.group({
+ * username: [""],
+ * email: [""],
+ * })
+ * });
+ * ```
+ **/
+ abstract formGroup(
+ options: FormCacheOptions,
+ ): TFormGroup;
+}
diff --git a/libs/angular/src/platform/services/noop-view-cache.service.ts b/libs/angular/src/platform/services/noop-view-cache.service.ts
new file mode 100644
index 00000000000..9953e80b3b0
--- /dev/null
+++ b/libs/angular/src/platform/services/noop-view-cache.service.ts
@@ -0,0 +1,33 @@
+import { Injectable, signal, WritableSignal } from "@angular/core";
+import type { FormGroup } from "@angular/forms";
+
+import {
+ FormCacheOptions,
+ SignalCacheOptions,
+ ViewCacheService,
+} from "../abstractions/view-cache.service";
+
+/**
+ * The functionality of the {@link ViewCacheService} is only needed in the browser extension popup,
+ * yet is provided to all clients to make sharing components easier.
+ *
+ * Non-extension clients use this noop implementation.
+ * */
+@Injectable({
+ providedIn: "root",
+})
+export class NoopViewCacheService implements ViewCacheService {
+ /**
+ * Return a normal signal.
+ */
+ signal(options: SignalCacheOptions): WritableSignal {
+ return signal(options.initialValue);
+ }
+
+ /**
+ * Return the original form group.
+ **/
+ formGroup(options: FormCacheOptions): TFormGroup {
+ return options.control;
+ }
+}
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index 0997fb68635..851e02c8e04 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -268,8 +268,10 @@ import {
} from "@bitwarden/vault-export-core";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
+import { ViewCacheService } from "../platform/abstractions/view-cache.service";
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
+import { NoopViewCacheService } from "../platform/services/noop-view-cache.service";
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction";
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
@@ -1290,6 +1292,11 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultRegistrationFinishService,
deps: [CryptoServiceAbstraction, AccountApiServiceAbstraction],
}),
+ safeProvider({
+ provide: ViewCacheService,
+ useExisting: NoopViewCacheService,
+ deps: [],
+ }),
];
@NgModule({
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html
index bd3de51c461..082edf40630 100644
--- a/libs/auth/src/angular/anon-layout/anon-layout.component.html
+++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html
@@ -2,7 +2,7 @@
class="tw-flex tw-min-h-screen tw-w-full tw-mx-auto tw-flex-col tw-gap-7 tw-bg-background-alt tw-px-8 tw-pb-4 tw-text-main"
[ngClass]="{ 'tw-pt-0': decreaseTopPadding, 'tw-pt-8': !decreaseTopPadding }"
>
-
+
diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts
index 9e9efa12bab..b112e5aa2ab 100644
--- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts
@@ -158,7 +158,10 @@ describe("AuthRequestLoginStrategy", () => {
decMasterKeyHash,
mockUserId,
);
- expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
+ expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
+ tokenResponse.key,
+ mockUserId,
+ );
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId);
expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled();
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
@@ -183,7 +186,10 @@ describe("AuthRequestLoginStrategy", () => {
expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled();
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
- expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
+ expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
+ tokenResponse.key,
+ mockUserId,
+ );
expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts
index 9998abb30d3..ae0024d2181 100644
--- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts
@@ -99,7 +99,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
const authRequestCredentials = this.cache.value.authRequestCredentials;
// User now may or may not have a master password
// but set the master key encrypted user key if it exists regardless
- await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
+ await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
if (authRequestCredentials.decryptedUserKey) {
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey, userId);
diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts
index 2065f898be6..ff6bf07af7e 100644
--- a/libs/auth/src/common/login-strategies/login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/login.strategy.ts
@@ -222,7 +222,11 @@ export abstract class LoginStrategy {
),
);
- await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
+ await this.billingAccountProfileStateService.setHasPremium(
+ accountInformation.premium,
+ false,
+ userId,
+ );
return userId;
}
diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts
index 6b9cddd99c5..16614497964 100644
--- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts
+++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts
@@ -172,7 +172,10 @@ describe("UserApiLoginStrategy", () => {
await apiLogInStrategy.logIn(credentials);
- expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
+ expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
+ tokenResponse.key,
+ userId,
+ );
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
});
diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts
index 1faac3f6c75..3b112c79a0f 100644
--- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts
@@ -64,7 +64,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
response: IdentityTokenResponse,
userId: UserId,
): Promise {
- await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
+ await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
if (response.apiUseKeyConnector) {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts
index cd868931f20..666487ecf09 100644
--- a/libs/common/spec/fake-state-provider.ts
+++ b/libs/common/spec/fake-state-provider.ts
@@ -32,7 +32,7 @@ export class FakeGlobalStateProvider implements GlobalStateProvider {
states: Map> = new Map();
get(keyDefinition: KeyDefinition): GlobalState {
this.mock.get(keyDefinition);
- const cacheKey = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
+ const cacheKey = this.cacheKey(keyDefinition);
let result = this.states.get(cacheKey);
if (result == null) {
@@ -53,94 +53,143 @@ export class FakeGlobalStateProvider implements GlobalStateProvider {
return result as GlobalState;
}
+ private cacheKey(keyDefinition: KeyDefinition) {
+ return `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
+ }
+
getFake(keyDefinition: KeyDefinition): FakeGlobalState {
return this.get(keyDefinition) as FakeGlobalState;
}
- mockFor(keyDefinitionKey: string, initialValue?: T): FakeGlobalState {
- if (!this.establishedMocks.has(keyDefinitionKey)) {
- this.establishedMocks.set(keyDefinitionKey, new FakeGlobalState(initialValue));
+ mockFor(keyDefinition: KeyDefinition, initialValue?: T): FakeGlobalState {
+ const cacheKey = this.cacheKey(keyDefinition);
+ if (!this.states.has(cacheKey)) {
+ this.states.set(cacheKey, new FakeGlobalState(initialValue));
}
- return this.establishedMocks.get(keyDefinitionKey) as FakeGlobalState;
+ return this.states.get(cacheKey) as FakeGlobalState;
}
}
export class FakeSingleUserStateProvider implements SingleUserStateProvider {
mock = mock();
- establishedMocks: Map> = new Map();
states: Map> = new Map();
+
+ constructor(
+ readonly updateSyncCallback?: (
+ key: UserKeyDefinition,
+ userId: UserId,
+ newValue: unknown,
+ ) => Promise,
+ ) {}
+
get(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState {
this.mock.get(userId, userKeyDefinition);
- const cacheKey = `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}_${userId}`;
+ const cacheKey = this.cacheKey(userId, userKeyDefinition);
let result = this.states.get(cacheKey);
if (result == null) {
- let fake: FakeSingleUserState;
- // Look for established mock
- if (this.establishedMocks.has(userKeyDefinition.key)) {
- fake = this.establishedMocks.get(userKeyDefinition.key) as FakeSingleUserState;
- } else {
- fake = new FakeSingleUserState(userId);
- }
- fake.keyDefinition = userKeyDefinition;
- result = fake;
+ result = this.buildFakeState(userId, userKeyDefinition);
this.states.set(cacheKey, result);
}
return result as SingleUserState;
}
- getFake(userId: UserId, userKeyDefinition: UserKeyDefinition): FakeSingleUserState {
+ getFake(
+ userId: UserId,
+ userKeyDefinition: UserKeyDefinition,
+ { allowInit }: { allowInit: boolean } = { allowInit: true },
+ ): FakeSingleUserState {
+ if (!allowInit && this.states.get(this.cacheKey(userId, userKeyDefinition)) == null) {
+ return null;
+ }
+
return this.get(userId, userKeyDefinition) as FakeSingleUserState;
}
- mockFor(userId: UserId, keyDefinitionKey: string, initialValue?: T): FakeSingleUserState {
- if (!this.establishedMocks.has(keyDefinitionKey)) {
- this.establishedMocks.set(keyDefinitionKey, new FakeSingleUserState(userId, initialValue));
+ mockFor(
+ userId: UserId,
+ userKeyDefinition: UserKeyDefinition,
+ initialValue?: T,
+ ): FakeSingleUserState {
+ const cacheKey = this.cacheKey(userId, userKeyDefinition);
+ if (!this.states.has(cacheKey)) {
+ this.states.set(cacheKey, this.buildFakeState(userId, userKeyDefinition, initialValue));
}
- return this.establishedMocks.get(keyDefinitionKey) as FakeSingleUserState;
+ return this.states.get(cacheKey) as FakeSingleUserState;
+ }
+
+ private buildFakeState(
+ userId: UserId,
+ userKeyDefinition: UserKeyDefinition,
+ initialValue?: T,
+ ) {
+ const state = new FakeSingleUserState(userId, initialValue, async (...args) => {
+ await this.updateSyncCallback?.(userKeyDefinition, ...args);
+ });
+ state.keyDefinition = userKeyDefinition;
+ return state;
+ }
+
+ private cacheKey(userId: UserId, userKeyDefinition: UserKeyDefinition) {
+ return `${userKeyDefinitionCacheKey(userKeyDefinition)}_${userId}`;
}
}
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
activeUserId$: Observable;
- establishedMocks: Map> = new Map();
-
states: Map> = new Map();
- constructor(public accountService: FakeAccountService) {
+ constructor(
+ public accountService: FakeAccountService,
+ readonly updateSyncCallback?: (
+ key: UserKeyDefinition,
+ userId: UserId,
+ newValue: unknown,
+ ) => Promise,
+ ) {
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id));
}
get(userKeyDefinition: UserKeyDefinition): ActiveUserState {
- const cacheKey = `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`;
+ const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition);
let result = this.states.get(cacheKey);
if (result == null) {
- // Look for established mock
- if (this.establishedMocks.has(userKeyDefinition.key)) {
- result = this.establishedMocks.get(userKeyDefinition.key);
- } else {
- result = new FakeActiveUserState(this.accountService);
- }
- result.keyDefinition = userKeyDefinition;
+ result = this.buildFakeState(userKeyDefinition);
this.states.set(cacheKey, result);
}
return result as ActiveUserState;
}
- getFake(userKeyDefinition: UserKeyDefinition): FakeActiveUserState {
+ getFake(
+ userKeyDefinition: UserKeyDefinition,
+ { allowInit }: { allowInit: boolean } = { allowInit: true },
+ ): FakeActiveUserState {
+ if (!allowInit && this.states.get(userKeyDefinitionCacheKey(userKeyDefinition)) == null) {
+ return null;
+ }
return this.get(userKeyDefinition) as FakeActiveUserState;
}
- mockFor(keyDefinitionKey: string, initialValue?: T): FakeActiveUserState {
- if (!this.establishedMocks.has(keyDefinitionKey)) {
- this.establishedMocks.set(
- keyDefinitionKey,
- new FakeActiveUserState(this.accountService, initialValue),
- );
+ mockFor(userKeyDefinition: UserKeyDefinition, initialValue?: T): FakeActiveUserState {
+ const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition);
+ if (!this.states.has(cacheKey)) {
+ this.states.set(cacheKey, this.buildFakeState(userKeyDefinition, initialValue));
}
- return this.establishedMocks.get(keyDefinitionKey) as FakeActiveUserState;
+ return this.states.get(cacheKey) as FakeActiveUserState;
}
+
+ private buildFakeState(userKeyDefinition: UserKeyDefinition, initialValue?: T) {
+ const state = new FakeActiveUserState(this.accountService, initialValue, async (...args) => {
+ await this.updateSyncCallback?.(userKeyDefinition, ...args);
+ });
+ state.keyDefinition = userKeyDefinition;
+ return state;
+ }
+}
+
+function userKeyDefinitionCacheKey(userKeyDefinition: UserKeyDefinition) {
+ return `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`;
}
export class FakeStateProvider implements StateProvider {
@@ -207,9 +256,35 @@ export class FakeStateProvider implements StateProvider {
constructor(public accountService: FakeAccountService) {}
+ private distributeSingleUserUpdate(
+ key: UserKeyDefinition,
+ userId: UserId,
+ newState: unknown,
+ ) {
+ if (this.activeUser.accountService.activeUserId === userId) {
+ const state = this.activeUser.getFake(key, { allowInit: false });
+ state?.nextState(newState, { syncValue: false });
+ }
+ }
+
+ private distributeActiveUserUpdate(
+ key: UserKeyDefinition,
+ userId: UserId,
+ newState: unknown,
+ ) {
+ this.singleUser
+ .getFake(userId, key, { allowInit: false })
+ ?.nextState(newState, { syncValue: false });
+ }
+
global: FakeGlobalStateProvider = new FakeGlobalStateProvider();
- singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider();
- activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(this.accountService);
+ singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider(
+ this.distributeSingleUserUpdate.bind(this),
+ );
+ activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(
+ this.accountService,
+ this.distributeActiveUserUpdate.bind(this),
+ );
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
activeUserId$: Observable = this.activeUser.activeUserId$;
}
diff --git a/libs/common/spec/fake-state.ts b/libs/common/spec/fake-state.ts
index 0f2a09d9c1b..2400e470d42 100644
--- a/libs/common/spec/fake-state.ts
+++ b/libs/common/spec/fake-state.ts
@@ -1,4 +1,4 @@
-import { Observable, ReplaySubject, concatMap, firstValueFrom, map, timeout } from "rxjs";
+import { Observable, ReplaySubject, concatMap, filter, firstValueFrom, map, timeout } from "rxjs";
import {
DerivedState,
@@ -41,6 +41,10 @@ export class FakeGlobalState implements GlobalState {
this.stateSubject.next(initialValue ?? null);
}
+ nextState(state: T) {
+ this.stateSubject.next(state);
+ }
+
async update(
configureState: (state: T, dependency: TCombine) => T,
options?: StateUpdateOptions,
@@ -89,7 +93,10 @@ export class FakeGlobalState implements GlobalState {
export class FakeSingleUserState implements SingleUserState {
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
- stateSubject = new ReplaySubject>(1);
+ stateSubject = new ReplaySubject<{
+ syncValue: boolean;
+ combinedState: CombinedState;
+ }>(1);
state$: Observable;
combinedState$: Observable>;
@@ -97,15 +104,28 @@ export class FakeSingleUserState implements SingleUserState {
constructor(
readonly userId: UserId,
initialValue?: T,
+ updateSyncCallback?: (userId: UserId, newValue: T) => Promise,
) {
- this.stateSubject.next([userId, initialValue ?? null]);
+ // Inform the state provider of updates to keep active user states in sync
+ this.stateSubject
+ .pipe(
+ filter((next) => next.syncValue),
+ concatMap(async ({ combinedState }) => {
+ await updateSyncCallback?.(...combinedState);
+ }),
+ )
+ .subscribe();
+ this.nextState(initialValue ?? null, { syncValue: initialValue != null });
- this.combinedState$ = this.stateSubject.asObservable();
+ this.combinedState$ = this.stateSubject.pipe(map((v) => v.combinedState));
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
}
- nextState(state: T) {
- this.stateSubject.next([this.userId, state]);
+ nextState(state: T, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
+ this.stateSubject.next({
+ syncValue,
+ combinedState: [this.userId, state],
+ });
}
async update(
@@ -122,7 +142,7 @@ export class FakeSingleUserState implements SingleUserState {
return current;
}
const newState = configureState(current, combinedDependencies);
- this.stateSubject.next([this.userId, newState]);
+ this.nextState(newState);
this.nextMock(newState);
return newState;
}
@@ -146,7 +166,10 @@ export class FakeActiveUserState implements ActiveUserState {
[activeMarker]: true;
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
- stateSubject = new ReplaySubject>(1);
+ stateSubject = new ReplaySubject<{
+ syncValue: boolean;
+ combinedState: CombinedState;
+ }>(1);
state$: Observable;
combinedState$: Observable>;
@@ -154,10 +177,18 @@ export class FakeActiveUserState implements ActiveUserState {
constructor(
private accountService: FakeAccountService,
initialValue?: T,
+ updateSyncCallback?: (userId: UserId, newValue: T) => Promise,
) {
- this.stateSubject.next([accountService.activeUserId, initialValue ?? null]);
+ // Inform the state provider of updates to keep single user states in sync
+ this.stateSubject.pipe(
+ filter((next) => next.syncValue),
+ concatMap(async ({ combinedState }) => {
+ await updateSyncCallback?.(...combinedState);
+ }),
+ );
+ this.nextState(initialValue ?? null, { syncValue: initialValue != null });
- this.combinedState$ = this.stateSubject.asObservable();
+ this.combinedState$ = this.stateSubject.pipe(map((v) => v.combinedState));
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
}
@@ -165,8 +196,11 @@ export class FakeActiveUserState implements ActiveUserState {
return this.accountService.activeUserId;
}
- nextState(state: T) {
- this.stateSubject.next([this.userId, state]);
+ nextState(state: T, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
+ this.stateSubject.next({
+ syncValue,
+ combinedState: [this.userId, state],
+ });
}
async update(
@@ -183,7 +217,7 @@ export class FakeActiveUserState implements ActiveUserState {
return [this.userId, current];
}
const newState = configureState(current, combinedDependencies);
- this.stateSubject.next([this.userId, newState]);
+ this.nextState(newState);
this.nextMock([this.userId, newState]);
return [this.userId, newState];
}
diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts
index 21669f78ad2..1067c242346 100644
--- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts
+++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts
@@ -77,5 +77,5 @@ export abstract class PolicyService {
export abstract class InternalPolicyService extends PolicyService {
upsert: (policy: PolicyData) => Promise;
- replace: (policies: { [id: string]: PolicyData }) => Promise;
+ replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise;
}
diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts
index 88264d1c3b7..d9802db9e38 100644
--- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts
+++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts
@@ -20,6 +20,7 @@ import { POLICIES, PolicyService } from "../../../admin-console/services/policy/
import { PolicyId, UserId } from "../../../types/guid";
describe("PolicyService", () => {
+ const userId = "userId" as UserId;
let stateProvider: FakeStateProvider;
let organizationService: MockProxy;
let activeUserState: FakeActiveUserState>;
@@ -27,7 +28,7 @@ describe("PolicyService", () => {
let policyService: PolicyService;
beforeEach(() => {
- const accountService = mockAccountServiceWith("userId" as UserId);
+ const accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
organizationService = mock();
@@ -95,9 +96,12 @@ describe("PolicyService", () => {
]),
);
- await policyService.replace({
- "2": policyData("2", "test-organization", PolicyType.DisableSend, true),
- });
+ await policyService.replace(
+ {
+ "2": policyData("2", "test-organization", PolicyType.DisableSend, true),
+ },
+ userId,
+ );
expect(await firstValueFrom(policyService.policies$)).toEqual([
{
diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts
index 2287ef9b4f4..f52d061ad9c 100644
--- a/libs/common/src/admin-console/services/policy/policy.service.ts
+++ b/libs/common/src/admin-console/services/policy/policy.service.ts
@@ -219,8 +219,8 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
});
}
- async replace(policies: { [id: string]: PolicyData }): Promise {
- await this.activeUserPolicyState.update(() => policies);
+ async replace(policies: { [id: string]: PolicyData }, userId: UserId): Promise {
+ await this.stateProvider.setUserState(POLICIES, policies, userId);
}
/**
diff --git a/libs/common/src/auth/abstractions/key-connector.service.ts b/libs/common/src/auth/abstractions/key-connector.service.ts
index b1b6727cd1b..26335ced489 100644
--- a/libs/common/src/auth/abstractions/key-connector.service.ts
+++ b/libs/common/src/auth/abstractions/key-connector.service.ts
@@ -4,17 +4,17 @@ import { IdentityTokenResponse } from "../models/response/identity-token.respons
export abstract class KeyConnectorService {
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise;
- getManagingOrganization: () => Promise;
- getUsesKeyConnector: () => Promise;
- migrateUser: () => Promise;
- userNeedsMigration: () => Promise;
+ getManagingOrganization: (userId?: UserId) => Promise;
+ getUsesKeyConnector: (userId: UserId) => Promise;
+ migrateUser: (userId?: UserId) => Promise;
+ userNeedsMigration: (userId: UserId) => Promise;
convertNewSsoUserToKeyConnector: (
tokenResponse: IdentityTokenResponse,
orgId: string,
userId: UserId,
) => Promise;
- setUsesKeyConnector: (enabled: boolean) => Promise;
- setConvertAccountRequired: (status: boolean) => Promise;
+ setUsesKeyConnector: (enabled: boolean, userId: UserId) => Promise;
+ setConvertAccountRequired: (status: boolean, userId?: UserId) => Promise;
getConvertAccountRequired: () => Promise;
- removeConvertAccountRequired: () => Promise;
+ removeConvertAccountRequired: (userId?: UserId) => Promise;
}
diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts
index c86b5f1ee39..9239a0db543 100644
--- a/libs/common/src/auth/abstractions/token.service.ts
+++ b/libs/common/src/auth/abstractions/token.service.ts
@@ -148,10 +148,11 @@ export abstract class TokenService {
/**
* Decodes the access token.
- * @param token The access token to decode.
+ * @param tokenOrUserId The access token to decode or the user id to retrieve the access token for, and then decode.
+ * If null, the currently active user's token is used.
* @returns A promise that resolves with the decoded access token.
*/
- decodeAccessToken: (token?: string) => Promise;
+ decodeAccessToken: (tokenOrUserId?: string | UserId) => Promise;
/**
* Gets the expiration date for the access token. Returns if token can't be decoded or has no expiration
@@ -212,9 +213,10 @@ export abstract class TokenService {
/**
* Gets whether or not the user authenticated via an external mechanism.
+ * @param userId The optional user id to check for external authN status; if not provided, the active user is used.
* @returns A promise that resolves with a boolean representing the user's external authN status.
*/
- getIsExternal: () => Promise;
+ getIsExternal: (userId: UserId) => Promise;
/** Gets the active or passed in user's security stamp */
getSecurityStamp: (userId?: UserId) => Promise;
diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts
index 0fc0267a533..5d1aff45f60 100644
--- a/libs/common/src/auth/services/key-connector.service.spec.ts
+++ b/libs/common/src/auth/services/key-connector.service.spec.ts
@@ -78,9 +78,9 @@ describe("KeyConnectorService", () => {
const newValue = true;
- await keyConnectorService.setUsesKeyConnector(newValue);
+ await keyConnectorService.setUsesKeyConnector(newValue, mockUserId);
- expect(await keyConnectorService.getUsesKeyConnector()).toBe(newValue);
+ expect(await keyConnectorService.getUsesKeyConnector(mockUserId)).toBe(newValue);
});
});
@@ -185,7 +185,7 @@ describe("KeyConnectorService", () => {
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
state.nextState(false);
- const result = await keyConnectorService.userNeedsMigration();
+ const result = await keyConnectorService.userNeedsMigration(mockUserId);
expect(result).toBe(true);
});
@@ -197,7 +197,7 @@ describe("KeyConnectorService", () => {
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
state.nextState(true);
- const result = await keyConnectorService.userNeedsMigration();
+ const result = await keyConnectorService.userNeedsMigration(mockUserId);
expect(result).toBe(false);
});
diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts
index 8f204e557ed..ad9b7081cdf 100644
--- a/libs/common/src/auth/services/key-connector.service.ts
+++ b/libs/common/src/auth/services/key-connector.service.ts
@@ -69,25 +69,25 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
);
}
- async setUsesKeyConnector(usesKeyConnector: boolean) {
- await this.usesKeyConnectorState.update(() => usesKeyConnector);
+ async setUsesKeyConnector(usesKeyConnector: boolean, userId: UserId) {
+ await this.stateProvider.getUser(userId, USES_KEY_CONNECTOR).update(() => usesKeyConnector);
}
- getUsesKeyConnector(): Promise {
- return firstValueFrom(this.usesKeyConnectorState.state$);
+ getUsesKeyConnector(userId: UserId): Promise {
+ return firstValueFrom(this.stateProvider.getUserState$(USES_KEY_CONNECTOR, userId));
}
- async userNeedsMigration() {
- const loggedInUsingSso = await this.tokenService.getIsExternal();
- const requiredByOrganization = (await this.getManagingOrganization()) != null;
- const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector());
+ async userNeedsMigration(userId: UserId) {
+ const loggedInUsingSso = await this.tokenService.getIsExternal(userId);
+ const requiredByOrganization = (await this.getManagingOrganization(userId)) != null;
+ const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector(userId));
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
}
- async migrateUser() {
- const organization = await this.getManagingOrganization();
- const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
+ async migrateUser(userId?: UserId) {
+ userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
+ const organization = await this.getManagingOrganization(userId);
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
@@ -115,8 +115,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
}
}
- async getManagingOrganization(): Promise {
- const orgs = await this.organizationService.getAll();
+ async getManagingOrganization(userId?: UserId): Promise {
+ const orgs = await this.organizationService.getAll(userId);
return orgs.find(
(o) =>
o.keyConnectorEnabled &&
@@ -178,16 +178,16 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
}
- async setConvertAccountRequired(status: boolean) {
- await this.convertAccountToKeyConnectorState.update(() => status);
+ async setConvertAccountRequired(status: boolean, userId?: UserId) {
+ await this.stateProvider.setUserState(CONVERT_ACCOUNT_TO_KEY_CONNECTOR, status, userId);
}
getConvertAccountRequired(): Promise {
return firstValueFrom(this.convertAccountToKeyConnectorState.state$);
}
- async removeConvertAccountRequired() {
- await this.setConvertAccountRequired(null);
+ async removeConvertAccountRequired(userId?: UserId) {
+ await this.setConvertAccountRequired(null, userId);
}
private handleKeyConnectorError(e: any) {
diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts
index 4be945de5f8..f8882e1b118 100644
--- a/libs/common/src/auth/services/token.service.spec.ts
+++ b/libs/common/src/auth/services/token.service.spec.ts
@@ -126,7 +126,7 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
// Act
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
@@ -139,11 +139,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
// Act
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
@@ -156,7 +156,7 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]);
+ .nextState("encryptedAccessToken");
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
@@ -282,7 +282,7 @@ describe("TokenService", () => {
// For testing purposes, let's assume that the access token is already in memory
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
keyGenerationService.createKey.mockResolvedValue(accessTokenKey);
@@ -411,9 +411,7 @@ describe("TokenService", () => {
it("returns null when no access token is found in memory, disk, or secure storage", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getAccessToken();
@@ -429,18 +427,16 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
// Need to have global active id set to the user id
if (!userId) {
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// Act
@@ -459,17 +455,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
// Need to have global active id set to the user id
if (!userId) {
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// Act
@@ -498,20 +492,18 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]);
+ .nextState("encryptedAccessToken");
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
encryptService.decryptToUtf8.mockResolvedValue("decryptedAccessToken");
// Need to have global active id set to the user id
if (!userId) {
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// Act
@@ -534,17 +526,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
// Need to have global active id set to the user id
if (!userId) {
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// No access token key set
@@ -564,11 +554,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]);
+ .nextState(encryptedAccessToken);
// No access token key set
@@ -596,11 +586,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]);
+ .nextState(encryptedAccessToken);
// Mock linux secure storage error
const secureStorageError = "Secure storage error";
@@ -655,17 +645,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
// Need to have global active id set to the user id
if (!userId) {
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// Act
@@ -688,8 +676,32 @@ describe("TokenService", () => {
});
describe("decodeAccessToken", () => {
+ it("retrieves the requested user's token when the passed in parameter is a Guid", async () => {
+ // Arrange
+ tokenService.getAccessToken = jest.fn().mockResolvedValue(accessTokenJwt);
+
+ // Act
+ const result = await tokenService.decodeAccessToken(userIdFromAccessToken);
+
+ // Assert
+ expect(result).toEqual(accessTokenDecoded);
+ expect(tokenService.getAccessToken).toHaveBeenCalledWith(userIdFromAccessToken);
+ });
+
+ it("decodes the given token when a string is passed in that is not a Guid", async () => {
+ // Arrange
+ tokenService.getAccessToken = jest.fn();
+
+ // Act
+ const result = await tokenService.decodeAccessToken(accessTokenJwt);
+
+ // Assert
+ expect(result).toEqual(accessTokenDecoded);
+ expect(tokenService.getAccessToken).not.toHaveBeenCalled();
+ });
+
it("throws an error when no access token is provided or retrievable from state", async () => {
- // Access
+ // Arrange
tokenService.getAccessToken = jest.fn().mockResolvedValue(null);
// Act
@@ -1194,7 +1206,7 @@ describe("TokenService", () => {
// Act
// note: don't await here because we want to test the error
- const result = tokenService.getIsExternal();
+ const result = tokenService.getIsExternal(null);
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
@@ -1210,7 +1222,7 @@ describe("TokenService", () => {
.mockResolvedValue(accessTokenDecodedWithoutExternalAmr);
// Act
- const result = await tokenService.getIsExternal();
+ const result = await tokenService.getIsExternal(null);
// Assert
expect(result).toEqual(false);
@@ -1227,11 +1239,22 @@ describe("TokenService", () => {
.mockResolvedValue(accessTokenDecodedWithExternalAmr);
// Act
- const result = await tokenService.getIsExternal();
+ const result = await tokenService.getIsExternal(null);
// Assert
expect(result).toEqual(true);
});
+
+ it("passes the requested userId to decode", async () => {
+ // Arrange
+ tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
+
+ // Act
+ await tokenService.getIsExternal(userIdFromAccessToken);
+
+ // Assert
+ expect(tokenService.decodeAccessToken).toHaveBeenCalledWith(userIdFromAccessToken);
+ });
});
});
});
@@ -1326,11 +1349,11 @@ describe("TokenService", () => {
// For testing purposes, let's assume that the token is already in disk and memory
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
// We immediately call to get the refresh token from secure storage after setting it to ensure it was set.
secureStorageService.get.mockResolvedValue(refreshToken);
@@ -1423,11 +1446,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
+ .nextState(accessTokenJwt);
// Mock linux secure storage error
const secureStorageError = "Secure storage error";
@@ -1480,11 +1503,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]);
+ .nextState(encryptedAccessToken);
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
encryptService.decryptToUtf8.mockRejectedValue(new Error("Decryption error"));
@@ -1520,9 +1543,7 @@ describe("TokenService", () => {
it("returns null when no refresh token is found in memory, disk, or secure storage", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await (tokenService as any).getRefreshToken();
@@ -1535,16 +1556,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
@@ -1557,11 +1576,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
@@ -1575,16 +1594,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
@@ -1596,11 +1613,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
@@ -1619,18 +1636,16 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
secureStorageService.get.mockResolvedValue(refreshToken);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
@@ -1643,11 +1658,11 @@ describe("TokenService", () => {
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
secureStorageService.get.mockResolvedValue(refreshToken);
@@ -1661,11 +1676,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
@@ -1681,16 +1696,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
@@ -1719,11 +1732,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
secureStorageService.get.mockResolvedValue(null);
@@ -1743,11 +1756,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
const secureStorageSvcMockErrorMsg = "Secure storage retrieval error";
@@ -1792,11 +1805,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
- .stateSubject.next([userIdFromAccessToken, refreshToken]);
+ .nextState(refreshToken);
// Act
await (tokenService as any).clearRefreshToken(userIdFromAccessToken);
@@ -1833,9 +1846,7 @@ describe("TokenService", () => {
it("should throw an error if the vault timeout is missing", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = tokenService.setClientId(clientId, VaultTimeoutAction.Lock, null);
@@ -1847,9 +1858,7 @@ describe("TokenService", () => {
it("should throw an error if the vault timeout action is missing", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = tokenService.setClientId(clientId, null, VaultTimeoutStringType.Never);
@@ -1861,9 +1870,7 @@ describe("TokenService", () => {
describe("Memory storage tests", () => {
it("sets the client id in memory when there is an active user in global state", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setClientId(clientId, memoryVaultTimeoutAction, memoryVaultTimeout);
@@ -1895,9 +1902,7 @@ describe("TokenService", () => {
describe("Disk storage tests", () => {
it("sets the client id in disk when there is an active user in global state", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setClientId(clientId, diskVaultTimeoutAction, diskVaultTimeout);
@@ -1935,9 +1940,7 @@ describe("TokenService", () => {
it("returns null when no client id is found in memory or disk", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
@@ -1950,17 +1953,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
- .stateSubject.next([userIdFromAccessToken, clientId]);
+ .nextState(clientId);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
@@ -1973,12 +1974,12 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
- .stateSubject.next([userIdFromAccessToken, clientId]);
+ .nextState(clientId);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
// Act
const result = await tokenService.getClientId(userIdFromAccessToken);
@@ -1992,16 +1993,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
- .stateSubject.next([userIdFromAccessToken, clientId]);
+ .nextState(clientId);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
@@ -2013,11 +2012,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
- .stateSubject.next([userIdFromAccessToken, clientId]);
+ .nextState(clientId);
// Act
const result = await tokenService.getClientId(userIdFromAccessToken);
@@ -2040,11 +2039,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
- .stateSubject.next([userIdFromAccessToken, clientId]);
+ .nextState(clientId);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
- .stateSubject.next([userIdFromAccessToken, clientId]);
+ .nextState(clientId);
// Act
await (tokenService as any).clearClientId(userIdFromAccessToken);
@@ -2062,16 +2061,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
- .stateSubject.next([userIdFromAccessToken, clientId]);
+ .nextState(clientId);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
- .stateSubject.next([userIdFromAccessToken, clientId]);
+ .nextState(clientId);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await (tokenService as any).clearClientId();
@@ -2106,9 +2103,7 @@ describe("TokenService", () => {
it("should throw an error if the vault timeout is missing", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = tokenService.setClientSecret(clientSecret, VaultTimeoutAction.Lock, null);
@@ -2120,9 +2115,7 @@ describe("TokenService", () => {
it("should throw an error if the vault timeout action is missing", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = tokenService.setClientSecret(
@@ -2138,9 +2131,7 @@ describe("TokenService", () => {
describe("Memory storage tests", () => {
it("sets the client secret in memory when there is an active user in global state", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setClientSecret(
@@ -2176,9 +2167,7 @@ describe("TokenService", () => {
describe("Disk storage tests", () => {
it("sets the client secret on disk when there is an active user in global state", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setClientSecret(
@@ -2222,9 +2211,7 @@ describe("TokenService", () => {
it("returns null when no client secret is found in memory or disk", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
@@ -2237,17 +2224,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
- .stateSubject.next([userIdFromAccessToken, clientSecret]);
+ .nextState(clientSecret);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
@@ -2260,12 +2245,12 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
- .stateSubject.next([userIdFromAccessToken, clientSecret]);
+ .nextState(clientSecret);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
// Act
const result = await tokenService.getClientSecret(userIdFromAccessToken);
@@ -2279,16 +2264,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
- .stateSubject.next([userIdFromAccessToken, clientSecret]);
+ .nextState(clientSecret);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
@@ -2300,11 +2283,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
- .stateSubject.next([userIdFromAccessToken, undefined]);
+ .nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
- .stateSubject.next([userIdFromAccessToken, clientSecret]);
+ .nextState(clientSecret);
// Act
const result = await tokenService.getClientSecret(userIdFromAccessToken);
@@ -2327,11 +2310,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
- .stateSubject.next([userIdFromAccessToken, clientSecret]);
+ .nextState(clientSecret);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
- .stateSubject.next([userIdFromAccessToken, clientSecret]);
+ .nextState(clientSecret);
// Act
await (tokenService as any).clearClientSecret(userIdFromAccessToken);
@@ -2351,16 +2334,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
- .stateSubject.next([userIdFromAccessToken, clientSecret]);
+ .nextState(clientSecret);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
- .stateSubject.next([userIdFromAccessToken, clientSecret]);
+ .nextState(clientSecret);
// Need to have global active id set to the user id
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await (tokenService as any).clearClientSecret();
@@ -2634,7 +2615,7 @@ describe("TokenService", () => {
// Arrange
const userId = "userId" as UserId;
- globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).stateSubject.next(userId);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userId);
tokenService.clearAccessToken = jest.fn();
(tokenService as any).clearRefreshToken = jest.fn();
@@ -2693,7 +2674,7 @@ describe("TokenService", () => {
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
- .stateSubject.next(initialTwoFactorTokenRecord);
+ .nextState(initialTwoFactorTokenRecord);
// Act
await tokenService.setTwoFactorToken(email, twoFactorToken);
@@ -2716,7 +2697,7 @@ describe("TokenService", () => {
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
- .stateSubject.next(initialTwoFactorTokenRecord);
+ .nextState(initialTwoFactorTokenRecord);
// Act
const result = await tokenService.getTwoFactorToken(email);
@@ -2734,7 +2715,7 @@ describe("TokenService", () => {
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
- .stateSubject.next(initialTwoFactorTokenRecord);
+ .nextState(initialTwoFactorTokenRecord);
// Act
const result = await tokenService.getTwoFactorToken(email);
@@ -2745,9 +2726,7 @@ describe("TokenService", () => {
it("returns null when there is no two factor token record", async () => {
// Arrange
- globalStateProvider
- .getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
- .stateSubject.next(null);
+ globalStateProvider.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL).nextState(null);
// Act
const result = await tokenService.getTwoFactorToken("testUser");
@@ -2768,7 +2747,7 @@ describe("TokenService", () => {
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
- .stateSubject.next(initialTwoFactorTokenRecord);
+ .nextState(initialTwoFactorTokenRecord);
// Act
await tokenService.clearTwoFactorToken(email);
@@ -2808,9 +2787,7 @@ describe("TokenService", () => {
it("sets the security stamp in memory when there is an active user in global state", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setSecurityStamp(mockSecurityStamp);
@@ -2843,13 +2820,11 @@ describe("TokenService", () => {
it("returns the security stamp from memory when no user id is specified (uses global active user)", async () => {
// Arrange
- globalStateProvider
- .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
- .stateSubject.next(userIdFromAccessToken);
+ globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY)
- .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]);
+ .nextState(mockSecurityStamp);
// Act
const result = await tokenService.getSecurityStamp();
@@ -2862,7 +2837,7 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY)
- .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]);
+ .nextState(mockSecurityStamp);
// Act
const result = await tokenService.getSecurityStamp(userIdFromAccessToken);
diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts
index ef7f23cb05a..c2150bc5c52 100644
--- a/libs/common/src/auth/services/token.service.ts
+++ b/libs/common/src/auth/services/token.service.ts
@@ -9,6 +9,7 @@ import { KeyGenerationService } from "../../platform/abstractions/key-generation
import { LogService } from "../../platform/abstractions/log.service";
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
import { StorageLocation } from "../../platform/enums";
+import { Utils } from "../../platform/misc/utils";
import { EncString, EncryptedString } from "../../platform/models/domain/enc-string";
import { StorageOptions } from "../../platform/models/domain/storage-options";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
@@ -875,8 +876,13 @@ export class TokenService implements TokenServiceAbstraction {
// jwthelper methods
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
- async decodeAccessToken(token?: string): Promise {
- token = token ?? (await this.getAccessToken());
+ async decodeAccessToken(tokenOrUserId?: string | UserId): Promise {
+ let token = tokenOrUserId as string;
+ if (Utils.isGuid(tokenOrUserId)) {
+ token = await this.getAccessToken(tokenOrUserId as UserId);
+ } else {
+ token ??= await this.getAccessToken();
+ }
if (token == null) {
throw new Error("Access token not found.");
@@ -1012,10 +1018,10 @@ export class TokenService implements TokenServiceAbstraction {
return decoded.iss;
}
- async getIsExternal(): Promise {
+ async getIsExternal(userId: UserId): Promise {
let decoded: DecodedAccessToken;
try {
- decoded = await this.decodeAccessToken();
+ decoded = await this.decodeAccessToken(userId);
} catch (error) {
throw new Error("Failed to decode access token: " + error.message);
}
diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts
index 4b36e8d2bfc..7f2e8c31508 100644
--- a/libs/common/src/autofill/services/domain-settings.service.ts
+++ b/libs/common/src/autofill/services/domain-settings.service.ts
@@ -15,6 +15,7 @@ import {
StateProvider,
UserKeyDefinition,
} from "../../platform/state";
+import { UserId } from "../../types/guid";
const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", {
deserializer: (value: boolean) => value ?? true,
@@ -44,7 +45,7 @@ export abstract class DomainSettingsService {
neverDomains$: Observable;
setNeverDomains: (newValue: NeverDomains) => Promise;
equivalentDomains$: Observable;
- setEquivalentDomains: (newValue: EquivalentDomains) => Promise;
+ setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise;
defaultUriMatchStrategy$: Observable;
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise;
getUrlEquivalentDomains: (url: string) => Observable>;
@@ -87,8 +88,8 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
await this.neverDomainsState.update(() => newValue);
}
- async setEquivalentDomains(newValue: EquivalentDomains): Promise {
- await this.equivalentDomainsState.update(() => newValue);
+ async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise {
+ await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue);
}
async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise {
diff --git a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts
index e07dec3cf90..080c61e9ffb 100644
--- a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts
+++ b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts
@@ -1,5 +1,7 @@
import { Observable } from "rxjs";
+import { UserId } from "../../../types/guid";
+
export type BillingAccountProfile = {
hasPremiumPersonally: boolean;
hasPremiumFromAnyOrganization: boolean;
@@ -32,5 +34,6 @@ export abstract class BillingAccountProfileStateService {
abstract setHasPremium(
hasPremiumPersonally: boolean,
hasPremiumFromAnyOrganization: boolean,
+ userId: UserId,
): Promise;
}
diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts
index 7f0f218a239..7e0dee0eedf 100644
--- a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts
+++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts
@@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs";
import {
FakeAccountService,
mockAccountServiceWith,
- FakeActiveUserState,
FakeStateProvider,
FakeSingleUserState,
} from "../../../../spec";
@@ -18,7 +17,6 @@ import {
describe("BillingAccountProfileStateService", () => {
let stateProvider: FakeStateProvider;
let sut: DefaultBillingAccountProfileStateService;
- let billingAccountProfileState: FakeActiveUserState;
let userBillingAccountProfileState: FakeSingleUserState;
let accountService: FakeAccountService;
@@ -30,10 +28,6 @@ describe("BillingAccountProfileStateService", () => {
sut = new DefaultBillingAccountProfileStateService(stateProvider);
- billingAccountProfileState = stateProvider.activeUser.getFake(
- BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
- );
-
userBillingAccountProfileState = stateProvider.singleUser.getFake(
userId,
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
@@ -133,12 +127,11 @@ describe("BillingAccountProfileStateService", () => {
describe("setHasPremium", () => {
it("should update the active users state when called", async () => {
- await sut.setHasPremium(true, false);
+ await sut.setHasPremium(true, false, userId);
- expect(billingAccountProfileState.nextMock).toHaveBeenCalledWith([
- userId,
- { hasPremiumPersonally: true, hasPremiumFromAnyOrganization: false },
- ]);
+ expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false);
+ expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true);
+ expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
});
});
});
diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts
index cf05df2f22b..7d256da9714 100644
--- a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts
+++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts
@@ -6,6 +6,7 @@ import {
StateProvider,
UserKeyDefinition,
} from "../../../platform/state";
+import { UserId } from "../../../types/guid";
import {
BillingAccountProfile,
BillingAccountProfileStateService,
@@ -27,7 +28,7 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP
hasPremiumPersonally$: Observable