1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

merge main

This commit is contained in:
rr-bw
2024-08-30 11:40:21 -07:00
615 changed files with 20276 additions and 5305 deletions

View File

@@ -14,6 +14,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { ToastService } from "@bitwarden/components";
@Directive()
export class CollectionsComponent implements OnInit {
@@ -39,6 +40,7 @@ export class CollectionsComponent implements OnInit {
private logService: LogService,
private configService: ConfigService,
private accountService: AccountService,
private toastService: ToastService,
) {}
async ngOnInit() {
@@ -82,11 +84,11 @@ export class CollectionsComponent implements OnInit {
})
.map((c) => c.id);
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("selectOneCollection"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("selectOneCollection"),
});
return false;
}
this.cipherDomain.collectionIds = selectedCollectionIds;
@@ -94,10 +96,18 @@ export class CollectionsComponent implements OnInit {
this.formPromise = this.saveCollections();
await this.formPromise;
this.onSavedCollections.emit();
this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("editedItem"),
});
return true;
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e.message,
});
return false;
}
}

View File

@@ -39,6 +39,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
enum State {
NewUser,
@@ -104,6 +105,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected accountService: AccountService,
protected toastService: ToastService,
) {}
async ngOnInit() {
@@ -275,11 +277,11 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
await this.apiService.postAccountKeys(keysRequest);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("accountSuccessfullyCreated"),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("accountSuccessfullyCreated"),
});
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);

View File

@@ -6,6 +6,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ToastService } from "@bitwarden/components";
@Directive()
export abstract class CaptchaProtectedComponent {
@@ -17,6 +18,7 @@ export abstract class CaptchaProtectedComponent {
protected environmentService: EnvironmentService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected toastService: ToastService,
) {}
async setupCaptcha() {
@@ -31,10 +33,18 @@ export abstract class CaptchaProtectedComponent {
this.captchaToken = token;
},
(error: string) => {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: error,
});
},
(info: string) => {
this.platformUtilsService.showToast("info", this.i18nService.t("info"), info);
this.toastService.showToast({
variant: "info",
title: this.i18nService.t("info"),
message: info,
});
},
);
}

View File

@@ -15,7 +15,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
@@ -49,6 +49,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
protected kdfConfigService: KdfConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected accountService: AccountService,
protected toastService: ToastService,
) {}
async ngOnInit() {
@@ -127,27 +128,27 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
async strongPassword(): Promise<boolean> {
if (this.masterPassword == null || this.masterPassword === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return false;
}
if (this.masterPassword.length < this.minimumLength) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordMinimumlength", this.minimumLength),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength),
});
return false;
}
if (this.masterPassword !== this.masterPasswordRetype) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPassDoesntMatch"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPassDoesntMatch"),
});
return false;
}
@@ -161,11 +162,11 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
this.enforcedPolicyOptions,
)
) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
});
return false;
}

View File

@@ -7,6 +7,7 @@ import {
} from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { ModalService } from "../../services/modal.service";
@@ -27,6 +28,7 @@ export class EnvironmentComponent {
protected environmentService: EnvironmentService,
protected i18nService: I18nService,
private modalService: ModalService,
private toastService: ToastService,
) {
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
if (env.getRegion() !== Region.SelfHosted) {
@@ -59,7 +61,11 @@ export class EnvironmentComponent {
notifications: this.notificationsUrl,
});
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("environmentSaved"),
});
this.saved();
}

View File

@@ -7,6 +7,7 @@ import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/passw
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
@Directive()
export class HintComponent implements OnInit {
@@ -23,6 +24,7 @@ export class HintComponent implements OnInit {
protected platformUtilsService: PlatformUtilsService,
private logService: LogService,
private loginEmailService: LoginEmailServiceAbstraction,
protected toastService: ToastService,
) {}
ngOnInit(): void {
@@ -31,26 +33,30 @@ export class HintComponent implements OnInit {
async submit() {
if (this.email == null || this.email === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("emailRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("emailRequired"),
});
return;
}
if (this.email.indexOf("@") === -1) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidEmail"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidEmail"),
});
return;
}
try {
this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email));
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("masterPassSent"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("masterPassSent"),
});
if (this.onSuccessfulSubmit != null) {
this.onSuccessfulSubmit();
} else if (this.router != null) {

View File

@@ -30,12 +30,13 @@ 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";
import { UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
@Directive()
export class LockComponent implements OnInit, OnDestroy {
@@ -84,10 +85,12 @@ 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,
protected syncService: SyncService,
protected toastService: ToastService,
) {}
async ngOnInit() {
@@ -146,6 +149,13 @@ export class LockComponent implements OnInit, OnDestroy {
return !!userKey;
}
async isBiometricUnlockAvailable(): Promise<boolean> {
if (!(await this.biometricsService.supportsBiometric())) {
return false;
}
return this.biometricsService.isBiometricUnlockAvailable();
}
togglePassword() {
this.showPassword = !this.showPassword;
const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword");
@@ -158,11 +168,11 @@ export class LockComponent implements OnInit, OnDestroy {
private async handlePinRequiredUnlock() {
if (this.pin == null || this.pin === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("pinRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("pinRequired"),
});
return;
}
@@ -186,36 +196,36 @@ export class LockComponent implements OnInit, OnDestroy {
// Log user out if they have entered an invalid PIN too many times
if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
);
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
});
this.messagingService.send("logout");
return;
}
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidPin"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidPin"),
});
} catch {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("unexpectedError"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("unexpectedError"),
});
}
}
private async handleMasterPasswordRequiredUnlock() {
if (this.masterPassword == null || this.masterPassword === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return;
}
await this.doUnlockWithMasterPassword();
@@ -249,11 +259,11 @@ export class LockComponent implements OnInit, OnDestroy {
}
if (!passwordValid) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidMasterPassword"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidMasterPassword"),
});
return;
}
@@ -327,7 +337,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)) ||

View File

@@ -32,6 +32,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
@@ -88,8 +89,9 @@ export class LoginViaAuthRequestComponent
private deviceTrustService: DeviceTrustServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private loginStrategyService: LoginStrategyServiceAbstraction,
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService);
super(environmentService, i18nService, platformUtilsService, toastService);
// TODO: I don't know why this is necessary.
// Why would the existence of the email depend on the navigation?
@@ -105,7 +107,11 @@ export class LoginViaAuthRequestComponent
.subscribe((id) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => {
this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: e.message,
});
this.logService.error("Failed to use approved auth request: " + e.message);
});
});
@@ -135,7 +141,11 @@ export class LoginViaAuthRequestComponent
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
if (!this.email) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("userEmailMissing"),
});
// 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(["/login-initiated"]);
@@ -158,7 +168,11 @@ export class LoginViaAuthRequestComponent
this.email = this.loginEmailService.getEmail();
if (!this.email) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("userEmailMissing"),
});
// 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(["/login"]);
@@ -402,7 +416,11 @@ export class LoginViaAuthRequestComponent
// TODO: this should eventually be enforced via deleting this on the server once it is used
await this.authRequestService.clearAdminAuthRequest(userId);
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("loginApproved"),
});
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device

View File

@@ -23,6 +23,8 @@ 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 { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
@@ -39,7 +41,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
showPassword = false;
formPromise: Promise<AuthResult>;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: () => Promise<any>;
onSuccessfulLoginNavigate: (userId: UserId) => Promise<any>;
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
showLoginWithDevice: boolean;
@@ -91,8 +93,9 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected webAuthnLoginService: WebAuthnLoginServiceAbstraction,
protected registerRouteService: RegisterRouteService,
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService);
super(environmentService, i18nService, platformUtilsService, toastService);
}
async ngOnInit() {
@@ -136,7 +139,11 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
//desktop, browser; This should be removed once all clients use reactive forms
if (this.formGroup.invalid && showToast) {
const errorText = this.getErrorToastMessage();
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: errorText,
});
return;
}
@@ -187,7 +194,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.
@@ -330,11 +337,11 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
return false;
}
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccured"),
this.i18nService.t("encryptionKeyMigrationRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccured"),
message: this.i18nService.t("encryptionKeyMigrationRequired"),
});
return true;
}

View File

@@ -17,7 +17,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 { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
@@ -97,8 +97,9 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
protected logService: LogService,
protected auditService: AuditService,
protected dialogService: DialogService,
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService);
super(environmentService, i18nService, platformUtilsService, toastService);
this.showTerms = !platformUtilsService.isSelfHost();
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
}
@@ -129,11 +130,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
}
if (this.isInTrialFlow) {
if (!this.accountCreated) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("trialAccountCreated"),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("trialAccountCreated"),
});
}
const loginResponse = await this.logIn(email, masterPassword, this.captchaBypassToken);
if (loginResponse.captchaRequired) {
@@ -141,11 +142,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
}
this.createdAccount.emit(this.formGroup.value.email);
} else {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("newAccountCreated"),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("newAccountCreated"),
});
// 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([this.successRoute], { queryParams: { email: email } });
@@ -210,11 +211,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
this.showErrorSummary = true;
if (this.formGroup.get("acceptPolicies").hasError("required")) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("acceptPoliciesRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("acceptPoliciesRequired"),
});
return { isValid: false };
}
@@ -226,7 +227,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
//desktop, browser
if (this.formGroup.invalid && showToast) {
const errorText = this.getErrorToastMessage();
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: errorText,
});
return { isValid: false };
}

View File

@@ -9,7 +9,7 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
@Directive()
export class RemovePasswordComponent implements OnInit {
@@ -30,6 +30,7 @@ export class RemovePasswordComponent implements OnInit {
private keyConnectorService: KeyConnectorService,
private organizationApiService: OrganizationApiServiceAbstraction,
private dialogService: DialogService,
private toastService: ToastService,
) {}
async ngOnInit() {
@@ -47,17 +48,21 @@ export class RemovePasswordComponent implements OnInit {
try {
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("removedMasterPassword"),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("removedMasterPassword"),
});
await this.keyConnectorService.removeConvertAccountRequired();
// 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([""]);
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e.message,
});
}
};
@@ -76,13 +81,21 @@ export class RemovePasswordComponent implements OnInit {
this.leaving = true;
this.actionPromise = this.organizationApiService.leave(this.organization.id);
await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("leftOrganization"),
});
await this.keyConnectorService.removeConvertAccountRequired();
// 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([""]);
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e,
});
}
};
}

View File

@@ -32,7 +32,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@@ -74,6 +74,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
dialogService: DialogService,
kdfConfigService: KdfConfigService,
private encryptService: EncryptService,
protected toastService: ToastService,
) {
super(
i18nService,
@@ -87,6 +88,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
kdfConfigService,
masterPasswordService,
accountService,
toastService,
);
}
@@ -137,7 +139,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
)
.subscribe({
error: () => {
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
},
});
}
@@ -237,7 +243,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
this.router.navigate([this.successRoute]);
}
} catch {
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
}
}

View File

@@ -28,6 +28,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { SsoComponent } from "./sso.component";
@@ -65,7 +66,7 @@ describe("SsoComponent", () => {
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockStateService: MockProxy<StateService>;
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
let mockToastService: MockProxy<ToastService>;
let mockApiService: MockProxy<ApiService>;
let mockCryptoFunctionService: MockProxy<CryptoFunctionService>;
let mockEnvironmentService: MockProxy<EnvironmentService>;
@@ -75,6 +76,7 @@ describe("SsoComponent", () => {
let mockConfigService: MockProxy<ConfigService>;
let mockMasterPasswordService: FakeMasterPasswordService;
let mockAccountService: FakeAccountService;
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
// Mock authService.logIn params
let code: string;
@@ -117,7 +119,7 @@ describe("SsoComponent", () => {
mockSsoLoginService = mock();
mockStateService = mock();
mockPlatformUtilsService = mock();
mockToastService = mock();
mockApiService = mock();
mockCryptoFunctionService = mock();
mockEnvironmentService = mock();
@@ -127,6 +129,7 @@ describe("SsoComponent", () => {
mockConfigService = mock();
mockAccountService = mockAccountServiceWith(userId);
mockMasterPasswordService = new FakeMasterPasswordService();
mockPlatformUtilsService = mock();
// Mock loginStrategyService.logIn params
code = "code";
@@ -196,7 +199,7 @@ describe("SsoComponent", () => {
{ provide: I18nService, useValue: mockI18nService },
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: StateService, useValue: mockStateService },
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
{ provide: ToastService, useValue: mockToastService },
{ provide: ApiService, useValue: mockApiService },
{ provide: CryptoFunctionService, useValue: mockCryptoFunctionService },
@@ -214,6 +217,7 @@ describe("SsoComponent", () => {
{ provide: ConfigService, useValue: mockConfigService },
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
{ provide: AccountService, useValue: mockAccountService },
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
],
});
@@ -594,12 +598,12 @@ describe("SsoComponent", () => {
expect(mockLogService.error).toHaveBeenCalledTimes(1);
expect(mockLogService.error).toHaveBeenCalledWith(error);
expect(mockPlatformUtilsService.showToast).toHaveBeenCalledTimes(1);
expect(mockPlatformUtilsService.showToast).toHaveBeenCalledWith(
"error",
null,
"ssoKeyConnectorError",
);
expect(mockToastService.showToast).toHaveBeenCalledTimes(1);
expect(mockToastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: null,
message: "ssoKeyConnectorError",
});
expect(mockRouter.navigate).not.toHaveBeenCalled();
});

View File

@@ -26,6 +26,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 { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@Directive()
@@ -71,6 +72,7 @@ export class SsoComponent implements OnInit {
protected configService: ConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected accountService: AccountService,
protected toastService: ToastService,
) {}
async ngOnInit() {
@@ -111,11 +113,11 @@ export class SsoComponent implements OnInit {
async submit(returnUri?: string, includeUserIdentifier?: boolean) {
if (this.identifier == null || this.identifier === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("ssoValidationFailed"),
this.i18nService.t("ssoIdentifierRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("ssoValidationFailed"),
message: this.i18nService.t("ssoIdentifierRequired"),
});
return;
}
@@ -382,11 +384,11 @@ export class SsoComponent implements OnInit {
// TODO: Key Connector Service should pass this error message to the logout callback instead of displaying here
if (e.message === "Key Connector error") {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("ssoKeyConnectorError"),
);
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("ssoKeyConnectorError"),
});
}
}

View File

@@ -20,6 +20,7 @@ import {
TypographyModule,
FormFieldModule,
AsyncActionsModule,
ToastService,
} from "@bitwarden/components";
@Component({
@@ -55,6 +56,7 @@ export class TwoFactorAuthEmailComponent implements OnInit {
protected logService: LogService,
protected apiService: ApiService,
protected appIdService: AppIdService,
private toastService: ToastService,
) {}
async ngOnInit(): Promise<void> {
@@ -74,11 +76,11 @@ export class TwoFactorAuthEmailComponent implements OnInit {
}
if ((await this.loginStrategyService.getEmail()) == null) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("sessionTimeout"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("sessionTimeout"),
});
return;
}
@@ -94,11 +96,11 @@ export class TwoFactorAuthEmailComponent implements OnInit {
this.emailPromise = this.apiService.postTwoFactorEmail(request);
await this.emailPromise;
if (doToast) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail),
});
}
} catch (e) {
this.logService.error(e);

View File

@@ -21,6 +21,7 @@ import {
TypographyModule,
FormFieldModule,
AsyncActionsModule,
ToastService,
} from "@bitwarden/components";
@Component({
@@ -56,6 +57,7 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy {
protected environmentService: EnvironmentService,
protected twoFactorService: TwoFactorService,
protected route: ActivatedRoute,
private toastService: ToastService,
) {
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
@@ -85,11 +87,11 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy {
this.token.emit(token);
},
(error: string) => {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("webauthnCancelOrTimeout"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("webauthnCancelOrTimeout"),
});
},
(info: string) => {
if (info === "ready") {

View File

@@ -1,79 +1,76 @@
<form [bitSubmit]="submitForm" [formGroup]="formGroup" autocomplete="off">
<div class="tw-min-w-96">
<app-two-factor-auth-email
(token)="token = $event"
*ngIf="selectedProviderType === providerType.Email"
/>
<app-two-factor-auth-authenticator
(token)="token = $event"
*ngIf="selectedProviderType === providerType.Authenticator"
/>
<app-two-factor-auth-yubikey
(token)="token = $event"
*ngIf="selectedProviderType === providerType.Yubikey"
/>
<app-two-factor-auth-webauthn
(token)="token = $event; submitForm()"
*ngIf="selectedProviderType === providerType.WebAuthn"
/>
<app-two-factor-auth-duo
(token)="token = $event; submitForm()"
[providerData]="providerData"
<app-two-factor-auth-email
(token)="token = $event"
*ngIf="selectedProviderType === providerType.Email"
/>
<app-two-factor-auth-authenticator
(token)="token = $event"
*ngIf="selectedProviderType === providerType.Authenticator"
/>
<app-two-factor-auth-yubikey
(token)="token = $event"
*ngIf="selectedProviderType === providerType.Yubikey"
/>
<app-two-factor-auth-webauthn
(token)="token = $event; submitForm()"
*ngIf="selectedProviderType === providerType.WebAuthn"
/>
<app-two-factor-auth-duo
(token)="token = $event; submitForm()"
[providerData]="providerData"
*ngIf="
selectedProviderType === providerType.OrganizationDuo ||
selectedProviderType === providerType.Duo
"
#duoComponent
/>
<bit-form-control *ngIf="selectedProviderType != null">
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="remember" />
</bit-form-control>
<ng-container *ngIf="selectedProviderType == null">
<p bitTypography="body1">{{ "noTwoStepProviders" | i18n }}</p>
<p bitTypography="body1">{{ "noTwoStepProviders2" | i18n }}</p>
</ng-container>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
<!-- Buttons -->
<div class="tw-flex tw-flex-col tw-space-y-2.5 tw-mb-3">
<button
type="submit"
buttonType="primary"
bitButton
bitFormButton
*ngIf="
selectedProviderType === providerType.OrganizationDuo ||
selectedProviderType === providerType.Duo
selectedProviderType != null &&
selectedProviderType !== providerType.WebAuthn &&
selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo
"
#duoComponent
/>
<bit-form-control *ngIf="selectedProviderType != null">
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="remember" />
</bit-form-control>
<ng-container *ngIf="selectedProviderType == null">
<p bitTypography="body1">{{ "noTwoStepProviders" | i18n }}</p>
<p bitTypography="body1">{{ "noTwoStepProviders2" | i18n }}</p>
</ng-container>
<hr />
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
<!-- <!-- Buttons -->
<div class="tw-flex tw-flex-col tw-space-y-2.5 tw-mb-3">
<button
type="submit"
buttonType="primary"
bitButton
bitFormButton
*ngIf="
selectedProviderType != null &&
selectedProviderType !== providerType.WebAuthn &&
selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo
"
>
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ actionButtonText }} </span>
</button>
<button
type="button"
buttonType="primary"
bitButton
(click)="launchDuo()"
*ngIf="
selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo
"
>
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "launchDuo" | i18n }}</span>
</button>
>
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ actionButtonText }} </span>
</button>
<button
type="button"
buttonType="primary"
bitButton
(click)="launchDuo()"
*ngIf="
selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo
"
>
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "launchDuo" | i18n }}</span>
</button>
<a routerLink="/login" bitButton buttonType="secondary">
{{ "cancel" | i18n }}
</a>
</div>
<div class="text-center">
<a bitLink href="#" appStopClick (click)="selectOtherTwofactorMethod()">{{
"useAnotherTwoStepMethod" | i18n
}}</a>
</div>
<a routerLink="/login" bitButton buttonType="secondary">
{{ "cancel" | i18n }}
</a>
</div>
<div class="text-center">
<a bitLink href="#" appStopClick (click)="selectOtherTwofactorMethod()">{{
"useAnotherTwoStepMethod" | i18n
}}</a>
</div>
</form>

View File

@@ -35,7 +35,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { TwoFactorAuthComponent } from "./two-factor-auth.component";
@@ -76,6 +76,7 @@ describe("TwoFactorComponent", () => {
let mockMasterPasswordService: FakeMasterPasswordService;
let mockAccountService: FakeAccountService;
let mockDialogService: MockProxy<DialogService>;
let mockToastService: MockProxy<ToastService>;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
@@ -113,6 +114,7 @@ describe("TwoFactorComponent", () => {
mockAccountService = mockAccountServiceWith(userId);
mockMasterPasswordService = new FakeMasterPasswordService();
mockDialogService = mock<DialogService>();
mockToastService = mock<ToastService>();
mockUserDecryptionOpts = {
noMasterPassword: new UserDecryptionOptions({
@@ -193,6 +195,7 @@ describe("TwoFactorComponent", () => {
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
{ provide: AccountService, useValue: mockAccountService },
{ provide: DialogService, useValue: mockDialogService },
{ provide: ToastService, useValue: mockToastService },
],
});

View File

@@ -34,6 +34,7 @@ import {
ButtonModule,
DialogService,
FormFieldModule,
ToastService,
} from "@bitwarden/components";
import { CaptchaProtectedComponent } from "../captcha-protected.component";
@@ -142,8 +143,9 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
private accountService: AccountService,
private formBuilder: FormBuilder,
@Inject(WINDOW) protected win: Window,
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService);
super(environmentService, i18nService, platformUtilsService, toastService);
}
async ngOnInit() {
@@ -184,11 +186,11 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
await this.setupCaptcha();
if (this.token == null || this.token === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("verificationCodeRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("verificationCodeRequired"),
});
return;
}
@@ -202,11 +204,11 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
await this.handleLoginResponse(authResult);
} catch {
this.logService.error("Error submitting two factor token");
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidVerificationCode"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidVerificationCode"),
});
}
}

View File

@@ -97,7 +97,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected accountService: AccountService,
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService);
super(environmentService, i18nService, platformUtilsService, toastService);
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
}
@@ -135,7 +135,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
this.submit();
},
(error: string) => {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: error,
});
},
(info: string) => {
if (info === "ready") {
@@ -201,11 +205,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
await this.setupCaptcha();
if (this.token == null || this.token === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("verificationCodeRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("verificationCodeRequired"),
});
return;
}
@@ -243,11 +247,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
return false;
}
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccured"),
this.i18nService.t("encryptionKeyMigrationRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccured"),
message: this.i18nService.t("encryptionKeyMigrationRequired"),
});
return true;
}
@@ -414,11 +418,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
}
if ((await this.loginStrategyService.getEmail()) == null) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("sessionTimeout"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("sessionTimeout"),
});
return;
}
@@ -434,11 +438,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
this.emailPromise = this.apiService.postTwoFactorEmail(request);
await this.emailPromise;
if (doToast) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail),
});
}
} catch (e) {
this.logService.error(e);

View File

@@ -19,7 +19,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@@ -50,6 +50,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
toastService: ToastService,
) {
super(
i18nService,
@@ -63,6 +64,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
kdfConfigService,
masterPasswordService,
accountService,
toastService,
);
}
@@ -77,11 +79,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
async setupSubmitActions(): Promise<boolean> {
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordRequired"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return false;
}
@@ -92,7 +94,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
try {
await this.userVerificationService.verifyUser(secret);
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e.message,
});
return false;
}
@@ -120,11 +126,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.apiService.postPassword(request);
this.platformUtilsService.showToast(
"success",
this.i18nService.t("masterPasswordChanged"),
this.i18nService.t("logBackIn"),
);
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("masterPasswordChanged"),
message: this.i18nService.t("logBackIn"),
});
if (this.onSuccessfulChangePassword != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@@ -24,7 +24,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@@ -64,6 +64,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
kdfConfigService: KdfConfigService,
accountService: AccountService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
toastService: ToastService,
) {
super(
i18nService,
@@ -77,6 +78,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
kdfConfigService,
masterPasswordService,
accountService,
toastService,
);
}
@@ -176,11 +178,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
}
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("updatedMasterPassword"),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("updatedMasterPassword"),
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(

View File

@@ -5,6 +5,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { Verification } from "@bitwarden/common/auth/types/verification";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { ModalRef } from "../../components/modal/modal.ref";
@@ -37,6 +38,7 @@ export class UserVerificationPromptComponent {
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private toastService: ToastService,
) {}
get secret() {
@@ -56,7 +58,11 @@ export class UserVerificationPromptComponent {
this.invalidSecret = false;
} catch (e) {
this.invalidSecret = true;
this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: e.message,
});
return;
}

View File

@@ -2,5 +2,3 @@ export * from "./add-account-credit-dialog/add-account-credit-dialog.component";
export * from "./invoices/invoices.component";
export * from "./invoices/no-invoices.component";
export * from "./manage-tax-information/manage-tax-information.component";
export * from "./select-payment-method/select-payment-method.component";
export * from "./verify-bank-account/verify-bank-account.component";

View File

@@ -1,151 +0,0 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<div class="tw-mb-4 tw-text-lg">
<bit-radio-group formControlName="paymentMethod">
<bit-radio-button id="card-payment-method" [value]="PaymentMethodType.Card">
<bit-label>
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>
{{ "creditCard" | i18n }}
</bit-label>
</bit-radio-button>
<bit-radio-button
id="bank-payment-method"
[value]="PaymentMethodType.BankAccount"
*ngIf="showBankAccount"
>
<bit-label>
<i class="bwi bwi-fw bwi-bank" aria-hidden="true"></i>
{{ "bankAccount" | i18n }}
</bit-label>
</bit-radio-button>
<bit-radio-button
id="paypal-payment-method"
[value]="PaymentMethodType.PayPal"
*ngIf="showPayPal"
>
<bit-label>
<i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i>
{{ "payPal" | i18n }}
</bit-label>
</bit-radio-button>
<bit-radio-button
id="credit-payment-method"
[value]="PaymentMethodType.Credit"
*ngIf="showAccountCredit"
>
<bit-label>
<i class="bwi bwi-fw bwi-dollar" aria-hidden="true"></i>
{{ "accountCredit" | i18n }}
</bit-label>
</bit-radio-button>
</bit-radio-group>
</div>
<!-- Card -->
<ng-container *ngIf="usingCard">
<div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4">
<div class="tw-col-span-1">
<label for="stripe-card-number">{{ "number" | i18n }}</label>
<div id="stripe-card-number" class="form-control stripe-form-control"></div>
</div>
<div class="tw-col-span-1 tw-flex tw-items-end">
<img
src="../../images/cards.png"
alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
class="tw-max-w-full"
/>
</div>
<div class="tw-col-span-1">
<label for="stripe-card-expiry">{{ "expiration" | i18n }}</label>
<div id="stripe-card-expiry" class="form-control stripe-form-control"></div>
</div>
<div class="tw-col-span-1">
<div class="tw-flex">
<label for="stripe-card-cvc">
{{ "securityCode" | i18n }}
</label>
<a
href="https://www.cvvnumber.com/cvv.html"
tabindex="-1"
target="_blank"
rel="noreferrer"
class="ml-auto"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
<div id="stripe-card-cvc" class="form-control stripe-form-control"></div>
</div>
</div>
</ng-container>
<!-- Bank Account -->
<ng-container *ngIf="showBankAccount && usingBankAccount">
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
{{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}
</app-callout>
<div class="tw-grid tw-grid-cols-2 tw-gap-4" formGroupName="bankInformation">
<bit-form-field class="tw-col-span-1" disableMargin>
<bit-label>{{ "routingNumber" | i18n }}</bit-label>
<input
bitInput
id="routingNumber"
type="text"
formControlName="routingNumber"
required
appInputVerbatim
/>
</bit-form-field>
<bit-form-field class="tw-col-span-1" disableMargin>
<bit-label>{{ "accountNumber" | i18n }}</bit-label>
<input
bitInput
id="accountNumber"
type="text"
formControlName="accountNumber"
required
appInputVerbatim
/>
</bit-form-field>
<bit-form-field class="tw-col-span-1" disableMargin>
<bit-label>{{ "accountHolderName" | i18n }}</bit-label>
<input
id="accountHolderName"
bitInput
type="text"
formControlName="accountHolderName"
required
appInputVerbatim
/>
</bit-form-field>
<bit-form-field class="tw-col-span-1" disableMargin>
<bit-label>{{ "bankAccountType" | i18n }}</bit-label>
<bit-select id="accountHolderType" formControlName="accountHolderType" required>
<bit-option [value]="''" label="-- {{ 'select' | i18n }} --"></bit-option>
<bit-option
[value]="'company'"
label="{{ 'bankAccountTypeCompany' | i18n }}"
></bit-option>
<bit-option
[value]="'individual'"
label="{{ 'bankAccountTypeIndividual' | i18n }}"
></bit-option>
</bit-select>
</bit-form-field>
</div>
</ng-container>
<!-- PayPal -->
<ng-container *ngIf="showPayPal && usingPayPal">
<div class="tw-mb-3">
<div id="braintree-container" class="tw-mb-1 tw-content-center"></div>
<small class="tw-text-muted">{{ "paypalClickSubmit" | i18n }}</small>
</div>
</ng-container>
<!-- Account Credit -->
<ng-container *ngIf="showAccountCredit && usingAccountCredit">
<app-callout type="note">
{{ "makeSureEnoughCredit" | i18n }}
</app-callout>
</ng-container>
<button *ngIf="!!onSubmit" bitButton bitFormButton buttonType="primary" type="submit">
{{ "submit" | i18n }}
</button>
</form>

View File

@@ -1,159 +0,0 @@
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
import {
BillingApiServiceAbstraction,
BraintreeServiceAbstraction,
StripeServiceAbstraction,
} from "@bitwarden/common/billing/abstractions";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { TokenizedPaymentMethod } from "@bitwarden/common/billing/models/domain";
@Component({
selector: "app-select-payment-method",
templateUrl: "./select-payment-method.component.html",
})
export class SelectPaymentMethodComponent implements OnInit, OnDestroy {
@Input() protected showAccountCredit: boolean = true;
@Input() protected showBankAccount: boolean = true;
@Input() protected showPayPal: boolean = true;
@Input() private startWith: PaymentMethodType = PaymentMethodType.Card;
@Input() protected onSubmit: (tokenizedPaymentMethod: TokenizedPaymentMethod) => Promise<void>;
private destroy$ = new Subject<void>();
protected formGroup = this.formBuilder.group({
paymentMethod: [this.startWith],
bankInformation: this.formBuilder.group({
routingNumber: ["", [Validators.required]],
accountNumber: ["", [Validators.required]],
accountHolderName: ["", [Validators.required]],
accountHolderType: ["", [Validators.required]],
}),
});
protected PaymentMethodType = PaymentMethodType;
constructor(
private billingApiService: BillingApiServiceAbstraction,
private braintreeService: BraintreeServiceAbstraction,
private formBuilder: FormBuilder,
private stripeService: StripeServiceAbstraction,
) {}
async tokenizePaymentMethod(): Promise<TokenizedPaymentMethod> {
const type = this.selected;
if (this.usingStripe) {
const clientSecret = await this.billingApiService.createSetupIntent(type);
if (this.usingBankAccount) {
const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, {
accountHolderName: this.formGroup.value.bankInformation.accountHolderName,
routingNumber: this.formGroup.value.bankInformation.routingNumber,
accountNumber: this.formGroup.value.bankInformation.accountNumber,
accountHolderType: this.formGroup.value.bankInformation.accountHolderType,
});
return {
type,
token,
};
}
if (this.usingCard) {
const token = await this.stripeService.setupCardPaymentMethod(clientSecret);
return {
type,
token,
};
}
}
if (this.usingPayPal) {
const token = await this.braintreeService.requestPaymentMethod();
return {
type,
token,
};
}
return null;
}
submit = async () => {
const tokenizedPaymentMethod = await this.tokenizePaymentMethod();
await this.onSubmit(tokenizedPaymentMethod);
};
ngOnInit(): void {
this.stripeService.loadStripe(
{
cardNumber: "#stripe-card-number",
cardExpiry: "#stripe-card-expiry",
cardCvc: "#stripe-card-cvc",
},
this.startWith === PaymentMethodType.Card,
);
if (this.showPayPal) {
this.braintreeService.loadBraintree(
"#braintree-container",
this.startWith === PaymentMethodType.PayPal,
);
}
this.formGroup
.get("paymentMethod")
.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((type) => {
this.onPaymentMethodChange(type);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.stripeService.unloadStripe();
if (this.showPayPal) {
this.braintreeService.unloadBraintree();
}
}
private onPaymentMethodChange(type: PaymentMethodType): void {
switch (type) {
case PaymentMethodType.Card: {
this.stripeService.mountElements();
break;
}
case PaymentMethodType.PayPal: {
this.braintreeService.createDropin();
break;
}
}
}
private get selected(): PaymentMethodType {
return this.formGroup.value.paymentMethod;
}
protected get usingAccountCredit(): boolean {
return this.selected === PaymentMethodType.Credit;
}
protected get usingBankAccount(): boolean {
return this.selected === PaymentMethodType.BankAccount;
}
protected get usingCard(): boolean {
return this.selected === PaymentMethodType.Card;
}
protected get usingPayPal(): boolean {
return this.selected === PaymentMethodType.PayPal;
}
private get usingStripe(): boolean {
return this.usingBankAccount || this.usingCard;
}
}

View File

@@ -1,18 +0,0 @@
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
<p>{{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}</p>
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-form-field class="tw-mr-2 tw-w-40">
<bit-label>{{ "amountX" | i18n: "1" }}</bit-label>
<input bitInput type="number" step="1" placeholder="xx" formControlName="amount1" />
<span bitPrefix>$0.</span>
</bit-form-field>
<bit-form-field class="tw-mr-2 tw-w-40">
<bit-label>{{ "amountX" | i18n: "2" }}</bit-label>
<input bitInput type="number" step="1" placeholder="xx" formControlName="amount2" />
<span bitPrefix>$0.</span>
</bit-form-field>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button>
</form>
</app-callout>

View File

@@ -1,33 +0,0 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";
@Component({
selector: "app-verify-bank-account",
templateUrl: "./verify-bank-account.component.html",
})
export class VerifyBankAccountComponent {
@Input() onSubmit?: (amount1: number, amount2: number) => Promise<void>;
@Output() verificationSubmitted = new EventEmitter();
protected formGroup = this.formBuilder.group({
amount1: new FormControl<number>(null, [
Validators.required,
Validators.min(0),
Validators.max(99),
]),
amount2: new FormControl<number>(null, [
Validators.required,
Validators.min(0),
Validators.max(99),
]),
});
constructor(private formBuilder: FormBuilder) {}
submit = async () => {
if (this.onSubmit) {
await this.onSubmit(this.formGroup.value.amount1, this.formGroup.value.amount2);
}
this.verificationSubmitted.emit();
};
}

View File

@@ -1,14 +1,5 @@
<div
#callout
class="callout callout-{{ calloutStyle }}"
[ngClass]="{ clickable: clickable }"
[attr.role]="useAlertRole ? 'alert' : null"
>
<h3 class="callout-heading" *ngIf="title">
<i class="bwi {{ icon }}" *ngIf="icon" aria-hidden="true"></i>
{{ title }}
</h3>
<div class="enforced-policy-options" *ngIf="enforcedPolicyOptions">
<bit-callout [icon]="icon" [title]="title" [type]="$any(type)" [useAlertRole]="useAlertRole">
<div class="tw-pl-7 tw-m-0" *ngIf="enforcedPolicyOptions">
{{ enforcedPolicyMessage }}
<ul>
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
@@ -32,4 +23,4 @@
</ul>
</div>
<ng-content></ng-content>
</div>
</bit-callout>

View File

@@ -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() {

View File

@@ -7,13 +7,12 @@ import {
InvoicesComponent,
NoInvoicesComponent,
ManageTaxInformationComponent,
SelectPaymentMethodComponent,
VerifyBankAccountComponent,
} from "@bitwarden/angular/billing/components";
import {
AsyncActionsModule,
AutofocusDirective,
ButtonModule,
CalloutModule,
CheckboxModule,
DialogModule,
FormFieldModule,
@@ -29,7 +28,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 +71,7 @@ import { IconComponent } from "./vault/components/icon.component";
FormFieldModule,
SelectModule,
ButtonModule,
CalloutModule,
CheckboxModule,
DialogModule,
TypographyModule,
@@ -88,7 +88,7 @@ import { IconComponent } from "./vault/components/icon.component";
ApiActionDirective,
AutofocusDirective,
BoxRowDirective,
CalloutComponent,
DeprecatedCalloutComponent,
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
@@ -114,8 +114,6 @@ import { IconComponent } from "./vault/components/icon.component";
InvoicesComponent,
NoInvoicesComponent,
ManageTaxInformationComponent,
SelectPaymentMethodComponent,
VerifyBankAccountComponent,
TwoFactorIconComponent,
],
exports: [
@@ -125,7 +123,7 @@ import { IconComponent } from "./vault/components/icon.component";
AutofocusDirective,
ToastModule,
BoxRowDirective,
CalloutComponent,
DeprecatedCalloutComponent,
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
@@ -151,8 +149,6 @@ import { IconComponent } from "./vault/components/icon.component";
InvoicesComponent,
NoInvoicesComponent,
ManageTaxInformationComponent,
SelectPaymentMethodComponent,
VerifyBankAccountComponent,
TwoFactorIconComponent,
],
providers: [

View File

@@ -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<T> = {
/**
* 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>) => T;
};
type BaseCacheOptions<T> = {
/** 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<T> : Required<Deserializer<T>>);
export type SignalCacheOptions<T> = BaseCacheOptions<T> & {
/** The initial value for the signal. */
initialValue: T;
};
/** Extract the value type from a FormGroup */
type FormValue<TFormGroup extends FormGroup> = TFormGroup["value"];
export type FormCacheOptions<TFormGroup extends FormGroup> = BaseCacheOptions<
FormValue<TFormGroup>
> & {
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<T>(options: SignalCacheOptions<T>): WritableSignal<T>;
/**
* - 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<TFormGroup extends FormGroup>(
options: FormCacheOptions<TFormGroup>,
): TFormGroup;
}

View File

@@ -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<T>(options: SignalCacheOptions<T>): WritableSignal<T> {
return signal(options.initialValue);
}
/**
* Return the original form group.
**/
formGroup<TFormGroup extends FormGroup>(options: FormCacheOptions<TFormGroup>): TFormGroup {
return options.control;
}
}

View File

@@ -270,8 +270,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";
@@ -1292,6 +1294,11 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultRegistrationFinishService,
deps: [CryptoServiceAbstraction, AccountApiServiceAbstraction],
}),
safeProvider({
provide: ViewCacheService,
useExisting: NoopViewCacheService,
deps: [],
}),
safeProvider({
provide: LoginService,
useClass: DefaultLoginService,

View File

@@ -0,0 +1,32 @@
import { Type, inject } from "@angular/core";
import { Route, Routes } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { componentRouteSwap } from "../../utils/component-route-swap";
/**
* Helper function to swap between two components based on the GeneratorToolsModernization feature flag.
* @param defaultComponent - The current non-refreshed component to render.
* @param refreshedComponent - The new refreshed component to render.
* @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
* @param altOptions - The alt route options to apply to the alt component.
*/
export function generatorSwap(
defaultComponent: Type<any>,
refreshedComponent: Type<any>,
options: Route,
altOptions?: Route,
): Routes {
return componentRouteSwap(
defaultComponent,
refreshedComponent,
async () => {
const configService = inject(ConfigService);
return configService.getFeatureFlag(FeatureFlag.GeneratorToolsModernization);
},
options,
altOptions,
);
}

View File

@@ -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 }"
>
<bit-icon *ngIf="!hideLogo" [icon]="logo" class="tw-max-w-36"></bit-icon>
<bit-icon *ngIf="!hideLogo" [icon]="logo" class="tw-w-[128px] [&>*]:tw-align-top"></bit-icon>
<div class="tw-text-center">
<div class="tw-mx-auto tw-max-w-28 sm:tw-max-w-32">

View File

@@ -10,9 +10,11 @@ import { RegisterVerificationEmailClickedRequest } from "@bitwarden/common/auth/
import { HttpStatusCode } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { ToastService } from "@bitwarden/components";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "../../../common";
import { InputPasswordComponent } from "../../input-password/input-password.component";
import { PasswordInputResult } from "../../input-password/password-input-result";
@@ -46,6 +48,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
private registrationFinishService: RegistrationFinishService,
private validationService: ValidationService,
private accountApiService: AccountApiService,
private loginStrategyService: LoginStrategyServiceAbstraction,
private logService: LogService,
) {}
async ngOnInit() {
@@ -90,8 +94,9 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
this.submitting = true;
let captchaBypassToken: string = null;
try {
await this.registrationFinishService.finishRegistration(
captchaBypassToken = await this.registrationFinishService.finishRegistration(
this.email,
passwordInputResult,
this.emailVerificationToken,
@@ -102,14 +107,37 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
return;
}
// Show acct created toast
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("newAccountCreated"),
message: this.i18nService.t("newAccountCreated2"),
});
// login with the new account
try {
const credentials = new PasswordLoginCredentials(
this.email,
passwordInputResult.password,
captchaBypassToken,
null,
);
await this.loginStrategyService.logIn(credentials);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("youHaveBeenLoggedIn"),
});
await this.router.navigate(["/vault"]);
} catch (e) {
// If login errors, redirect to login page per product. Don't show error
this.logService.error("Error logging in after registration: ", e.message);
await this.router.navigate(["/login"], { queryParams: { email: this.email } });
}
this.submitting = false;
await this.router.navigate(["/login"], { queryParams: { email: this.email } });
}
private async registerVerificationEmailClicked(email: string, emailVerificationToken: string) {

View File

@@ -15,6 +15,7 @@ import {
CalloutModule,
DialogModule,
DialogService,
ToastService,
} from "@bitwarden/components";
import { ActiveClientVerificationOption } from "./active-client-verification-option.enum";
@@ -58,6 +59,7 @@ export class UserVerificationDialogComponent {
private userVerificationService: UserVerificationService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private toastService: ToastService,
) {}
/**
@@ -256,19 +258,27 @@ export class UserVerificationDialogComponent {
// Only pin should ever get here, but added this check to be safe.
if (this.activeClientVerificationOption === ActiveClientVerificationOption.Pin) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidPin"),
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: this.i18nService.t("invalidPin"),
});
} else {
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("unexpectedError"),
});
}
}
} catch (e) {
// Catch handles OTP and MP verification scenarios as those throw errors on verification failure instead of returning false like PIN and biometrics.
this.invalidSecret = true;
this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: e.message,
});
return;
}
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);
});

View File

@@ -64,7 +64,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
if (response.apiUseKeyConnector) {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));

View File

@@ -32,7 +32,7 @@ export class FakeGlobalStateProvider implements GlobalStateProvider {
states: Map<string, GlobalState<unknown>> = new Map();
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
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<T>;
}
private cacheKey(keyDefinition: KeyDefinition<unknown>) {
return `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
}
getFake<T>(keyDefinition: KeyDefinition<T>): FakeGlobalState<T> {
return this.get(keyDefinition) as FakeGlobalState<T>;
}
mockFor<T>(keyDefinitionKey: string, initialValue?: T): FakeGlobalState<T> {
if (!this.establishedMocks.has(keyDefinitionKey)) {
this.establishedMocks.set(keyDefinitionKey, new FakeGlobalState<T>(initialValue));
mockFor<T>(keyDefinition: KeyDefinition<T>, initialValue?: T): FakeGlobalState<T> {
const cacheKey = this.cacheKey(keyDefinition);
if (!this.states.has(cacheKey)) {
this.states.set(cacheKey, new FakeGlobalState<T>(initialValue));
}
return this.establishedMocks.get(keyDefinitionKey) as FakeGlobalState<T>;
return this.states.get(cacheKey) as FakeGlobalState<T>;
}
}
export class FakeSingleUserStateProvider implements SingleUserStateProvider {
mock = mock<SingleUserStateProvider>();
establishedMocks: Map<string, FakeSingleUserState<unknown>> = new Map();
states: Map<string, SingleUserState<unknown>> = new Map();
constructor(
readonly updateSyncCallback?: (
key: UserKeyDefinition<unknown>,
userId: UserId,
newValue: unknown,
) => Promise<void>,
) {}
get<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): SingleUserState<T> {
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<T>;
// Look for established mock
if (this.establishedMocks.has(userKeyDefinition.key)) {
fake = this.establishedMocks.get(userKeyDefinition.key) as FakeSingleUserState<T>;
} else {
fake = new FakeSingleUserState<T>(userId);
}
fake.keyDefinition = userKeyDefinition;
result = fake;
result = this.buildFakeState(userId, userKeyDefinition);
this.states.set(cacheKey, result);
}
return result as SingleUserState<T>;
}
getFake<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): FakeSingleUserState<T> {
getFake<T>(
userId: UserId,
userKeyDefinition: UserKeyDefinition<T>,
{ allowInit }: { allowInit: boolean } = { allowInit: true },
): FakeSingleUserState<T> {
if (!allowInit && this.states.get(this.cacheKey(userId, userKeyDefinition)) == null) {
return null;
}
return this.get(userId, userKeyDefinition) as FakeSingleUserState<T>;
}
mockFor<T>(userId: UserId, keyDefinitionKey: string, initialValue?: T): FakeSingleUserState<T> {
if (!this.establishedMocks.has(keyDefinitionKey)) {
this.establishedMocks.set(keyDefinitionKey, new FakeSingleUserState<T>(userId, initialValue));
mockFor<T>(
userId: UserId,
userKeyDefinition: UserKeyDefinition<T>,
initialValue?: T,
): FakeSingleUserState<T> {
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<T>;
return this.states.get(cacheKey) as FakeSingleUserState<T>;
}
private buildFakeState<T>(
userId: UserId,
userKeyDefinition: UserKeyDefinition<T>,
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<unknown>) {
return `${userKeyDefinitionCacheKey(userKeyDefinition)}_${userId}`;
}
}
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
activeUserId$: Observable<UserId>;
establishedMocks: Map<string, FakeActiveUserState<unknown>> = new Map();
states: Map<string, FakeActiveUserState<unknown>> = new Map();
constructor(public accountService: FakeAccountService) {
constructor(
public accountService: FakeAccountService,
readonly updateSyncCallback?: (
key: UserKeyDefinition<unknown>,
userId: UserId,
newValue: unknown,
) => Promise<void>,
) {
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id));
}
get<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
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<T>(this.accountService);
}
result.keyDefinition = userKeyDefinition;
result = this.buildFakeState(userKeyDefinition);
this.states.set(cacheKey, result);
}
return result as ActiveUserState<T>;
}
getFake<T>(userKeyDefinition: UserKeyDefinition<T>): FakeActiveUserState<T> {
getFake<T>(
userKeyDefinition: UserKeyDefinition<T>,
{ allowInit }: { allowInit: boolean } = { allowInit: true },
): FakeActiveUserState<T> {
if (!allowInit && this.states.get(userKeyDefinitionCacheKey(userKeyDefinition)) == null) {
return null;
}
return this.get(userKeyDefinition) as FakeActiveUserState<T>;
}
mockFor<T>(keyDefinitionKey: string, initialValue?: T): FakeActiveUserState<T> {
if (!this.establishedMocks.has(keyDefinitionKey)) {
this.establishedMocks.set(
keyDefinitionKey,
new FakeActiveUserState<T>(this.accountService, initialValue),
);
mockFor<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T): FakeActiveUserState<T> {
const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition);
if (!this.states.has(cacheKey)) {
this.states.set(cacheKey, this.buildFakeState(userKeyDefinition, initialValue));
}
return this.establishedMocks.get(keyDefinitionKey) as FakeActiveUserState<T>;
return this.states.get(cacheKey) as FakeActiveUserState<T>;
}
private buildFakeState<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T) {
const state = new FakeActiveUserState<T>(this.accountService, initialValue, async (...args) => {
await this.updateSyncCallback?.(userKeyDefinition, ...args);
});
state.keyDefinition = userKeyDefinition;
return state;
}
}
function userKeyDefinitionCacheKey(userKeyDefinition: UserKeyDefinition<unknown>) {
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<unknown>,
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<unknown>,
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<UserId> = this.activeUser.activeUserId$;
}

View File

@@ -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<T> implements GlobalState<T> {
this.stateSubject.next(initialValue ?? null);
}
nextState(state: T) {
this.stateSubject.next(state);
}
async update<TCombine>(
configureState: (state: T, dependency: TCombine) => T,
options?: StateUpdateOptions<T, TCombine>,
@@ -89,7 +93,10 @@ export class FakeGlobalState<T> implements GlobalState<T> {
export class FakeSingleUserState<T> implements SingleUserState<T> {
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
stateSubject = new ReplaySubject<CombinedState<T>>(1);
stateSubject = new ReplaySubject<{
syncValue: boolean;
combinedState: CombinedState<T>;
}>(1);
state$: Observable<T>;
combinedState$: Observable<CombinedState<T>>;
@@ -97,15 +104,28 @@ export class FakeSingleUserState<T> implements SingleUserState<T> {
constructor(
readonly userId: UserId,
initialValue?: T,
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
) {
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<TCombine>(
@@ -122,7 +142,7 @@ export class FakeSingleUserState<T> implements SingleUserState<T> {
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<T> implements ActiveUserState<T> {
[activeMarker]: true;
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
stateSubject = new ReplaySubject<CombinedState<T>>(1);
stateSubject = new ReplaySubject<{
syncValue: boolean;
combinedState: CombinedState<T>;
}>(1);
state$: Observable<T>;
combinedState$: Observable<CombinedState<T>>;
@@ -154,10 +177,18 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
constructor(
private accountService: FakeAccountService,
initialValue?: T,
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
) {
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<T> implements ActiveUserState<T> {
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<TCombine>(
@@ -183,7 +217,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
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];
}

View File

@@ -90,7 +90,6 @@ import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
import { EventRequest } from "../models/request/event.request";
import { KdfRequest } from "../models/request/kdf.request";
import { KeysRequest } from "../models/request/keys.request";
import { OrganizationImportRequest } from "../models/request/organization-import.request";
import { PreloginRequest } from "../models/request/prelogin.request";
import { RegisterRequest } from "../models/request/register.request";
import { StorageRequest } from "../models/request/storage.request";
@@ -301,7 +300,6 @@ export abstract class ApiService {
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
getSync: () => Promise<SyncResponse>;
postPublicImportDirectory: (request: OrganizationImportRequest) => Promise<any>;
getSettingsDomains: () => Promise<DomainsResponse>;
putSettingsDomains: (request: UpdateDomainsRequest) => Promise<DomainsResponse>;

View File

@@ -210,19 +210,19 @@ export abstract class OrganizationUserService {
): Promise<void>;
/**
* Delete an organization user
* Remove an organization user
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
*/
abstract deleteOrganizationUser(organizationId: string, id: string): Promise<void>;
abstract removeOrganizationUser(organizationId: string, id: string): Promise<void>;
/**
* Delete many organization users
* Remove many organization users
* @param organizationId - Identifier for the organization the users belongs to
* @param ids - List of organization user identifiers to delete
* @return List of user ids, including both those that were successfully deleted and those that had an error
* @param ids - List of organization user identifiers to remove
* @return List of user ids, including both those that were successfully removed and those that had an error
*/
abstract deleteManyOrganizationUsers(
abstract removeManyOrganizationUsers(
organizationId: string,
ids: string[],
): Promise<ListResponse<OrganizationUserBulkResponse>>;

View File

@@ -77,5 +77,5 @@ export abstract class PolicyService {
export abstract class InternalPolicyService extends PolicyService {
upsert: (policy: PolicyData) => Promise<void>;
replace: (policies: { [id: string]: PolicyData }) => Promise<void>;
replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise<void>;
}

View File

@@ -274,7 +274,7 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe
);
}
deleteOrganizationUser(organizationId: string, id: string): Promise<any> {
removeOrganizationUser(organizationId: string, id: string): Promise<any> {
return this.apiService.send(
"DELETE",
"/organizations/" + organizationId + "/users/" + id,
@@ -284,7 +284,7 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe
);
}
async deleteManyOrganizationUsers(
async removeManyOrganizationUsers(
organizationId: string,
ids: string[],
): Promise<ListResponse<OrganizationUserBulkResponse>> {

View File

@@ -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<OrganizationService>;
let activeUserState: FakeActiveUserState<Record<PolicyId, PolicyData>>;
@@ -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<OrganizationService>();
@@ -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([
{

View File

@@ -219,8 +219,8 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
});
}
async replace(policies: { [id: string]: PolicyData }): Promise<void> {
await this.activeUserPolicyState.update(() => policies);
async replace(policies: { [id: string]: PolicyData }, userId: UserId): Promise<void> {
await this.stateProvider.setUserState(POLICIES, policies, userId);
}
/**

View File

@@ -4,17 +4,17 @@ import { IdentityTokenResponse } from "../models/response/identity-token.respons
export abstract class KeyConnectorService {
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>;
userNeedsMigration: () => Promise<boolean>;
getManagingOrganization: (userId?: UserId) => Promise<Organization>;
getUsesKeyConnector: (userId: UserId) => Promise<boolean>;
migrateUser: (userId?: UserId) => Promise<void>;
userNeedsMigration: (userId: UserId) => Promise<boolean>;
convertNewSsoUserToKeyConnector: (
tokenResponse: IdentityTokenResponse,
orgId: string,
userId: UserId,
) => Promise<void>;
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
setConvertAccountRequired: (status: boolean) => Promise<void>;
setUsesKeyConnector: (enabled: boolean, userId: UserId) => Promise<void>;
setConvertAccountRequired: (status: boolean, userId?: UserId) => Promise<void>;
getConvertAccountRequired: () => Promise<boolean>;
removeConvertAccountRequired: () => Promise<void>;
removeConvertAccountRequired: (userId?: UserId) => Promise<void>;
}

View File

@@ -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<DecodedAccessToken>;
decodeAccessToken: (tokenOrUserId?: string | UserId) => Promise<DecodedAccessToken>;
/**
* 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<boolean>;
getIsExternal: (userId: UserId) => Promise<boolean>;
/** Gets the active or passed in user's security stamp */
getSecurityStamp: (userId?: UserId) => Promise<string | null>;

View File

@@ -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);
});

View File

@@ -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<boolean> {
return firstValueFrom(this.usesKeyConnectorState.state$);
getUsesKeyConnector(userId: UserId): Promise<boolean> {
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<Organization> {
const orgs = await this.organizationService.getAll();
async getManagingOrganization(userId?: UserId): Promise<Organization> {
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<boolean> {
return firstValueFrom(this.convertAccountToKeyConnectorState.state$);
}
async removeConvertAccountRequired() {
await this.setConvertAccountRequired(null);
async removeConvertAccountRequired(userId?: UserId) {
await this.setConvertAccountRequired(null, userId);
}
private handleKeyConnectorError(e: any) {

View File

@@ -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);

View File

@@ -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<DecodedAccessToken> {
token = token ?? (await this.getAccessToken());
async decodeAccessToken(tokenOrUserId?: string | UserId): Promise<DecodedAccessToken> {
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<boolean> {
async getIsExternal(userId: UserId): Promise<boolean> {
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);
}

View File

@@ -23,6 +23,8 @@ export const EVENTS = {
VISIBILITYCHANGE: "visibilitychange",
MOUSEENTER: "mouseenter",
MOUSELEAVE: "mouseleave",
MOUSEUP: "mouseup",
SUBMIT: "submit",
} as const;
export const ClearClipboardDelay = {
@@ -58,6 +60,8 @@ export const AUTOFILL_OVERLAY_HANDLE_REPOSITION = "autofill-overlay-handle-repos
export const UPDATE_PASSKEYS_HEADINGS_ON_SCROLL = "update-passkeys-headings-on-scroll";
export const AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT = "autofill-trigger-form-field-submit";
export const AutofillOverlayVisibility = {
Off: 0,
OnButtonClick: 1,
@@ -101,3 +105,5 @@ export const ExtensionCommand = {
} as const;
export type ExtensionCommandType = (typeof ExtensionCommand)[keyof typeof ExtensionCommand];
export const CLEAR_NOTIFICATION_LOGIN_DATA_DURATION = 60 * 1000; // 1 minute

View File

@@ -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<NeverDomains>;
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
equivalentDomains$: Observable<EquivalentDomains>;
setEquivalentDomains: (newValue: EquivalentDomains) => Promise<void>;
setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise<void>;
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
@@ -87,8 +88,8 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
await this.neverDomainsState.update(() => newValue);
}
async setEquivalentDomains(newValue: EquivalentDomains): Promise<void> {
await this.equivalentDomainsState.update(() => newValue);
async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue);
}
async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise<void> {

View File

@@ -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<void>;
}

View File

@@ -1,10 +1,10 @@
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { TokenizedPaymentMethodRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-method.request";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request";
import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response";
import { PaymentInformationResponse } from "@bitwarden/common/billing/models/response/payment-information.response";
import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response";
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
@@ -33,6 +33,8 @@ export abstract class BillingApiServiceAbstraction {
organizationId: string,
) => Promise<OrganizationBillingMetadataResponse>;
getOrganizationPaymentMethod: (organizationId: string) => Promise<PaymentMethodResponse>;
getPlans: () => Promise<ListResponse<PlanResponse>>;
getProviderClientInvoiceReport: (providerId: string, invoiceId: string) => Promise<string>;
@@ -43,37 +45,31 @@ export abstract class BillingApiServiceAbstraction {
getProviderInvoices: (providerId: string) => Promise<InvoicesResponse>;
/**
* @deprecated This endpoint is currently deactivated.
*/
getProviderPaymentInformation: (providerId: string) => Promise<PaymentInformationResponse>;
getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>;
updateOrganizationPaymentMethod: (
organizationId: string,
request: UpdatePaymentMethodRequest,
) => Promise<void>;
updateOrganizationTaxInformation: (
organizationId: string,
request: ExpandedTaxInfoUpdateRequest,
) => Promise<void>;
updateProviderClientOrganization: (
providerId: string,
organizationId: string,
request: UpdateClientOrganizationRequest,
) => Promise<any>;
/**
* @deprecated This endpoint is currently deactivated.
*/
updateProviderPaymentMethod: (
providerId: string,
request: TokenizedPaymentMethodRequest,
) => Promise<void>;
updateProviderTaxInformation: (
providerId: string,
request: ExpandedTaxInfoUpdateRequest,
) => Promise<void>;
/**
* @deprecated This endpoint is currently deactivated.
*/
verifyProviderBankAccount: (
providerId: string,
verifyOrganizationBankAccount: (
organizationId: string,
request: VerifyBankAccountRequest,
) => Promise<void>;
}

View File

@@ -1,5 +1,5 @@
export * from "./account/billing-account-profile-state.service";
export * from "./billilng-api.service.abstraction";
export * from "./billing-api.service.abstraction";
export * from "./organization-billing.service";
export * from "./payment-processors/braintree.service.abstraction";
export * from "./payment-processors/stripe.service.abstraction";

View File

@@ -1,4 +1,2 @@
export * from "./bank-account";
export * from "./masked-payment-method";
export * from "./tax-information";
export * from "./tokenized-payment-method";

View File

@@ -1,17 +0,0 @@
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { MaskedPaymentMethodResponse } from "@bitwarden/common/billing/models/response/masked-payment-method.response";
export class MaskedPaymentMethod {
type: PaymentMethodType;
description: string;
needsVerification: boolean;
static from(response: MaskedPaymentMethodResponse | undefined) {
if (response === undefined) {
return null;
}
return {
...response,
};
}
}

View File

@@ -1,14 +0,0 @@
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { TokenizedPaymentMethod } from "@bitwarden/common/billing/models/domain";
export class TokenizedPaymentMethodRequest {
type: PaymentMethodType;
token: string;
static From(tokenizedPaymentMethod: TokenizedPaymentMethod): TokenizedPaymentMethodRequest {
const request = new TokenizedPaymentMethodRequest();
request.type = tokenizedPaymentMethod.type;
request.token = tokenizedPaymentMethod.token;
return request;
}
}

View File

@@ -1,6 +1,6 @@
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
export type TokenizedPaymentMethod = {
export class TokenizedPaymentSourceRequest {
type: PaymentMethodType;
token: string;
};
}

View File

@@ -0,0 +1,7 @@
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request";
export class UpdatePaymentMethodRequest {
paymentSource: TokenizedPaymentSourceRequest;
taxInformation: ExpandedTaxInfoUpdateRequest;
}

View File

@@ -1,13 +0,0 @@
import { BaseResponse } from "../../../models/response/base.response";
export class OrganizationRisksSubscriptionFailureResponse extends BaseResponse {
organizationId: string;
risksSubscriptionFailure: boolean;
constructor(response: any) {
super(response);
this.organizationId = this.getResponseProperty("OrganizationId");
this.risksSubscriptionFailure = this.getResponseProperty("RisksSubscriptionFailure");
}
}

View File

@@ -1,22 +1,25 @@
import { BaseResponse } from "../../../models/response/base.response";
import { MaskedPaymentMethodResponse } from "./masked-payment-method.response";
import { PaymentSourceResponse } from "./payment-source.response";
import { TaxInfoResponse } from "./tax-info.response";
export class PaymentInformationResponse extends BaseResponse {
export class PaymentMethodResponse extends BaseResponse {
accountCredit: number;
paymentMethod?: MaskedPaymentMethodResponse;
paymentSource?: PaymentSourceResponse;
subscriptionStatus?: string;
taxInformation?: TaxInfoResponse;
constructor(response: any) {
super(response);
this.accountCredit = this.getResponseProperty("AccountCredit");
const paymentMethod = this.getResponseProperty("PaymentMethod");
if (paymentMethod) {
this.paymentMethod = new MaskedPaymentMethodResponse(paymentMethod);
const paymentSource = this.getResponseProperty("PaymentSource");
if (paymentSource) {
this.paymentSource = new PaymentSourceResponse(paymentSource);
}
this.subscriptionStatus = this.getResponseProperty("SubscriptionStatus");
const taxInformation = this.getResponseProperty("TaxInformation");
if (taxInformation) {
this.taxInformation = new TaxInfoResponse(taxInformation);

View File

@@ -1,7 +1,7 @@
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class MaskedPaymentMethodResponse extends BaseResponse {
export class PaymentSourceResponse extends BaseResponse {
type: PaymentMethodType;
description: string;
needsVerification: boolean;

View File

@@ -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<BillingAccountProfile>;
let userBillingAccountProfileState: FakeSingleUserState<BillingAccountProfile>;
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);
});
});
});

View File

@@ -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<boolean>;
hasPremiumFromAnySource$: Observable<boolean>;
constructor(stateProvider: StateProvider) {
constructor(private readonly stateProvider: StateProvider) {
this.billingAccountProfileState = stateProvider.getActive(
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
);
@@ -62,8 +63,9 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP
async setHasPremium(
hasPremiumPersonally: boolean,
hasPremiumFromAnyOrganization: boolean,
userId: UserId,
): Promise<void> {
await this.billingAccountProfileState.update((billingAccountProfile) => {
await this.stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).update((_) => {
return {
hasPremiumPersonally: hasPremiumPersonally,
hasPremiumFromAnyOrganization: hasPremiumFromAnyOrganization,

View File

@@ -1,5 +1,8 @@
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request";
import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response";
import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ToastService } from "@bitwarden/components";
@@ -9,10 +12,7 @@ import { BillingApiServiceAbstraction } from "../../billing/abstractions";
import { PaymentMethodType } from "../../billing/enums";
import { ExpandedTaxInfoUpdateRequest } from "../../billing/models/request/expanded-tax-info-update.request";
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { TokenizedPaymentMethodRequest } from "../../billing/models/request/tokenized-payment-method.request";
import { VerifyBankAccountRequest } from "../../billing/models/request/verify-bank-account.request";
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
import { PaymentInformationResponse } from "../../billing/models/response/payment-information.response";
import { PlanResponse } from "../../billing/models/response/plan.response";
import { ListResponse } from "../../models/response/list.response";
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
@@ -85,6 +85,19 @@ export class BillingApiService implements BillingApiServiceAbstraction {
return new OrganizationBillingMetadataResponse(r);
}
async getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse> {
const response = await this.execute(() =>
this.apiService.send(
"GET",
"/organizations/" + organizationId + "/billing/payment-method",
null,
true,
true,
),
);
return new PaymentMethodResponse(response);
}
async getPlans(): Promise<ListResponse<PlanResponse>> {
const r = await this.apiService.send("GET", "/plans", null, false, true);
return new ListResponse(r, PlanResponse);
@@ -123,19 +136,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
return new InvoicesResponse(response);
}
async getProviderPaymentInformation(providerId: string): Promise<PaymentInformationResponse> {
const response = await this.execute(() =>
this.apiService.send(
"GET",
"/providers/" + providerId + "/billing/payment-information",
null,
true,
true,
),
);
return new PaymentInformationResponse(response);
}
async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> {
const response = await this.execute(() =>
this.apiService.send(
@@ -149,6 +149,32 @@ export class BillingApiService implements BillingApiServiceAbstraction {
return new ProviderSubscriptionResponse(response);
}
async updateOrganizationPaymentMethod(
organizationId: string,
request: UpdatePaymentMethodRequest,
): Promise<void> {
return await this.apiService.send(
"PUT",
"/organizations/" + organizationId + "/billing/payment-method",
request,
true,
false,
);
}
async updateOrganizationTaxInformation(
organizationId: string,
request: ExpandedTaxInfoUpdateRequest,
): Promise<void> {
return await this.apiService.send(
"PUT",
"/organizations/" + organizationId + "/billing/tax-information",
request,
true,
false,
);
}
async updateProviderClientOrganization(
providerId: string,
organizationId: string,
@@ -163,19 +189,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
);
}
async updateProviderPaymentMethod(
providerId: string,
request: TokenizedPaymentMethodRequest,
): Promise<void> {
return await this.apiService.send(
"PUT",
"/providers/" + providerId + "/billing/payment-method",
request,
true,
false,
);
}
async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) {
return await this.apiService.send(
"PUT",
@@ -186,10 +199,13 @@ export class BillingApiService implements BillingApiServiceAbstraction {
);
}
async verifyProviderBankAccount(providerId: string, request: VerifyBankAccountRequest) {
async verifyOrganizationBankAccount(
organizationId: string,
request: VerifyBankAccountRequest,
): Promise<void> {
return await this.apiService.send(
"POST",
"/providers/" + providerId + "/billing/payment-method/verify-bank-account",
"/organizations/" + organizationId + "/billing/payment-method/verify-bank-account",
request,
true,
false,

View File

@@ -2,4 +2,5 @@
export enum EventSystemUser {
SCIM = 1,
DomainVerification = 2,
PublicApi = 3,
}

View File

@@ -32,6 +32,8 @@ export enum FeatureFlag {
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
AccountDeprovisioning = "pm-10308-account-deprovisioning",
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -74,6 +76,8 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
[FeatureFlag.AccountDeprovisioning]: FALSE,
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -19,6 +19,12 @@ import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export class UserPrivateKeyDecryptionFailedError extends Error {
constructor() {
super("Failed to decrypt the user's private key.");
}
}
/**
* An object containing all the users key needed to decrypt a users personal and organization vaults.
*/
@@ -58,6 +64,20 @@ export abstract class CryptoService {
* @param userId The desired user
*/
abstract setUserKey(key: UserKey, userId?: string): Promise<void>;
/**
* Sets the provided user keys and stores any other necessary versions
* (such as auto, biometrics, or pin).
* Also sets the user's encrypted private key in storage and
* clears the decrypted private key from memory
* Note: does not clear the private key if null is provided
*
* @throws Error when userKey, encPrivateKey or userId is null
* @throws UserPrivateKeyDecryptionFailedError when the userKey cannot decrypt encPrivateKey
* @param userKey The user key to set
* @param encPrivateKey An encrypted private key
* @param userId The desired user
*/
abstract setUserKeys(userKey: UserKey, encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Gets the user key from memory and sets it again,
* kicking off a refresh of any additional keys
@@ -143,7 +163,7 @@ export abstract class CryptoService {
* @param userKeyMasterKey The master key encrypted user key to set
* @param userId The desired user
*/
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise<void>;
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId: string): Promise<void>;
/**
* @param password The user's master password that will be used to derive a master key if one isn't found
* @param userId The desired user

View File

@@ -43,26 +43,6 @@ export abstract class PlatformUtilsService {
abstract isSelfHost(): boolean;
abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean;
abstract readFromClipboard(): Promise<string>;
abstract supportsBiometric(): Promise<boolean>;
/**
* Determine whether biometrics support requires going through a setup process.
* This is currently only needed on Linux.
*
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
*/
abstract biometricsNeedsSetup: () => Promise<boolean>;
/**
* Determine whether biometrics support can be automatically setup, or requires user interaction.
* Auto-setup is prevented by sandboxed environments, such as Snap and Flatpak.
*
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
*/
abstract biometricsSupportsAutoSetup(): Promise<boolean>;
/**
* Start automatic biometric setup, which places the required configuration files / changes the required settings.
*/
abstract biometricsSetup: () => Promise<void>;
abstract authenticateBiometric(): Promise<boolean>;
abstract supportsSecureStorage(): boolean;
abstract getAutofillKeyboardShortcut(): Promise<string>;
}

View File

@@ -119,7 +119,7 @@ describe("BiometricStateService", () => {
describe("getRequirePasswordOnStart", () => {
it("returns the requirePasswordOnStart state value", async () => {
stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START.key, true);
stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START, true);
expect(await sut.getRequirePasswordOnStart(userId)).toBe(true);
});

View File

@@ -0,0 +1,37 @@
/**
* The biometrics service is used to provide access to the status of and access to biometric functionality on the platforms.
*/
export abstract class BiometricsService {
/**
* Check if the platform supports biometric authentication.
*/
abstract supportsBiometric(): Promise<boolean>;
/**
* Checks whether biometric unlock is currently available at the moment (e.g. if the laptop lid is shut, biometric unlock may not be available)
*/
abstract isBiometricUnlockAvailable(): Promise<boolean>;
/**
* Performs biometric authentication
*/
abstract authenticateBiometric(): Promise<boolean>;
/**
* Determine whether biometrics support requires going through a setup process.
* This is currently only needed on Linux.
*
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
*/
abstract biometricsNeedsSetup(): Promise<boolean>;
/**
* Determine whether biometrics support can be automatically setup, or requires user interaction.
* Auto-setup is prevented by sandboxed environments, such as Snap and Flatpak.
*
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
*/
abstract biometricsSupportsAutoSetup(): Promise<boolean>;
/**
* Start automatic biometric setup, which places the required configuration files / changes the required settings.
*/
abstract biometricsSetup(): Promise<void>;
}

View File

@@ -3,8 +3,8 @@
* @jest-environment ../../libs/shared/test.environment.ts
*/
import { mock } from "jest-mock-extended";
import { Subject, firstValueFrom, of } from "rxjs";
import { matches, mock } from "jest-mock-extended";
import { BehaviorSubject, Subject, bufferCount, firstValueFrom, of } from "rxjs";
import {
FakeGlobalState,
@@ -35,6 +35,7 @@ import {
RETRIEVAL_INTERVAL,
GLOBAL_SERVER_CONFIGURATIONS,
USER_SERVER_CONFIG,
SLOW_EMISSION_GUARD,
} from "./default-config.service";
describe("ConfigService", () => {
@@ -65,12 +66,14 @@ describe("ConfigService", () => {
describe.each([null, userId])("active user: %s", (activeUserId) => {
let sut: DefaultConfigService;
const environmentSubject = new BehaviorSubject(environmentFactory(activeApiUrl));
beforeAll(async () => {
await accountService.switchAccount(activeUserId);
});
beforeEach(() => {
environmentService.environment$ = of(environmentFactory(activeApiUrl));
environmentService.environment$ = environmentSubject;
sut = new DefaultConfigService(
configApiService,
environmentService,
@@ -129,7 +132,8 @@ describe("ConfigService", () => {
await firstValueFrom(sut.serverConfig$);
expect(logService.error).toHaveBeenCalledWith(
`Unable to fetch ServerConfig from ${activeApiUrl}: Unable to fetch`,
`Unable to fetch ServerConfig from ${activeApiUrl}`,
matches<Error>((e) => e.message === "Unable to fetch"),
);
});
});
@@ -138,6 +142,10 @@ describe("ConfigService", () => {
const response = serverConfigResponseFactory();
const newConfig = new ServerConfig(new ServerConfigData(response));
beforeEach(() => {
configApiService.get.mockResolvedValue(response);
});
it("should be a new config", async () => {
expect(newConfig).not.toEqual(activeUserId ? userStored : globalStored[activeApiUrl]);
});
@@ -149,8 +157,6 @@ describe("ConfigService", () => {
});
it("returns the updated config", async () => {
configApiService.get.mockResolvedValue(response);
const actual = await firstValueFrom(sut.serverConfig$);
// This is the time the response is converted to a config
@@ -270,6 +276,54 @@ describe("ConfigService", () => {
});
});
});
describe("slow configuration", () => {
const environmentSubject = new BehaviorSubject<Environment>(null);
let sut: DefaultConfigService = null;
beforeEach(async () => {
const config = serverConfigFactory("existing-data", tooOld);
environmentService.environment$ = environmentSubject;
globalState.stateSubject.next({ [apiUrl(0)]: config });
userState.stateSubject.next({
syncValue: true,
combinedState: [userId, config],
});
configApiService.get.mockImplementation(() => {
return new Promise<ServerConfigResponse>((resolve) => {
setTimeout(() => {
resolve(serverConfigResponseFactory("slow-response"));
}, SLOW_EMISSION_GUARD + 20);
});
});
sut = new DefaultConfigService(
configApiService,
environmentService,
logService,
stateProvider,
authService,
);
});
afterEach(() => {
jest.resetAllMocks();
});
it("emits old configuration when the http call takes a long time", async () => {
environmentSubject.next(environmentFactory(apiUrl(0)));
const configs = await firstValueFrom(sut.serverConfig$.pipe(bufferCount(2)));
await jest.runOnlyPendingTimersAsync();
expect(configs[0].gitHash).toBe("existing-data");
expect(configs[1].gitHash).toBe("slow-response");
});
});
});
function apiUrl(count: number) {
@@ -305,8 +359,9 @@ function serverConfigResponseFactory(hash?: string) {
});
}
function environmentFactory(apiUrl: string) {
function environmentFactory(apiUrl: string, isCloud: boolean = true) {
return {
getApiUrl: () => apiUrl,
isCloud: () => isCloud,
} as Environment;
}

View File

@@ -24,7 +24,7 @@ import { UserId } from "../../../types/guid";
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
import { ConfigService } from "../../abstractions/config/config.service";
import { ServerConfig } from "../../abstractions/config/server-config";
import { EnvironmentService, Region } from "../../abstractions/environment.service";
import { Environment, EnvironmentService, Region } from "../../abstractions/environment.service";
import { LogService } from "../../abstractions/log.service";
import { devFlagEnabled, devFlagValue } from "../../misc/flags";
import { ServerConfigData } from "../../models/data/server-config.data";
@@ -34,6 +34,8 @@ export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs")
? (devFlagValue("configRetrievalIntervalMs") as number)
: 3_600_000; // 1 hour
export const SLOW_EMISSION_GUARD = 800;
export type ApiUrl = string;
export const USER_SERVER_CONFIG = new UserKeyDefinition<ServerConfig>(CONFIG_DISK, "serverConfig", {
@@ -64,29 +66,32 @@ export class DefaultConfigService implements ConfigService {
private stateProvider: StateProvider,
private authService: AuthService,
) {
const apiUrl$ = this.environmentService.environment$.pipe(
map((environment) => environment.getApiUrl()),
);
const userId$ = this.stateProvider.activeUserId$;
const authStatus$ = userId$.pipe(
switchMap((userId) => (userId == null ? of(null) : this.authService.authStatusFor$(userId))),
);
this.serverConfig$ = combineLatest([userId$, apiUrl$, authStatus$]).pipe(
switchMap(([userId, apiUrl, authStatus]) => {
this.serverConfig$ = combineLatest([
userId$,
this.environmentService.environment$,
authStatus$,
]).pipe(
switchMap(([userId, environment, authStatus]) => {
if (userId == null || authStatus !== AuthenticationStatus.Unlocked) {
return this.globalConfigFor$(apiUrl).pipe(
map((config) => [config, null, apiUrl] as const),
return this.globalConfigFor$(environment.getApiUrl()).pipe(
map((config) => [config, null, environment] as const),
);
}
return this.userConfigFor$(userId).pipe(map((config) => [config, userId, apiUrl] as const));
return this.userConfigFor$(userId).pipe(
map((config) => [config, userId, environment] as const),
);
}),
tap(async (rec) => {
const [existingConfig, userId, apiUrl] = rec;
const [existingConfig, userId, environment] = rec;
// Grab new config if older retrieval interval
if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) {
await this.renewConfig(existingConfig, userId, apiUrl);
await this.renewConfig(existingConfig, userId, environment);
}
}),
switchMap(([existingConfig]) => {
@@ -149,10 +154,20 @@ export class DefaultConfigService implements ConfigService {
private async renewConfig(
existingConfig: ServerConfig,
userId: UserId,
apiUrl: string,
environment: Environment,
): Promise<void> {
try {
// Feature flags often have a big impact on user experience, lets ensure we return some value
// somewhat quickly even though it may not be accurate, we won't cancel the HTTP request
// though so that hopefully it can have finished and hydrated a more accurate value.
const handle = setTimeout(() => {
this.logService.info(
"Self-host environment did not respond in time, emitting previous config.",
);
this.failedFetchFallbackSubject.next(existingConfig);
}, SLOW_EMISSION_GUARD);
const response = await this.configApiService.get(userId);
clearTimeout(handle);
const newConfig = new ServerConfig(new ServerConfigData(response));
// Update the environment region
@@ -167,7 +182,7 @@ export class DefaultConfigService implements ConfigService {
if (userId == null) {
// update global state with new pulled config
await this.stateProvider.getGlobal(GLOBAL_SERVER_CONFIGURATIONS).update((configs) => {
return { ...configs, [apiUrl]: newConfig };
return { ...configs, [environment.getApiUrl()]: newConfig };
});
} else {
// update state with new pulled config
@@ -175,9 +190,7 @@ export class DefaultConfigService implements ConfigService {
}
} catch (e) {
// mutate error to be handled by catchError
this.logService.error(
`Unable to fetch ServerConfig from ${apiUrl}: ${(e as Error)?.message}`,
);
this.logService.error(`Unable to fetch ServerConfig from ${environment.getApiUrl()}`, e);
// Emit the existing config
this.failedFetchFallbackSubject.next(existingConfig);
}

View File

@@ -20,6 +20,7 @@ import { OrganizationId, UserId } from "../../types/guid";
import { UserKey, MasterKey } from "../../types/key";
import { VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { UserPrivateKeyDecryptionFailedError } from "../abstractions/crypto.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { KeyGenerationService } from "../abstractions/key-generation.service";
import { LogService } from "../abstractions/log.service";
@@ -318,6 +319,70 @@ describe("cryptoService", () => {
});
});
describe("setUserKeys", () => {
let mockUserKey: UserKey;
let mockEncPrivateKey: EncryptedString;
let everHadUserKeyState: FakeSingleUserState<boolean>;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
mockEncPrivateKey = new SymmetricCryptoKey(mockRandomBytes).toString() as EncryptedString;
everHadUserKeyState = stateProvider.singleUser.getFake(mockUserId, USER_EVER_HAD_USER_KEY);
// Initialize storage
everHadUserKeyState.nextState(null);
// Mock private key decryption
encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes);
});
it("throws if userKey is null", async () => {
await expect(cryptoService.setUserKeys(null, mockEncPrivateKey, mockUserId)).rejects.toThrow(
"No userKey provided.",
);
});
it("throws if encPrivateKey is null", async () => {
await expect(cryptoService.setUserKeys(mockUserKey, null, mockUserId)).rejects.toThrow(
"No encPrivateKey provided.",
);
});
it("throws if userId is null", async () => {
await expect(cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, null)).rejects.toThrow(
"No userId provided.",
);
});
it("throws if encPrivateKey cannot be decrypted with the userKey", async () => {
encryptService.decryptToBytes.mockResolvedValue(null);
await expect(
cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId),
).rejects.toThrow(UserPrivateKeyDecryptionFailedError);
});
// We already have tests for setUserKey, so we just need to test that the correct methods are called
it("calls setUserKey with the userKey and userId", async () => {
const setUserKeySpy = jest.spyOn(cryptoService, "setUserKey");
await cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId);
expect(setUserKeySpy).toHaveBeenCalledWith(mockUserKey, mockUserId);
});
// We already have tests for setPrivateKey, so we just need to test that the correct methods are called
// TODO: Move those tests into here since `setPrivateKey` will be converted to a private method
it("calls setPrivateKey with the encPrivateKey and userId", async () => {
const setEncryptedPrivateKeySpy = jest.spyOn(cryptoService, "setPrivateKey");
await cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId);
expect(setEncryptedPrivateKeySpy).toHaveBeenCalledWith(mockEncPrivateKey, mockUserId);
});
});
describe("clearKeys", () => {
it("resolves active user id when called with no user id", async () => {
let callCount = 0;
@@ -365,9 +430,9 @@ describe("cryptoService", () => {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null;
masterPasswordService.masterKeySubject.next(fakeMasterKey);
userKeyState.stateSubject.next([mockUserId, null]);
userKeyState.nextState(null);
const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey<UserKey>(64) : null;
userKeyState.stateSubject.next([mockUserId, fakeUserKey]);
userKeyState.nextState(fakeUserKey);
return [fakeUserKey, fakeMasterKey];
}
@@ -384,10 +449,7 @@ describe("cryptoService", () => {
const fakeEncryptedUserPrivateKey = makeEncString("1");
userEncryptedPrivateKeyState.stateSubject.next([
mockUserId,
fakeEncryptedUserPrivateKey.encryptedString,
]);
userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString);
// Decryption of the user private key
const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1);
@@ -423,7 +485,7 @@ describe("cryptoService", () => {
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
encryptedUserPrivateKeyState.stateSubject.next([mockUserId, null]);
encryptedUserPrivateKeyState.nextState(null);
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
expect(userPrivateKey).toBeFalsy();
@@ -463,7 +525,7 @@ describe("cryptoService", () => {
function updateKeys(keys: Partial<UpdateKeysParams> = {}) {
if ("userKey" in keys) {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
userKeyState.stateSubject.next([mockUserId, keys.userKey]);
userKeyState.nextState(keys.userKey);
}
if ("encryptedPrivateKey" in keys) {
@@ -471,10 +533,7 @@ describe("cryptoService", () => {
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
userEncryptedPrivateKey.stateSubject.next([
mockUserId,
keys.encryptedPrivateKey.encryptedString,
]);
userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey.encryptedString);
}
if ("orgKeys" in keys) {
@@ -482,7 +541,7 @@ describe("cryptoService", () => {
mockUserId,
USER_ENCRYPTED_ORGANIZATION_KEYS,
);
orgKeysState.stateSubject.next([mockUserId, keys.orgKeys]);
orgKeysState.nextState(keys.orgKeys);
}
if ("providerKeys" in keys) {
@@ -490,7 +549,7 @@ describe("cryptoService", () => {
mockUserId,
USER_ENCRYPTED_PROVIDER_KEYS,
);
providerKeysState.stateSubject.next([mockUserId, keys.providerKeys]);
providerKeysState.nextState(keys.providerKeys);
}
encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => {

View File

@@ -38,6 +38,7 @@ import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import {
CipherDecryptionKeys,
CryptoService as CryptoServiceAbstraction,
UserPrivateKeyDecryptionFailedError,
} from "../abstractions/crypto.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { KeyGenerationService } from "../abstractions/key-generation.service";
@@ -104,6 +105,30 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.storeAdditionalKeys(key, userId);
}
async setUserKeys(
userKey: UserKey,
encPrivateKey: EncryptedString,
userId: UserId,
): Promise<void> {
if (userKey == null) {
throw new Error("No userKey provided. Lock the user to clear the key");
}
if (encPrivateKey == null) {
throw new Error("No encPrivateKey provided.");
}
if (userId == null) {
throw new Error("No userId provided.");
}
const decryptedPrivateKey = await this.decryptPrivateKey(encPrivateKey, userKey);
if (decryptedPrivateKey == null) {
throw new UserPrivateKeyDecryptionFailedError();
}
await this.setUserKey(userKey, userId);
await this.setPrivateKey(encPrivateKey, userId);
}
async refreshAdditionalKeys(): Promise<void> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
@@ -225,7 +250,7 @@ export class CryptoService implements CryptoServiceAbstraction {
}
}
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> {
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
new EncString(userKeyMasterKey),

View File

@@ -117,6 +117,13 @@ export class StateService<
state.accounts = {};
}
state.accounts[userId] = this.createAccount();
if (diskAccount == null) {
// Return early because we can't set the diskAccount.profile
// if diskAccount itself is null
return state;
}
state.accounts[userId].profile = diskAccount.profile;
return state;
});

View File

@@ -143,7 +143,7 @@ describe("DefaultStateProvider", () => {
it("should not emit any values until a truthy user id is supplied", async () => {
accountService.activeAccountSubject.next(null);
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.stateSubject.next([userId, "value"]);
state.nextState("value");
const emissions = trackEmissions(sut.getUserState$(keyDefinition));

View File

@@ -124,12 +124,12 @@ export class DefaultSyncService extends CoreSyncService {
const response = await this.apiService.getSync();
await this.syncProfile(response.profile);
await this.syncFolders(response.folders);
await this.syncCollections(response.collections);
await this.syncCiphers(response.ciphers);
await this.syncSends(response.sends);
await this.syncSettings(response.domains);
await this.syncPolicies(response.policies);
await this.syncFolders(response.folders, response.profile.id);
await this.syncCollections(response.collections, response.profile.id);
await this.syncCiphers(response.ciphers, response.profile.id);
await this.syncSends(response.sends, response.profile.id);
await this.syncSettings(response.domains, response.profile.id);
await this.syncPolicies(response.policies, response.profile.id);
await this.setLastSync(now, userId);
return this.syncCompleted(true);
@@ -190,8 +190,9 @@ export class DefaultSyncService extends CoreSyncService {
await this.billingAccountProfileStateService.setHasPremium(
response.premiumPersonally,
response.premiumFromOrganization,
response.id,
);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector, response.id);
await this.setForceSetPasswordReasonIfNeeded(response);
@@ -200,17 +201,17 @@ export class DefaultSyncService extends CoreSyncService {
providers[p.id] = new ProviderData(p);
});
await this.providerService.save(providers);
await this.providerService.save(providers, response.id);
await this.syncProfileOrganizations(response);
await this.syncProfileOrganizations(response, response.id);
if (await this.keyConnectorService.userNeedsMigration()) {
await this.keyConnectorService.setConvertAccountRequired(true);
if (await this.keyConnectorService.userNeedsMigration(response.id)) {
await this.keyConnectorService.setConvertAccountRequired(true, response.id);
this.messageSender.send("convertAccountToKeyConnector");
} else {
// 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.keyConnectorService.removeConvertAccountRequired();
this.keyConnectorService.removeConvertAccountRequired(response.id);
}
}
@@ -261,7 +262,7 @@ export class DefaultSyncService extends CoreSyncService {
}
}
private async syncProfileOrganizations(response: ProfileResponse) {
private async syncProfileOrganizations(response: ProfileResponse, userId: UserId) {
const organizations: { [id: string]: OrganizationData } = {};
response.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o, {
@@ -281,42 +282,42 @@ export class DefaultSyncService extends CoreSyncService {
}
});
await this.organizationService.replace(organizations);
await this.organizationService.replace(organizations, userId);
}
private async syncFolders(response: FolderResponse[]) {
private async syncFolders(response: FolderResponse[], userId: UserId) {
const folders: { [id: string]: FolderData } = {};
response.forEach((f) => {
folders[f.id] = new FolderData(f);
});
return await this.folderService.replace(folders);
return await this.folderService.replace(folders, userId);
}
private async syncCollections(response: CollectionDetailsResponse[]) {
private async syncCollections(response: CollectionDetailsResponse[], userId: UserId) {
const collections: { [id: string]: CollectionData } = {};
response.forEach((c) => {
collections[c.id] = new CollectionData(c);
});
return await this.collectionService.replace(collections);
return await this.collectionService.replace(collections, userId);
}
private async syncCiphers(response: CipherResponse[]) {
private async syncCiphers(response: CipherResponse[], userId: UserId) {
const ciphers: { [id: string]: CipherData } = {};
response.forEach((c) => {
ciphers[c.id] = new CipherData(c);
});
return await this.cipherService.replace(ciphers);
return await this.cipherService.replace(ciphers, userId);
}
private async syncSends(response: SendResponse[]) {
private async syncSends(response: SendResponse[], userId: UserId) {
const sends: { [id: string]: SendData } = {};
response.forEach((s) => {
sends[s.id] = new SendData(s);
});
return await this.sendService.replace(sends);
return await this.sendService.replace(sends, userId);
}
private async syncSettings(response: DomainsResponse) {
private async syncSettings(response: DomainsResponse, userId: UserId) {
let eqDomains: string[][] = [];
if (response != null && response.equivalentDomains != null) {
eqDomains = eqDomains.concat(response.equivalentDomains);
@@ -330,16 +331,16 @@ export class DefaultSyncService extends CoreSyncService {
});
}
return this.domainSettingsService.setEquivalentDomains(eqDomains);
return this.domainSettingsService.setEquivalentDomains(eqDomains, userId);
}
private async syncPolicies(response: PolicyResponse[]) {
private async syncPolicies(response: PolicyResponse[], userId: UserId) {
const policies: { [id: string]: PolicyData } = {};
if (response != null) {
response.forEach((p) => {
policies[p.id] = new PolicyData(p);
});
}
return await this.policyService.replace(policies);
return await this.policyService.replace(policies, userId);
}
}

View File

@@ -102,7 +102,6 @@ import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
import { EventRequest } from "../models/request/event.request";
import { KdfRequest } from "../models/request/kdf.request";
import { KeysRequest } from "../models/request/keys.request";
import { OrganizationImportRequest } from "../models/request/organization-import.request";
import { PreloginRequest } from "../models/request/prelogin.request";
import { RegisterRequest } from "../models/request/register.request";
import { StorageRequest } from "../models/request/storage.request";
@@ -893,10 +892,6 @@ export class ApiService implements ApiServiceAbstraction {
return new ListResponse(r, PlanResponse);
}
async postPublicImportDirectory(request: OrganizationImportRequest): Promise<any> {
return this.send("POST", "/public/organization/import", request, true, false);
}
async getTaxRates(): Promise<ListResponse<TaxRateResponse>> {
const r = await this.send("GET", "/plans/sales-tax-rates/", null, true, true);
return new ListResponse(r, TaxRateResponse);

View File

@@ -1,15 +1,19 @@
import { Observable } from "rxjs";
import type { Simplify } from "type-fest";
import { CombinedState } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { SendData } from "../models/data/send.data";
import { SendView } from "../models/view/send.view";
type EncryptedSendState = Simplify<CombinedState<Record<string, SendData>>>;
export abstract class SendStateProvider {
encryptedState$: Observable<Record<string, SendData>>;
encryptedState$: Observable<EncryptedSendState>;
decryptedState$: Observable<SendView[]>;
getEncryptedSends: () => Promise<{ [id: string]: SendData }>;
getEncryptedSends: () => Promise<EncryptedSendState>;
setEncryptedSends: (value: { [id: string]: SendData }) => Promise<void>;
setEncryptedSends: (value: { [id: string]: SendData }, userId: UserId) => Promise<void>;
getDecryptedSends: () => Promise<SendView[]>;

View File

@@ -27,11 +27,11 @@ describe("Send State Provider", () => {
describe("Encrypted Sends", () => {
it("should return SendData", async () => {
const sendData = { "1": testSendData("1", "Test Send Data") };
await sendStateProvider.setEncryptedSends(sendData);
await sendStateProvider.setEncryptedSends(sendData, mockUserId);
await awaitAsync();
const actual = await sendStateProvider.getEncryptedSends();
expect(actual).toStrictEqual(sendData);
expect(actual).toStrictEqual([mockUserId, sendData]);
});
});

View File

@@ -1,6 +1,7 @@
import { Observable, firstValueFrom } from "rxjs";
import { ActiveUserState, StateProvider } from "../../../platform/state";
import { ActiveUserState, CombinedState, StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { SendData } from "../models/data/send.data";
import { SendView } from "../models/view/send.view";
@@ -10,7 +11,7 @@ import { SendStateProvider as SendStateProviderAbstraction } from "./send-state.
/** State provider for sends */
export class SendStateProvider implements SendStateProviderAbstraction {
/** Observable for the encrypted sends for an active user */
encryptedState$: Observable<Record<string, SendData>>;
encryptedState$: Observable<CombinedState<Record<string, SendData>>>;
/** Observable with the decrypted sends for an active user */
decryptedState$: Observable<SendView[]>;
@@ -19,20 +20,20 @@ export class SendStateProvider implements SendStateProviderAbstraction {
constructor(protected stateProvider: StateProvider) {
this.activeUserEncryptedState = this.stateProvider.getActive(SEND_USER_ENCRYPTED);
this.encryptedState$ = this.activeUserEncryptedState.state$;
this.encryptedState$ = this.activeUserEncryptedState.combinedState$;
this.activeUserDecryptedState = this.stateProvider.getActive(SEND_USER_DECRYPTED);
this.decryptedState$ = this.activeUserDecryptedState.state$;
}
/** Gets the encrypted sends from state for an active user */
async getEncryptedSends(): Promise<{ [id: string]: SendData }> {
async getEncryptedSends(): Promise<CombinedState<{ [id: string]: SendData }>> {
return await firstValueFrom(this.encryptedState$);
}
/** Sets the encrypted send state for an active user */
async setEncryptedSends(value: { [id: string]: SendData }): Promise<void> {
await this.activeUserEncryptedState.update(() => value);
async setEncryptedSends(value: { [id: string]: SendData }, userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, SEND_USER_ENCRYPTED).update(() => value);
}
/** Gets the decrypted sends from state for the active user */

View File

@@ -55,6 +55,6 @@ export abstract class SendService implements UserKeyRotationDataProvider<SendWit
export abstract class InternalSendService extends SendService {
upsert: (send: SendData | SendData[]) => Promise<any>;
replace: (sends: { [id: string]: SendData }) => Promise<void>;
replace: (sends: { [id: string]: SendData }, userId: UserId) => Promise<void>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@@ -110,9 +110,12 @@ describe("SendService", () => {
const result = await firstValueFrom(singleSendObservable);
expect(result).toEqual(testSend("1", "Test Send"));
await sendService.replace({
"1": testSendData("1", "Test Send Updated"),
});
await sendService.replace(
{
"1": testSendData("1", "Test Send Updated"),
},
mockUserId,
);
const result2 = await firstValueFrom(singleSendObservable);
expect(result2).toEqual(testSend("1", "Test Send Updated"));
@@ -127,10 +130,13 @@ describe("SendService", () => {
//it is immediately called when subscribed, we need to reset the value
changed = false;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
@@ -138,10 +144,13 @@ describe("SendService", () => {
it("reports a change when notes changes on a new send", async () => {
const sendDataObject = createSendData() as SendData;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
let changed = false;
sendService.get$("1").subscribe(() => {
@@ -152,10 +161,13 @@ describe("SendService", () => {
//it is immediately called when subscribed, we need to reset the value
changed = false;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
@@ -163,10 +175,13 @@ describe("SendService", () => {
it("reports a change when Text changes on a new send", async () => {
const sendDataObject = createSendData() as SendData;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
let changed = false;
sendService.get$("1").subscribe(() => {
@@ -177,10 +192,13 @@ describe("SendService", () => {
changed = false;
sendDataObject.text.text = "new text";
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
@@ -188,10 +206,13 @@ describe("SendService", () => {
it("reports a change when Text is set as null on a new send", async () => {
const sendDataObject = createSendData() as SendData;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
let changed = false;
sendService.get$("1").subscribe(() => {
@@ -202,10 +223,13 @@ describe("SendService", () => {
changed = false;
sendDataObject.text = null;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
@@ -215,10 +239,13 @@ describe("SendService", () => {
type: SendType.File,
file: new SendFileData(new SendFileApi({ FileName: "name of file" })),
}) as SendData;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
sendDataObject.file = new SendFileData(new SendFileApi({ FileName: "updated name of file" }));
let changed = false;
@@ -229,10 +256,13 @@ describe("SendService", () => {
//it is immediately called when subscribed, we need to reset the value
changed = false;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(false);
});
@@ -240,10 +270,13 @@ describe("SendService", () => {
it("reports a change when key changes on a new send", async () => {
const sendDataObject = createSendData() as SendData;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
let changed = false;
sendService.get$("1").subscribe(() => {
@@ -254,10 +287,13 @@ describe("SendService", () => {
changed = false;
sendDataObject.key = "newKey";
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
@@ -265,10 +301,13 @@ describe("SendService", () => {
it("reports a change when revisionDate changes on a new send", async () => {
const sendDataObject = createSendData() as SendData;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
let changed = false;
sendService.get$("1").subscribe(() => {
@@ -279,10 +318,13 @@ describe("SendService", () => {
changed = false;
sendDataObject.revisionDate = "2025-04-05";
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
@@ -290,10 +332,13 @@ describe("SendService", () => {
it("reports a change when a property is set as null on a new send", async () => {
const sendDataObject = createSendData() as SendData;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
let changed = false;
sendService.get$("1").subscribe(() => {
@@ -304,10 +349,13 @@ describe("SendService", () => {
changed = false;
sendDataObject.name = null;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
@@ -317,10 +365,13 @@ describe("SendService", () => {
text: new SendTextData(new SendTextApi({ Text: null })),
}) as SendData;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
let changed = false;
sendService.get$("1").subscribe(() => {
@@ -330,23 +381,29 @@ describe("SendService", () => {
//it is immediately called when subscribed, we need to reset the value
changed = false;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(false);
sendDataObject.text.text = "Asdf";
await sendService.replace({
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
it("do not reports a change when nothing changes on the observed send", async () => {
it("do not report a change when nothing changes on the observed send", async () => {
let changed = false;
sendService.get$("1").subscribe(() => {
changed = true;
@@ -357,10 +414,13 @@ describe("SendService", () => {
//it is immediately called when subscribed, we need to reset the value
changed = false;
await sendService.replace({
"1": sendDataObject,
"2": testSendData("3", "Test Send 3"),
});
await sendService.replace(
{
"1": sendDataObject,
"2": testSendData("3", "Test Send 3"),
},
mockUserId,
);
expect(changed).toEqual(false);
});
@@ -373,9 +433,12 @@ describe("SendService", () => {
//it is immediately called when subscribed, we need to reset the value
changed = false;
await sendService.replace({
"2": testSendData("2", "Test Send 2"),
});
await sendService.replace(
{
"2": testSendData("2", "Test Send 2"),
},
mockUserId,
);
expect(changed).toEqual(true);
});
@@ -426,7 +489,7 @@ describe("SendService", () => {
});
it("returns empty array if there are no sends", async () => {
await sendService.replace(null);
await sendService.replace(null, mockUserId);
await awaitAsync();
@@ -461,16 +524,11 @@ describe("SendService", () => {
});
it("replace", async () => {
await sendService.replace({ "2": testSendData("2", "test 2") });
await sendService.replace({ "2": testSendData("2", "test 2") }, mockUserId);
expect(await firstValueFrom(sendService.sends$)).toEqual([testSend("2", "test 2")]);
});
it("clear", async () => {
await sendService.clear();
await awaitAsync();
expect(await firstValueFrom(sendService.sends$)).toEqual([]);
});
describe("Delete", () => {
it("Sends count should decrease after delete", async () => {
const sendsBeforeDelete = await firstValueFrom(sendService.sends$);
@@ -488,7 +546,7 @@ describe("SendService", () => {
});
it("Deleting on an empty sends array should not throw", async () => {
sendStateProvider.getEncryptedSends = jest.fn().mockResolvedValue(null);
stateProvider.activeUser.getFake(SEND_USER_ENCRYPTED).nextState(null);
await expect(sendService.delete("2")).resolves.not.toThrow();
});

View File

@@ -28,10 +28,10 @@ export class SendService implements InternalSendServiceAbstraction {
readonly sendKeyPurpose = "send";
sends$ = this.stateProvider.encryptedState$.pipe(
map((record) => Object.values(record || {}).map((data) => new Send(data))),
map(([, record]) => Object.values(record || {}).map((data) => new Send(data))),
);
sendViews$ = this.stateProvider.encryptedState$.pipe(
concatMap((record) =>
concatMap(([, record]) =>
this.decryptSends(Object.values(record || {}).map((data) => new Send(data))),
),
);
@@ -167,7 +167,7 @@ export class SendService implements InternalSendServiceAbstraction {
}
async getFromState(id: string): Promise<Send> {
const sends = await this.stateProvider.getEncryptedSends();
const [, sends] = await this.stateProvider.getEncryptedSends();
// eslint-disable-next-line
if (sends == null || !sends.hasOwnProperty(id)) {
return null;
@@ -177,7 +177,7 @@ export class SendService implements InternalSendServiceAbstraction {
}
async getAll(): Promise<Send[]> {
const sends = await this.stateProvider.getEncryptedSends();
const [, sends] = await this.stateProvider.getEncryptedSends();
const response: Send[] = [];
for (const id in sends) {
// eslint-disable-next-line
@@ -214,7 +214,8 @@ export class SendService implements InternalSendServiceAbstraction {
}
async upsert(send: SendData | SendData[]): Promise<any> {
let sends = await this.stateProvider.getEncryptedSends();
const [userId, currentSends] = await this.stateProvider.getEncryptedSends();
let sends = currentSends;
if (sends == null) {
sends = {};
}
@@ -227,16 +228,11 @@ export class SendService implements InternalSendServiceAbstraction {
});
}
await this.replace(sends);
}
async clear(userId?: string): Promise<any> {
await this.stateProvider.setDecryptedSends(null);
await this.stateProvider.setEncryptedSends(null);
await this.replace(sends, userId);
}
async delete(id: string | string[]): Promise<any> {
const sends = await this.stateProvider.getEncryptedSends();
const [userId, sends] = await this.stateProvider.getEncryptedSends();
if (sends == null) {
return;
}
@@ -252,11 +248,11 @@ export class SendService implements InternalSendServiceAbstraction {
});
}
await this.replace(sends);
await this.replace(sends, userId);
}
async replace(sends: { [id: string]: SendData }): Promise<any> {
await this.stateProvider.setEncryptedSends(sends);
async replace(sends: { [id: string]: SendData }, userId: UserId): Promise<any> {
await this.stateProvider.setEncryptedSends(sends, userId);
}
async getRotatedData(

View File

@@ -257,8 +257,8 @@ describe("UserStateSubject", () => {
let actual: TestType = null;
subject.subscribe({
error: (value) => {
actual = value;
error: (value: unknown) => {
actual = value as any;
},
});
subject.error(expected);
@@ -275,8 +275,8 @@ describe("UserStateSubject", () => {
let actual: TestType = null;
subject.subscribe({
error: (value) => {
actual = value;
error: (value: unknown) => {
actual = value as any;
},
});
subject.error("expectedError");
@@ -415,8 +415,8 @@ describe("UserStateSubject", () => {
let error = false;
subject.subscribe({
error: (e) => {
error = e;
error: (e: unknown) => {
error = e as any;
},
});
singleUserId$.next(errorUserId);
@@ -434,8 +434,8 @@ describe("UserStateSubject", () => {
let actual = false;
subject.subscribe({
error: (e) => {
actual = e;
error: (e: unknown) => {
actual = e as any;
},
});
singleUserId$.error(expected);
@@ -454,8 +454,8 @@ describe("UserStateSubject", () => {
let actual = false;
subject.subscribe({
error: (e) => {
actual = e;
error: (e: unknown) => {
actual = e as any;
},
});
when$.error(expected);
@@ -464,4 +464,14 @@ describe("UserStateSubject", () => {
expect(actual).toEqual(expected);
});
});
describe("userId", () => {
it("returns the userId to which the subject is bound", () => {
const state = new FakeSingleUserState<TestType>(SomeUser, { foo: "init" });
const singleUserId$ = new Subject<UserId>();
const subject = new UserStateSubject(state, { singleUserId$ });
expect(subject.userId).toEqual(SomeUser);
});
});
});

View File

@@ -15,6 +15,8 @@ import {
ignoreElements,
endWith,
startWith,
Observable,
Subscription,
} from "rxjs";
import { Simplify } from "type-fest";
@@ -59,7 +61,10 @@ export type UserStateSubjectDependencies<State, Dependency> = Simplify<
* @template State the state stored by the subject
* @template Dependencies use-specific dependencies provided by the user.
*/
export class UserStateSubject<State, Dependencies = null> implements SubjectLike<State> {
export class UserStateSubject<State, Dependencies = null>
extends Observable<State>
implements SubjectLike<State>
{
/**
* Instantiates the user state subject
* @param state the backing store of the subject
@@ -76,6 +81,8 @@ export class UserStateSubject<State, Dependencies = null> implements SubjectLike
private state: SingleUserState<State>,
private dependencies: UserStateSubjectDependencies<State, Dependencies>,
) {
super();
// normalize dependencies
const when$ = (this.dependencies.when$ ?? new BehaviorSubject(true)).pipe(
distinctUntilChanged(),
@@ -114,6 +121,12 @@ export class UserStateSubject<State, Dependencies = null> implements SubjectLike
});
}
/** The userId to which the subject is bound.
*/
get userId() {
return this.state.userId;
}
next(value: State) {
this.input?.next(value);
}
@@ -130,7 +143,7 @@ export class UserStateSubject<State, Dependencies = null> implements SubjectLike
* @param observer listening for events
* @returns the subscription
*/
subscribe(observer: Partial<Observer<State>> | ((value: State) => void)): Unsubscribable {
subscribe(observer?: Partial<Observer<State>> | ((value: State) => void) | null): Subscription {
return this.output.subscribe(observer);
}

View File

@@ -0,0 +1,48 @@
import { Simplify } from "type-fest";
/** Constraints that are shared by all primitive field types */
type PrimitiveConstraint = {
/** presence indicates the field is required */
required?: true;
};
/** Constraints that are shared by string fields */
type StringConstraints = {
/** minimum string length. When absent, min length is 0. */
minLength?: number;
/** maximum string length. When absent, max length is unbounded. */
maxLength?: number;
};
/** Constraints that are shared by number fields */
type NumberConstraints = {
/** minimum number value. When absent, min value is unbounded. */
min?: number;
/** maximum number value. When absent, min value is unbounded. */
max?: number;
/** presence indicates the field only accepts integer values */
integer?: true;
/** requires the number be a multiple of the step value */
step?: number;
};
/** Utility type that transforms keys of T into their supported
* validators.
*/
export type Constraints<T> = {
[Key in keyof T]: Simplify<
PrimitiveConstraint &
(T[Key] extends string
? StringConstraints
: T[Key] extends number
? NumberConstraints
: never)
>;
};
/** utility type for methods that evaluate constraints generically. */
export type AnyConstraint = PrimitiveConstraint & StringConstraints & NumberConstraints;

View File

@@ -133,7 +133,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated
*/
upsert: (cipher: CipherData | CipherData[]) => Promise<Record<CipherId, CipherData>>;
replace: (ciphers: { [id: string]: CipherData }) => Promise<any>;
replace: (ciphers: { [id: string]: CipherData }, userId: UserId) => Promise<any>;
clear: (userId?: string) => Promise<void>;
moveManyWithServer: (ids: string[], folderId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;

View File

@@ -1,6 +1,6 @@
import { Observable } from "rxjs";
import { CollectionId } from "../../types/guid";
import { CollectionId, UserId } from "../../types/guid";
import { CollectionData } from "../models/data/collection.data";
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/tree-node";
@@ -22,7 +22,7 @@ export abstract class CollectionService {
getAllNested: (collections?: CollectionView[]) => Promise<TreeNode<CollectionView>[]>;
getNested: (id: string) => Promise<TreeNode<CollectionView>>;
upsert: (collection: CollectionData | CollectionData[]) => Promise<any>;
replace: (collections: { [id: string]: CollectionData }) => Promise<any>;
replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise<any>;
clear: (userId?: string) => Promise<void>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@@ -45,7 +45,7 @@ export abstract class FolderService implements UserKeyRotationDataProvider<Folde
export abstract class InternalFolderService extends FolderService {
upsert: (folder: FolderData | FolderData[]) => Promise<void>;
replace: (folders: { [id: string]: FolderData }) => Promise<void>;
replace: (folders: { [id: string]: FolderData }, userId: UserId) => Promise<void>;
clear: (userId?: string) => Promise<void>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@@ -913,8 +913,8 @@ export class CipherService implements CipherServiceAbstraction {
});
}
async replace(ciphers: { [id: string]: CipherData }): Promise<any> {
await this.updateEncryptedCipherState(() => ciphers);
async replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise<any> {
await this.updateEncryptedCipherState(() => ciphers, userId);
}
/**
@@ -924,15 +924,18 @@ export class CipherService implements CipherServiceAbstraction {
*/
private async updateEncryptedCipherState(
update: (current: Record<CipherId, CipherData>) => Record<CipherId, CipherData>,
userId: UserId = null,
): Promise<Record<CipherId, CipherData>> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
userId ||= await firstValueFrom(this.stateProvider.activeUserId$);
// Store that we should wait for an update to return any ciphers
await this.ciphersExpectingUpdate.forceValue(true);
await this.clearDecryptedCiphersState(userId);
const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => {
const result = update(current ?? {});
return result;
});
const updatedCiphers = await this.stateProvider
.getUser(userId, ENCRYPTED_CIPHERS)
.update((current) => {
const result = update(current ?? {});
return result;
});
return updatedCiphers;
}

Some files were not shown because too many files have changed in this diff Show More