1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

Conflict resolution

This commit is contained in:
Carlos Gonçalves
2024-04-02 17:18:45 +01:00
parent 8a1df6671a
commit 0c0c2039ed
699 changed files with 17230 additions and 8095 deletions

View File

@@ -1,5 +1,7 @@
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
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";
@@ -19,6 +21,8 @@ export class CollectionsComponent implements OnInit {
cipher: CipherView;
collectionIds: string[];
collections: CollectionView[] = [];
organization: Organization;
flexibleCollectionsV1Enabled: boolean;
protected cipherDomain: Cipher;
@@ -27,6 +31,7 @@ export class CollectionsComponent implements OnInit {
protected platformUtilsService: PlatformUtilsService,
protected i18nService: I18nService,
protected cipherService: CipherService,
protected organizationService: OrganizationService,
private logService: LogService,
) {}
@@ -48,11 +53,21 @@ export class CollectionsComponent implements OnInit {
(c as any).checked = this.collectionIds != null && this.collectionIds.indexOf(c.id) > -1;
});
}
if (this.organization == null) {
this.organization = await this.organizationService.get(this.cipher.organizationId);
}
}
async submit() {
const selectedCollectionIds = this.collections
.filter((c) => !!(c as any).checked)
.filter((c) => {
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return !!(c as any).checked;
} else {
return !!(c as any).checked && c.readOnly == null;
}
})
.map((c) => c.id);
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
this.platformUtilsService.showToast(

View File

@@ -14,12 +14,17 @@ import {
throwError,
} from "rxjs";
import {
LoginEmailServiceAbstraction,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
@@ -30,7 +35,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { UserId } from "@bitwarden/common/types/guid";
enum State {
NewUser,
@@ -62,6 +67,8 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected data?: Data;
protected loading = true;
activeAccountId: UserId;
// Remember device means for the user to trust the device
rememberDeviceForm = this.formBuilder.group({
rememberDevice: [true],
@@ -79,7 +86,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected activatedRoute: ActivatedRoute,
protected messagingService: MessagingService,
protected tokenService: TokenService,
protected loginService: LoginService,
protected loginEmailService: LoginEmailServiceAbstraction,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
@@ -88,12 +95,15 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected validationService: ValidationService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected accountService: AccountService,
) {}
async ngOnInit() {
this.loading = true;
this.activeAccountId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
this.setupRememberDeviceValueChanges();
@@ -101,14 +111,15 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
await this.setRememberDeviceDefaultValue();
try {
const accountDecryptionOptions: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
const userDecryptionOptions = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
// see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response..
// can we check if they have a user key or master key in crypto service? Would that be sufficient?
if (
!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
!accountDecryptionOptions?.hasMasterPassword
!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
!userDecryptionOptions?.hasMasterPassword
) {
// We are dealing with a new account if:
// - User does not have admin approval (i.e. has not enrolled into admin reset)
@@ -118,7 +129,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.loadNewUserData();
} else {
this.loadUntrustedDeviceData(accountDecryptionOptions);
this.loadUntrustedDeviceData(userDecryptionOptions);
}
// Note: this is probably not a comprehensive write up of all scenarios:
@@ -145,7 +156,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
}
private async setRememberDeviceDefaultValue() {
const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice();
const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice(
this.activeAccountId,
);
const rememberDevice = rememberDeviceFromState ?? true;
@@ -156,7 +169,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
this.rememberDevice.valueChanges
.pipe(
switchMap((value) =>
defer(() => this.deviceTrustCryptoService.setShouldTrustDevice(value)),
defer(() =>
this.deviceTrustCryptoService.setShouldTrustDevice(this.activeAccountId, value),
),
),
takeUntil(this.destroy$),
)
@@ -195,7 +210,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
this.loading = false;
}
loadUntrustedDeviceData(accountDecryptionOptions: AccountDecryptionOptions) {
loadUntrustedDeviceData(userDecryptionOptions: UserDecryptionOptions) {
this.loading = true;
const email$ = from(this.stateService.getEmail()).pipe(
@@ -215,13 +230,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
)
.subscribe((email) => {
const showApproveFromOtherDeviceBtn =
accountDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
userDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
const showReqAdminApprovalBtn =
!!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
!!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
const showApproveWithMasterPasswordBtn =
accountDecryptionOptions?.hasMasterPassword || false;
const showApproveWithMasterPasswordBtn = userDecryptionOptions?.hasMasterPassword || false;
const userEmail = email;
@@ -240,23 +254,17 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
return;
}
this.loginService.setEmail(this.data.userEmail);
// 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-with-device"]);
this.loginEmailService.setEmail(this.data.userEmail);
await this.router.navigate(["/login-with-device"]);
}
async requestAdminApproval() {
this.loginService.setEmail(this.data.userEmail);
// 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(["/admin-approval-requested"]);
this.loginEmailService.setEmail(this.data.userEmail);
await this.router.navigate(["/admin-approval-requested"]);
}
async approveWithMasterPassword() {
// 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(["/lock"], { queryParams: { from: "login-initiated" } });
await this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } });
}
async createUser() {
@@ -280,7 +288,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);
if (this.rememberDeviceForm.value.rememberDevice) {
await this.deviceTrustCryptoService.trustDevice();
await this.deviceTrustCryptoService.trustDevice(this.activeAccountId);
}
} catch (error) {
this.validationService.showError(error);

View File

@@ -1,4 +1,5 @@
import { Directive, Input } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -19,7 +20,8 @@ export abstract class CaptchaProtectedComponent {
) {}
async setupCaptcha() {
const webVaultUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
this.captcha = new CaptchaIFrame(
window,

View File

@@ -7,17 +7,15 @@
#trigger="cdkOverlayOrigin"
aria-haspopup="dialog"
aria-controls="cdk-overlay-container"
[ngSwitch]="selectedEnvironment"
>
<span *ngSwitchCase="ServerEnvironmentType.US" class="text-primary">{{
"usDomain" | i18n
}}</span>
<span *ngSwitchCase="ServerEnvironmentType.EU" class="text-primary">{{
"euDomain" | i18n
}}</span>
<span *ngSwitchCase="ServerEnvironmentType.SelfHosted" class="text-primary">{{
"selfHostedServer" | i18n
}}</span>
<span class="text-primary">
<ng-container *ngIf="selectedRegion$ | async as selectedRegion; else fallback">
{{ selectedRegion.domain }}
</ng-container>
<ng-template #fallback>
{{ "selfHostedServer" | i18n }}
</ng-template>
</span>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</button>
</div>
@@ -41,40 +39,23 @@
role="dialog"
aria-modal="true"
>
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(ServerEnvironmentType.US)"
[attr.aria-pressed]="selectedEnvironment === ServerEnvironmentType.US ? 'true' : 'false'"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px"
aria-hidden="true"
[style.visibility]="
selectedEnvironment === ServerEnvironmentType.US ? 'visible' : 'hidden'
"
></i>
<span>{{ "usDomain" | i18n }}</span>
</button>
<br />
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(ServerEnvironmentType.EU)"
[attr.aria-pressed]="selectedEnvironment === ServerEnvironmentType.EU ? 'true' : 'false'"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px"
aria-hidden="true"
[style.visibility]="
selectedEnvironment === ServerEnvironmentType.EU ? 'visible' : 'hidden'
"
></i>
<span>{{ "euDomain" | i18n }}</span>
</button>
<br />
<ng-container *ngFor="let region of availableRegions">
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(region.key)"
[attr.aria-pressed]="selectedEnvironment === region.key ? 'true' : 'false'"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px"
aria-hidden="true"
[style.visibility]="selectedEnvironment === region.key ? 'visible' : 'hidden'"
></i>
<span>{{ region.domain }}</span>
</button>
<br />
</ng-container>
<button
type="button"
class="environment-selector-dialog-item"

View File

@@ -1,13 +1,13 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
import { Component, EventEmitter, Output } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { Observable, map } from "rxjs";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import {
EnvironmentService as EnvironmentServiceAbstraction,
EnvironmentService,
Region,
RegionConfig,
} from "@bitwarden/common/platform/abstractions/environment.service";
@Component({
@@ -34,7 +34,7 @@ import {
]),
],
})
export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
export class EnvironmentSelectorComponent {
@Output() onOpenSelfHostedSettings = new EventEmitter();
isOpen = false;
showingModal = false;
@@ -48,59 +48,34 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
overlayY: "top",
},
];
protected componentDestroyed$: Subject<void> = new Subject();
protected availableRegions = this.environmentService.availableRegions();
protected selectedRegion$: Observable<RegionConfig | undefined> =
this.environmentService.environment$.pipe(
map((e) => e.getRegion()),
map((r) => this.availableRegions.find((ar) => ar.key === r)),
);
constructor(
protected environmentService: EnvironmentServiceAbstraction,
protected configService: ConfigServiceAbstraction,
protected environmentService: EnvironmentService,
protected router: Router,
) {}
async ngOnInit() {
this.configService.serverConfig$.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => {
// 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.updateEnvironmentInfo();
});
// 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.updateEnvironmentInfo();
}
ngOnDestroy(): void {
this.componentDestroyed$.next();
this.componentDestroyed$.complete();
}
async toggle(option: Region) {
this.isOpen = !this.isOpen;
if (option === null) {
return;
}
// 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.updateEnvironmentInfo();
if (option === Region.SelfHosted) {
this.onOpenSelfHostedSettings.emit();
return;
}
await this.environmentService.setRegion(option);
// 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.updateEnvironmentInfo();
}
async updateEnvironmentInfo() {
this.selectedEnvironment = this.environmentService.selectedRegion;
await this.environmentService.setEnvironment(option);
}
close() {
this.isOpen = false;
// 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.updateEnvironmentInfo();
}
}

View File

@@ -1,4 +1,5 @@
import { Directive, EventEmitter, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
EnvironmentService,
@@ -27,21 +28,29 @@ export class EnvironmentComponent {
protected i18nService: I18nService,
private modalService: ModalService,
) {
const urls = this.environmentService.getUrls();
if (this.environmentService.selectedRegion != Region.SelfHosted) {
return;
}
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
if (env.getRegion() !== Region.SelfHosted) {
this.baseUrl = "";
this.webVaultUrl = "";
this.apiUrl = "";
this.identityUrl = "";
this.iconsUrl = "";
this.notificationsUrl = "";
return;
}
this.baseUrl = urls.base || "";
this.webVaultUrl = urls.webVault || "";
this.apiUrl = urls.api || "";
this.identityUrl = urls.identity || "";
this.iconsUrl = urls.icons || "";
this.notificationsUrl = urls.notifications || "";
const urls = env.getUrls();
this.baseUrl = urls.base || "";
this.webVaultUrl = urls.webVault || "";
this.apiUrl = urls.api || "";
this.identityUrl = urls.identity || "";
this.iconsUrl = urls.icons || "";
this.notificationsUrl = urls.notifications || "";
});
}
async submit() {
const resUrls = await this.environmentService.setUrls({
await this.environmentService.setEnvironment(Region.SelfHosted, {
base: this.baseUrl,
api: this.apiUrl,
identity: this.identityUrl,
@@ -50,14 +59,6 @@ export class EnvironmentComponent {
notifications: this.notificationsUrl,
});
// re-set urls since service can change them, ex: prefixing https://
this.baseUrl = resUrls.base;
this.apiUrl = resUrls.api;
this.identityUrl = resUrls.identity;
this.webVaultUrl = resUrls.webVault;
this.iconsUrl = resUrls.icons;
this.notificationsUrl = resUrls.notifications;
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
this.saved();
}

View File

@@ -1,8 +1,8 @@
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/password-hint.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -22,11 +22,11 @@ export class HintComponent implements OnInit {
protected apiService: ApiService,
protected platformUtilsService: PlatformUtilsService,
private logService: LogService,
private loginService: LoginService,
private loginEmailService: LoginEmailServiceAbstraction,
) {}
ngOnInit(): void {
this.email = this.loginService.getEmail() ?? "";
this.email = this.loginEmailService.getEmail() ?? "";
}
async submit() {

View File

@@ -10,6 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@@ -75,6 +76,7 @@ export class LockComponent implements OnInit, OnDestroy {
protected userVerificationService: UserVerificationService,
protected pinCryptoService: PinCryptoServiceAbstraction,
protected biometricStateService: BiometricStateService,
protected accountService: AccountService,
) {}
async ngOnInit() {
@@ -119,7 +121,7 @@ export class LockComponent implements OnInit, OnDestroy {
return;
}
await this.biometricStateService.setPromptCancelled();
await this.biometricStateService.setUserPromptCancelled();
const userKey = await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
if (userKey) {
@@ -269,14 +271,15 @@ export class LockComponent implements OnInit, OnDestroy {
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
await this.deviceTrustCryptoService.trustDeviceIfRequired();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id);
await this.doContinue(evaluatePasswordAfterUnlock);
}
private async doContinue(evaluatePasswordAfterUnlock: boolean) {
await this.stateService.setEverBeenUnlocked(true);
await this.biometricStateService.resetPromptCancelled();
await this.biometricStateService.resetUserPromptCancelled();
this.messagingService.send("unlocked");
if (evaluatePasswordAfterUnlock) {
@@ -346,7 +349,7 @@ export class LockComponent implements OnInit, OnDestroy {
!this.platformUtilsService.supportsSecureStorage());
this.email = await this.stateService.getEmail();
this.webVaultHostname = await this.environmentService.getHost();
this.webVaultHostname = (await this.environmentService.getEnvironment()).getHostname();
}
/**

View File

@@ -1,17 +1,18 @@
import { Directive, OnDestroy, OnInit } from "@angular/core";
import { IsActiveMatchOptions, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { Subject, firstValueFrom, takeUntil } from "rxjs";
import {
AuthRequestLoginCredentials,
AuthRequestServiceAbstraction,
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
@@ -68,7 +69,6 @@ export class LoginViaAuthRequestComponent
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
// TODO: in future, go to child components and remove child constructors and let deps fall through to the super class
constructor(
protected router: Router,
private cryptoService: CryptoService,
@@ -84,10 +84,11 @@ export class LoginViaAuthRequestComponent
private anonymousHubService: AnonymousHubService,
private validationService: ValidationService,
private stateService: StateService,
private loginService: LoginService,
private loginEmailService: LoginEmailServiceAbstraction,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private loginStrategyService: LoginStrategyServiceAbstraction,
private accountService: AccountService,
) {
super(environmentService, i18nService, platformUtilsService);
@@ -95,17 +96,19 @@ export class LoginViaAuthRequestComponent
// Why would the existence of the email depend on the navigation?
const navigation = this.router.getCurrentNavigation();
if (navigation) {
this.email = this.loginService.getEmail();
this.email = this.loginEmailService.getEmail();
}
//gets signalR push notification
this.loginStrategyService.authRequestPushNotification$
// Gets signalR push notification
// Only fires on approval to prevent enumeration
this.authRequestService.authRequestPushNotification$
.pipe(takeUntil(this.destroy$))
.subscribe((id) => {
// Only fires on approval currently
// 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.verifyAndHandleApprovedAuthReq(id);
this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => {
this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message);
this.logService.error("Failed to use approved auth request: " + e.message);
});
});
}
@@ -150,7 +153,7 @@ export class LoginViaAuthRequestComponent
} else {
// Standard auth request
// TODO: evaluate if we can remove the setting of this.email in the constructor
this.email = this.loginService.getEmail();
this.email = this.loginEmailService.getEmail();
if (!this.email) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
@@ -164,10 +167,10 @@ export class LoginViaAuthRequestComponent
}
}
ngOnDestroy(): void {
async ngOnDestroy() {
await this.anonymousHubService.stopHubConnection();
this.destroy$.next();
this.destroy$.complete();
this.anonymousHubService.stopHubConnection();
}
private async handleExistingAdminAuthRequest(adminAuthReqStorable: AdminAuthRequestStorable) {
@@ -213,7 +216,7 @@ export class LoginViaAuthRequestComponent
// Request still pending response from admin
// So, create hub connection so that any approvals will be received via push notification
this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
}
private async handleExistingAdminAuthReqDeletedOrDenied() {
@@ -273,7 +276,7 @@ export class LoginViaAuthRequestComponent
}
if (reqResponse.id) {
this.anonymousHubService.createHubConnection(reqResponse.id);
await this.anonymousHubService.createHubConnection(reqResponse.id);
}
} catch (e) {
this.logService.error(e);
@@ -387,7 +390,8 @@ export class LoginViaAuthRequestComponent
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
await this.deviceTrustCryptoService.trustDeviceIfRequired();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id);
// TODO: don't forget to use auto enrollment service everywhere we trust device
@@ -471,17 +475,10 @@ export class LoginViaAuthRequestComponent
}
}
async setRememberEmailValues() {
const rememberEmail = this.loginService.getRememberEmail();
const rememberedEmail = this.loginService.getEmail();
await this.stateService.setRememberedEmail(rememberEmail ? rememberedEmail : null);
this.loginService.clearValues();
}
private async handleSuccessfulLoginNavigation() {
if (this.state === State.StandardAuthRequest) {
// Only need to set remembered email on standard login with auth req flow
await this.setRememberEmailValues();
await this.loginEmailService.saveEmailSettings();
}
if (this.onSuccessfulLogin != null) {

View File

@@ -1,12 +1,15 @@
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject } from "rxjs";
import { Subject, firstValueFrom } from "rxjs";
import { take, takeUntil } from "rxjs/operators";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
PasswordLoginCredentials,
} from "@bitwarden/auth/common";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
@@ -77,17 +80,13 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
protected formBuilder: FormBuilder,
protected formValidationErrorService: FormValidationErrorsService,
protected route: ActivatedRoute,
protected loginService: LoginService,
protected loginEmailService: LoginEmailServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected webAuthnLoginService: WebAuthnLoginServiceAbstraction,
) {
super(environmentService, i18nService, platformUtilsService);
}
get selfHostedDomain() {
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
}
async ngOnInit() {
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
if (!params) {
@@ -97,25 +96,23 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
const queryParamsEmail = params.email;
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
this.formGroup.get("email").setValue(queryParamsEmail);
this.loginService.setEmail(queryParamsEmail);
this.formGroup.controls.email.setValue(queryParamsEmail);
this.paramEmailSet = true;
}
});
let email = this.loginService.getEmail();
if (email == null || email === "") {
email = await this.stateService.getRememberedEmail();
}
if (!this.paramEmailSet) {
this.formGroup.get("email")?.setValue(email ?? "");
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
this.formGroup.controls.email.setValue(storedEmail ?? "");
}
let rememberEmail = this.loginService.getRememberEmail();
let rememberEmail = this.loginEmailService.getRememberEmail();
if (rememberEmail == null) {
rememberEmail = (await this.stateService.getRememberedEmail()) != null;
rememberEmail = (await firstValueFrom(this.loginEmailService.storedEmail$)) != null;
}
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
}
ngOnDestroy() {
@@ -152,8 +149,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
this.formPromise = this.loginStrategyService.logIn(credentials);
const response = await this.formPromise;
this.setFormValues();
await this.loginService.saveEmailSettings();
this.setLoginEmailValues();
await this.loginEmailService.saveEmailSettings();
if (this.handleCaptchaRequired(response)) {
return;
} else if (this.handleMigrateEncryptionKey(response)) {
@@ -218,7 +217,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
return;
}
this.setFormValues();
this.setLoginEmailValues();
// 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-with-device"]);
@@ -245,7 +244,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
// Build URI
const webUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const webUrl = env.getWebVaultUrl();
// Launch browser
this.platformUtilsService.launchUri(
@@ -295,14 +295,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
}
}
setFormValues() {
this.loginService.setEmail(this.formGroup.value.email);
this.loginService.setRememberEmail(this.formGroup.value.rememberEmail);
setLoginEmailValues() {
this.loginEmailService.setEmail(this.formGroup.value.email);
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
}
async saveEmailSettings() {
this.setFormValues();
await this.loginService.saveEmailSettings();
this.setLoginEmailValues();
await this.loginEmailService.saveEmailSettings();
// Save off email for SSO
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);

View File

@@ -1,8 +1,9 @@
import { Directive } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { of } from "rxjs";
import { firstValueFrom, of } from "rxjs";
import { filter, first, switchMap, tap } from "rxjs/operators";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@@ -26,7 +27,6 @@ import {
DEFAULT_KDF_CONFIG,
} from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
@@ -64,6 +64,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
stateService: StateService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
) {
@@ -228,11 +229,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None);
// User now has a password so update account decryption options in state
const acctDecryptionOpts: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
acctDecryptionOpts.hasMasterPassword = true;
await this.stateService.setAccountDecryptionOptions(acctDecryptionOpts);
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
userDecryptionOpts.hasMasterPassword = true;
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.stateService.setKdfType(this.kdf);
await this.stateService.setKdfConfig(this.kdfConfig);

View File

@@ -2,24 +2,27 @@ import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended";
import { Observable, of } from "rxjs";
import { BehaviorSubject, Observable, of } from "rxjs";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
LoginStrategyServiceAbstraction,
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SsoComponent } from "./sso.component";
@@ -62,7 +65,8 @@ describe("SsoComponent", () => {
let mockEnvironmentService: MockProxy<EnvironmentService>;
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
let mockLogService: MockProxy<LogService>;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>;
// Mock authService.logIn params
let code: string;
@@ -77,17 +81,19 @@ describe("SsoComponent", () => {
let mockOnSuccessfulLoginForceResetNavigate: jest.Mock;
let mockOnSuccessfulLoginTdeNavigate: jest.Mock;
let mockAcctDecryptionOpts: {
noMasterPassword: AccountDecryptionOptions;
withMasterPassword: AccountDecryptionOptions;
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
withMasterPasswordAndKeyConnector: AccountDecryptionOptions;
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
noMasterPasswordWithKeyConnector: AccountDecryptionOptions;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
withMasterPassword: UserDecryptionOptions;
withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
withMasterPasswordAndKeyConnector: UserDecryptionOptions;
noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
noMasterPasswordWithKeyConnector: UserDecryptionOptions;
};
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
beforeEach(() => {
// Mock Services
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
@@ -101,15 +107,16 @@ describe("SsoComponent", () => {
queryParams: mockQueryParams,
} as any as ActivatedRoute;
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockStateService = mock<StateService>();
mockPlatformUtilsService = mock<PlatformUtilsService>();
mockApiService = mock<ApiService>();
mockCryptoFunctionService = mock<CryptoFunctionService>();
mockEnvironmentService = mock<EnvironmentService>();
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
mockLogService = mock<LogService>();
mockConfigService = mock<ConfigServiceAbstraction>();
mockSsoLoginService = mock();
mockStateService = mock();
mockPlatformUtilsService = mock();
mockApiService = mock();
mockCryptoFunctionService = mock();
mockEnvironmentService = mock();
mockPasswordGenerationService = mock();
mockLogService = mock();
mockUserDecryptionOptionsService = mock();
mockConfigService = mock();
// Mock loginStrategyService.logIn params
code = "code";
@@ -124,49 +131,52 @@ describe("SsoComponent", () => {
mockOnSuccessfulLoginForceResetNavigate = jest.fn();
mockOnSuccessfulLoginTdeNavigate = jest.fn();
mockAcctDecryptionOpts = {
noMasterPassword: new AccountDecryptionOptions({
mockUserDecryptionOpts = {
noMasterPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: undefined,
keyConnectorOption: undefined,
}),
withMasterPassword: new AccountDecryptionOptions({
withMasterPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: undefined,
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined,
}),
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}),
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined,
}),
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}),
};
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
TestBed.configureTestingModule({
declarations: [TestSsoComponent],
providers: [
@@ -183,8 +193,12 @@ describe("SsoComponent", () => {
{ provide: EnvironmentService, useValue: mockEnvironmentService },
{ provide: PasswordGenerationServiceAbstraction, useValue: mockPasswordGenerationService },
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: mockUserDecryptionOptionsService,
},
{ provide: LogService, useValue: mockLogService },
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
{ provide: ConfigService, useValue: mockConfigService },
],
});
@@ -230,9 +244,7 @@ describe("SsoComponent", () => {
authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]);
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
mockLoginStrategyService.logIn.mockResolvedValue(authResult);
});
@@ -341,8 +353,8 @@ describe("SsoComponent", () => {
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
);
authResult = new AuthResult();
@@ -377,8 +389,8 @@ describe("SsoComponent", () => {
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
);
authResult = new AuthResult();
@@ -394,8 +406,8 @@ describe("SsoComponent", () => {
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
);
authResult = new AuthResult();
@@ -440,9 +452,7 @@ describe("SsoComponent", () => {
describe("Given user needs to set a master password", () => {
beforeEach(() => {
// Only need to test the case where the user has no master password to test the primary change mp flow here
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
});
testChangePasswordOnSuccessfulLogin();
@@ -450,9 +460,7 @@ describe("SsoComponent", () => {
});
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPasswordWithKeyConnector);
await _component.logIn(code, codeVerifier, orgIdFromState);
expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1);
@@ -475,9 +483,7 @@ describe("SsoComponent", () => {
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
const authResult = new AuthResult();
authResult.forcePasswordReset = forceResetPasswordReason;
@@ -494,9 +500,7 @@ describe("SsoComponent", () => {
const authResult = new AuthResult();
authResult.twoFactorProviders = null;
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
authResult.forcePasswordReset = ForceSetPasswordReason.None;
mockLoginStrategyService.logIn.mockResolvedValue(authResult);
});

View File

@@ -1,15 +1,21 @@
import { Directive } from "@angular/core";
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { LoginStrategyServiceAbstraction, SsoLoginCredentials } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
SsoLoginCredentials,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -17,7 +23,6 @@ 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 { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
@Directive()
@@ -59,7 +64,8 @@ export class SsoComponent {
protected environmentService: EnvironmentService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected logService: LogService,
protected configService: ConfigServiceAbstraction,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected configService: ConfigService,
) {}
async ngOnInit() {
@@ -151,8 +157,10 @@ export class SsoComponent {
// Save state (regardless of new or existing)
await this.ssoLoginService.setSsoState(state);
const env = await firstValueFrom(this.environmentService.environment$);
let authorizeUrl =
this.environmentService.getIdentityUrl() +
env.getIdentityUrl() +
"/connect/authorize?" +
"client_id=" +
this.clientId +
@@ -194,9 +202,6 @@ export class SsoComponent {
this.formPromise = this.loginStrategyService.logIn(credentials);
const authResult = await this.formPromise;
const acctDecryptionOpts: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
if (authResult.requiresTwoFactor) {
return await this.handleTwoFactorRequired(orgSsoIdentifier);
}
@@ -217,15 +222,20 @@ export class SsoComponent {
return await this.handleForcePasswordReset(orgSsoIdentifier);
}
// must come after 2fa check since user decryption options aren't available if 2fa is required
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
const tdeEnabled = await this.isTrustedDeviceEncEnabled(
acctDecryptionOpts.trustedDeviceOption,
userDecryptionOpts.trustedDeviceOption,
);
if (tdeEnabled) {
return await this.handleTrustedDeviceEncryptionEnabled(
authResult,
orgSsoIdentifier,
acctDecryptionOpts,
userDecryptionOpts,
);
}
@@ -233,8 +243,8 @@ export class SsoComponent {
// have one and they aren't using key connector.
// Note: TDE & Key connector are mutually exclusive org config options.
const requireSetPassword =
!acctDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.keyConnectorOption === undefined;
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.keyConnectorOption === undefined;
if (requireSetPassword || authResult.resetMasterPassword) {
// Change implies going no password -> password in this case
@@ -270,12 +280,12 @@ export class SsoComponent {
private async handleTrustedDeviceEncryptionEnabled(
authResult: AuthResult,
orgIdentifier: string,
acctDecryptionOpts: AccountDecryptionOptions,
userDecryptionOpts: UserDecryptionOptions,
): Promise<void> {
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (
!acctDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) {
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
// Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and

View File

@@ -1,5 +1,6 @@
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@@ -31,8 +32,9 @@ export class TwoFactorOptionsComponent implements OnInit {
this.onProviderSelected.emit(p.type);
}
recover() {
const webVault = this.environmentService.getWebVaultUrl();
async recover() {
const env = await firstValueFrom(this.environmentService.environment$);
const webVault = env.getWebVaultUrl();
this.platformUtilsService.launchUri(webVault + "/#/recover-2fa");
this.onRecoverSelected.emit();
}

View File

@@ -1,28 +1,32 @@
import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended";
import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { TwoFactorComponent } from "./two-factor.component";
@@ -55,21 +59,24 @@ describe("TwoFactorComponent", () => {
let mockLogService: MockProxy<LogService>;
let mockTwoFactorService: MockProxy<TwoFactorService>;
let mockAppIdService: MockProxy<AppIdService>;
let mockLoginService: MockProxy<LoginService>;
let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>;
let mockAcctDecryptionOpts: {
noMasterPassword: AccountDecryptionOptions;
withMasterPassword: AccountDecryptionOptions;
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
withMasterPasswordAndKeyConnector: AccountDecryptionOptions;
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
noMasterPasswordWithKeyConnector: AccountDecryptionOptions;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
withMasterPassword: UserDecryptionOptions;
withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
withMasterPasswordAndKeyConnector: UserDecryptionOptions;
noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
noMasterPasswordWithKeyConnector: UserDecryptionOptions;
};
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
beforeEach(() => {
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
mockRouter = mock<Router>();
@@ -82,53 +89,57 @@ describe("TwoFactorComponent", () => {
mockLogService = mock<LogService>();
mockTwoFactorService = mock<TwoFactorService>();
mockAppIdService = mock<AppIdService>();
mockLoginService = mock<LoginService>();
mockLoginEmailService = mock<LoginEmailServiceAbstraction>();
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockConfigService = mock<ConfigServiceAbstraction>();
mockConfigService = mock<ConfigService>();
mockAcctDecryptionOpts = {
noMasterPassword: new AccountDecryptionOptions({
mockUserDecryptionOpts = {
noMasterPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: undefined,
keyConnectorOption: undefined,
}),
withMasterPassword: new AccountDecryptionOptions({
withMasterPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: undefined,
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined,
}),
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}),
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined,
}),
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}),
};
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
TestBed.configureTestingModule({
declarations: [TestTwoFactorComponent],
providers: [
@@ -152,9 +163,13 @@ describe("TwoFactorComponent", () => {
{ provide: LogService, useValue: mockLogService },
{ provide: TwoFactorService, useValue: mockTwoFactorService },
{ provide: AppIdService, useValue: mockAppIdService },
{ provide: LoginService, useValue: mockLoginService },
{ provide: LoginEmailServiceAbstraction, useValue: mockLoginEmailService },
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: mockUserDecryptionOptionsService,
},
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
{ provide: ConfigService, useValue: mockConfigService },
],
});
@@ -213,9 +228,7 @@ describe("TwoFactorComponent", () => {
component.remember = remember;
component.captchaToken = captchaToken;
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
});
it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => {
@@ -267,11 +280,11 @@ describe("TwoFactorComponent", () => {
expect(component.onSuccessfulLogin).toHaveBeenCalled();
});
it("calls loginService.clearValues() when login is successful", async () => {
it("calls loginEmailService.clearValues() when login is successful", async () => {
// Arrange
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
// spy on loginService.clearValues
const clearValuesSpy = jest.spyOn(mockLoginService, "clearValues");
// spy on loginEmailService.clearValues
const clearValuesSpy = jest.spyOn(mockLoginEmailService, "clearValues");
// Act
await component.doSubmit();
@@ -289,17 +302,15 @@ describe("TwoFactorComponent", () => {
describe("Given user needs to set a master password", () => {
beforeEach(() => {
// Only need to test the case where the user has no master password to test the primary change mp flow here
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
});
testChangePasswordOnSuccessfulLogin();
});
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
);
await component.doSubmit();
@@ -321,9 +332,7 @@ describe("TwoFactorComponent", () => {
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
const authResult = new AuthResult();
authResult.forcePasswordReset = forceResetPasswordReason;
@@ -385,8 +394,8 @@ describe("TwoFactorComponent", () => {
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
);
const authResult = new AuthResult();
@@ -420,8 +429,8 @@ describe("TwoFactorComponent", () => {
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
);
const authResult = new AuthResult();
@@ -436,8 +445,8 @@ describe("TwoFactorComponent", () => {
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
);
authResult = new AuthResult();

View File

@@ -6,28 +6,31 @@ import { first } from "rxjs/operators";
// eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
@@ -85,9 +88,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected logService: LogService,
protected twoFactorService: TwoFactorService,
protected appIdService: AppIdService,
protected loginService: LoginService,
protected loginEmailService: LoginEmailServiceAbstraction,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected configService: ConfigServiceAbstraction,
protected configService: ConfigService,
) {
super(environmentService, i18nService, platformUtilsService);
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
@@ -112,7 +116,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
}
if (this.win != null && this.webAuthnSupported) {
const webVaultUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
this.webAuthn = new WebAuthnIFrame(
this.win,
webVaultUrl,
@@ -283,29 +288,30 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
// - TDE login decryption options component
// - Browser SSO on extension open
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
this.loginService.clearValues();
this.loginEmailService.clearValues();
// note: this flow affects both TDE & standard users
if (this.isForcePasswordResetRequired(authResult)) {
return await this.handleForcePasswordReset(this.orgIdentifier);
}
const acctDecryptionOpts: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
const tdeEnabled = await this.isTrustedDeviceEncEnabled(acctDecryptionOpts.trustedDeviceOption);
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
if (tdeEnabled) {
return await this.handleTrustedDeviceEncryptionEnabled(
authResult,
this.orgIdentifier,
acctDecryptionOpts,
userDecryptionOpts,
);
}
// User must set password if they don't have one and they aren't using either TDE or key connector.
const requireSetPassword =
!acctDecryptionOpts.hasMasterPassword && acctDecryptionOpts.keyConnectorOption === undefined;
!userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined;
if (requireSetPassword || authResult.resetMasterPassword) {
// Change implies going no password -> password in this case
@@ -326,12 +332,12 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
private async handleTrustedDeviceEncryptionEnabled(
authResult: AuthResult,
orgIdentifier: string,
acctDecryptionOpts: AccountDecryptionOptions,
userDecryptionOpts: UserDecryptionOptions,
): Promise<void> {
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (
!acctDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) {
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
// Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and
@@ -489,5 +495,5 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
}
// implemented in clients
launchDuoFrameless() {}
async launchDuoFrameless() {}
}

View File

@@ -53,7 +53,7 @@ export function lockGuard(): CanActivateFn {
// User is authN and in locked state.
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
// Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow
// The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP

View File

@@ -46,7 +46,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
// login decryption options component.
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) {
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });

View File

@@ -26,7 +26,7 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn {
const router = inject(Router);
const authStatus = await authService.getAuthStatus();
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) {
return router.createUrlTree(["/"]);

View File

@@ -62,6 +62,7 @@ export class ShareComponent implements OnInit, OnDestroy {
this.organizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
if (this.organizationId == null && orgs.length > 0) {
this.organizationId = orgs[0].id;
this.filterCollections();
}
});
@@ -69,8 +70,6 @@ export class ShareComponent implements OnInit, OnDestroy {
this.cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain),
);
this.filterCollections();
}
filterCollections() {

View File

@@ -4,7 +4,7 @@ import { By } from "@angular/platform-browser";
import { mock, MockProxy } from "jest-mock-extended";
import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { IfFeatureDirective } from "./if-feature.directive";
@@ -39,7 +39,7 @@ class TestComponent {
describe("IfFeatureDirective", () => {
let fixture: ComponentFixture<TestComponent>;
let content: HTMLElement;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>;
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => {
mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) =>
@@ -51,14 +51,14 @@ describe("IfFeatureDirective", () => {
fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement;
beforeEach(async () => {
mockConfigService = mock<ConfigServiceAbstraction>();
mockConfigService = mock<ConfigService>();
await TestBed.configureTestingModule({
declarations: [IfFeatureDirective, TestComponent],
providers: [
{ provide: LogService, useValue: mock<LogService>() },
{
provide: ConfigServiceAbstraction,
provide: ConfigService,
useValue: mockConfigService,
},
],

View File

@@ -1,7 +1,7 @@
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
/**
@@ -30,7 +30,7 @@ export class IfFeatureDirective implements OnInit {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private configService: ConfigServiceAbstraction,
private configService: ConfigService,
private logService: LogService,
) {}

View File

@@ -29,7 +29,6 @@ import { UserTypePipe } from "./pipes/user-type.pipe";
import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe";
import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe";
import { I18nPipe } from "./platform/pipes/i18n.pipe";
import { ExportScopeCalloutComponent } from "./tools/export/components/export-scope-callout.component";
import { PasswordStrengthComponent } from "./tools/password-strength/password-strength.component";
import { IconComponent } from "./vault/components/icon.component";
@@ -54,7 +53,6 @@ import { IconComponent } from "./vault/components/icon.component";
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
ExportScopeCalloutComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,
@@ -85,7 +83,6 @@ import { IconComponent } from "./vault/components/icon.component";
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
ExportScopeCalloutComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,

View File

@@ -9,5 +9,5 @@ export interface FormGroupControls {
}
export abstract class FormValidationErrorsService {
getFormValidationErrors: (controls: FormGroupControls) => AllValidationErrors[];
abstract getFormValidationErrors(controls: FormGroupControls): AllValidationErrors[];
}

View File

@@ -5,7 +5,7 @@ import { RouterTestingModule } from "@angular/router/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
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";
@@ -21,11 +21,11 @@ describe("canAccessFeature", () => {
const featureRoute = "enabled-feature";
const redirectRoute = "redirect";
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>;
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
const setup = (featureGuard: CanActivateFn, flagValue: any) => {
mockConfigService = mock<ConfigServiceAbstraction>();
mockConfigService = mock<ConfigService>();
mockPlatformUtilsService = mock<PlatformUtilsService>();
// Mock the correct getter based on the type of flagValue; also mock default values if one is not provided
@@ -56,7 +56,7 @@ describe("canAccessFeature", () => {
]),
],
providers: [
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
{ provide: LogService, useValue: mock<LogService>() },
{

View File

@@ -2,7 +2,7 @@ import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
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";
@@ -23,7 +23,7 @@ export const canAccessFeature = (
redirectUrlOnDisabled?: string,
): CanActivateFn => {
return async () => {
const configService = inject(ConfigServiceAbstraction);
const configService = inject(ConfigService);
const platformUtilsService = inject(PlatformUtilsService);
const router = inject(Router);
const i18nService = inject(I18nService);

View File

@@ -0,0 +1,22 @@
import { ErrorHandler, Injectable, Injector, inject } from "@angular/core";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@Injectable()
export class LoggingErrorHandler extends ErrorHandler {
/**
* When injecting services into an `ErrorHandler`, we must use the `Injector` manually to avoid circular dependency errors.
*
* https://stackoverflow.com/a/57115053
*/
private injector = inject(Injector);
override handleError(error: any): void {
try {
const logService = this.injector.get(LogService, null);
logService.error(error);
} catch {
super.handleError(error);
}
}
}

View File

@@ -11,12 +11,12 @@ export abstract class AbstractThemingService {
* The effective theme based on the user configured choice and the current system theme if
* the configured choice is {@link ThemeType.System}.
*/
theme$: Observable<ThemeType>;
abstract theme$: Observable<ThemeType>;
/**
* Listens for effective theme changes and applies changes to the provided document.
* @param document The document that should have theme classes applied to it.
*
* @returns A subscription that can be unsubscribed from to cancel the application of theme classes.
*/
applyThemeChangesTo: (document: Document) => Subscription;
abstract applyThemeChangesTo(document: Document): Subscription;
}

View File

@@ -4,7 +4,7 @@ import { Constructor, Opaque } from "type-fest";
import { SafeInjectionToken } from "../../services/injection-tokens";
/**
* The return type of our dependency helper functions.
* The return type of the {@link safeProvider} helper function.
* Used to distinguish a type safe provider definition from a non-type safe provider definition.
*/
export type SafeProvider = Opaque<Provider>;
@@ -18,12 +18,22 @@ type MapParametersToDeps<T> = {
type SafeInjectionTokenType<T> = T extends SafeInjectionToken<infer J> ? J : never;
/**
* Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken
*/
type ProviderInstanceType<T> =
T extends SafeInjectionToken<any>
? InstanceType<SafeInjectionTokenType<T>>
: T extends Constructor<any> | AbstractConstructor<any>
? InstanceType<T>
: never;
/**
* Represents a dependency provided with the useClass option.
*/
type SafeClassProvider<
A extends AbstractConstructor<any>,
I extends Constructor<InstanceType<A>>,
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
I extends Constructor<ProviderInstanceType<A>>,
D extends MapParametersToDeps<ConstructorParameters<I>>,
> = {
provide: A;
@@ -40,42 +50,41 @@ type SafeValueProvider<A extends SafeInjectionToken<any>, V extends SafeInjectio
};
/**
* Represents a dependency provided with the useFactory option where a SafeInjectionToken is used as the token.
* Represents a dependency provided with the useFactory option.
*/
type SafeFactoryProviderWithToken<
A extends SafeInjectionToken<any>,
I extends (...args: any) => InstanceType<SafeInjectionTokenType<A>>,
D extends MapParametersToDeps<Parameters<I>>,
> = {
provide: A;
useFactory: I;
deps: D;
};
/**
* Represents a dependency provided with the useFactory option where an abstract class is used as the token.
*/
type SafeFactoryProviderWithClass<
A extends AbstractConstructor<any>,
I extends (...args: any) => InstanceType<A>,
type SafeFactoryProvider<
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
I extends (...args: any) => ProviderInstanceType<A>,
D extends MapParametersToDeps<Parameters<I>>,
> = {
provide: A;
useFactory: I;
deps: D;
multi?: boolean;
};
/**
* Represents a dependency provided with the useExisting option.
*/
type SafeExistingProvider<
A extends Constructor<any> | AbstractConstructor<any>,
I extends Constructor<InstanceType<A>> | AbstractConstructor<InstanceType<A>>,
A extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
I extends Constructor<ProviderInstanceType<A>> | AbstractConstructor<ProviderInstanceType<A>>,
> = {
provide: A;
useExisting: I;
};
/**
* Represents a dependency where there is no abstract token, the token is the implementation
*/
type SafeConcreteProvider<
I extends Constructor<any>,
D extends MapParametersToDeps<ConstructorParameters<I>>,
> = {
provide: I;
deps: D;
};
/**
* A factory function that creates a provider for the ngModule providers array.
* This guarantees type safety for your provider definition. It does nothing at runtime.
@@ -84,31 +93,30 @@ type SafeExistingProvider<
*/
export const safeProvider = <
// types for useClass
AClass extends AbstractConstructor<any>,
IClass extends Constructor<InstanceType<AClass>>,
AClass extends AbstractConstructor<any> | SafeInjectionToken<any>,
IClass extends Constructor<ProviderInstanceType<AClass>>,
DClass extends MapParametersToDeps<ConstructorParameters<IClass>>,
// types for useValue
AValue extends SafeInjectionToken<any>,
VValue extends SafeInjectionTokenType<AValue>,
// types for useFactoryWithToken
AFactoryToken extends SafeInjectionToken<any>,
IFactoryToken extends (...args: any) => InstanceType<SafeInjectionTokenType<AFactoryToken>>,
DFactoryToken extends MapParametersToDeps<Parameters<IFactoryToken>>,
// types for useFactoryWithClass
AFactoryClass extends AbstractConstructor<any>,
IFactoryClass extends (...args: any) => InstanceType<AFactoryClass>,
DFactoryClass extends MapParametersToDeps<Parameters<IFactoryClass>>,
// types for useFactory
AFactory extends AbstractConstructor<any> | SafeInjectionToken<any>,
IFactory extends (...args: any) => ProviderInstanceType<AFactory>,
DFactory extends MapParametersToDeps<Parameters<IFactory>>,
// types for useExisting
AExisting extends Constructor<any> | AbstractConstructor<any>,
AExisting extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
IExisting extends
| Constructor<InstanceType<AExisting>>
| AbstractConstructor<InstanceType<AExisting>>,
| Constructor<ProviderInstanceType<AExisting>>
| AbstractConstructor<ProviderInstanceType<AExisting>>,
// types for no token
IConcrete extends Constructor<any>,
DConcrete extends MapParametersToDeps<ConstructorParameters<IConcrete>>,
>(
provider:
| SafeClassProvider<AClass, IClass, DClass>
| SafeValueProvider<AValue, VValue>
| SafeFactoryProviderWithToken<AFactoryToken, IFactoryToken, DFactoryToken>
| SafeFactoryProviderWithClass<AFactoryClass, IFactoryClass, DFactoryClass>
| SafeFactoryProvider<AFactory, IFactory, DFactory>
| SafeExistingProvider<AExisting, IExisting>
| SafeConcreteProvider<IConcrete, DConcrete>
| Constructor<unknown>,
): SafeProvider => provider as SafeProvider;

View File

@@ -1,5 +1,4 @@
import { LOCALE_ID, NgModule } from "@angular/core";
import { UnwrapOpaque } from "type-fest";
import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
import {
AuthRequestServiceAbstraction,
@@ -8,6 +7,11 @@ import {
PinCryptoService,
LoginStrategyServiceAbstraction,
LoginStrategyService,
LoginEmailServiceAbstraction,
LoginEmailService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@@ -56,7 +60,6 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
@@ -75,7 +78,6 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
@@ -108,11 +110,11 @@ import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/platform/abstractions/environment.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@@ -132,12 +134,12 @@ import { Account } from "@bitwarden/common/platform/models/domain/account";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
@@ -236,6 +238,7 @@ import { UnauthGuard } from "../auth/guards/unauth.guard";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
import { BroadcasterService } from "../platform/services/broadcaster.service";
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
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";
@@ -243,8 +246,8 @@ import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
import {
LOCALES_DIRECTORY,
LOCKED_CALLBACK,
LOG_MAC_FAILURES,
LOGOUT_CALLBACK,
LOG_MAC_FAILURES,
MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
@@ -264,7 +267,7 @@ import { ModalService } from "./modal.service";
* Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety.
* If you need help please ask for it, do NOT change the type of this array.
*/
const typesafeProviders: Array<SafeProvider> = [
const safeProviders: SafeProvider[] = [
safeProvider(AuthGuard),
safeProvider(UnauthGuard),
safeProvider(ModalService),
@@ -342,10 +345,12 @@ const typesafeProviders: Array<SafeProvider> = [
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
AccountServiceAbstraction,
MessagingServiceAbstraction,
CryptoServiceAbstraction,
ApiServiceAbstraction,
StateServiceAbstraction,
TokenServiceAbstraction,
],
}),
safeProvider({
@@ -360,7 +365,7 @@ const typesafeProviders: Array<SafeProvider> = [
MessagingServiceAbstraction,
LogService,
KeyConnectorServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
@@ -369,6 +374,7 @@ const typesafeProviders: Array<SafeProvider> = [
PolicyServiceAbstraction,
DeviceTrustCryptoServiceAbstraction,
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
GlobalStateProvider,
BillingAccountProfileStateService,
],
@@ -395,7 +401,7 @@ const typesafeProviders: Array<SafeProvider> = [
autofillSettingsService: AutofillSettingsServiceAbstraction,
encryptService: EncryptService,
fileUploadService: CipherFileUploadServiceAbstraction,
configService: ConfigServiceAbstraction,
configService: ConfigService,
stateProvider: StateProvider,
) =>
new CipherService(
@@ -421,7 +427,7 @@ const typesafeProviders: Array<SafeProvider> = [
AutofillSettingsServiceAbstraction,
EncryptService,
CipherFileUploadServiceAbstraction,
ConfigServiceAbstraction,
configService,
StateProvider,
],
}),
@@ -475,10 +481,19 @@ const typesafeProviders: Array<SafeProvider> = [
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, StateProvider],
}),
safeProvider({
provide: EnvironmentServiceAbstraction,
useClass: EnvironmentService,
provide: EnvironmentService,
useClass: DefaultEnvironmentService,
deps: [StateProvider, AccountServiceAbstraction],
}),
safeProvider({
provide: InternalUserDecryptionOptionsServiceAbstraction,
useClass: UserDecryptionOptionsService,
deps: [StateProvider],
}),
safeProvider({
provide: UserDecryptionOptionsServiceAbstraction,
useExisting: InternalUserDecryptionOptionsServiceAbstraction,
}),
safeProvider({
provide: TotpServiceAbstraction,
useClass: TotpService,
@@ -491,7 +506,10 @@ const typesafeProviders: Array<SafeProvider> = [
SingleUserStateProvider,
GlobalStateProvider,
SUPPORTS_SECURE_STORAGE,
AbstractStorageService,
SECURE_STORAGE,
KeyGenerationServiceAbstraction,
EncryptService,
LogService,
],
}),
safeProvider({
@@ -534,7 +552,7 @@ const typesafeProviders: Array<SafeProvider> = [
deps: [
TokenServiceAbstraction,
PlatformUtilsServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
AppIdServiceAbstraction,
StateServiceAbstraction,
LOGOUT_CALLBACK,
@@ -579,6 +597,7 @@ const typesafeProviders: Array<SafeProvider> = [
FolderApiServiceAbstraction,
InternalOrganizationServiceAbstraction,
SendApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
AvatarServiceAbstraction,
LOGOUT_CALLBACK,
BillingAccountProfileStateService,
@@ -589,6 +608,7 @@ const typesafeProviders: Array<SafeProvider> = [
provide: VaultTimeoutSettingsServiceAbstraction,
useClass: VaultTimeoutSettingsService,
deps: [
UserDecryptionOptionsServiceAbstraction,
CryptoServiceAbstraction,
TokenServiceAbstraction,
PolicyServiceAbstraction,
@@ -634,7 +654,7 @@ const typesafeProviders: Array<SafeProvider> = [
LogService,
STATE_FACTORY,
AccountServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
TokenServiceAbstraction,
MigrationRunner,
STATE_SERVICE_USE_CACHE,
@@ -698,7 +718,7 @@ const typesafeProviders: Array<SafeProvider> = [
SyncServiceAbstraction,
AppIdServiceAbstraction,
ApiServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
LOGOUT_CALLBACK,
StateServiceAbstraction,
AuthServiceAbstraction,
@@ -749,7 +769,6 @@ const typesafeProviders: Array<SafeProvider> = [
provide: KeyConnectorServiceAbstraction,
useClass: KeyConnectorService,
deps: [
StateServiceAbstraction,
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
@@ -757,6 +776,7 @@ const typesafeProviders: Array<SafeProvider> = [
OrganizationServiceAbstraction,
KeyGenerationServiceAbstraction,
LOGOUT_CALLBACK,
StateProvider,
],
}),
safeProvider({
@@ -767,6 +787,7 @@ const typesafeProviders: Array<SafeProvider> = [
CryptoServiceAbstraction,
I18nServiceAbstraction,
UserVerificationApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
PinCryptoServiceAbstraction,
LogService,
VaultTimeoutSettingsServiceAbstraction,
@@ -833,29 +854,23 @@ const typesafeProviders: Array<SafeProvider> = [
deps: [],
}),
safeProvider({
provide: ConfigService,
useClass: ConfigService,
deps: [
StateServiceAbstraction,
ConfigApiServiceAbstraction,
AuthServiceAbstraction,
EnvironmentServiceAbstraction,
LogService,
],
provide: DefaultConfigService,
useClass: DefaultConfigService,
deps: [ConfigApiServiceAbstraction, EnvironmentService, LogService, StateProvider],
}),
safeProvider({
provide: ConfigServiceAbstraction,
useExisting: ConfigService,
provide: ConfigService,
useExisting: DefaultConfigService,
}),
safeProvider({
provide: ConfigApiServiceAbstraction,
useClass: ConfigApiService,
deps: [ApiServiceAbstraction, AuthServiceAbstraction],
deps: [ApiServiceAbstraction, TokenServiceAbstraction],
}),
safeProvider({
provide: AnonymousHubServiceAbstraction,
useClass: AnonymousHubService,
deps: [EnvironmentServiceAbstraction, LoginStrategyServiceAbstraction, LogService],
deps: [EnvironmentService, AuthRequestServiceAbstraction],
}),
safeProvider({
provide: ValidationServiceAbstraction,
@@ -863,9 +878,9 @@ const typesafeProviders: Array<SafeProvider> = [
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
}),
safeProvider({
provide: LoginServiceAbstraction,
useClass: LoginService,
deps: [StateServiceAbstraction],
provide: LoginEmailServiceAbstraction,
useClass: LoginEmailService,
deps: [StateProvider],
}),
safeProvider({
provide: OrgDomainInternalServiceAbstraction,
@@ -899,11 +914,13 @@ const typesafeProviders: Array<SafeProvider> = [
CryptoFunctionServiceAbstraction,
CryptoServiceAbstraction,
EncryptService,
StateServiceAbstraction,
AppIdServiceAbstraction,
DevicesApiServiceAbstraction,
I18nServiceAbstraction,
PlatformUtilsServiceAbstraction,
StateProvider,
SECURE_STORAGE,
UserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
@@ -934,7 +951,7 @@ const typesafeProviders: Array<SafeProvider> = [
safeProvider({
provide: WebAuthnLoginApiServiceAbstraction,
useClass: WebAuthnLoginApiService,
deps: [ApiServiceAbstraction, EnvironmentServiceAbstraction],
deps: [ApiServiceAbstraction, EnvironmentService],
}),
safeProvider({
provide: WebAuthnLoginServiceAbstraction,
@@ -1050,13 +1067,18 @@ const typesafeProviders: Array<SafeProvider> = [
safeProvider({
provide: BillingAccountProfileStateService,
useClass: DefaultBillingAccountProfileStateService,
deps: [ActiveUserStateProvider],
deps: [StateProvider],
}),
safeProvider({
provide: OrganizationManagementPreferencesService,
useClass: DefaultOrganizationManagementPreferencesService,
deps: [StateProvider],
}),
safeProvider({
provide: ErrorHandler,
useClass: LoggingErrorHandler,
deps: [],
}),
];
function encryptServiceFactory(
@@ -1072,6 +1094,6 @@ function encryptServiceFactory(
@NgModule({
declarations: [],
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
providers: typesafeProviders as UnwrapOpaque<SafeProvider>[],
providers: safeProviders,
})
export class JslibServicesModule {}

View File

@@ -1,5 +0,0 @@
<ng-container *ngIf="show">
<app-callout type="info" title="{{ scopeConfig.title | i18n }}">
{{ scopeConfig.description | i18n: scopeConfig.scopeIdentifier }}
</app-callout>
</ng-container>

View File

@@ -1,59 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@Component({
selector: "app-export-scope-callout",
templateUrl: "export-scope-callout.component.html",
})
export class ExportScopeCalloutComponent implements OnInit {
show = false;
scopeConfig: {
title: string;
description: string;
scopeIdentifier: string;
};
private _organizationId: string;
get organizationId(): string {
return this._organizationId;
}
@Input() set organizationId(value: string) {
this._organizationId = value;
// 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.getScopeMessage(this._organizationId);
}
constructor(
protected organizationService: OrganizationService,
protected stateService: StateService,
) {}
async ngOnInit(): Promise<void> {
if (!(await this.organizationService.hasOrganizations())) {
return;
}
await this.getScopeMessage(this.organizationId);
this.show = true;
}
private async getScopeMessage(organizationId: string) {
this.scopeConfig =
organizationId != null
? {
title: "exportingOrganizationVaultTitle",
description: "exportingOrganizationVaultDesc",
scopeIdentifier: (await this.organizationService.get(organizationId)).name,
}
: {
title: "exportingPersonalVaultTitle",
description: "exportingIndividualVaultDescription",
scopeIdentifier: await this.stateService.getEmail(),
};
}
}

View File

@@ -1,276 +0,0 @@
import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-export-type.enum";
import { DialogService } from "@bitwarden/components";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { PasswordStrengthComponent } from "../../password-strength/password-strength.component";
@Directive()
export class ExportComponent implements OnInit, OnDestroy {
@Output() onSaved = new EventEmitter();
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
encryptedExportType = EncryptedExportType;
protected showFilePassword: boolean;
filePasswordValue: string = null;
private _disabledByPolicy = false;
protected organizationId: string = null;
organizations$: Observable<Organization[]>;
protected get disabledByPolicy(): boolean {
return this._disabledByPolicy;
}
exportForm = this.formBuilder.group({
vaultSelector: [
"myVault",
{
nonNullable: true,
validators: [Validators.required],
},
],
format: ["json", Validators.required],
secret: [""],
filePassword: ["", Validators.required],
confirmFilePassword: ["", Validators.required],
fileEncryptionType: [EncryptedExportType.AccountEncrypted],
});
formatOptions = [
{ name: ".json", value: "json" },
{ name: ".csv", value: "csv" },
{ name: ".json (Encrypted)", value: "encrypted_json" },
];
private destroy$ = new Subject<void>();
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected exportService: VaultExportServiceAbstraction,
protected eventCollectionService: EventCollectionService,
private policyService: PolicyService,
private logService: LogService,
private userVerificationService: UserVerificationService,
private formBuilder: UntypedFormBuilder,
protected fileDownloadService: FileDownloadService,
protected dialogService: DialogService,
protected organizationService: OrganizationService,
) {}
async ngOnInit() {
this.policyService
.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this._disabledByPolicy = policyAppliesToActiveUser;
if (this.disabledByPolicy) {
this.exportForm.disable();
}
});
merge(
this.exportForm.get("format").valueChanges,
this.exportForm.get("fileEncryptionType").valueChanges,
)
.pipe(takeUntil(this.destroy$))
.pipe(startWith(0))
.subscribe(() => this.adjustValidators());
if (this.organizationId) {
this.organizations$ = this.organizationService.memberOrganizations$.pipe(
map((orgs) => orgs.filter((org) => org.id == this.organizationId)),
);
this.exportForm.controls.vaultSelector.patchValue(this.organizationId);
this.exportForm.controls.vaultSelector.disable();
return;
}
this.organizations$ = this.organizationService.memberOrganizations$.pipe(
map((orgs) =>
orgs
.filter((org) => org.flexibleCollections)
.sort(Utils.getSortFunction(this.i18nService, "name")),
),
);
this.exportForm.controls.vaultSelector.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((value) => {
this.organizationId = value != "myVault" ? value : undefined;
});
this.exportForm.controls.vaultSelector.setValue("myVault");
}
ngOnDestroy(): void {
this.destroy$.next();
}
get encryptedFormat() {
return this.format === "encrypted_json";
}
get isFileEncryptedExport() {
return (
this.format === "encrypted_json" &&
this.fileEncryptionType === EncryptedExportType.FileEncrypted
);
}
get isAccountEncryptedExport() {
return (
this.format === "encrypted_json" &&
this.fileEncryptionType === EncryptedExportType.AccountEncrypted
);
}
protected async doExport() {
try {
const data = await this.getExportData();
this.downloadFile(data);
this.saved();
await this.collectEvent();
this.exportForm.get("secret").setValue("");
this.exportForm.clearValidators();
} catch (e) {
this.logService.error(e);
}
}
async submit() {
if (this.disabledByPolicy) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("personalVaultExportPolicyInEffect"),
);
return;
}
const acceptedWarning = await this.warningDialog();
if (!acceptedWarning) {
return;
}
const secret = this.exportForm.get("secret").value;
try {
await this.userVerificationService.verifyUser(secret);
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
return;
}
// 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.doExport();
}
async warningDialog() {
if (this.encryptedFormat) {
return await this.dialogService.openSimpleDialog({
title: { key: "confirmVaultExport" },
content:
this.i18nService.t("encExportKeyWarningDesc") +
" " +
this.i18nService.t("encExportAccountWarningDesc"),
acceptButtonText: { key: "exportVault" },
type: "warning",
});
} else {
return await this.dialogService.openSimpleDialog({
title: { key: "confirmVaultExport" },
content: { key: "exportWarningDesc" },
acceptButtonText: { key: "exportVault" },
type: "warning",
});
}
}
protected saved() {
this.onSaved.emit();
}
protected async getExportData(): Promise<string> {
return Utils.isNullOrWhitespace(this.organizationId)
? this.exportService.getExport(this.format, this.filePassword)
: this.exportService.getOrganizationExport(
this.organizationId,
this.format,
this.filePassword,
true,
);
}
protected getFileName(prefix?: string) {
let extension = this.format;
if (this.format === "encrypted_json") {
if (prefix == null) {
prefix = "encrypted";
} else {
prefix = "encrypted_" + prefix;
}
extension = "json";
}
return this.exportService.getFileName(prefix, extension);
}
protected async collectEvent(): Promise<void> {
await this.eventCollectionService.collect(EventType.User_ClientExportedVault);
}
get format() {
return this.exportForm.get("format").value;
}
get filePassword() {
return this.exportForm.get("filePassword").value;
}
get confirmFilePassword() {
return this.exportForm.get("confirmFilePassword").value;
}
get fileEncryptionType() {
return this.exportForm.get("fileEncryptionType").value;
}
adjustValidators() {
this.exportForm.get("confirmFilePassword").reset();
this.exportForm.get("filePassword").reset();
if (this.encryptedFormat && this.fileEncryptionType == EncryptedExportType.FileEncrypted) {
this.exportForm.controls.filePassword.enable();
this.exportForm.controls.confirmFilePassword.enable();
} else {
this.exportForm.controls.filePassword.disable();
this.exportForm.controls.confirmFilePassword.disable();
}
}
private downloadFile(csv: string): void {
const fileName = this.getFileName();
this.fileDownloadService.download({
fileName: fileName,
blobData: csv,
blobOptions: { type: "text/plain" },
});
}
}

View File

@@ -1,7 +1,7 @@
import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { BehaviorSubject, Subject, concatMap, firstValueFrom, map, takeUntil } from "rxjs";
import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -123,7 +123,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
{ name: i18nService.t("sendTypeFile"), value: SendType.File, premium: true },
{ name: i18nService.t("sendTypeText"), value: SendType.Text, premium: false },
];
this.sendLinkBaseUrl = this.environmentService.getSendUrl();
}
get link(): string {
@@ -190,6 +189,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
});
const env = await firstValueFrom(this.environmentService.environment$);
this.sendLinkBaseUrl = env.getSendUrl();
this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.destroy$))
.subscribe((hasPremiumFromAnySource) => {

View File

@@ -1,5 +1,5 @@
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { Subject, firstValueFrom, takeUntil } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -198,9 +198,9 @@ export class SendComponent implements OnInit, OnDestroy {
return true;
}
copy(s: SendView) {
const sendLinkBaseUrl = this.environmentService.getSendUrl();
const link = sendLinkBaseUrl + s.accessId + "/" + s.urlB64Key;
async copy(s: SendView) {
const env = await firstValueFrom(this.environmentService.environment$);
const link = env.getSendUrl() + s.accessId + "/" + s.urlB64Key;
this.platformUtilsService.copyToClipboard(link);
this.platformUtilsService.showToast(
"success",

View File

@@ -14,7 +14,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -119,7 +119,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected dialogService: DialogService,
protected win: Window,
protected datePipe: DatePipe,
protected configService: ConfigServiceAbstraction,
protected configService: ConfigService,
) {
this.typeOptions = [
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
@@ -402,6 +402,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
}
removePasskey() {
if (this.cipher.type !== CipherType.Login || this.cipher.login.fido2Credentials == null) {
return;
}
this.cipher.login.fido2Credentials = null;
}
onCardNumberChange(): void {
this.cipher.card.brand = CardView.getCardBrandByPatterns(this.cipher.card.number);
}
@@ -650,11 +658,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected saveCipher(cipher: Cipher) {
const isNotClone = this.editMode && !this.cloneMode;
let orgAdmin = this.organization?.isAdmin;
let orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
if (this.flexibleCollectionsV1Enabled) {
// Flexible Collections V1 restricts admins, check the organization setting via canEditAllCiphers
orgAdmin = this.organization?.canEditAllCiphers(true);
// if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection
if (!cipher.collectionIds) {
orgAdmin = this.organization?.canEditAnyCollection;
}
return this.cipher.id == null

View File

@@ -39,11 +39,12 @@ export class IconComponent implements OnInit {
) {}
async ngOnInit() {
const iconsUrl = this.environmentService.getIconsUrl();
this.data$ = combineLatest([
this.environmentService.environment$.pipe(map((e) => e.getIconsUrl())),
this.domainSettingsService.showFavicons$.pipe(distinctUntilChanged()),
this.cipher$.pipe(filter((c) => c !== undefined)),
]).pipe(map(([showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)));
]).pipe(
map(([iconsUrl, showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
);
}
}

View File

@@ -1,5 +1,5 @@
import { Directive } from "@angular/core";
import { Observable, Subject } from "rxjs";
import { OnInit, Directive } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
@@ -11,12 +11,11 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { DialogService } from "@bitwarden/components";
@Directive()
export class PremiumComponent {
export class PremiumComponent implements OnInit {
isPremium$: Observable<boolean>;
price = 10;
refreshPromise: Promise<any>;
cloudWebVaultUrl: string;
private directiveIsDestroyed$ = new Subject<boolean>();
constructor(
protected i18nService: I18nService,
@@ -25,13 +24,16 @@ export class PremiumComponent {
private logService: LogService,
protected stateService: StateService,
protected dialogService: DialogService,
environmentService: EnvironmentService,
private environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
this.cloudWebVaultUrl = environmentService.getCloudWebVaultUrl();
this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
}
async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
}
async refresh() {
try {
this.refreshPromise = this.apiService.refreshIdentityToken();