mirror of
https://github.com/bitwarden/browser
synced 2026-02-19 02:44:01 +00:00
232 lines
8.2 KiB
TypeScript
232 lines
8.2 KiB
TypeScript
import { ChangeDetectionStrategy, Component, input, OnInit, output, signal } from "@angular/core";
|
|
import { FormBuilder } from "@angular/forms";
|
|
import { firstValueFrom } from "rxjs";
|
|
|
|
import {
|
|
emailAndOtpRequired,
|
|
emailRequired,
|
|
otpInvalid,
|
|
passwordHashB64Invalid,
|
|
passwordHashB64Required,
|
|
SendAccessDomainCredentials,
|
|
SendAccessToken,
|
|
SendHashedPasswordB64,
|
|
sendIdInvalid,
|
|
SendOtp,
|
|
SendTokenService,
|
|
} from "@bitwarden/common/auth/send-access";
|
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
|
|
import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response";
|
|
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf";
|
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
|
import { AuthType } from "@bitwarden/common/tools/send/types/auth-type";
|
|
import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components";
|
|
|
|
import { SharedModule } from "../../../shared";
|
|
|
|
import { SendAccessEmailComponent } from "./send-access-email.component";
|
|
import { SendAccessPasswordComponent } from "./send-access-password.component";
|
|
|
|
@Component({
|
|
selector: "app-send-auth",
|
|
templateUrl: "send-auth.component.html",
|
|
imports: [SendAccessPasswordComponent, SendAccessEmailComponent, SharedModule],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class SendAuthComponent implements OnInit {
|
|
protected readonly id = input.required<string>();
|
|
protected readonly key = input.required<string>();
|
|
|
|
protected accessGranted = output<{
|
|
response?: SendAccessResponse;
|
|
request?: SendAccessRequest;
|
|
accessToken?: SendAccessToken;
|
|
}>();
|
|
|
|
authType = AuthType;
|
|
|
|
private expiredAuthAttempts = 0;
|
|
|
|
readonly loading = signal<boolean>(false);
|
|
readonly error = signal<boolean>(false);
|
|
readonly unavailable = signal<boolean>(false);
|
|
readonly sendAuthType = signal<AuthType>(AuthType.None);
|
|
readonly enterOtp = signal<boolean>(false);
|
|
|
|
sendAccessForm = this.formBuilder.group<{ password?: string; email?: string; otp?: string }>({});
|
|
|
|
constructor(
|
|
private cryptoFunctionService: CryptoFunctionService,
|
|
private sendApiService: SendApiService,
|
|
private toastService: ToastService,
|
|
private i18nService: I18nService,
|
|
private formBuilder: FormBuilder,
|
|
private configService: ConfigService,
|
|
private sendTokenService: SendTokenService,
|
|
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
|
) {}
|
|
|
|
ngOnInit() {
|
|
this.updatePageTitle();
|
|
void this.onSubmit();
|
|
}
|
|
|
|
async onSubmit() {
|
|
this.loading.set(true);
|
|
this.unavailable.set(false);
|
|
this.error.set(false);
|
|
const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP);
|
|
if (sendEmailOtp) {
|
|
await this.attemptV2Access();
|
|
} else {
|
|
await this.attemptV1Access();
|
|
}
|
|
this.loading.set(false);
|
|
}
|
|
|
|
private async attemptV1Access() {
|
|
try {
|
|
const accessRequest = new SendAccessRequest();
|
|
if (this.sendAuthType() === AuthType.Password) {
|
|
const password = this.sendAccessForm.value.password;
|
|
if (password == null) {
|
|
return;
|
|
}
|
|
accessRequest.password = await this.getPasswordHashB64(password, this.key());
|
|
}
|
|
const sendResponse = await this.sendApiService.postSendAccess(this.id(), accessRequest);
|
|
this.accessGranted.emit({ request: accessRequest, response: sendResponse });
|
|
} catch (e) {
|
|
if (e instanceof ErrorResponse) {
|
|
if (e.statusCode === 401) {
|
|
this.sendAuthType.set(AuthType.Password);
|
|
} else if (e.statusCode === 404) {
|
|
this.unavailable.set(true);
|
|
} else {
|
|
this.error.set(true);
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: e.message,
|
|
});
|
|
}
|
|
} else {
|
|
this.error.set(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async attemptV2Access(): Promise<void> {
|
|
let sendAccessCreds: SendAccessDomainCredentials | null = null;
|
|
if (this.sendAuthType() === AuthType.Email) {
|
|
const email = this.sendAccessForm.value.email;
|
|
if (email == null) {
|
|
return;
|
|
}
|
|
if (!this.enterOtp()) {
|
|
sendAccessCreds = { kind: "email", email };
|
|
} else {
|
|
const otp = this.sendAccessForm.value.otp as SendOtp;
|
|
if (otp == null) {
|
|
return;
|
|
}
|
|
sendAccessCreds = { kind: "email_otp", email, otp };
|
|
}
|
|
} else if (this.sendAuthType() === AuthType.Password) {
|
|
const password = this.sendAccessForm.value.password;
|
|
if (password == null) {
|
|
return;
|
|
}
|
|
const passwordHashB64 = await this.getPasswordHashB64(password, this.key());
|
|
sendAccessCreds = { kind: "password", passwordHashB64 };
|
|
}
|
|
const response = !sendAccessCreds
|
|
? await firstValueFrom(this.sendTokenService.tryGetSendAccessToken$(this.id()))
|
|
: await firstValueFrom(this.sendTokenService.getSendAccessToken$(this.id(), sendAccessCreds));
|
|
if (response instanceof SendAccessToken) {
|
|
this.expiredAuthAttempts = 0;
|
|
this.accessGranted.emit({ accessToken: response });
|
|
} else if (response.kind === "expired") {
|
|
if (this.expiredAuthAttempts > 2) {
|
|
return;
|
|
}
|
|
this.expiredAuthAttempts++;
|
|
await this.attemptV2Access();
|
|
} else if (response.kind === "expected_server") {
|
|
this.expiredAuthAttempts = 0;
|
|
if (emailRequired(response.error)) {
|
|
this.sendAuthType.set(AuthType.Email);
|
|
this.updatePageTitle();
|
|
} else if (emailAndOtpRequired(response.error)) {
|
|
this.enterOtp.set(true);
|
|
this.updatePageTitle();
|
|
} else if (otpInvalid(response.error)) {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: this.i18nService.t("invalidVerificationCode"),
|
|
});
|
|
} else if (passwordHashB64Required(response.error)) {
|
|
this.sendAuthType.set(AuthType.Password);
|
|
this.updatePageTitle();
|
|
} else if (passwordHashB64Invalid(response.error)) {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: this.i18nService.t("invalidSendPassword"),
|
|
});
|
|
} else if (sendIdInvalid(response.error)) {
|
|
this.unavailable.set(true);
|
|
} else {
|
|
this.error.set(true);
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: response.error.error_description ?? "",
|
|
});
|
|
}
|
|
} else {
|
|
this.expiredAuthAttempts = 0;
|
|
this.error.set(true);
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: response.error,
|
|
});
|
|
}
|
|
}
|
|
|
|
private async getPasswordHashB64(password: string, key: string) {
|
|
const keyArray = Utils.fromUrlB64ToArray(key);
|
|
const passwordHash = await this.cryptoFunctionService.pbkdf2(
|
|
password,
|
|
keyArray,
|
|
"sha256",
|
|
SEND_KDF_ITERATIONS,
|
|
);
|
|
return Utils.fromBufferToB64(passwordHash) as SendHashedPasswordB64;
|
|
}
|
|
|
|
private updatePageTitle(): void {
|
|
const authType = this.sendAuthType();
|
|
|
|
if (authType === AuthType.Email) {
|
|
if (this.enterOtp()) {
|
|
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
|
pageTitle: { key: "enterTheCodeSentToYourEmail" },
|
|
});
|
|
} else {
|
|
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
|
pageTitle: { key: "verifyYourEmailToViewThisSend" },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|