Break up LoginComponent into client-specific services.

This commit is contained in:
Alec Rippberger 2024-09-25 22:07:37 -05:00
parent 7f14851147
commit a1b921691a
No known key found for this signature in database
GPG Key ID: 9DD8DA583B28154A
6 changed files with 263 additions and 45 deletions

View File

@ -0,0 +1,57 @@
import { Injectable, NgZone } from "@angular/core";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
/**
* Functionality for the desktop login component.
*/
@Injectable({
providedIn: "root",
})
export class DesktopLoginService {
private deferFocus: boolean = null;
constructor(
private broadcasterService: BroadcasterService,
private messagingService: MessagingService,
private ngZone: NgZone,
) {}
/**
* Sets up the window focus handler.
*
* @param focusInputCallback
*/
setupWindowFocusHandler(focusInputCallback: () => void): void {
const subscriptionId = "LoginComponent";
// TODO-rr-bw: refactor to not use deprecated broadcaster service.
this.broadcasterService.subscribe(subscriptionId, (message: any) => {
this.ngZone.run(() => {
if (message.command === "windowIsFocused") {
this.handleWindowFocus(message.windowIsFocused, focusInputCallback);
}
});
});
this.messagingService.send("getWindowIsFocused");
}
/**
* Handles the window focus event.
*
* @param windowIsFocused
* @param focusInputCallback
*/
private handleWindowFocus(windowIsFocused: boolean, focusInputCallback: () => void): void {
if (this.deferFocus === null) {
this.deferFocus = !windowIsFocused;
if (!this.deferFocus) {
focusInputCallback();
}
} else if (this.deferFocus && windowIsFocused) {
focusInputCallback();
this.deferFocus = false;
}
}
}

View File

@ -0,0 +1,45 @@
import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { ExtensionLoginService } from "./extension-login.service";
describe("ExtensionLoginService", () => {
let service: ExtensionLoginService;
let routerMock: jest.Mocked<Router>;
let loginEmailServiceMock: jest.Mocked<LoginEmailServiceAbstraction>;
beforeEach(() => {
routerMock = {
navigate: jest.fn(),
} as unknown as jest.Mocked<Router>;
loginEmailServiceMock = {
clearValues: jest.fn(),
} as unknown as jest.Mocked<LoginEmailServiceAbstraction>;
TestBed.configureTestingModule({
providers: [
ExtensionLoginService,
{ provide: Router, useValue: routerMock },
{ provide: LoginEmailServiceAbstraction, useValue: loginEmailServiceMock },
],
});
service = TestBed.inject(ExtensionLoginService);
});
it("creates the service", () => {
expect(service).toBeTruthy();
});
describe("handleSuccessfulLogin", () => {
it("clears login email service values and navigates to vault", async () => {
await service.handleSuccessfulLogin();
expect(loginEmailServiceMock.clearValues).toHaveBeenCalled();
expect(routerMock.navigate).toHaveBeenCalledWith(["/tabs/vault"]);
});
});
});

View File

@ -0,0 +1,25 @@
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
/**
* Functionality for the extension login component.
*/
@Injectable({
providedIn: "root",
})
export class ExtensionLoginService {
constructor(
private router: Router,
private loginEmailService: LoginEmailServiceAbstraction,
) {}
/**
* Handles the successful login - clears the login email service values and navigates to the vault.
*/
async handleSuccessfulLogin(): Promise<void> {
this.loginEmailService.clearValues();
await this.router.navigate(["/tabs/vault"]);
}
}

View File

@ -42,7 +42,10 @@ import {
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
import { WaveIcon } from "../icons";
import { DesktopLoginService } from "./desktop-login.service";
import { ExtensionLoginService } from "./extension-login.service";
import { LoginComponentService } from "./login-component.service";
import { WebLoginService } from "./web-login.service";
const BroadcasterSubscriptionId = "LoginComponent";
@ -136,6 +139,9 @@ export class LoginComponent implements OnInit, OnDestroy {
private router: Router,
private syncService: SyncService,
private toastService: ToastService,
private webLoginService: WebLoginService,
private desktopLoginService: DesktopLoginService,
private extensionLoginService: ExtensionLoginService,
) {
this.clientType = this.platformUtilsService.getClientType();
this.showPasswordless = this.loginComponentService.getShowPasswordlessFlag();
@ -266,16 +272,15 @@ export class LoginComponent implements OnInit, OnDestroy {
// If none of the above cases are true, proceed with login...
// ...on Web
if (this.clientType === ClientType.Web) {
// ...on Browser/Desktop
await this.goAfterLogIn(authResult.userId);
// ...on Browser/Desktop
} else if (this.clientType === ClientType.Browser) {
this.loginEmailService.clearValues();
await this.router.navigate(["/tabs/vault"]);
}
await this.extensionLoginService.handleSuccessfulLogin();
} else {
await this.router.navigate(["vault"]);
this.loginEmailService.clearValues();
}
}
protected async launchSsoBrowserWindow(clientId: "browser" | "desktop"): Promise<void> {
await this.loginComponentService.launchSsoBrowserWindow(this.loggedEmail, clientId);
@ -518,23 +523,20 @@ export class LoginComponent implements OnInit, OnDestroy {
}
private async webOnInit(): Promise<void> {
this.activatedRoute.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => {
if (qParams.org != null) {
const route = this.router.createUrlTree(["create-organization"], {
queryParams: { plan: qParams.org },
this.activatedRoute.queryParams
.pipe(
first(),
switchMap((qParams) => this.webLoginService.handleQueryParams(qParams)),
takeUntil(this.destroy$),
)
.subscribe({
error: (error: unknown) => {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: String(error),
});
this.loginComponentService.setPreviousUrl(route);
}
/* If there is a parameter called 'sponsorshipToken', they are coming
from an email for sponsoring a families organization. Therefore set
the prevousUrl to /setup/families-for-enterprise?token=<paramValue> */
if (qParams.sponsorshipToken != null) {
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
queryParams: { token: qParams.sponsorshipToken },
});
this.loginComponentService.setPreviousUrl(route);
}
},
});
/**
@ -554,27 +556,6 @@ export class LoginComponent implements OnInit, OnDestroy {
private async desktopOnInit(): Promise<void> {
await this.getLoginWithDevice(this.loggedEmail);
// TODO-rr-bw: refactor to not use deprecated broadcaster service.
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
this.desktopLoginService.setupWindowFocusHandler(() => this.focusInput());
}
}

View File

@ -0,0 +1,72 @@
import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { LoginComponentService } from "./login-component.service";
import { WebLoginService } from "./web-login.service";
describe("WebLoginService", () => {
let service: WebLoginService;
let routerMock: jest.Mocked<Router>;
let loginComponentServiceMock: jest.Mocked<LoginComponentService>;
beforeEach(() => {
routerMock = {
createUrlTree: jest.fn(),
} as unknown as jest.Mocked<Router>;
loginComponentServiceMock = {
setPreviousUrl: jest.fn(),
} as unknown as jest.Mocked<LoginComponentService>;
TestBed.configureTestingModule({
providers: [
WebLoginService,
{ provide: Router, useValue: routerMock },
{ provide: LoginComponentService, useValue: loginComponentServiceMock },
],
});
service = TestBed.inject(WebLoginService);
});
it("creates the service", () => {
expect(service).toBeTruthy();
});
describe("handleQueryParams", () => {
it("sets previous URL for organization creation when org param is present", async () => {
const qParams = { org: "some-org" };
const mockUrlTree = {} as any;
routerMock.createUrlTree.mockReturnValue(mockUrlTree);
await service.handleQueryParams(qParams);
expect(routerMock.createUrlTree).toHaveBeenCalledWith(["create-organization"], {
queryParams: { plan: "some-org" },
});
expect(loginComponentServiceMock.setPreviousUrl).toHaveBeenCalledWith(mockUrlTree);
});
it("sets previous URL for families sponsorship when sponsorshipToken param is present", async () => {
const qParams = { sponsorshipToken: "test-token" };
const mockUrlTree = {} as any;
routerMock.createUrlTree.mockReturnValue(mockUrlTree);
await service.handleQueryParams(qParams);
expect(routerMock.createUrlTree).toHaveBeenCalledWith(["setup/families-for-enterprise"], {
queryParams: { token: "test-token" },
});
expect(loginComponentServiceMock.setPreviousUrl).toHaveBeenCalledWith(mockUrlTree);
});
it("does not set previous URL when no relevant params are present", async () => {
const qParams = { someOtherParam: "value" };
await service.handleQueryParams(qParams);
expect(routerMock.createUrlTree).not.toHaveBeenCalled();
expect(loginComponentServiceMock.setPreviousUrl).not.toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,38 @@
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { LoginComponentService } from "./login-component.service";
/**
* Functionality for the web login component.
*/
@Injectable({
providedIn: "root",
})
export class WebLoginService {
constructor(
private router: Router,
private loginComponentService: LoginComponentService,
) {}
async handleQueryParams(qParams: any): Promise<void> {
if (qParams.org != null) {
const route = this.router.createUrlTree(["create-organization"], {
queryParams: { plan: qParams.org },
});
this.loginComponentService.setPreviousUrl(route);
}
/**
* If there is a parameter called 'sponsorshipToken', they are coming
* from an email for sponsoring a families organization. Therefore set
* the previousUrl to /setup/families-for-enterprise?token=<paramValue>
*/
if (qParams.sponsorshipToken != null) {
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
queryParams: { token: qParams.sponsorshipToken },
});
this.loginComponentService.setPreviousUrl(route);
}
}
}