mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 10:33:31 +00:00
Update Desktop to use the AuthRequestAnsweringService, bringing it into feature parity with the Extension.
216 lines
7.0 KiB
TypeScript
216 lines
7.0 KiB
TypeScript
import { CommonModule } from "@angular/common";
|
|
import { Component, OnInit, OnDestroy, Inject } from "@angular/core";
|
|
import { firstValueFrom, map } from "rxjs";
|
|
|
|
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 { 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 {
|
|
DIALOG_DATA,
|
|
DialogRef,
|
|
AsyncActionsModule,
|
|
ButtonModule,
|
|
DialogModule,
|
|
DialogService,
|
|
ToastService,
|
|
} from "@bitwarden/components";
|
|
import { LogService } from "@bitwarden/logging";
|
|
|
|
const RequestTimeOut = 60000 * 15; // 15 Minutes
|
|
const RequestTimeUpdate = 60000 * 5; // 5 Minutes
|
|
|
|
export interface LoginApprovalDialogParams {
|
|
notificationId: string;
|
|
}
|
|
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
|
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
|
@Component({
|
|
templateUrl: "login-approval-dialog.component.html",
|
|
imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, JslibModule],
|
|
})
|
|
export class LoginApprovalDialogComponent implements OnInit, OnDestroy {
|
|
authRequestId: string;
|
|
authRequestResponse?: AuthRequestResponse;
|
|
email?: string;
|
|
fingerprintPhrase?: string;
|
|
interval?: NodeJS.Timeout;
|
|
loading = true;
|
|
readableDeviceTypeName?: string;
|
|
requestTimeText?: string;
|
|
|
|
constructor(
|
|
@Inject(DIALOG_DATA) private params: LoginApprovalDialogParams,
|
|
private accountService: AccountService,
|
|
private apiService: ApiService,
|
|
private authRequestService: AuthRequestServiceAbstraction,
|
|
private devicesService: DevicesServiceAbstraction,
|
|
private dialogRef: DialogRef,
|
|
private i18nService: I18nService,
|
|
private logService: LogService,
|
|
private toastService: ToastService,
|
|
private validationService: ValidationService,
|
|
) {
|
|
this.authRequestId = params.notificationId;
|
|
}
|
|
|
|
async ngOnDestroy(): Promise<void> {
|
|
clearInterval(this.interval);
|
|
}
|
|
|
|
async ngOnInit() {
|
|
if (this.authRequestId == null) {
|
|
this.logService.error("LoginApprovalDialogComponent: authRequestId is null");
|
|
return;
|
|
}
|
|
|
|
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);
|
|
|
|
this.loading = false;
|
|
}
|
|
|
|
/**
|
|
* Strongly-typed helper to open a LoginApprovalDialog
|
|
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
|
* @param data Configuration for the dialog
|
|
*/
|
|
static open(dialogService: DialogService, data: LoginApprovalDialogParams) {
|
|
return dialogService.open(LoginApprovalDialogComponent, { data });
|
|
}
|
|
|
|
denyLogin = async () => {
|
|
await this.retrieveAuthRequestAndRespond(false);
|
|
};
|
|
|
|
approveLogin = async () => {
|
|
await this.retrieveAuthRequestAndRespond(true);
|
|
};
|
|
|
|
private async retrieveAuthRequestAndRespond(approve: boolean) {
|
|
this.authRequestResponse = await this.apiService.getAuthRequest(this.authRequestId);
|
|
if (this.authRequestResponse.requestApproved || this.authRequestResponse.responseDate != null) {
|
|
this.toastService.showToast({
|
|
variant: "info",
|
|
message: this.i18nService.t("thisRequestIsNoLongerValid"),
|
|
});
|
|
} else {
|
|
const loginResponse = await this.authRequestService.approveOrDenyAuthRequest(
|
|
approve,
|
|
this.authRequestResponse,
|
|
);
|
|
this.showResultToast(loginResponse);
|
|
}
|
|
|
|
this.dialogRef.close(approve);
|
|
}
|
|
|
|
showResultToast(loginResponse: AuthRequestResponse) {
|
|
if (loginResponse.requestApproved) {
|
|
this.toastService.showToast({
|
|
variant: "success",
|
|
message: this.i18nService.t(
|
|
"loginRequestApprovedForEmailOnDevice",
|
|
this.email,
|
|
this.devicesService.getReadableDeviceTypeName(loginResponse.requestDeviceTypeValue),
|
|
),
|
|
});
|
|
} else {
|
|
this.toastService.showToast({
|
|
variant: "info",
|
|
message: this.i18nService.t("youDeniedLoginAttemptFromAnotherDevice"),
|
|
});
|
|
}
|
|
}
|
|
|
|
updateTimeText() {
|
|
if (this.authRequestResponse == null) {
|
|
this.logService.error("LoginApprovalDialogComponent: authRequestResponse not found");
|
|
return;
|
|
}
|
|
|
|
const requestDate = new Date(this.authRequestResponse.creationDate);
|
|
const requestDateUTC = Date.UTC(
|
|
requestDate.getUTCFullYear(),
|
|
requestDate.getUTCMonth(),
|
|
requestDate.getDate(),
|
|
requestDate.getUTCHours(),
|
|
requestDate.getUTCMinutes(),
|
|
requestDate.getUTCSeconds(),
|
|
requestDate.getUTCMilliseconds(),
|
|
);
|
|
|
|
const dateNow = new Date(Date.now());
|
|
const dateNowUTC = Date.UTC(
|
|
dateNow.getUTCFullYear(),
|
|
dateNow.getUTCMonth(),
|
|
dateNow.getDate(),
|
|
dateNow.getUTCHours(),
|
|
dateNow.getUTCMinutes(),
|
|
dateNow.getUTCSeconds(),
|
|
dateNow.getUTCMilliseconds(),
|
|
);
|
|
|
|
const diffInMinutes = dateNowUTC - requestDateUTC;
|
|
|
|
if (diffInMinutes <= RequestTimeUpdate) {
|
|
this.requestTimeText = this.i18nService.t("justNow");
|
|
} else if (diffInMinutes < RequestTimeOut) {
|
|
this.requestTimeText = this.i18nService.t(
|
|
"requestedXMinutesAgo",
|
|
(diffInMinutes / 60000).toFixed(),
|
|
);
|
|
} else {
|
|
clearInterval(this.interval);
|
|
this.dialogRef.close();
|
|
this.toastService.showToast({
|
|
variant: "info",
|
|
message: this.i18nService.t("loginRequestHasAlreadyExpired"),
|
|
});
|
|
}
|
|
}
|
|
}
|