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

Merge branch 'main' of https://github.com/bitwarden/clients into vault/pm-18707/desktop-sync-issues

This commit is contained in:
Nick Krantz
2025-03-19 16:12:06 -05:00
777 changed files with 27348 additions and 27039 deletions

View File

@@ -1,307 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import {
firstValueFrom,
switchMap,
Subject,
catchError,
from,
of,
finalize,
takeUntil,
defer,
throwError,
map,
Observable,
take,
} from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
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";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
enum State {
NewUser,
ExistingUserUntrustedDevice,
}
type NewUserData = {
readonly state: State.NewUser;
readonly organizationId: string;
readonly userEmail: string;
};
type ExistingUserUntrustedDeviceData = {
readonly state: State.ExistingUserUntrustedDevice;
readonly showApproveFromOtherDeviceBtn: boolean;
readonly showReqAdminApprovalBtn: boolean;
readonly showApproveWithMasterPasswordBtn: boolean;
readonly userEmail: string;
};
type Data = NewUserData | ExistingUserUntrustedDeviceData;
@Directive()
export class BaseLoginDecryptionOptionsComponentV1 implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected State = State;
protected data?: Data;
protected loading = true;
private email$: Observable<string>;
activeAccountId: UserId;
// Remember device means for the user to trust the device
rememberDeviceForm = this.formBuilder.group({
rememberDevice: [true],
});
get rememberDevice(): FormControl<boolean> {
return this.rememberDeviceForm?.controls.rememberDevice;
}
constructor(
protected formBuilder: FormBuilder,
protected devicesService: DevicesServiceAbstraction,
protected stateService: StateService,
protected router: Router,
protected activatedRoute: ActivatedRoute,
protected messagingService: MessagingService,
protected tokenService: TokenService,
protected loginEmailService: LoginEmailServiceAbstraction,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected keyService: KeyService,
protected organizationUserApiService: OrganizationUserApiService,
protected apiService: ApiService,
protected i18nService: I18nService,
protected validationService: ValidationService,
protected deviceTrustService: DeviceTrustServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected accountService: AccountService,
protected toastService: ToastService,
) {}
async ngOnInit() {
this.loading = true;
this.activeAccountId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
this.email$ = this.accountService.activeAccount$.pipe(
map((a) => a?.email),
catchError((err: unknown) => {
this.validationService.showError(err);
return of(undefined);
}),
takeUntil(this.destroy$),
);
this.setupRememberDeviceValueChanges();
// Persist user choice from state if it exists
await this.setRememberDeviceDefaultValue();
try {
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 (
!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)
// - AND does not have a master password
// 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.loadNewUserData();
} else {
this.loadUntrustedDeviceData(userDecryptionOptions);
}
// Note: this is probably not a comprehensive write up of all scenarios:
// If the TDE feature flag is enabled and TDE is configured for the org that the user is a member of,
// then new and existing users can be redirected here after completing the SSO flow (and 2FA if enabled).
// First we must determine user type (new or existing):
// New User
// - present user with option to remember the device or not (trust the device)
// - present a continue button to proceed to the vault
// - loadNewUserData() --> will need to load enrollment status and user email address.
// Existing User
// - Determine if user is an admin with access to account recovery in admin console
// - Determine if user has a MP or not, if not, they must be redirected to set one (see PM-1035)
// - Determine if device is trusted or not via device crypto service (method not yet written)
// - If not trusted, present user with login decryption options (approve from other device, approve with master password, request admin approval)
// - loadUntrustedDeviceData()
} catch (err) {
this.validationService.showError(err);
}
}
private async setRememberDeviceDefaultValue() {
const rememberDeviceFromState = await this.deviceTrustService.getShouldTrustDevice(
this.activeAccountId,
);
const rememberDevice = rememberDeviceFromState ?? true;
this.rememberDevice.setValue(rememberDevice);
}
private setupRememberDeviceValueChanges() {
this.rememberDevice.valueChanges
.pipe(
switchMap((value) =>
defer(() => this.deviceTrustService.setShouldTrustDevice(this.activeAccountId, value)),
),
takeUntil(this.destroy$),
)
.subscribe();
}
async loadNewUserData() {
const autoEnrollStatus$ = defer(() =>
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId),
).pipe(
switchMap((organizationIdentifier) => {
if (organizationIdentifier == undefined) {
return throwError(() => new Error(this.i18nService.t("ssoIdentifierRequired")));
}
return from(this.organizationApiService.getAutoEnrollStatus(organizationIdentifier));
}),
catchError((err: unknown) => {
this.validationService.showError(err);
return of(undefined);
}),
);
const autoEnrollStatus = await firstValueFrom(autoEnrollStatus$);
const email = await firstValueFrom(this.email$);
this.data = { state: State.NewUser, organizationId: autoEnrollStatus.id, userEmail: email };
this.loading = false;
}
loadUntrustedDeviceData(userDecryptionOptions: UserDecryptionOptions) {
this.loading = true;
this.email$
.pipe(
take(1),
finalize(() => {
this.loading = false;
}),
)
.subscribe((email) => {
const showApproveFromOtherDeviceBtn =
userDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
const showReqAdminApprovalBtn =
!!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
const showApproveWithMasterPasswordBtn = userDecryptionOptions?.hasMasterPassword || false;
const userEmail = email;
this.data = {
state: State.ExistingUserUntrustedDevice,
showApproveFromOtherDeviceBtn,
showReqAdminApprovalBtn,
showApproveWithMasterPasswordBtn,
userEmail,
};
});
}
async approveFromOtherDevice() {
if (this.data.state !== State.ExistingUserUntrustedDevice) {
return;
}
this.loginEmailService.setLoginEmail(this.data.userEmail);
await this.router.navigate(["/login-with-device"]);
}
async requestAdminApproval() {
this.loginEmailService.setLoginEmail(this.data.userEmail);
await this.router.navigate(["/admin-approval-requested"]);
}
async approveWithMasterPassword() {
await this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } });
}
async createUser() {
if (this.data.state !== State.NewUser) {
return;
}
// this.loading to support clients without async-actions-support
this.loading = true;
// errors must be caught in child components to prevent navigation
try {
const { publicKey, privateKey } = await this.keyService.initAccount();
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
await this.apiService.postAccountKeys(keysRequest);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("accountSuccessfullyCreated"),
});
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);
if (this.rememberDeviceForm.value.rememberDevice) {
await this.deviceTrustService.trustDevice(this.activeAccountId);
}
} finally {
this.loading = false;
}
}
logOut() {
this.loading = true; // to avoid an awkward delay in browser extension
this.messagingService.send("logout");
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -6,7 +6,7 @@ import { Subject, firstValueFrom, map, takeUntil } from "rxjs";
import { PolicyService } 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angular/core";
@@ -7,8 +5,6 @@ import { ActivatedRoute } from "@angular/router";
import { Observable, map, Subject, takeUntil } from "rxjs";
import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
EnvironmentService,
Region,
@@ -88,7 +84,6 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
protected environmentService: EnvironmentService,
private route: ActivatedRoute,
private dialogService: DialogService,
private configService: ConfigService,
private toastService: ToastService,
private i18nService: I18nService,
) {}
@@ -113,24 +108,18 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
}
/**
* Opens the self-hosted settings dialog.
*
* If the `UnauthenticatedExtensionUIRefresh` feature flag is enabled,
* the self-hosted settings dialog is opened directly. Otherwise, the
* `onOpenSelfHostedSettings` event is emitted.
* Opens the self-hosted settings dialog when the self-hosted option is selected.
*/
if (option === Region.SelfHosted) {
if (await this.configService.getFeatureFlag(FeatureFlag.UnauthenticatedExtensionUIRefresh)) {
if (await SelfHostedEnvConfigDialogComponent.open(this.dialogService)) {
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("environmentSaved"),
});
}
} else {
this.onOpenSelfHostedSettings.emit();
}
if (
option === Region.SelfHosted &&
(await SelfHostedEnvConfigDialogComponent.open(this.dialogService))
) {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("environmentSaved"),
});
return;
}

View File

@@ -1,82 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, EventEmitter, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
EnvironmentService,
Region,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { ModalService } from "../../services/modal.service";
@Directive()
export class EnvironmentComponent {
@Output() onSaved = new EventEmitter();
iconsUrl: string;
identityUrl: string;
apiUrl: string;
webVaultUrl: string;
notificationsUrl: string;
baseUrl: string;
showCustom = false;
constructor(
protected platformUtilsService: PlatformUtilsService,
protected environmentService: EnvironmentService,
protected i18nService: I18nService,
private modalService: ModalService,
private toastService: ToastService,
) {
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;
}
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() {
await this.environmentService.setEnvironment(Region.SelfHosted, {
base: this.baseUrl,
api: this.apiUrl,
identity: this.identityUrl,
webVault: this.webVaultUrl,
icons: this.iconsUrl,
notifications: this.notificationsUrl,
});
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("environmentSaved"),
});
this.saved();
}
toggleCustom() {
this.showCustom = !this.showCustom;
}
protected saved() {
this.onSaved.emit();
this.modalService.closeAll();
}
}

View File

@@ -1,74 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.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";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
@Directive()
export class HintComponent implements OnInit {
email = "";
formPromise: Promise<any>;
protected successRoute = "login";
protected onSuccessfulSubmit: () => void;
constructor(
protected router: Router,
protected i18nService: I18nService,
protected apiService: ApiService,
protected platformUtilsService: PlatformUtilsService,
private logService: LogService,
private loginEmailService: LoginEmailServiceAbstraction,
protected toastService: ToastService,
) {}
async ngOnInit(): Promise<void> {
this.email = (await firstValueFrom(this.loginEmailService.loginEmail$)) ?? "";
}
async submit() {
if (this.email == null || this.email === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("emailRequired"),
});
return;
}
if (this.email.indexOf("@") === -1) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidEmail"),
});
return;
}
try {
this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email));
await this.formPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("masterPassSent"),
});
if (this.onSuccessfulSubmit != null) {
this.onSuccessfulSubmit();
} else if (this.router != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);
}
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,401 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, NavigationSkipped, Router } from "@angular/router";
import { Subject, firstValueFrom, of } from "rxjs";
import { switchMap, take, takeUntil } from "rxjs/operators";
import {
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
PasswordLoginCredentials,
} from "@bitwarden/auth/common";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
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 { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
AllValidationErrors,
FormValidationErrorsService,
} from "../../platform/abstractions/form-validation-errors.service";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
@Directive()
export class LoginComponentV1 extends CaptchaProtectedComponent implements OnInit, OnDestroy {
@ViewChild("masterPasswordInput", { static: true }) masterPasswordInput: ElementRef;
showPassword = false;
formPromise: Promise<AuthResult>;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: (userId: UserId) => Promise<any>;
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
showLoginWithDevice: boolean;
validatedEmail = false;
paramEmailSet = false;
get emailFormControl() {
return this.formGroup.controls.email;
}
formGroup = this.formBuilder.nonNullable.group({
email: ["", [Validators.required, Validators.email]],
masterPassword: [
"",
[Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)],
],
rememberEmail: [false],
});
protected twoFactorRoute = "2fa";
protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password";
protected destroy$ = new Subject<void>();
get loggedEmail() {
return this.formGroup.controls.email.value;
}
constructor(
protected devicesApiService: DevicesApiServiceAbstraction,
protected appIdService: AppIdService,
protected loginStrategyService: LoginStrategyServiceAbstraction,
protected router: Router,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
protected stateService: StateService,
environmentService: EnvironmentService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected cryptoFunctionService: CryptoFunctionService,
protected logService: LogService,
protected ngZone: NgZone,
protected formBuilder: FormBuilder,
protected formValidationErrorService: FormValidationErrorsService,
protected route: ActivatedRoute,
protected loginEmailService: LoginEmailServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService, toastService);
}
async ngOnInit() {
this.route?.queryParams
.pipe(
switchMap((params) => {
if (!params) {
// If no params,loadEmailSettings from state
return this.loadEmailSettings();
}
const queryParamsEmail = params.email;
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
this.formGroup.controls.email.setValue(queryParamsEmail);
this.paramEmailSet = true;
}
// If paramEmailSet is false, loadEmailSettings from state
return this.paramEmailSet ? of(null) : this.loadEmailSettings();
}),
takeUntil(this.destroy$),
)
.subscribe();
// If the user navigates to /login from /login, reset the validatedEmail flag
// This should bring the user back to the login screen with the email field
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
if (event instanceof NavigationSkipped && event.url === "/login") {
this.validatedEmail = false;
}
});
// Backup check to handle unknown case where activatedRoute is not available
// This shouldn't happen under normal circumstances
if (!this.route) {
await this.loadEmailSettings();
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
async submit(showToast = true) {
await this.setupCaptcha();
this.formGroup.markAllAsTouched();
//web
if (this.formGroup.invalid && !showToast) {
return;
}
//desktop, browser; This should be removed once all clients use reactive forms
if (this.formGroup.invalid && showToast) {
const errorText = this.getErrorToastMessage();
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: errorText,
});
return;
}
try {
const credentials = new PasswordLoginCredentials(
this.formGroup.controls.email.value,
this.formGroup.controls.masterPassword.value,
this.captchaToken,
undefined,
);
this.formPromise = this.loginStrategyService.logIn(credentials);
const response = await this.formPromise;
await this.saveEmailSettings();
if (this.handleCaptchaRequired(response)) {
return;
} else if (await this.handleMigrateEncryptionKey(response)) {
return;
} else if (response.requiresTwoFactor) {
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulLoginTwoFactorNavigate();
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.twoFactorRoute]);
}
} else if (response.forcePasswordReset != ForceSetPasswordReason.None) {
if (this.onSuccessfulLoginForceResetNavigate != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulLoginForceResetNavigate();
} else {
this.loginEmailService.clearValues();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.forcePasswordResetRoute]);
}
} else {
if (this.onSuccessfulLogin != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulLoginNavigate(response.userId);
} else {
this.loginEmailService.clearValues();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);
}
}
} catch (e) {
this.logService.error(e);
}
}
togglePassword() {
this.showPassword = !this.showPassword;
if (this.ngZone.isStable) {
document.getElementById("masterPassword").focus();
} else {
this.ngZone.onStable
.pipe(take(1))
.subscribe(() => document.getElementById("masterPassword").focus());
}
}
async startAuthRequestLogin() {
this.formGroup.get("masterPassword")?.clearValidators();
this.formGroup.get("masterPassword")?.updateValueAndValidity();
if (!this.formGroup.valid) {
return;
}
await this.saveEmailSettings();
await this.router.navigate(["/login-with-device"]);
}
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
// Save off email for SSO
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
// Generate necessary sso params
const passwordOptions: any = {
type: "password",
length: 64,
uppercase: true,
lowercase: true,
numbers: true,
special: false,
};
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
// Save sso params
await this.ssoLoginService.setSsoState(state);
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
// Build URI
const env = await firstValueFrom(this.environmentService.environment$);
const webUrl = env.getWebVaultUrl();
// Launch browser
this.platformUtilsService.launchUri(
webUrl +
"/#/sso?clientId=" +
clientId +
"&redirectUri=" +
encodeURIComponent(ssoRedirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge +
"&email=" +
encodeURIComponent(this.formGroup.controls.email.value),
);
}
async validateEmail() {
this.formGroup.controls.email.markAsTouched();
const emailValid = this.formGroup.get("email").valid;
if (emailValid) {
this.toggleValidateEmail(true);
await this.getLoginWithDevice(this.loggedEmail);
}
}
toggleValidateEmail(value: boolean) {
this.validatedEmail = value;
if (!this.validatedEmail) {
// Reset master password only when going from validated to not validated
// so that autofill can work properly
this.formGroup.controls.masterPassword.reset();
} else {
// Mark MP as untouched so that, when users enter email and hit enter,
// the MP field doesn't load with validation errors
this.formGroup.controls.masterPassword.markAsUntouched();
// When email is validated, focus on master password after
// waiting for input to be rendered
if (this.ngZone.isStable) {
this.masterPasswordInput?.nativeElement?.focus();
} else {
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
this.masterPasswordInput?.nativeElement?.focus();
});
}
}
}
private async loadEmailSettings() {
// Try to load from memory first
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail();
if (email) {
this.formGroup.controls.email.setValue(email);
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
} else {
// If not in memory, check email on disk
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
if (storedEmail) {
// If we have a stored email, rememberEmail should default to true
this.formGroup.controls.email.setValue(storedEmail);
this.formGroup.controls.rememberEmail.setValue(true);
}
}
}
protected async saveEmailSettings() {
// Save off email for SSO
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
this.loginEmailService.setLoginEmail(this.formGroup.value.email);
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
await this.loginEmailService.saveEmailSettings();
}
// Legacy accounts used the master key to encrypt data. Migration is required but only performed on web
protected async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
if (!result.requiresEncryptionKeyMigration) {
return false;
}
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccured"),
message: this.i18nService.t("encryptionKeyMigrationRequired"),
});
return true;
}
private getErrorToastMessage() {
const error: AllValidationErrors = this.formValidationErrorService
.getFormValidationErrors(this.formGroup.controls)
.shift();
if (error) {
switch (error.errorName) {
case "email":
return this.i18nService.t("invalidEmail");
case "minlength":
return this.i18nService.t("masterPasswordMinlength", Utils.originalMinimumPasswordLength);
default:
return this.i18nService.t(this.errorTag(error));
}
}
return;
}
private errorTag(error: AllValidationErrors): string {
const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1);
return `${error.controlName}${name}`;
}
async getLoginWithDevice(email: string) {
try {
const deviceIdentifier = await this.appIdService.getAppId();
this.showLoginWithDevice = await this.devicesApiService.getKnownDevice(
email,
deviceIdentifier,
);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.showLoginWithDevice = false;
}
}
}

View File

@@ -1,539 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnDestroy, OnInit } from "@angular/core";
import { IsActiveMatchOptions, Router } from "@angular/router";
import { Subject, firstValueFrom, map, 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
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";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService } from "@bitwarden/key-management";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
enum State {
StandardAuthRequest,
AdminAuthRequest,
}
@Directive()
export class LoginViaAuthRequestComponentV1
extends CaptchaProtectedComponent
implements OnInit, OnDestroy
{
private destroy$ = new Subject<void>();
userAuthNStatus: AuthenticationStatus;
email: string;
showResendNotification = false;
authRequest: AuthRequest;
fingerprintPhrase: string;
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: () => Promise<any>;
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
protected adminApprovalRoute = "admin-approval-requested";
protected StateEnum = State;
protected state = State.StandardAuthRequest;
protected webVaultUrl: string;
protected twoFactorRoute = "2fa";
protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password";
private resendTimeout = 12000;
protected deviceManagementUrl: string;
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
constructor(
protected router: Router,
private keyService: KeyService,
private cryptoFunctionService: CryptoFunctionService,
private appIdService: AppIdService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private apiService: ApiService,
private authService: AuthService,
private logService: LogService,
environmentService: EnvironmentService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
private anonymousHubService: AnonymousHubService,
private validationService: ValidationService,
private accountService: AccountService,
private loginEmailService: LoginEmailServiceAbstraction,
private deviceTrustService: DeviceTrustServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private loginStrategyService: LoginStrategyServiceAbstraction,
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService, toastService);
// Get the web vault URL from the environment service
environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => {
this.webVaultUrl = env.getWebVaultUrl();
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
});
// Gets signalR push notification
// Only fires on approval to prevent enumeration
this.authRequestService.authRequestPushNotification$
.pipe(takeUntil(this.destroy$))
.subscribe((id) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: e.message,
});
this.logService.error("Failed to use approved auth request: " + e.message);
});
});
}
async ngOnInit() {
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
this.userAuthNStatus = await this.authService.getAuthStatus();
const matchOptions: IsActiveMatchOptions = {
paths: "exact",
queryParams: "ignored",
fragment: "ignored",
matrixParams: "ignored",
};
if (this.router.isActive(this.adminApprovalRoute, matchOptions)) {
this.state = State.AdminAuthRequest;
}
if (this.state === State.AdminAuthRequest) {
// Pull email from state for admin auth reqs b/c it is available
// This also prevents it from being lost on refresh as the
// login service email does not persist.
this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
if (!this.email) {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("userEmailMissing"),
});
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/login-initiated"]);
return;
}
// We only allow a single admin approval request to be active at a time
// so must check state to see if we have an existing one or not
const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId);
if (adminAuthReqStorable) {
await this.handleExistingAdminAuthRequest(adminAuthReqStorable, userId);
} else {
// No existing admin auth request; so we need to create one
await this.startAuthRequestLogin();
}
} else {
// Standard auth request
// TODO: evaluate if we can remove the setting of this.email in the constructor
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
if (!this.email) {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("userEmailMissing"),
});
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/login"]);
return;
}
await this.startAuthRequestLogin();
}
}
async ngOnDestroy() {
await this.anonymousHubService.stopHubConnection();
this.destroy$.next();
this.destroy$.complete();
}
private async handleExistingAdminAuthRequest(
adminAuthReqStorable: AdminAuthRequestStorable,
userId: UserId,
) {
// Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req
// has been approved and handle it if so.
// Regardless, we always retrieve the auth request from the server verify and handle status changes here as well
let adminAuthReqResponse: AuthRequestResponse;
try {
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
}
}
// Request doesn't exist anymore
if (!adminAuthReqResponse) {
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
}
// Re-derive the user's fingerprint phrase
// It is important to not use the server's public key here as it could have been compromised via MITM
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
adminAuthReqStorable.privateKey,
);
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
this.email,
derivedPublicKeyArrayBuffer,
);
// Request denied
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
}
// Request approved
if (adminAuthReqResponse.requestApproved) {
return await this.handleApprovedAdminAuthRequest(
adminAuthReqResponse,
adminAuthReqStorable.privateKey,
userId,
);
}
// Request still pending response from admin
// set keypair and create hub connection so that any approvals will be received via push notification
this.authRequestKeyPair = { privateKey: adminAuthReqStorable.privateKey, publicKey: null };
await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
}
private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) {
// clear the admin auth request from state
await this.authRequestService.clearAdminAuthRequest(userId);
// start new auth request
// 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.startAuthRequestLogin();
}
private async buildAuthRequest(authRequestType: AuthRequestType) {
const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
this.authRequestKeyPair = {
publicKey: authRequestKeyPairArray[0],
privateKey: authRequestKeyPairArray[1],
};
const deviceIdentifier = await this.appIdService.getAppId();
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
const accessCode = await this.passwordGenerationService.generatePassword({
type: "password",
length: 25,
});
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
this.email,
this.authRequestKeyPair.publicKey,
);
this.authRequest = new AuthRequest(
this.email,
deviceIdentifier,
publicKey,
authRequestType,
accessCode,
);
}
async startAuthRequestLogin() {
this.showResendNotification = false;
try {
let reqResponse: AuthRequestResponse;
if (this.state === State.AdminAuthRequest) {
await this.buildAuthRequest(AuthRequestType.AdminApproval);
reqResponse = await this.apiService.postAdminAuthRequest(this.authRequest);
const adminAuthReqStorable = new AdminAuthRequestStorable({
id: reqResponse.id,
privateKey: this.authRequestKeyPair.privateKey,
});
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId);
} else {
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
reqResponse = await this.apiService.postAuthRequest(this.authRequest);
}
if (reqResponse.id) {
await this.anonymousHubService.createHubConnection(reqResponse.id);
}
} catch (e) {
this.logService.error(e);
}
setTimeout(() => {
this.showResendNotification = true;
}, this.resendTimeout);
}
private async verifyAndHandleApprovedAuthReq(requestId: string) {
try {
// Retrieve the auth request from server and verify it's approved
let authReqResponse: AuthRequestResponse;
switch (this.state) {
case State.StandardAuthRequest:
// Unauthed - access code required for user verification
authReqResponse = await this.apiService.getAuthResponse(
requestId,
this.authRequest.accessCode,
);
break;
case State.AdminAuthRequest:
// Authed - no access code required
authReqResponse = await this.apiService.getAuthRequest(requestId);
break;
default:
break;
}
if (!authReqResponse.requestApproved) {
return;
}
// Approved so proceed:
// 4 Scenarios to handle for approved auth requests:
// Existing flow 1:
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(masterKey)
// > decrypt masterKey > must authenticate > gets masterKey(userKey) > decrypt userKey and proceed to vault
// 3 new flows from TDE:
// Flow 2:
// - Post SSO > User is AuthN > SSO login strategy success sets masterKey(userKey) > receives approval from device with pubKey(masterKey)
// > decrypt masterKey > decrypt userKey > establish trust if required > proceed to vault
// Flow 3:
// - Post SSO > User is AuthN > Receives approval from device with pubKey(userKey) > decrypt userKey > establish trust if required > proceed to vault
// Flow 4:
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(userKey)
// > decrypt userKey > must authenticate > set userKey > proceed to vault
// if user has authenticated via SSO
if (this.userAuthNStatus === AuthenticationStatus.Locked) {
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
return await this.handleApprovedAdminAuthRequest(
authReqResponse,
this.authRequestKeyPair.privateKey,
userId,
);
}
// Flow 1 and 4:
const loginAuthResult = await this.loginViaAuthRequestStrategy(requestId, authReqResponse);
await this.handlePostLoginNavigation(loginAuthResult);
} catch (error) {
if (error instanceof ErrorResponse) {
let errorRoute = "/login";
if (this.state === State.AdminAuthRequest) {
errorRoute = "/login-initiated";
}
// 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([errorRoute]);
this.validationService.showError(error);
return;
}
this.logService.error(error);
}
}
async handleApprovedAdminAuthRequest(
adminAuthReqResponse: AuthRequestResponse,
privateKey: ArrayBuffer,
userId: UserId,
) {
// See verifyAndHandleApprovedAuthReq(...) for flow details
// it's flow 2 or 3 based on presence of masterPasswordHash
if (adminAuthReqResponse.masterPasswordHash) {
// Flow 2: masterPasswordHash is not null
// key is authRequestPublicKey(masterKey) + we have authRequestPublicKey(masterPasswordHash)
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
adminAuthReqResponse,
privateKey,
userId,
);
} else {
// Flow 3: masterPasswordHash is null
// we can assume key is authRequestPublicKey(userKey) and we can just decrypt with userKey and proceed to vault
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
adminAuthReqResponse,
privateKey,
userId,
);
}
// clear the admin auth request from state so it cannot be used again (it's a one time use)
// TODO: this should eventually be enforced via deleting this on the server once it is used
await this.authRequestService.clearAdminAuthRequest(userId);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("loginApproved"),
});
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
// TODO: don't forget to use auto enrollment service everywhere we trust device
await this.handleSuccessfulLoginNavigation();
}
// Authentication helper
private async buildAuthRequestLoginCredentials(
requestId: string,
response: AuthRequestResponse,
): Promise<AuthRequestLoginCredentials> {
// if masterPasswordHash has a value, we will always receive key as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
// if masterPasswordHash is null, we will always receive key as authRequestPublicKey(userKey)
if (response.masterPasswordHash) {
const { masterKey, masterKeyHash } =
await this.authRequestService.decryptPubKeyEncryptedMasterKeyAndHash(
response.key,
response.masterPasswordHash,
this.authRequestKeyPair.privateKey,
);
return new AuthRequestLoginCredentials(
this.email,
this.authRequest.accessCode,
requestId,
null, // no userKey
masterKey,
masterKeyHash,
);
} else {
const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey(
response.key,
this.authRequestKeyPair.privateKey,
);
return new AuthRequestLoginCredentials(
this.email,
this.authRequest.accessCode,
requestId,
userKey,
null, // no masterKey
null, // no masterKeyHash
);
}
}
private async loginViaAuthRequestStrategy(
requestId: string,
authReqResponse: AuthRequestResponse,
): Promise<AuthResult> {
// Note: credentials change based on if the authReqResponse.key is a encryptedMasterKey or UserKey
const credentials = await this.buildAuthRequestLoginCredentials(requestId, authReqResponse);
// Note: keys are set by AuthRequestLoginStrategy success handling
return await this.loginStrategyService.logIn(credentials);
}
// Routing logic
private async handlePostLoginNavigation(loginResponse: AuthResult) {
if (loginResponse.requiresTwoFactor) {
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulLoginTwoFactorNavigate();
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.twoFactorRoute]);
}
} else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) {
if (this.onSuccessfulLoginForceResetNavigate != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulLoginForceResetNavigate();
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.forcePasswordResetRoute]);
}
} else {
await this.handleSuccessfulLoginNavigation();
}
}
private async handleSuccessfulLoginNavigation() {
if (this.state === State.StandardAuthRequest) {
// Only need to set remembered email on standard login with auth req flow
await this.loginEmailService.saveEmailSettings();
}
if (this.onSuccessfulLogin != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onSuccessfulLoginNavigate();
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);
}
}
}

View File

@@ -7,7 +7,7 @@ import { firstValueFrom, map } from "rxjs";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";

View File

@@ -17,11 +17,12 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -62,6 +63,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
private policyApiService: PolicyApiServiceAbstraction,
policyService: PolicyService,
protected router: Router,
private masterPasswordApiService: MasterPasswordApiService,
private apiService: ApiService,
private syncService: SyncService,
private route: ActivatedRoute,
@@ -195,7 +197,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
);
try {
if (this.resetPasswordAutoEnroll) {
this.formPromise = this.apiService
this.formPromise = this.masterPasswordApiService
.setPassword(request)
.then(async () => {
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
@@ -222,7 +224,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
);
});
} else {
this.formPromise = this.apiService.setPassword(request).then(async () => {
this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => {
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
});
}

View File

@@ -13,12 +13,12 @@ import {
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
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 { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
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";

View File

@@ -14,11 +14,11 @@ import {
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
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 { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.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";

View File

@@ -4,7 +4,6 @@ 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,
@@ -16,13 +15,13 @@ import {
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
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 { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";

View File

@@ -6,7 +6,6 @@ import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
// eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import {
LoginStrategyServiceAbstraction,
@@ -17,7 +16,6 @@ import {
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
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";
@@ -28,6 +26,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";

View File

@@ -3,15 +3,15 @@
import { Directive } from "@angular/core";
import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { Verification } from "@bitwarden/common/auth/types/verification";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
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";
@@ -40,7 +40,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
policyService: PolicyService,
keyService: KeyService,
messagingService: MessagingService,
private apiService: ApiService,
private masterPasswordApiService: MasterPasswordApiService,
private userVerificationService: UserVerificationService,
private logService: LogService,
dialogService: DialogService,
@@ -117,9 +117,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
request.key = newUserKey[1].encryptedString;
// Update user's password
// 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.apiService.postPassword(request);
await this.masterPasswordApiService.postPassword(request);
this.toastService.showToast({
variant: "success",

View File

@@ -4,11 +4,10 @@ import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@@ -16,6 +15,7 @@ import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
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";
@@ -52,7 +52,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
policyService: PolicyService,
keyService: KeyService,
messagingService: MessagingService,
private apiService: ApiService,
private masterPasswordApiService: MasterPasswordApiService,
private syncService: SyncService,
private logService: LogService,
private userVerificationService: UserVerificationService,
@@ -202,7 +202,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
return this.apiService.putUpdateTempPassword(request);
return this.masterPasswordApiService.putUpdateTempPassword(request);
}
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
@@ -214,7 +214,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
request.newMasterPasswordHash = newMasterPasswordHash;
request.key = userKey[1].encryptedString;
return this.apiService.postPassword(request);
return this.masterPasswordApiService.postPassword(request);
}
private async updateTdeOffboardingPassword(
@@ -226,6 +226,6 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
return this.apiService.putUpdateTdeOffboardingPassword(request);
return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
}
}

View File

@@ -23,7 +23,6 @@ import { KeyService } from "@bitwarden/key-management";
@Directive({
selector: "app-user-verification",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
private _invalidSecret = false;
@Input()

View File

@@ -1,66 +0,0 @@
import { TestBed } from "@angular/core/testing";
import { Navigation, Router, UrlTree } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { unauthUiRefreshRedirect } from "./unauth-ui-refresh-redirect";
describe("unauthUiRefreshRedirect", () => {
let configService: MockProxy<ConfigService>;
let router: MockProxy<Router>;
beforeEach(() => {
configService = mock<ConfigService>();
router = mock<Router>();
TestBed.configureTestingModule({
providers: [
{ provide: ConfigService, useValue: configService },
{ provide: Router, useValue: router },
],
});
});
it("returns true when UnauthenticatedExtensionUIRefresh flag is disabled", async () => {
configService.getFeatureFlag.mockResolvedValue(false);
const result = await TestBed.runInInjectionContext(() =>
unauthUiRefreshRedirect("/redirect")(),
);
expect(result).toBe(true);
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.UnauthenticatedExtensionUIRefresh,
);
expect(router.parseUrl).not.toHaveBeenCalled();
});
it("returns UrlTree when UnauthenticatedExtensionUIRefresh flag is enabled and preserves query params", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const urlTree = new UrlTree();
urlTree.queryParams = { test: "test" };
const navigation: Navigation = {
extras: {},
id: 0,
initialUrl: new UrlTree(),
extractedUrl: urlTree,
trigger: "imperative",
previousNavigation: undefined,
};
router.getCurrentNavigation.mockReturnValue(navigation);
await TestBed.runInInjectionContext(() => unauthUiRefreshRedirect("/redirect")());
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.UnauthenticatedExtensionUIRefresh,
);
expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], {
queryParams: urlTree.queryParams,
});
});
});

View File

@@ -1,29 +0,0 @@
import { inject } from "@angular/core";
import { UrlTree, Router } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
/**
* Helper function to redirect to a new URL based on the UnauthenticatedExtensionUIRefresh feature flag.
* @param redirectUrl - The URL to redirect to if the UnauthenticatedExtensionUIRefresh flag is enabled.
*/
export function unauthUiRefreshRedirect(redirectUrl: string): () => Promise<boolean | UrlTree> {
return async () => {
const configService = inject(ConfigService);
const router = inject(Router);
const shouldRedirect = await configService.getFeatureFlag(
FeatureFlag.UnauthenticatedExtensionUIRefresh,
);
if (shouldRedirect) {
const currentNavigation = router.getCurrentNavigation();
const queryParams = currentNavigation?.extractedUrl?.queryParams || {};
// Preserve query params when redirecting as it is likely that the refreshed component
// will be consuming the same query params.
return router.createUrlTree([redirectUrl], { queryParams });
} else {
return true;
}
};
}

View File

@@ -11,10 +11,10 @@ import {
AccountService,
} from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { UserId } from "@bitwarden/common/types/guid";

View File

@@ -12,10 +12,10 @@ import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
export const authGuard: CanActivateFn = async (

View File

@@ -11,10 +11,10 @@ import {
AccountService,
} from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ClientType } from "@bitwarden/common/enums";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";

View File

@@ -9,10 +9,10 @@ import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ClientType } from "@bitwarden/common/enums";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";

View File

@@ -3,8 +3,8 @@ import { CanActivateFn, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { KeyService } from "@bitwarden/key-management";

View File

@@ -8,8 +8,8 @@ import {
import { firstValueFrom } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { KeyService } from "@bitwarden/key-management";

View File

@@ -7,8 +7,8 @@ import { BehaviorSubject } from "rxjs";
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService } from "@bitwarden/key-management";

View File

@@ -4,8 +4,8 @@ import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { KeyService } from "@bitwarden/key-management";

View File

@@ -0,0 +1,9 @@
import { Observable } from "rxjs";
export abstract class DeviceTrustToastService {
/**
* An observable pipeline that observes any cross-application toast messages
* that need to be shown as part of the trusted device encryption (TDE) process.
*/
abstract setupListeners$: Observable<void>;
}

View File

@@ -0,0 +1,44 @@
import { merge, Observable, tap } from "rxjs";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ToastService } from "@bitwarden/components";
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "./device-trust-toast.service.abstraction";
export class DeviceTrustToastService implements DeviceTrustToastServiceAbstraction {
private adminLoginApproved$: Observable<void>;
private deviceTrusted$: Observable<void>;
setupListeners$: Observable<void>;
constructor(
private authRequestService: AuthRequestServiceAbstraction,
private deviceTrustService: DeviceTrustServiceAbstraction,
private i18nService: I18nService,
private toastService: ToastService,
) {
this.adminLoginApproved$ = this.authRequestService.adminLoginApproved$.pipe(
tap(() => {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("loginApproved"),
});
}),
);
this.deviceTrusted$ = this.deviceTrustService.deviceTrusted$.pipe(
tap(() => {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("deviceTrusted"),
});
}),
);
this.setupListeners$ = merge(this.adminLoginApproved$, this.deviceTrusted$);
}
}

View File

@@ -0,0 +1,167 @@
import { mock, MockProxy } from "jest-mock-extended";
import { EMPTY, of } from "rxjs";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ToastService } from "@bitwarden/components";
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "./device-trust-toast.service.abstraction";
import { DeviceTrustToastService } from "./device-trust-toast.service.implementation";
describe("DeviceTrustToastService", () => {
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
let i18nService: MockProxy<I18nService>;
let toastService: MockProxy<ToastService>;
let sut: DeviceTrustToastServiceAbstraction;
beforeEach(() => {
authRequestService = mock<AuthRequestServiceAbstraction>();
deviceTrustService = mock<DeviceTrustServiceAbstraction>();
i18nService = mock<I18nService>();
toastService = mock<ToastService>();
i18nService.t.mockImplementation((key: string) => key); // just return the key that was given
});
const initService = () => {
return new DeviceTrustToastService(
authRequestService,
deviceTrustService,
i18nService,
toastService,
);
};
const loginApprovalToastOptions = {
variant: "success",
title: "",
message: "loginApproved",
};
const deviceTrustedToastOptions = {
variant: "success",
title: "",
message: "deviceTrusted",
};
describe("setupListeners$", () => {
describe("given adminLoginApproved$ emits and deviceTrusted$ emits", () => {
beforeEach(() => {
// Arrange
authRequestService.adminLoginApproved$ = of(undefined);
deviceTrustService.deviceTrusted$ = of(undefined);
sut = initService();
});
it("should trigger a toast for login approval", (done) => {
// Act
sut.setupListeners$.subscribe({
complete: () => {
expect(toastService.showToast).toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
done();
},
});
});
it("should trigger a toast for device trust", (done) => {
// Act
sut.setupListeners$.subscribe({
complete: () => {
expect(toastService.showToast).toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
done();
},
});
});
});
describe("given adminLoginApproved$ emits and deviceTrusted$ does not emit", () => {
beforeEach(() => {
// Arrange
authRequestService.adminLoginApproved$ = of(undefined);
deviceTrustService.deviceTrusted$ = EMPTY;
sut = initService();
});
it("should trigger a toast for login approval", (done) => {
// Act
sut.setupListeners$.subscribe({
complete: () => {
expect(toastService.showToast).toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
done();
},
});
});
it("should NOT trigger a toast for device trust", (done) => {
// Act
sut.setupListeners$.subscribe({
complete: () => {
expect(toastService.showToast).not.toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
done();
},
});
});
});
describe("given adminLoginApproved$ does not emit and deviceTrusted$ emits", () => {
beforeEach(() => {
// Arrange
authRequestService.adminLoginApproved$ = EMPTY;
deviceTrustService.deviceTrusted$ = of(undefined);
sut = initService();
});
it("should NOT trigger a toast for login approval", (done) => {
// Act
sut.setupListeners$.subscribe({
complete: () => {
expect(toastService.showToast).not.toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
done();
},
});
});
it("should trigger a toast for device trust", (done) => {
// Act
sut.setupListeners$.subscribe({
complete: () => {
expect(toastService.showToast).toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
done();
},
});
});
});
describe("given adminLoginApproved$ does not emit and deviceTrusted$ does not emit", () => {
beforeEach(() => {
// Arrange
authRequestService.adminLoginApproved$ = EMPTY;
deviceTrustService.deviceTrusted$ = EMPTY;
sut = initService();
});
it("should NOT trigger a toast for login approval", (done) => {
// Act
sut.setupListeners$.subscribe({
complete: () => {
expect(toastService.showToast).not.toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
done();
},
});
});
it("should NOT trigger a toast for device trust", (done) => {
// Act
sut.setupListeners$.subscribe({
complete: () => {
expect(toastService.showToast).not.toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
done();
},
});
});
});
});
});

View File

@@ -1,10 +1,10 @@
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<bit-table *ngIf="!loading">
<ng-container header>

View File

@@ -59,8 +59,6 @@ export class AngularThemingService implements AbstractThemingService {
document.documentElement.classList.remove(
"theme_" + ThemeTypes.Light,
"theme_" + ThemeTypes.Dark,
"theme_" + ThemeTypes.Nord,
"theme_" + ThemeTypes.SolarizedDark,
);
document.documentElement.classList.add("theme_" + theme);
});

View File

@@ -87,14 +87,9 @@ import {
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
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 {
InternalMasterPasswordServiceAbstraction,
MasterPasswordServiceAbstraction,
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
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";
@@ -109,11 +104,9 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation";
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 { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
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";
@@ -153,6 +146,15 @@ import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abst
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
import {
InternalMasterPasswordServiceAbstraction,
MasterPasswordServiceAbstraction,
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
import {
DefaultVaultTimeoutService,
DefaultVaultTimeoutSettingsService,
@@ -304,6 +306,8 @@ import {
import { SafeInjectionToken } from "@bitwarden/ui-common";
import {
DefaultTaskService,
DefaultEndUserNotificationService,
EndUserNotificationService,
NewDeviceVerificationNoticeService,
PasswordRepromptService,
TaskService,
@@ -317,6 +321,8 @@ import {
IndividualVaultExportServiceAbstraction,
} from "@bitwarden/vault-export-core";
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
@@ -408,7 +414,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: ThemeStateService,
useClass: DefaultThemeStateService,
deps: [GlobalStateProvider, ConfigService],
deps: [GlobalStateProvider],
}),
safeProvider({
provide: AbstractThemingService,
@@ -603,7 +609,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: TotpServiceAbstraction,
useClass: TotpService,
deps: [CryptoFunctionServiceAbstraction, LogService],
deps: [SdkService],
}),
safeProvider({
provide: TokenServiceAbstraction,
@@ -1282,7 +1288,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: BillingApiServiceAbstraction,
useClass: BillingApiService,
deps: [ApiServiceAbstraction, LogService, ToastService],
deps: [ApiServiceAbstraction],
}),
safeProvider({
provide: TaxServiceAbstraction,
@@ -1346,6 +1352,7 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultSetPasswordJitService,
deps: [
ApiServiceAbstraction,
MasterPasswordApiServiceAbstraction,
KeyService,
EncryptService,
I18nServiceAbstraction,
@@ -1417,7 +1424,12 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: CipherAuthorizationService,
useClass: DefaultCipherAuthorizationService,
deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction],
deps: [
CollectionService,
OrganizationServiceAbstraction,
AccountServiceAbstraction,
ConfigService,
],
}),
safeProvider({
provide: AuthRequestApiService,
@@ -1463,6 +1475,26 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultTaskService,
deps: [StateProvider, ApiServiceAbstraction, OrganizationServiceAbstraction, ConfigService],
}),
safeProvider({
provide: EndUserNotificationService,
useClass: DefaultEndUserNotificationService,
deps: [StateProvider, ApiServiceAbstraction],
}),
safeProvider({
provide: DeviceTrustToastServiceAbstraction,
useClass: DeviceTrustToastService,
deps: [
AuthRequestServiceAbstraction,
DeviceTrustServiceAbstraction,
I18nServiceAbstraction,
ToastService,
],
}),
safeProvider({
provide: MasterPasswordApiServiceAbstraction,
useClass: MasterPasswordApiService,
deps: [ApiServiceAbstraction, LogService],
}),
];
@NgModule({

View File

@@ -14,6 +14,8 @@ import {
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.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";
@@ -79,9 +81,12 @@ export class SendComponent implements OnInit, OnDestroy {
protected sendApiService: SendApiService,
protected dialogService: DialogService,
protected toastService: ToastService,
private accountService: AccountService,
) {}
async ngOnInit() {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.policyService
.policyAppliesToActiveUser$(PolicyType.DisableSend)
.pipe(takeUntil(this.destroy$))
@@ -91,7 +96,7 @@ export class SendComponent implements OnInit, OnDestroy {
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
switchMap((searchText) => from(this.searchService.isSearchable(userId, searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {

View File

@@ -15,7 +15,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -41,7 +40,7 @@ import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { generate_ssh_key } from "@bitwarden/sdk-internal";
import { PasswordRepromptService } from "@bitwarden/vault";
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
@Directive()
export class AddEditComponent implements OnInit, OnDestroy {
@@ -131,7 +130,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected configService: ConfigService,
protected cipherAuthorizationService: CipherAuthorizationService,
protected toastService: ToastService,
private sdkService: SdkService,
protected sdkService: SdkService,
private sshImportPromptService: SshImportPromptService,
) {
this.typeOptions = [
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
@@ -207,10 +207,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.writeableCollections = await this.loadCollections();
this.canUseReprompt = await this.passwordRepromptService.enabled();
const sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
if (sshKeysEnabled) {
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
}
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
}
ngOnDestroy() {
@@ -824,6 +821,15 @@ export class AddEditComponent implements OnInit, OnDestroy {
return true;
}
async importSshKeyFromClipboard() {
const key = await this.sshImportPromptService.importSshKeyFromClipboard();
if (key != null) {
this.cipher.sshKey.privateKey = key.privateKey;
this.cipher.sshKey.publicKey = key.publicKey;
this.cipher.sshKey.keyFingerprint = key.keyFingerprint;
}
}
private async generateSshKey(showNotification: boolean = true) {
await firstValueFrom(this.sdkService.client$);
const sshKey = generate_ssh_key("Ed25519");

View File

@@ -2,16 +2,18 @@
<ng-container *ngIf="data$ | async as data">
<img
[src]="data.image"
[appFallbackSrc]="data.fallbackImage"
*ngIf="data.imageEnabled && data.image"
class="tw-size-6 tw-rounded-md"
alt=""
decoding="async"
loading="lazy"
[ngClass]="{ 'tw-invisible tw-absolute': !imageLoaded() }"
(load)="imageLoaded.set(true)"
(error)="imageLoaded.set(false)"
/>
<i
class="tw-w-6 tw-text-muted bwi bwi-lg {{ data.icon }}"
*ngIf="!data.imageEnabled || !data.image"
*ngIf="!data.imageEnabled || !data.image || !imageLoaded()"
></i>
</ng-container>
</div>

View File

@@ -1,18 +1,18 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectionStrategy, Component, Input, OnInit } from "@angular/core";
import { ChangeDetectionStrategy, Component, input, signal } from "@angular/core";
import { toObservable } from "@angular/core/rxjs-interop";
import {
BehaviorSubject,
combineLatest,
distinctUntilChanged,
filter,
map,
tap,
Observable,
startWith,
pairwise,
} from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
import { buildCipherIcon, CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Component({
@@ -20,33 +20,40 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
templateUrl: "icon.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IconComponent implements OnInit {
@Input()
set cipher(value: CipherView) {
this.cipher$.next(value);
}
export class IconComponent {
/**
* The cipher to display the icon for.
*/
cipher = input.required<CipherView>();
protected data$: Observable<{
imageEnabled: boolean;
image?: string;
fallbackImage: string;
icon?: string;
}>;
imageLoaded = signal(false);
private cipher$ = new BehaviorSubject<CipherView>(undefined);
protected data$: Observable<CipherIconDetails>;
constructor(
private environmentService: EnvironmentService,
private domainSettingsService: DomainSettingsService,
) {}
async ngOnInit() {
this.data$ = combineLatest([
) {
const iconSettings$ = combineLatest([
this.environmentService.environment$.pipe(map((e) => e.getIconsUrl())),
this.domainSettingsService.showFavicons$.pipe(distinctUntilChanged()),
this.cipher$.pipe(filter((c) => c !== undefined)),
]).pipe(
map(([iconsUrl, showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
map(([iconsUrl, showFavicon]) => ({ iconsUrl, showFavicon })),
startWith({ iconsUrl: null, showFavicon: false }), // Start with a safe default to avoid flickering icons
distinctUntilChanged(),
);
this.data$ = combineLatest([iconSettings$, toObservable(this.cipher)]).pipe(
map(([{ iconsUrl, showFavicon }, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
startWith(null),
pairwise(),
tap(([prev, next]) => {
if (prev?.image !== next?.image) {
// The image changed, reset the loaded state to not show an empty icon
this.imageLoaded.set(false);
}
}),
map(([_, next]) => next!),
);
}
}

View File

@@ -2,12 +2,22 @@
// @ts-strict-ignore
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject, Subject, combineLatest, filter, from, switchMap, takeUntil } from "rxjs";
import {
BehaviorSubject,
Subject,
combineLatest,
filter,
firstValueFrom,
from,
switchMap,
takeUntil,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -28,6 +38,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
/** Construct filters as an observable so it can be appended to the cipher stream. */
private _filter$ = new BehaviorSubject<(cipher: CipherView) => boolean | null>(null);
private userId: UserId;
private destroy$ = new Subject<void>();
private isSearchable: boolean = false;
private _searchText$ = new BehaviorSubject<string>("");
@@ -55,10 +66,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
this.subscribeToCiphers();
}
ngOnInit(): void {
async ngOnInit() {
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
switchMap((searchText) => from(this.searchService.isSearchable(this.userId, searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
@@ -138,6 +151,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
allCiphers = [..._failedCiphers, ...allCiphers];
return this.searchService.searchCiphers(
this.userId,
searchText,
[filter, this.deletedFilter],
allCiphers,

View File

@@ -51,6 +51,7 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { TotpInfo } from "@bitwarden/common/vault/services/totp.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -87,20 +88,19 @@ export class ViewComponent implements OnDestroy, OnInit {
showPrivateKey: boolean;
canAccessPremium: boolean;
showPremiumRequiredTotp: boolean;
totpCode: string;
totpCodeFormatted: string;
totpDash: number;
totpSec: number;
totpLow: boolean;
fieldType = FieldType;
checkPasswordPromise: Promise<number>;
folder: FolderView;
cipherType = CipherType;
private totpInterval: any;
private previousCipherId: string;
private passwordReprompted = false;
/**
* Represents TOTP information including display formatting and timing
*/
protected totpInfo$: Observable<TotpInfo> | undefined;
get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated");
const creationDate = this.datePipe.transform(
@@ -504,57 +504,12 @@ export class ViewComponent implements OnDestroy, OnInit {
}
private cleanUp() {
this.totpCode = null;
this.cipher = null;
this.folder = null;
this.showPassword = false;
this.showCardNumber = false;
this.showCardCode = false;
this.passwordReprompted = false;
if (this.totpInterval) {
clearInterval(this.totpInterval);
}
}
private async totpUpdateCode() {
if (
this.cipher == null ||
this.cipher.type !== CipherType.Login ||
this.cipher.login.totp == null
) {
if (this.totpInterval) {
clearInterval(this.totpInterval);
}
return;
}
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
if (this.totpCode != null) {
if (this.totpCode.length > 4) {
const half = Math.floor(this.totpCode.length / 2);
this.totpCodeFormatted =
this.totpCode.substring(0, half) + " " + this.totpCode.substring(half);
} else {
this.totpCodeFormatted = this.totpCode;
}
} else {
this.totpCodeFormatted = null;
if (this.totpInterval) {
clearInterval(this.totpInterval);
}
}
}
private async totpTick(intervalSeconds: number) {
const epoch = Math.round(new Date().getTime() / 1000.0);
const mod = epoch % intervalSeconds;
this.totpSec = intervalSeconds - mod;
this.totpDash = +(Math.round(((78.6 / intervalSeconds) * mod + "e+2") as any) + "e-2");
this.totpLow = this.totpSec <= 7;
if (mod === 0) {
await this.totpUpdateCode();
}
}
/**
@@ -577,19 +532,33 @@ export class ViewComponent implements OnDestroy, OnInit {
).find((f) => f.id == this.cipher.folderId);
}
if (
const canGenerateTotp =
this.cipher.type === CipherType.Login &&
this.cipher.login.totp &&
(this.cipher.organizationUseTotp || this.canAccessPremium)
) {
await this.totpUpdateCode();
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
await this.totpTick(interval);
(this.cipher.organizationUseTotp || this.canAccessPremium);
this.totpInterval = setInterval(async () => {
await this.totpTick(interval);
}, 1000);
}
this.totpInfo$ = canGenerateTotp
? this.totpService.getCode$(this.cipher.login.totp).pipe(
map((response) => {
const epoch = Math.round(new Date().getTime() / 1000.0);
const mod = epoch % response.period;
// Format code
const totpCodeFormatted =
response.code.length > 4
? `${response.code.slice(0, Math.floor(response.code.length / 2))} ${response.code.slice(Math.floor(response.code.length / 2))}`
: response.code;
return {
totpCode: response.code,
totpCodeFormatted,
totpDash: +(Math.round(((78.6 / response.period) * mod + "e+2") as any) + "e-2"),
totpSec: response.period - mod,
totpLow: response.period - mod <= 7,
} as TotpInfo;
}),
)
: undefined;
if (this.previousCipherId !== this.cipherId) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@@ -13,6 +13,8 @@
"@bitwarden/generator-history": ["../tools/generator/extensions/history/src"],
"@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"],
"@bitwarden/importer/core": ["../importer/src"],
"@bitwarden/importer-ui": ["../importer/src/components"],
"@bitwarden/key-management": ["../key-management/src"],
"@bitwarden/platform": ["../platform/src"],
"@bitwarden/ui-common": ["../ui/common/src"],