diff --git a/libs/auth/src/angular/login/login.component.html b/libs/auth/src/angular/login/login.component.html
index 54a04d3de6c..c7837db74f2 100644
--- a/libs/auth/src/angular/login/login.component.html
+++ b/libs/auth/src/angular/login/login.component.html
@@ -20,8 +20,8 @@
formControlName="email"
bitInput
appAutofocus
- (blur)="onEmailBlur($event)"
- (keyup.enter)="continue()"
+ (input)="onEmailInput($event)"
+ (keyup.enter)="continuePressed()"
/>
@@ -33,7 +33,7 @@
diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts
index 33c167dcaed..40f85e6d75c 100644
--- a/libs/auth/src/angular/login/login.component.ts
+++ b/libs/auth/src/angular/login/login.component.ts
@@ -1,5 +1,3 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
@@ -12,7 +10,6 @@ import {
LoginStrategyServiceAbstraction,
LoginSuccessHandlerService,
PasswordLoginCredentials,
- RegisterRouteService,
} from "@bitwarden/auth/common";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
@@ -72,16 +69,15 @@ export enum LoginUiState {
],
})
export class LoginComponent implements OnInit, OnDestroy {
- @ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef;
+ @ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined;
private destroy$ = new Subject();
- private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
+ private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions | undefined = undefined;
readonly Icons = { WaveIcon, VaultIcon };
clientType: ClientType;
ClientType = ClientType;
LoginUiState = LoginUiState;
- registerRoute$ = this.registerRouteService.registerRoute$(); // TODO: remove when email verification flag is removed
isKnownDevice = false;
loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY;
@@ -97,13 +93,13 @@ export class LoginComponent implements OnInit, OnDestroy {
{ updateOn: "submit" },
);
- get emailFormControl(): FormControl {
+ get emailFormControl(): FormControl {
return this.formGroup.controls.email;
}
// Web properties
- enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
- policies: Policy[];
+ enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
+ policies: Policy[] | undefined;
showResetPasswordAutoEnrollWarning = false;
// Desktop properties
@@ -125,7 +121,6 @@ export class LoginComponent implements OnInit, OnDestroy {
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private policyService: InternalPolicyService,
- private registerRouteService: RegisterRouteService,
private router: Router,
private toastService: ToastService,
private logService: LogService,
@@ -200,12 +195,12 @@ export class LoginComponent implements OnInit, OnDestroy {
return;
}
- const credentials = new PasswordLoginCredentials(
- email,
- masterPassword,
- null, // captcha no longer used in new login / registration scenarios
- null,
- );
+ if (!email || !masterPassword) {
+ this.logService.error("Email and master password are required");
+ return;
+ }
+
+ const credentials = new PasswordLoginCredentials(email, masterPassword);
try {
const authResult = await this.loginStrategyService.logIn(credentials);
@@ -301,7 +296,12 @@ export class LoginComponent implements OnInit, OnDestroy {
}
protected async launchSsoBrowserWindow(clientId: "browser" | "desktop"): Promise {
- await this.loginComponentService.launchSsoBrowserWindow(this.emailFormControl.value, clientId);
+ const email = this.emailFormControl.value;
+ if (!email) {
+ this.logService.error("Email is required for SSO login");
+ return;
+ }
+ await this.loginComponentService.launchSsoBrowserWindow(email, clientId);
}
protected async evaluatePassword(): Promise {
@@ -337,9 +337,14 @@ export class LoginComponent implements OnInit, OnDestroy {
const masterPassword = this.formGroup.controls.masterPassword.value;
+ // Return false if masterPassword is null/undefined since this is only evaluated after successful login
+ if (!masterPassword) {
+ return false;
+ }
+
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
- this.formGroup.value.email,
+ this.formGroup.value.email ?? undefined,
)?.score;
return !this.policyService.evaluateMasterPassword(
@@ -363,6 +368,7 @@ export class LoginComponent implements OnInit, OnDestroy {
protected async validateEmail(): Promise {
this.formGroup.controls.email.markAsTouched();
+ this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true });
return this.formGroup.controls.email.valid;
}
@@ -404,7 +410,10 @@ export class LoginComponent implements OnInit, OnDestroy {
}
// Check to see if the device is known so we can show the Login with Device option
- await this.getKnownDevice(this.emailFormControl.value);
+ const email = this.emailFormControl.value;
+ if (email) {
+ await this.getKnownDevice(email);
+ }
}
}
@@ -412,11 +421,10 @@ export class LoginComponent implements OnInit, OnDestroy {
* Set the email value from the input field.
* @param event The event object from the input field.
*/
- onEmailBlur(event: Event) {
+ onEmailInput(event: Event) {
const emailInput = event.target as HTMLInputElement;
this.formGroup.controls.email.setValue(emailInput.value);
- // Call setLoginEmail so that the email is pre-populated when navigating to the "enter password" screen.
- this.loginEmailService.setLoginEmail(this.formGroup.value.email);
+ this.loginEmailService.setLoginEmail(emailInput.value);
}
isLoginWithPasskeySupported() {
@@ -428,28 +436,36 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.router.navigateByUrl("/hint");
}
- protected async goToRegister(): Promise {
- // TODO: remove when email verification flag is removed
- const registerRoute = await firstValueFrom(this.registerRoute$);
-
- if (this.emailFormControl.valid) {
- await this.router.navigate([registerRoute], {
- queryParams: { email: this.emailFormControl.value },
- });
+ protected async saveEmailSettings(): Promise {
+ const email = this.formGroup.value.email;
+ if (!email) {
+ this.logService.error("Email is required to save email settings.");
return;
}
- await this.router.navigate([registerRoute]);
+ await this.loginEmailService.setLoginEmail(email);
+ this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail ?? false);
+ await this.loginEmailService.saveEmailSettings();
}
- protected async saveEmailSettings(): Promise {
- await this.loginEmailService.setLoginEmail(this.formGroup.value.email);
- this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
- await this.loginEmailService.saveEmailSettings();
+ /**
+ * Continue button clicked (or enter key pressed).
+ * Adds the login url to the browser's history so that the back button can be used to go back to the email entry state.
+ * Needs to be separate from the continue() function because that can be triggered by the browser's forward button.
+ */
+ protected async continuePressed() {
+ // Add a new entry to the browser's history so that there is a history entry to go back to
+ history.pushState({}, "", window.location.href);
+ await this.continue();
}
+ /**
+ * Continue to the master password entry state (only if email is validated)
+ */
protected async continue(): Promise {
- if (await this.validateEmail()) {
+ const isEmailValid = await this.validateEmail();
+
+ if (isEmailValid) {
await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY);
}
}
@@ -460,6 +476,11 @@ export class LoginComponent implements OnInit, OnDestroy {
* @param email - The user's email
*/
private async getKnownDevice(email: string): Promise {
+ if (!email) {
+ this.isKnownDevice = false;
+ return;
+ }
+
try {
const deviceIdentifier = await this.appIdService.getAppId();
this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier);
@@ -503,7 +524,7 @@ export class LoginComponent implements OnInit, OnDestroy {
const orgPolicies = await this.loginComponentService.getOrgPolicies();
this.policies = orgPolicies?.policies;
- this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled;
+ this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled ?? false;
let paramEmailIsSet = false;
@@ -525,7 +546,9 @@ export class LoginComponent implements OnInit, OnDestroy {
}
// Check to see if the device is known so that we can show the Login with Device option
- await this.getKnownDevice(this.emailFormControl.value);
+ if (this.emailFormControl.value) {
+ await this.getKnownDevice(this.emailFormControl.value);
+ }
// Backup check to handle unknown case where activatedRoute is not available
// This shouldn't happen under normal circumstances
@@ -573,23 +596,50 @@ export class LoginComponent implements OnInit, OnDestroy {
* Handle the back button click to transition back to the email entry state.
*/
protected async backButtonClicked() {
- // Replace the history so the "forward" button doesn't show (which wouldn't do anything)
- history.pushState(null, "", window.location.pathname);
- await this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY);
+ history.back();
}
/**
* Handle the popstate event to transition back to the email entry state when the back button is clicked.
+ * Also handles the case where the user clicks the forward button.
* @param event - The popstate event.
*/
- private handlePopState = (event: PopStateEvent) => {
+ private handlePopState = async (event: PopStateEvent) => {
if (this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY) {
- // Prevent default navigation
+ // Prevent default navigation when the browser's back button is clicked
event.preventDefault();
- // Replace the history so the "forward" button doesn't show (which wouldn't do anything)
- history.pushState(null, "", window.location.pathname);
// Transition back to email entry state
void this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY);
+ } else if (this.loginUiState === LoginUiState.EMAIL_ENTRY) {
+ // Prevent default navigation when the browser's forward button is clicked
+ event.preventDefault();
+ // Continue to the master password entry state
+ await this.continue();
}
};
+
+ /**
+ * Handle the SSO button click.
+ * @param event - The event object.
+ */
+ async handleSsoClick() {
+ const isEmailValid = await this.validateEmail();
+
+ if (!isEmailValid) {
+ return;
+ }
+
+ await this.saveEmailSettings();
+
+ if (this.clientType === ClientType.Web) {
+ await this.router.navigate(["/sso"], {
+ queryParams: { email: this.formGroup.value.email },
+ });
+ return;
+ }
+
+ await this.launchSsoBrowserWindow(
+ this.clientType === ClientType.Browser ? "browser" : "desktop",
+ );
+ }
}