1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +00:00

refactor(login-approval-component) [Auth/PM-14940] Update LoginApprovalComponent (#15511)

- Renames the `LoginApprovalComponent` to `LoginApprovalDialogComponent`
- Renames the property `notificationId` to `authRequestId` for clarity
- Updates text content on the component
This commit is contained in:
rr-bw
2025-08-04 09:20:12 -07:00
committed by GitHub
parent 0bd48f6e58
commit 25ada6f80f
22 changed files with 261 additions and 256 deletions

View File

@@ -3657,25 +3657,6 @@
"thisRequestIsNoLongerValid": { "thisRequestIsNoLongerValid": {
"message": "This request is no longer valid." "message": "This request is no longer valid."
}, },
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "iOS"
}
}
},
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
},
"loginRequestHasAlreadyExpired": { "loginRequestHasAlreadyExpired": {
"message": "Login request has already expired." "message": "Login request has already expired."
}, },

View File

@@ -24,11 +24,12 @@ import {
} from "rxjs"; } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common"; import { CollectionService } from "@bitwarden/admin-console/common";
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n"; import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular"; import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { import {
DESKTOP_SSO_CALLBACK, DESKTOP_SSO_CALLBACK,
LogoutReason, LogoutReason,
@@ -476,7 +477,7 @@ export class AppComponent implements OnInit, OnDestroy {
case "openLoginApproval": case "openLoginApproval":
if (message.notificationId != null) { if (message.notificationId != null) {
this.dialogService.closeAll(); this.dialogService.closeAll();
const dialogRef = LoginApprovalComponent.open(this.dialogService, { const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, {
notificationId: message.notificationId, notificationId: message.notificationId,
}); });
await firstValueFrom(dialogRef.closed); await firstValueFrom(dialogRef.closed);

View File

@@ -5,6 +5,7 @@ import { Router } from "@angular/router";
import { Subject, merge } from "rxjs"; import { Subject, merge } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { LoginApprovalDialogComponentServiceAbstraction } from "@bitwarden/angular/auth/login-approval";
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { import {
@@ -31,7 +32,6 @@ import {
} from "@bitwarden/auth/angular"; } from "@bitwarden/auth/angular";
import { import {
InternalUserDecryptionOptionsServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction,
LoginApprovalComponentServiceAbstraction,
LoginEmailService, LoginEmailService,
SsoUrlService, SsoUrlService,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
@@ -107,7 +107,7 @@ import {
import { LockComponentService } from "@bitwarden/key-management-ui"; import { LockComponentService } from "@bitwarden/key-management-ui";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault"; import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service"; import { DesktopLoginApprovalDialogComponentService } from "../../auth/login/desktop-login-approval-dialog-component.service";
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service"; import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
@@ -444,8 +444,8 @@ const safeProviders: SafeProvider[] = [
deps: [], deps: [],
}), }),
safeProvider({ safeProvider({
provide: LoginApprovalComponentServiceAbstraction, provide: LoginApprovalDialogComponentServiceAbstraction,
useClass: DesktopLoginApprovalComponentService, useClass: DesktopLoginApprovalDialogComponentService,
deps: [I18nServiceAbstraction], deps: [I18nServiceAbstraction],
}), }),
safeProvider({ safeProvider({

View File

@@ -2,13 +2,13 @@ import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { LoginApprovalComponent } from "@bitwarden/auth/angular"; import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DesktopLoginApprovalComponentService } from "./desktop-login-approval-component.service"; import { DesktopLoginApprovalDialogComponentService } from "./desktop-login-approval-dialog-component.service";
describe("DesktopLoginApprovalComponentService", () => { describe("DesktopLoginApprovalDialogComponentService", () => {
let service: DesktopLoginApprovalComponentService; let service: DesktopLoginApprovalDialogComponentService;
let i18nService: MockProxy<I18nServiceAbstraction>; let i18nService: MockProxy<I18nServiceAbstraction>;
let originalIpc: any; let originalIpc: any;
@@ -31,12 +31,12 @@ describe("DesktopLoginApprovalComponentService", () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
DesktopLoginApprovalComponentService, DesktopLoginApprovalDialogComponentService,
{ provide: I18nServiceAbstraction, useValue: i18nService }, { provide: I18nServiceAbstraction, useValue: i18nService },
], ],
}); });
service = TestBed.inject(DesktopLoginApprovalComponentService); service = TestBed.inject(DesktopLoginApprovalDialogComponentService);
}); });
afterEach(() => { afterEach(() => {
@@ -54,7 +54,7 @@ describe("DesktopLoginApprovalComponentService", () => {
const message = `Confirm access attempt for ${email}`; const message = `Confirm access attempt for ${email}`;
const closeText = "Close"; const closeText = "Close";
const loginApprovalComponent = { email } as LoginApprovalComponent; const loginApprovalDialogComponent = { email } as LoginApprovalDialogComponent;
i18nService.t.mockImplementation((key: string) => { i18nService.t.mockImplementation((key: string) => {
switch (key) { switch (key) {
case "accountAccessRequested": case "accountAccessRequested":
@@ -71,18 +71,20 @@ describe("DesktopLoginApprovalComponentService", () => {
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(false); jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(false);
jest.spyOn(ipc.auth, "loginRequest").mockResolvedValue(); jest.spyOn(ipc.auth, "loginRequest").mockResolvedValue();
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email); await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email);
expect(ipc.auth.loginRequest).toHaveBeenCalledWith(title, message, closeText); expect(ipc.auth.loginRequest).toHaveBeenCalledWith(title, message, closeText);
}); });
it("does not call ipc.auth.loginRequest when window is visible", async () => { it("does not call ipc.auth.loginRequest when window is visible", async () => {
const loginApprovalComponent = { email: "test@bitwarden.com" } as LoginApprovalComponent; const loginApprovalDialogComponent = {
email: "test@bitwarden.com",
} as LoginApprovalDialogComponent;
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(true); jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(true);
jest.spyOn(ipc.auth, "loginRequest"); jest.spyOn(ipc.auth, "loginRequest");
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email); await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email);
expect(ipc.auth.loginRequest).not.toHaveBeenCalled(); expect(ipc.auth.loginRequest).not.toHaveBeenCalled();
}); });

View File

@@ -1,13 +1,15 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { DefaultLoginApprovalComponentService } from "@bitwarden/auth/angular"; import {
import { LoginApprovalComponentServiceAbstraction } from "@bitwarden/auth/common"; DefaultLoginApprovalDialogComponentService,
LoginApprovalDialogComponentServiceAbstraction,
} from "@bitwarden/angular/auth/login-approval";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
@Injectable() @Injectable()
export class DesktopLoginApprovalComponentService export class DesktopLoginApprovalDialogComponentService
extends DefaultLoginApprovalComponentService extends DefaultLoginApprovalDialogComponentService
implements LoginApprovalComponentServiceAbstraction implements LoginApprovalDialogComponentServiceAbstraction
{ {
constructor(private i18nService: I18nServiceAbstraction) { constructor(private i18nService: I18nServiceAbstraction) {
super(); super();

View File

@@ -3027,9 +3027,6 @@
"message": "Toggle character count", "message": "Toggle character count",
"description": "'Character count' describes a feature that displays a number next to each character of the password." "description": "'Character count' describes a feature that displays a number next to each character of the password."
}, },
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": { "accessAttemptBy": {
"message": "Access attempt by $EMAIL$", "message": "Access attempt by $EMAIL$",
"placeholders": { "placeholders": {
@@ -3039,6 +3036,50 @@
} }
} }
}, },
"loginRequestApprovedForEmailOnDevice": {
"message": "Login request approved for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "Web app - Chrome"
}
}
},
"youDeniedLoginAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
},
"webApp": {
"message": "Web app"
},
"mobile": {
"message": "Mobile",
"description": "Mobile app"
},
"extension": {
"message": "Extension",
"description": "Browser extension/addon"
},
"desktop": {
"message": "Desktop",
"description": "Desktop app"
},
"cli": {
"message": "CLI"
},
"sdk": {
"message": "SDK",
"description": "Software Development Kit"
},
"server": {
"message": "Server"
},
"loginRequest": {
"message": "Login request"
},
"deviceType": { "deviceType": {
"message": "Device Type" "message": "Device Type"
}, },
@@ -3054,22 +3095,6 @@
"denyAccess": { "denyAccess": {
"message": "Deny access" "message": "Deny access"
}, },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "iOS"
}
}
},
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
},
"justNow": { "justNow": {
"message": "Just now" "message": "Just now"
}, },

View File

@@ -3,7 +3,7 @@ import { Component, DestroyRef } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { LoginApprovalComponent } from "@bitwarden/auth/angular"; import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common"; import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { import {
@@ -325,7 +325,7 @@ export class DeviceManagementOldComponent {
return; return;
} }
const dialogRef = LoginApprovalComponent.open(this.dialogService, { const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, {
notificationId: device.devicePendingAuthRequest.id, notificationId: device.devicePendingAuthRequest.id,
}); });

View File

@@ -1428,9 +1428,6 @@
"notificationSentDevicePart1": { "notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the " "message": "Unlock Bitwarden on your device or on the "
}, },
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": { "accessAttemptBy": {
"message": "Access attempt by $EMAIL$", "message": "Access attempt by $EMAIL$",
"placeholders": { "placeholders": {
@@ -3981,22 +3978,6 @@
"thisRequestIsNoLongerValid": { "thisRequestIsNoLongerValid": {
"message": "This request is no longer valid." "message": "This request is no longer valid."
}, },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "iOS"
}
}
},
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
},
"loginRequestApprovedForEmailOnDevice": { "loginRequestApprovedForEmailOnDevice": {
"message": "Login request approved for $EMAIL$ on $DEVICE$", "message": "Login request approved for $EMAIL$ on $DEVICE$",
"placeholders": { "placeholders": {

View File

@@ -2,13 +2,12 @@ import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
import { BadgeModule, DialogService, ItemModule } from "@bitwarden/components"; import { BadgeModule, DialogService, ItemModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
import { LoginApprovalDialogComponent } from "../login-approval/login-approval-dialog.component";
import { DeviceDisplayData } from "./device-management.component"; import { DeviceDisplayData } from "./device-management.component";
import { clearAuthRequestAndResortDevices } from "./resort-devices.helper"; import { clearAuthRequestAndResortDevices } from "./resort-devices.helper";
@@ -29,7 +28,7 @@ export class DeviceManagementItemGroupComponent {
return; return;
} }
const loginApprovalDialog = LoginApprovalComponent.open(this.dialogService, { const loginApprovalDialog = LoginApprovalDialogComponent.open(this.dialogService, {
notificationId: pendingAuthRequest.id, notificationId: pendingAuthRequest.id,
}); });

View File

@@ -3,9 +3,6 @@ import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { import {
@@ -17,6 +14,8 @@ import {
TableModule, TableModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { LoginApprovalDialogComponent } from "../login-approval/login-approval-dialog.component";
import { DeviceDisplayData } from "./device-management.component"; import { DeviceDisplayData } from "./device-management.component";
import { clearAuthRequestAndResortDevices } from "./resort-devices.helper"; import { clearAuthRequestAndResortDevices } from "./resort-devices.helper";
@@ -68,7 +67,7 @@ export class DeviceManagementTableComponent implements OnChanges {
return; return;
} }
const loginApprovalDialog = LoginApprovalComponent.open(this.dialogService, { const loginApprovalDialog = LoginApprovalDialogComponent.open(this.dialogService, {
notificationId: pendingAuthRequest.id, notificationId: pendingAuthRequest.id,
}); });

View File

@@ -0,0 +1,30 @@
import { TestBed } from "@angular/core/testing";
import { DefaultLoginApprovalDialogComponentService } from "./default-login-approval-dialog-component.service";
import { LoginApprovalDialogComponent } from "./login-approval-dialog.component";
describe("DefaultLoginApprovalDialogComponentService", () => {
let service: DefaultLoginApprovalDialogComponentService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [DefaultLoginApprovalDialogComponentService],
});
service = TestBed.inject(DefaultLoginApprovalDialogComponentService);
});
it("is created successfully", () => {
expect(service).toBeTruthy();
});
it("has showLoginRequestedAlertIfWindowNotVisible method that is a no-op", async () => {
const loginApprovalDialogComponent = {} as LoginApprovalDialogComponent;
const result = await service.showLoginRequestedAlertIfWindowNotVisible(
loginApprovalDialogComponent.email,
);
expect(result).toBeUndefined();
});
});

View File

@@ -0,0 +1,16 @@
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
/**
* Default implementation of the LoginApprovalDialogComponentServiceAbstraction.
*/
export class DefaultLoginApprovalDialogComponentService
implements LoginApprovalDialogComponentServiceAbstraction
{
/**
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
* @returns
*/
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
return;
}
}

View File

@@ -0,0 +1,3 @@
export * from "./login-approval-dialog.component";
export * from "./login-approval-dialog-component.service.abstraction";
export * from "./default-login-approval-dialog-component.service";

View File

@@ -1,7 +1,7 @@
/** /**
* Abstraction for the LoginApprovalComponent service. * Abstraction for the LoginApprovalDialogComponent service.
*/ */
export abstract class LoginApprovalComponentServiceAbstraction { export abstract class LoginApprovalDialogComponentServiceAbstraction {
/** /**
* Shows a login requested alert if the window is not visible. * Shows a login requested alert if the window is not visible.
*/ */

View File

@@ -1,5 +1,6 @@
<bit-dialog> <bit-dialog>
<span bitDialogTitle>{{ "areYouTryingToAccessYourAccount" | i18n }}</span> <span bitDialogTitle>{{ "loginRequest" | i18n }}</span>
<ng-container bitDialogContent> <ng-container bitDialogContent>
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading"> <div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
@@ -8,28 +9,29 @@
</ng-container> </ng-container>
<ng-container *ngIf="!loading"> <ng-container *ngIf="!loading">
<h4 class="tw-mb-3">{{ "accessAttemptBy" | i18n: email }}</h4> <p>{{ "accessAttemptBy" | i18n: email }}</p>
<div> <div>
<b>{{ "fingerprintPhraseHeader" | i18n }}</b> <span class="tw-font-medium">{{ "fingerprintPhraseHeader" | i18n }}</span>
<p class="tw-text-code">{{ fingerprintPhrase }}</p> <p class="tw-text-code">{{ fingerprintPhrase }}</p>
</div> </div>
<div> <div>
<b>{{ "deviceType" | i18n }}</b> <span class="tw-font-medium">{{ "deviceType" | i18n }}</span>
<p>{{ authRequestResponse?.requestDeviceType }}</p> <p>{{ readableDeviceTypeName }}</p>
</div> </div>
<div> <div>
<b>{{ "location" | i18n }}</b> <span class="tw-font-medium">{{ "location" | i18n }}</span>
<p> <p>
<span class="tw-capitalize">{{ authRequestResponse?.requestCountryName }} </span> <span class="tw-capitalize">{{ authRequestResponse?.requestCountryName }} </span>
({{ authRequestResponse?.requestIpAddress }}) ({{ authRequestResponse?.requestIpAddress }})
</p> </p>
</div> </div>
<div> <div>
<b>{{ "time" | i18n }}</b> <span class="tw-font-medium">{{ "time" | i18n }}</span>
<p>{{ requestTimeText }}</p> <p>{{ requestTimeText }}</p>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button <button
bitButton bitButton

View File

@@ -2,34 +2,33 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
import {
AuthRequestServiceAbstraction,
LoginApprovalComponentServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { UserId } from "@bitwarden/common/types/guid";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogRef, DIALOG_DATA, ToastService } from "@bitwarden/components"; import { DialogRef, DIALOG_DATA, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management"; import { LogService } from "@bitwarden/logging";
import { LoginApprovalComponent } from "./login-approval.component"; import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
import { LoginApprovalDialogComponent } from "./login-approval-dialog.component";
describe("LoginApprovalComponent", () => { describe("LoginApprovalDialogComponent", () => {
let component: LoginApprovalComponent; let component: LoginApprovalDialogComponent;
let fixture: ComponentFixture<LoginApprovalComponent>; let fixture: ComponentFixture<LoginApprovalDialogComponent>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let accountService: MockProxy<AccountService>; let accountService: MockProxy<AccountService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
let i18nService: MockProxy<I18nService>; let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let devicesService: MockProxy<DevicesServiceAbstraction>;
let dialogRef: MockProxy<DialogRef>; let dialogRef: MockProxy<DialogRef>;
let i18nService: MockProxy<I18nService>;
let logService: MockProxy<LogService>;
let toastService: MockProxy<ToastService>; let toastService: MockProxy<ToastService>;
let validationService: MockProxy<ValidationService>; let validationService: MockProxy<ValidationService>;
@@ -38,11 +37,13 @@ describe("LoginApprovalComponent", () => {
const testPublicKey = "test-public-key"; const testPublicKey = "test-public-key";
beforeEach(async () => { beforeEach(async () => {
authRequestService = mock<AuthRequestServiceAbstraction>();
accountService = mock<AccountService>(); accountService = mock<AccountService>();
apiService = mock<ApiService>(); apiService = mock<ApiService>();
i18nService = mock<I18nService>(); authRequestService = mock<AuthRequestServiceAbstraction>();
devicesService = mock<DevicesServiceAbstraction>();
dialogRef = mock<DialogRef>(); dialogRef = mock<DialogRef>();
i18nService = mock<I18nService>();
logService = mock<LogService>();
toastService = mock<ToastService>(); toastService = mock<ToastService>();
validationService = mock<ValidationService>(); validationService = mock<ValidationService>();
@@ -54,27 +55,26 @@ describe("LoginApprovalComponent", () => {
}); });
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LoginApprovalComponent], imports: [LoginApprovalDialogComponent],
providers: [ providers: [
{ provide: DIALOG_DATA, useValue: { notificationId: testNotificationId } }, { provide: DIALOG_DATA, useValue: { notificationId: testNotificationId } },
{ provide: AuthRequestServiceAbstraction, useValue: authRequestService },
{ provide: AccountService, useValue: accountService }, { provide: AccountService, useValue: accountService },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: I18nService, useValue: i18nService },
{ provide: ApiService, useValue: apiService }, { provide: ApiService, useValue: apiService },
{ provide: AppIdService, useValue: mock<AppIdService>() }, { provide: AuthRequestServiceAbstraction, useValue: authRequestService },
{ provide: KeyService, useValue: mock<KeyService>() }, { provide: DevicesServiceAbstraction, useValue: devicesService },
{ provide: DialogRef, useValue: dialogRef }, { provide: DialogRef, useValue: dialogRef },
{ provide: I18nService, useValue: i18nService },
{ provide: LogService, useValue: logService },
{ provide: ToastService, useValue: toastService }, { provide: ToastService, useValue: toastService },
{ provide: ValidationService, useValue: validationService }, { provide: ValidationService, useValue: validationService },
{ {
provide: LoginApprovalComponentServiceAbstraction, provide: LoginApprovalDialogComponentServiceAbstraction,
useValue: mock<LoginApprovalComponentServiceAbstraction>(), useValue: mock<LoginApprovalDialogComponentServiceAbstraction>(),
}, },
], ],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(LoginApprovalComponent); fixture = TestBed.createComponent(LoginApprovalDialogComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
}); });
@@ -119,7 +119,6 @@ describe("LoginApprovalComponent", () => {
expect(authRequestService.approveOrDenyAuthRequest).toHaveBeenCalledWith(false, response); expect(authRequestService.approveOrDenyAuthRequest).toHaveBeenCalledWith(false, response);
expect(toastService.showToast).toHaveBeenCalledWith({ expect(toastService.showToast).toHaveBeenCalledWith({
variant: "info", variant: "info",
title: null,
message: "denied message", message: "denied message",
}); });
}); });

View File

@@ -1,24 +1,18 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, OnInit, OnDestroy, Inject } from "@angular/core"; import { Component, OnInit, OnDestroy, Inject } from "@angular/core";
import { Subject, firstValueFrom, map } from "rxjs"; import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
AuthRequestServiceAbstraction,
LoginApprovalComponentServiceAbstraction as LoginApprovalComponentService,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { import {
DIALOG_DATA, DIALOG_DATA,
DialogRef, DialogRef,
@@ -28,84 +22,100 @@ import {
DialogService, DialogService,
ToastService, ToastService,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management"; import { LogService } from "@bitwarden/logging";
const RequestTimeOut = 60000 * 15; //15 Minutes import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
const RequestTimeUpdate = 60000 * 5; //5 Minutes
const RequestTimeOut = 60000 * 15; // 15 Minutes
const RequestTimeUpdate = 60000 * 5; // 5 Minutes
export interface LoginApprovalDialogParams { export interface LoginApprovalDialogParams {
notificationId: string; notificationId: string;
} }
@Component({ @Component({
selector: "login-approval", templateUrl: "login-approval-dialog.component.html",
templateUrl: "login-approval.component.html", imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, JslibModule],
imports: [CommonModule, AsyncActionsModule, ButtonModule, DialogModule, JslibModule],
}) })
export class LoginApprovalComponent implements OnInit, OnDestroy { export class LoginApprovalDialogComponent implements OnInit, OnDestroy {
authRequestId: string;
authRequestResponse?: AuthRequestResponse;
email?: string;
fingerprintPhrase?: string;
interval?: NodeJS.Timeout;
loading = true; loading = true;
readableDeviceTypeName?: string;
notificationId: string; requestTimeText?: string;
private destroy$ = new Subject<void>();
email: string;
fingerprintPhrase: string;
authRequestResponse: AuthRequestResponse;
interval: NodeJS.Timeout;
requestTimeText: string;
constructor( constructor(
@Inject(DIALOG_DATA) private params: LoginApprovalDialogParams, @Inject(DIALOG_DATA) private params: LoginApprovalDialogParams,
protected authRequestService: AuthRequestServiceAbstraction, private accountService: AccountService,
protected accountService: AccountService, private apiService: ApiService,
protected platformUtilsService: PlatformUtilsService, private authRequestService: AuthRequestServiceAbstraction,
protected i18nService: I18nService, private devicesService: DevicesServiceAbstraction,
protected apiService: ApiService,
protected appIdService: AppIdService,
protected keyService: KeyService,
private dialogRef: DialogRef, private dialogRef: DialogRef,
private i18nService: I18nService,
private loginApprovalDialogComponentService: LoginApprovalDialogComponentServiceAbstraction,
private logService: LogService,
private toastService: ToastService, private toastService: ToastService,
private loginApprovalComponentService: LoginApprovalComponentService,
private validationService: ValidationService, private validationService: ValidationService,
) { ) {
this.notificationId = params.notificationId; this.authRequestId = params.notificationId;
} }
async ngOnDestroy(): Promise<void> { async ngOnDestroy(): Promise<void> {
clearInterval(this.interval); clearInterval(this.interval);
this.destroy$.next();
this.destroy$.complete();
} }
async ngOnInit() { async ngOnInit() {
if (this.notificationId != null) { if (this.authRequestId == null) {
try { this.logService.error("LoginApprovalDialogComponent: authRequestId is null");
this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); return;
} catch (error) {
this.validationService.showError(error);
}
const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey);
this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
this.email,
publicKey,
);
this.updateTimeText();
this.interval = setInterval(() => {
this.updateTimeText();
}, RequestTimeUpdate);
// 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.loginApprovalComponentService.showLoginRequestedAlertIfWindowNotVisible(this.email);
this.loading = false;
} }
try {
this.authRequestResponse = await this.apiService.getAuthRequest(this.authRequestId);
} catch (error) {
this.validationService.showError(error);
this.logService.error("LoginApprovalDialogComponent: getAuthRequest error", error);
}
if (this.authRequestResponse == null) {
this.logService.error("LoginApprovalDialogComponent: authRequestResponse not found");
return;
}
const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey);
this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
if (!this.email) {
this.logService.error("LoginApprovalDialogComponent: email not found");
return;
}
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
this.email,
publicKey,
);
this.readableDeviceTypeName = this.devicesService.getReadableDeviceTypeName(
this.authRequestResponse.requestDeviceTypeValue,
);
this.updateTimeText();
this.interval = setInterval(() => {
this.updateTimeText();
}, RequestTimeUpdate);
await this.loginApprovalDialogComponentService.showLoginRequestedAlertIfWindowNotVisible(
this.email,
);
this.loading = false;
} }
/** /**
@@ -114,7 +124,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
* @param data Configuration for the dialog * @param data Configuration for the dialog
*/ */
static open(dialogService: DialogService, data: LoginApprovalDialogParams) { static open(dialogService: DialogService, data: LoginApprovalDialogParams) {
return dialogService.open(LoginApprovalComponent, { data }); return dialogService.open(LoginApprovalDialogComponent, { data });
} }
denyLogin = async () => { denyLogin = async () => {
@@ -126,11 +136,10 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
}; };
private async retrieveAuthRequestAndRespond(approve: boolean) { private async retrieveAuthRequestAndRespond(approve: boolean) {
this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); this.authRequestResponse = await this.apiService.getAuthRequest(this.authRequestId);
if (this.authRequestResponse.requestApproved || this.authRequestResponse.responseDate != null) { if (this.authRequestResponse.requestApproved || this.authRequestResponse.responseDate != null) {
this.toastService.showToast({ this.toastService.showToast({
variant: "info", variant: "info",
title: null,
message: this.i18nService.t("thisRequestIsNoLongerValid"), message: this.i18nService.t("thisRequestIsNoLongerValid"),
}); });
} else { } else {
@@ -148,23 +157,26 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
if (loginResponse.requestApproved) { if (loginResponse.requestApproved) {
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
title: null,
message: this.i18nService.t( message: this.i18nService.t(
"logInConfirmedForEmailOnDevice", "loginRequestApprovedForEmailOnDevice",
this.email, this.email,
loginResponse.requestDeviceType, this.devicesService.getReadableDeviceTypeName(loginResponse.requestDeviceTypeValue),
), ),
}); });
} else { } else {
this.toastService.showToast({ this.toastService.showToast({
variant: "info", variant: "info",
title: null, message: this.i18nService.t("youDeniedLoginAttemptFromAnotherDevice"),
message: this.i18nService.t("youDeniedALogInAttemptFromAnotherDevice"),
}); });
} }
} }
updateTimeText() { updateTimeText() {
if (this.authRequestResponse == null) {
this.logService.error("LoginApprovalDialogComponent: authRequestResponse not found");
return;
}
const requestDate = new Date(this.authRequestResponse.creationDate); const requestDate = new Date(this.authRequestResponse.creationDate);
const requestDateUTC = Date.UTC( const requestDateUTC = Date.UTC(
requestDate.getUTCFullYear(), requestDate.getUTCFullYear(),
@@ -201,7 +213,6 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
this.dialogRef.close(); this.dialogRef.close();
this.toastService.showToast({ this.toastService.showToast({
variant: "info", variant: "info",
title: null,
message: this.i18nService.t("loginRequestHasAlreadyExpired"), message: this.i18nService.t("loginRequestHasAlreadyExpired"),
}); });
} }

View File

@@ -18,7 +18,6 @@ import {
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { import {
DefaultLoginApprovalComponentService,
DefaultLoginComponentService, DefaultLoginComponentService,
DefaultLoginDecryptionOptionsService, DefaultLoginDecryptionOptionsService,
DefaultRegistrationFinishService, DefaultRegistrationFinishService,
@@ -40,7 +39,6 @@ import {
DefaultLoginSuccessHandlerService, DefaultLoginSuccessHandlerService,
DefaultLogoutService, DefaultLogoutService,
InternalUserDecryptionOptionsServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction,
LoginApprovalComponentServiceAbstraction,
LoginEmailService, LoginEmailService,
LoginEmailServiceAbstraction, LoginEmailServiceAbstraction,
LoginStrategyService, LoginStrategyService,
@@ -343,6 +341,8 @@ import {
VaultExportServiceAbstraction, VaultExportServiceAbstraction,
} from "@bitwarden/vault-export-core"; } from "@bitwarden/vault-export-core";
import { DefaultLoginApprovalDialogComponentService } from "../auth/login-approval/default-login-approval-dialog-component.service";
import { LoginApprovalDialogComponentServiceAbstraction } from "../auth/login-approval/login-approval-dialog-component.service.abstraction";
import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation"; import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction"; import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
@@ -1494,8 +1494,8 @@ const safeProviders: SafeProvider[] = [
deps: [CryptoFunctionServiceAbstraction], deps: [CryptoFunctionServiceAbstraction],
}), }),
safeProvider({ safeProvider({
provide: LoginApprovalComponentServiceAbstraction, provide: LoginApprovalDialogComponentServiceAbstraction,
useClass: DefaultLoginApprovalComponentService, useClass: DefaultLoginApprovalDialogComponentService,
deps: [], deps: [],
}), }),
safeProvider({ safeProvider({

View File

@@ -57,10 +57,6 @@ export * from "./sso/default-sso-component.service";
// self hosted environment configuration dialog // self hosted environment configuration dialog
export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.component"; export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.component";
// login approval
export * from "./login-approval/login-approval.component";
export * from "./login-approval/default-login-approval-component.service";
// two factor auth // two factor auth
export * from "./two-factor-auth"; export * from "./two-factor-auth";

View File

@@ -1,25 +0,0 @@
import { TestBed } from "@angular/core/testing";
import { DefaultLoginApprovalComponentService } from "./default-login-approval-component.service";
import { LoginApprovalComponent } from "./login-approval.component";
describe("DefaultLoginApprovalComponentService", () => {
let service: DefaultLoginApprovalComponentService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [DefaultLoginApprovalComponentService],
});
service = TestBed.inject(DefaultLoginApprovalComponentService);
});
it("is created successfully", () => {
expect(service).toBeTruthy();
});
it("has showLoginRequestedAlertIfWindowNotVisible method that is a no-op", async () => {
const loginApprovalComponent = {} as LoginApprovalComponent;
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email);
});
});

View File

@@ -1,16 +0,0 @@
import { LoginApprovalComponentServiceAbstraction } from "../../common/abstractions/login-approval-component.service.abstraction";
/**
* Default implementation of the LoginApprovalComponentServiceAbstraction.
*/
export class DefaultLoginApprovalComponentService
implements LoginApprovalComponentServiceAbstraction
{
/**
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
* @returns
*/
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
return;
}
}

View File

@@ -3,6 +3,5 @@ export * from "./login-email.service";
export * from "./login-strategy.service"; export * from "./login-strategy.service";
export * from "./user-decryption-options.service.abstraction"; export * from "./user-decryption-options.service.abstraction";
export * from "./auth-request.service.abstraction"; export * from "./auth-request.service.abstraction";
export * from "./login-approval-component.service.abstraction";
export * from "./login-success-handler.service"; export * from "./login-success-handler.service";
export * from "./logout.service"; export * from "./logout.service";