mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-10 19:38:11 +01:00
[PM-2014] fix: move error handling to components
After discussing it with Jake we decided that following convention was best.
This commit is contained in:
parent
38adb9ee0a
commit
72f10bab65
@ -1,20 +1,12 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
|
||||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
|
||||||
import { Verification } from "@bitwarden/common/types/verification";
|
|
||||||
|
|
||||||
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
|
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
|
||||||
|
|
||||||
import { CredentialCreateOptionsResponse } from "./response/credential-create-options.response";
|
|
||||||
import { WebauthnApiService } from "./webauthn-api.service";
|
import { WebauthnApiService } from "./webauthn-api.service";
|
||||||
import { WebauthnService } from "./webauthn.service";
|
import { WebauthnService } from "./webauthn.service";
|
||||||
|
|
||||||
describe("WebauthnService", () => {
|
describe("WebauthnService", () => {
|
||||||
let apiService!: MockProxy<WebauthnApiService>;
|
let apiService!: MockProxy<WebauthnApiService>;
|
||||||
let platformUtilsService!: MockProxy<PlatformUtilsService>;
|
|
||||||
let i18nService!: MockProxy<I18nService>;
|
|
||||||
let credentials: MockProxy<CredentialsContainer>;
|
let credentials: MockProxy<CredentialsContainer>;
|
||||||
let webauthnService!: WebauthnService;
|
let webauthnService!: WebauthnService;
|
||||||
|
|
||||||
@ -23,39 +15,8 @@ describe("WebauthnService", () => {
|
|||||||
window.PublicKeyCredential = class {} as any;
|
window.PublicKeyCredential = class {} as any;
|
||||||
window.AuthenticatorAttestationResponse = class {} as any;
|
window.AuthenticatorAttestationResponse = class {} as any;
|
||||||
apiService = mock<WebauthnApiService>();
|
apiService = mock<WebauthnApiService>();
|
||||||
platformUtilsService = mock<PlatformUtilsService>();
|
|
||||||
i18nService = mock<I18nService>();
|
|
||||||
credentials = mock<CredentialsContainer>();
|
credentials = mock<CredentialsContainer>();
|
||||||
webauthnService = new WebauthnService(
|
webauthnService = new WebauthnService(apiService, credentials);
|
||||||
apiService,
|
|
||||||
platformUtilsService,
|
|
||||||
i18nService,
|
|
||||||
credentials
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getNewCredentialOptions", () => {
|
|
||||||
it("should return undefined and show toast when api service call throws", async () => {
|
|
||||||
apiService.getCredentialCreateOptions.mockRejectedValue(new Error("Mock error"));
|
|
||||||
const verification = createVerification();
|
|
||||||
|
|
||||||
const result = await webauthnService.getCredentialCreateOptions(verification);
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
expect(platformUtilsService.showToast).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return options when api service call is successfull", async () => {
|
|
||||||
const options = Symbol() as any;
|
|
||||||
const token = Symbol() as any;
|
|
||||||
const response = { options, token } as CredentialCreateOptionsResponse;
|
|
||||||
apiService.getCredentialCreateOptions.mockResolvedValue(response);
|
|
||||||
const verification = createVerification();
|
|
||||||
|
|
||||||
const result = await webauthnService.getCredentialCreateOptions(verification);
|
|
||||||
|
|
||||||
expect(result).toEqual({ options, token });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createCredential", () => {
|
describe("createCredential", () => {
|
||||||
@ -78,40 +39,8 @@ describe("WebauthnService", () => {
|
|||||||
expect(result).toBe(credential);
|
expect(result).toBe(credential);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("saveCredential", () => {
|
|
||||||
it("should return false and show toast when api service call throws", async () => {
|
|
||||||
apiService.saveCredential.mockRejectedValue(new Error("Mock error"));
|
|
||||||
const options = createCredentialCreateOptions();
|
|
||||||
const deviceResponse = Symbol() as any;
|
|
||||||
|
|
||||||
const result = await webauthnService.saveCredential(options, deviceResponse, "name");
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
expect(platformUtilsService.showToast).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return true when api service call is successfull", async () => {
|
|
||||||
apiService.saveCredential.mockResolvedValue(true);
|
|
||||||
const options = createCredentialCreateOptions();
|
|
||||||
const deviceResponse = createDeviceResponse();
|
|
||||||
|
|
||||||
const result = await webauthnService.saveCredential(options, deviceResponse, "name");
|
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(apiService.saveCredential).toHaveBeenCalled();
|
|
||||||
expect(platformUtilsService.showToast).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function createVerification(): Verification {
|
|
||||||
return {
|
|
||||||
type: VerificationType.MasterPassword,
|
|
||||||
secret: "secret",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCredentialCreateOptions(): CredentialCreateOptionsView {
|
function createCredentialCreateOptions(): CredentialCreateOptionsView {
|
||||||
return new CredentialCreateOptionsView(Symbol() as any, Symbol() as any);
|
return new CredentialCreateOptionsView(Symbol() as any, Symbol() as any);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { Injectable, Optional } from "@angular/core";
|
import { Injectable, Optional } from "@angular/core";
|
||||||
import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs";
|
import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
|
||||||
import { Verification } from "@bitwarden/common/types/verification";
|
import { Verification } from "@bitwarden/common/types/verification";
|
||||||
|
|
||||||
import { CoreAuthModule } from "../../core.module";
|
import { CoreAuthModule } from "../../core.module";
|
||||||
@ -31,8 +28,6 @@ export class WebauthnService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: WebauthnApiService,
|
private apiService: WebauthnApiService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
@Optional() navigatorCredentials?: CredentialsContainer,
|
@Optional() navigatorCredentials?: CredentialsContainer,
|
||||||
@Optional() private logService?: LogService
|
@Optional() private logService?: LogService
|
||||||
) {
|
) {
|
||||||
@ -43,22 +38,8 @@ export class WebauthnService {
|
|||||||
async getCredentialCreateOptions(
|
async getCredentialCreateOptions(
|
||||||
verification: Verification
|
verification: Verification
|
||||||
): Promise<CredentialCreateOptionsView | undefined> {
|
): Promise<CredentialCreateOptionsView | undefined> {
|
||||||
try {
|
const response = await this.apiService.getCredentialCreateOptions(verification);
|
||||||
const response = await this.apiService.getCredentialCreateOptions(verification);
|
return new CredentialCreateOptionsView(response.options, response.token);
|
||||||
return new CredentialCreateOptionsView(response.options, response.token);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ErrorResponse && error.statusCode === 400) {
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"error",
|
|
||||||
this.i18nService.t("error"),
|
|
||||||
this.i18nService.t("invalidMasterPassword")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.logService?.error(error);
|
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCredential(
|
async createCredential(
|
||||||
@ -85,24 +66,12 @@ export class WebauthnService {
|
|||||||
deviceResponse: PublicKeyCredential,
|
deviceResponse: PublicKeyCredential,
|
||||||
name: string
|
name: string
|
||||||
) {
|
) {
|
||||||
try {
|
const request = new SaveCredentialRequest();
|
||||||
const request = new SaveCredentialRequest();
|
request.deviceResponse = new WebauthnAttestationResponseRequest(deviceResponse);
|
||||||
request.deviceResponse = new WebauthnAttestationResponseRequest(deviceResponse);
|
request.token = credentialOptions.token;
|
||||||
request.token = credentialOptions.token;
|
request.name = name;
|
||||||
request.name = name;
|
await this.apiService.saveCredential(request);
|
||||||
await this.apiService.saveCredential(request);
|
this.refresh();
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("passkeySaved", name)
|
|
||||||
);
|
|
||||||
this.refresh();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
this.logService?.error(error);
|
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredential$(credentialId: string): Observable<WebauthnCredentialView> {
|
getCredential$(credentialId: string): Observable<WebauthnCredentialView> {
|
||||||
@ -114,25 +83,9 @@ export class WebauthnService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCredential(credentialId: string, verification: Verification): Promise<boolean> {
|
async deleteCredential(credentialId: string, verification: Verification): Promise<void> {
|
||||||
try {
|
await this.apiService.deleteCredential(credentialId, verification);
|
||||||
await this.apiService.deleteCredential(credentialId, verification);
|
this.refresh();
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("passkeyRemoved"));
|
|
||||||
this.refresh();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ErrorResponse && error.statusCode === 400) {
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"error",
|
|
||||||
this.i18nService.t("error"),
|
|
||||||
this.i18nService.t("invalidMasterPassword")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.logService?.error(error);
|
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCredentials$(): Observable<WebauthnCredentialView[]> {
|
private getCredentials$(): Observable<WebauthnCredentialView[]> {
|
||||||
|
@ -4,7 +4,11 @@ import { FormBuilder, Validators } from "@angular/forms";
|
|||||||
import { map, Observable } from "rxjs";
|
import { map, Observable } from "rxjs";
|
||||||
|
|
||||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||||
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
|
||||||
import { WebauthnService } from "../../../core";
|
import { WebauthnService } from "../../../core";
|
||||||
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
|
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
|
||||||
@ -46,7 +50,10 @@ export class CreateCredentialDialogComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private dialogRef: DialogRef,
|
private dialogRef: DialogRef,
|
||||||
private webauthnService: WebauthnService
|
private webauthnService: WebauthnService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private logService: LogService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -80,12 +87,22 @@ export class CreateCredentialDialogComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.credentialOptions = await this.webauthnService.getCredentialCreateOptions({
|
try {
|
||||||
type: VerificationType.MasterPassword,
|
this.credentialOptions = await this.webauthnService.getCredentialCreateOptions({
|
||||||
secret: this.formGroup.value.userVerification.masterPassword,
|
type: VerificationType.MasterPassword,
|
||||||
});
|
secret: this.formGroup.value.userVerification.masterPassword,
|
||||||
|
});
|
||||||
if (this.credentialOptions === undefined) {
|
} catch (error) {
|
||||||
|
if (error instanceof ErrorResponse && error.statusCode === 400) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("error"),
|
||||||
|
this.i18nService.t("invalidMasterPassword")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.logService?.error(error);
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,16 +131,21 @@ export class CreateCredentialDialogComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.webauthnService.saveCredential(
|
const name = this.formGroup.value.credentialNaming.name;
|
||||||
this.credentialOptions,
|
try {
|
||||||
this.deviceResponse,
|
await this.webauthnService.saveCredential(
|
||||||
this.formGroup.value.credentialNaming.name
|
this.credentialOptions,
|
||||||
);
|
this.deviceResponse,
|
||||||
|
this.formGroup.value.credentialNaming.name
|
||||||
if (!result) {
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logService?.error(error);
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("passkeySaved", name));
|
||||||
|
|
||||||
this.dialogRef.close(CreateCredentialDialogResult.Success);
|
this.dialogRef.close(CreateCredentialDialogResult.Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,11 @@ import { FormBuilder, Validators } from "@angular/forms";
|
|||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||||
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
|
||||||
import { WebauthnService } from "../../../core";
|
import { WebauthnService } from "../../../core";
|
||||||
import { WebauthnCredentialView } from "../../../core/views/webauth-credential.view";
|
import { WebauthnCredentialView } from "../../../core/views/webauth-credential.view";
|
||||||
@ -28,7 +32,10 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy {
|
|||||||
@Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams,
|
@Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private dialogRef: DialogRef,
|
private dialogRef: DialogRef,
|
||||||
private webauthnService: WebauthnService
|
private webauthnService: WebauthnService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private logService: LogService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -44,14 +51,28 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.dialogRef.disableClose = true;
|
this.dialogRef.disableClose = true;
|
||||||
const success = await this.webauthnService.deleteCredential(this.credential.id, {
|
try {
|
||||||
type: VerificationType.MasterPassword,
|
await this.webauthnService.deleteCredential(this.credential.id, {
|
||||||
secret: this.formGroup.value.masterPassword,
|
type: VerificationType.MasterPassword,
|
||||||
});
|
secret: this.formGroup.value.masterPassword,
|
||||||
if (!success) {
|
});
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("passkeyRemoved"));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ErrorResponse && error.statusCode === 400) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("error"),
|
||||||
|
this.i18nService.t("invalidMasterPassword")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.logService.error(error);
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
this.dialogRef.disableClose = false;
|
this.dialogRef.disableClose = false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user