diff --git a/apps/web/src/app/tools/send/send-access/access.component.html b/apps/web/src/app/tools/send/send-access/access.component.html index b86933410b8..6cda4cf4d7d 100644 --- a/apps/web/src/app/tools/send/send-access/access.component.html +++ b/apps/web/src/app/tools/send/send-access/access.component.html @@ -1,4 +1,4 @@ -@switch (viewState) { +@switch (viewState()) { @case ("auth") { } @@ -6,6 +6,7 @@ (SendViewState.Auth); id: string; key: string; + sendAccessToken: SendAccessToken | null = null; sendAccessResponse: SendAccessResponse | null = null; sendAccessRequest: SendAccessRequest = new SendAccessRequest(); - constructor(private route: ActivatedRoute) {} + constructor( + private route: ActivatedRoute, + private destroyRef: DestroyRef, + ) {} - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.params.subscribe(async (params) => { + ngOnInit() { + this.route.params.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => { this.id = params.sendId; this.key = params.key; - - if (this.id && this.key) { - this.viewState = SendViewState.View; - this.sendAccessResponse = null; - this.sendAccessRequest = new SendAccessRequest(); - } }); } onAuthRequired() { - this.viewState = SendViewState.Auth; + this.viewState.set(SendViewState.Auth); } - onAccessGranted(event: { response: SendAccessResponse; request: SendAccessRequest }) { + onAccessGranted(event: { + response?: SendAccessResponse; + request?: SendAccessRequest; + accessToken?: SendAccessToken; + }) { this.sendAccessResponse = event.response; this.sendAccessRequest = event.request; - this.viewState = SendViewState.View; + this.sendAccessToken = event.accessToken; + this.viewState.set(SendViewState.View); } } diff --git a/apps/web/src/app/tools/send/send-access/send-access-email.component.html b/apps/web/src/app/tools/send/send-access/send-access-email.component.html new file mode 100644 index 00000000000..ee5a03670bb --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-access-email.component.html @@ -0,0 +1,35 @@ +@if (!enterOtp()) { + + {{ "email" | i18n }} + + +
+ +
+} @else { + + {{ "verificationCode" | i18n }} + + +
+ +
+} diff --git a/apps/web/src/app/tools/send/send-access/send-access-email.component.ts b/apps/web/src/app/tools/send/send-access/send-access-email.component.ts new file mode 100644 index 00000000000..b1374cd6c66 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-access-email.component.ts @@ -0,0 +1,35 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; + +import { SharedModule } from "../../../shared"; + +@Component({ + selector: "app-send-access-email", + templateUrl: "send-access-email.component.html", + imports: [SharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SendAccessEmailComponent implements OnInit, OnDestroy { + protected readonly formGroup = input.required(); + protected readonly enterOtp = input.required(); + protected email: FormControl; + protected otp: FormControl; + + readonly loading = input.required(); + + constructor() {} + + ngOnInit() { + this.email = new FormControl("", Validators.required); + this.otp = new FormControl("", Validators.required); + this.formGroup().addControl("email", this.email); + this.formGroup().addControl("otp", this.otp); + } + + ngOnDestroy() { + this.formGroup().removeControl("email"); + this.formGroup().removeControl("otp"); + } +} diff --git a/apps/web/src/app/tools/send/send-access/send-access-file.component.html b/apps/web/src/app/tools/send/send-access/send-access-file.component.html index 8cbe6a975ef..4088b3a7034 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-file.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-file.component.html @@ -1,5 +1,5 @@ -

{{ send.file.fileName }}

+

{{ send().file.fileName }}

diff --git a/apps/web/src/app/tools/send/send-access/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access/send-access-file.component.ts index dc7689f011a..bb45e83d110 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-file.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-file.component.ts @@ -1,8 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, input } from "@angular/core"; +import { SendAccessToken } from "@bitwarden/common/auth/send-access"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -15,40 +18,39 @@ import { ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-send-access-file", templateUrl: "send-access-file.component.html", imports: [SharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SendAccessFileComponent { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() send: SendAccessView; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() decKey: SymmetricCryptoKey; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() accessRequest: SendAccessRequest; + readonly send = input(null); + readonly decKey = input(null); + readonly accessRequest = input(null); + readonly accessToken = input(null); + constructor( private i18nService: I18nService, private toastService: ToastService, private encryptService: EncryptService, private fileDownloadService: FileDownloadService, private sendApiService: SendApiService, + private configService: ConfigService, ) {} protected download = async () => { - if (this.send == null || this.decKey == null) { + const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP); + const accessToken = this.accessToken(); + const accessRequest = this.accessRequest(); + const authMissing = (sendEmailOtp && !accessToken) || (!sendEmailOtp && !accessRequest); + if (this.send() == null || this.decKey() == null || authMissing) { return; } - const downloadData = await this.sendApiService.getSendFileDownloadData( - this.send, - this.accessRequest, - ); + const downloadData = sendEmailOtp + ? await this.sendApiService.getSendFileDownloadDataV2(this.send(), accessToken) + : await this.sendApiService.getSendFileDownloadData(this.send(), accessRequest); if (Utils.isNullOrWhitespace(downloadData.url)) { this.toastService.showToast({ @@ -71,9 +73,9 @@ export class SendAccessFileComponent { try { const encBuf = await EncArrayBuffer.fromResponse(response); - const decBuf = await this.encryptService.decryptFileData(encBuf, this.decKey); + const decBuf = await this.encryptService.decryptFileData(encBuf, this.decKey()); this.fileDownloadService.download({ - fileName: this.send.file.fileName, + fileName: this.send().file.fileName, blobData: decBuf, downloadMethod: "save", }); diff --git a/apps/web/src/app/tools/send/send-access/send-access-password.component.html b/apps/web/src/app/tools/send/send-access/send-access-password.component.html index 8bb2c306010..deca7ad3d24 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-password.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-password.component.html @@ -1,28 +1,19 @@

{{ "sendProtectedPassword" | i18n }}

{{ "sendProtectedPasswordDontKnow" | i18n }}

-
- - {{ "password" | i18n }} - - - -
- -
+ + {{ "password" | i18n }} + + + +
+
diff --git a/apps/web/src/app/tools/send/send-access/send-access-password.component.ts b/apps/web/src/app/tools/send/send-access/send-access-password.component.ts index 34b183be10e..b2ee222ae86 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-password.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-password.component.ts @@ -1,43 +1,30 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; import { SharedModule } from "../../../shared"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-send-access-password", templateUrl: "send-access-password.component.html", imports: [SharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SendAccessPasswordComponent implements OnInit, OnDestroy { - private destroy$ = new Subject(); - protected formGroup = this.formBuilder.group({ - password: ["", [Validators.required]], - }); + protected readonly formGroup = input.required(); + protected password: FormControl; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() loading: boolean; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref - @Output() setPasswordEvent = new EventEmitter(); + readonly loading = input.required(); - constructor(private formBuilder: FormBuilder) {} + constructor() {} - async ngOnInit() { - this.formGroup.controls.password.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe((val) => { - this.setPasswordEvent.emit(val); - }); + ngOnInit() { + this.password = new FormControl("", Validators.required); + this.formGroup().addControl("password", this.password); } ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); + this.formGroup().removeControl("password"); } } diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.html b/apps/web/src/app/tools/send/send-access/send-auth.component.html index 21a6de50ba8..c3e90cea4ea 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.html +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.html @@ -1,14 +1,38 @@ -
-
-

{{ "sendAccessUnavailable" | i18n }}

+@if (loading()) { +
+ + {{ "loading" | i18n }}
-
-

{{ "unexpectedErrorSend" | i18n }}

-
- - +} + + @if (error()) { +
+

{{ "unexpectedErrorSend" | i18n }}

+
+ } + @if (unavailable()) { +
+

{{ "sendAccessUnavailable" | i18n }}

+
+ } @else { + @switch (sendAuthType()) { + @case (authType.Password) { + + } + @case (authType.Email) { + + } + } + } diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index b360044a8b6..13e82bd4cfa 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -1,86 +1,211 @@ -import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; +import { ChangeDetectionStrategy, Component, input, OnInit, output, signal } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; +import { + emailAndOtpRequiredEmailSent, + emailInvalid, + 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 { 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, SharedModule], + imports: [SendAccessPasswordComponent, SendAccessEmailComponent, SharedModule], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SendAuthComponent { - readonly id = input.required(); - readonly key = input.required(); +export class SendAuthComponent implements OnInit { + protected readonly id = input.required(); + protected readonly key = input.required(); - accessGranted = output<{ - response: SendAccessResponse; - request: SendAccessRequest; + protected accessGranted = output<{ + response?: SendAccessResponse; + request?: SendAccessRequest; + accessToken?: SendAccessToken; }>(); - loading = false; - error = false; - unavailable = false; - password?: string; + authType = AuthType; - private accessRequest!: SendAccessRequest; + private expiredAuthAttempts = 0; + + readonly loading = signal(false); + readonly error = signal(false); + readonly unavailable = signal(false); + readonly sendAuthType = signal(AuthType.None); + readonly enterOtp = signal(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, ) {} - async onSubmit(password: string) { - this.password = password; - this.loading = true; - this.error = false; - this.unavailable = false; + ngOnInit() { + 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 keyArray = Utils.fromUrlB64ToArray(this.key()); - this.accessRequest = new SendAccessRequest(); - - const passwordHash = await this.cryptoFunctionService.pbkdf2( - this.password, - keyArray, - "sha256", - SEND_KDF_ITERATIONS, - ); - this.accessRequest.password = Utils.fromBufferToB64(passwordHash); - - const sendResponse = await this.sendApiService.postSendAccess(this.id(), this.accessRequest); - this.accessGranted.emit({ response: sendResponse, request: this.accessRequest }); + 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 === 404) { - this.unavailable = true; - } else if (e.statusCode === 400) { + 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 = true; } } else { - this.error = true; + this.error.set(true); } - } finally { - this.loading = false; } } + + private async attemptV2Access(): Promise { + 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); + } else if (emailAndOtpRequiredEmailSent(response.error) || emailInvalid(response.error)) { + this.enterOtp.set(true); + } 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); + } 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; + } } diff --git a/apps/web/src/app/tools/send/send-access/send-view.component.html b/apps/web/src/app/tools/send/send-access/send-view.component.html index dd0b770b261..3536499ddad 100644 --- a/apps/web/src/app/tools/send/send-access/send-view.component.html +++ b/apps/web/src/app/tools/send/send-access/send-view.component.html @@ -1,41 +1,13 @@ - - {{ "viewSendHiddenEmailWarning" | i18n }} - {{ - "learnMore" | i18n - }}. - +@if (hideEmail()) { + + {{ "viewSendHiddenEmailWarning" | i18n }} + {{ + "learnMore" | i18n + }} + +} - -
-

{{ "sendAccessUnavailable" | i18n }}

-
-
-

{{ "unexpectedErrorSend" | i18n }}

-
-
-

- {{ send.name }} -

-
- - - - - - - - -

- Expires: {{ expirationDate | date: "medium" }} -

-
-
- +@if (loading()) {
{{ "loading" | i18n }}
-
+} @else { + @if (unavailable()) { +
+

{{ "sendAccessUnavailable" | i18n }}

+
+ } + @if (error()) { +
+

{{ "unexpectedErrorSend" | i18n }}

+
+ } + @if (send()) { +
+

+ {{ send().name }} +

+
+ @switch (send().type) { + @case (sendType.Text) { + + } + @case (sendType.File) { + + } + } + @if (expirationDate()) { +

Expires: {{ expirationDate() | date: "medium" }}

+ } +
+ } +} diff --git a/apps/web/src/app/tools/send/send-access/send-view.component.ts b/apps/web/src/app/tools/send/send-access/send-view.component.ts index 060dc1958b1..1ab9a121ace 100644 --- a/apps/web/src/app/tools/send/send-access/send-view.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-view.component.ts @@ -1,13 +1,17 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, + computed, input, OnInit, output, + signal, } from "@angular/core"; +import { SendAccessToken } from "@bitwarden/common/auth/send-access"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; 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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -34,17 +38,25 @@ import { SendAccessTextComponent } from "./send-access-text.component"; export class SendViewComponent implements OnInit { readonly id = input.required(); readonly key = input.required(); + readonly accessToken = input(null); readonly sendResponse = input(null); readonly accessRequest = input(new SendAccessRequest()); authRequired = output(); - send: SendAccessView | null = null; + readonly send = signal(null); + readonly expirationDate = computed(() => this.send()?.expirationDate ?? null); + readonly creatorIdentifier = computed( + () => this.send()?.creatorIdentifier ?? null, + ); + readonly hideEmail = computed( + () => this.send() != null && this.creatorIdentifier() == null, + ); + readonly loading = signal(false); + readonly unavailable = signal(false); + readonly error = signal(false); + sendType = SendType; - loading = true; - unavailable = false; - error = false; - hideEmail = false; decKey!: SymmetricCryptoKey; constructor( @@ -53,50 +65,48 @@ export class SendViewComponent implements OnInit { private toastService: ToastService, private i18nService: I18nService, private layoutWrapperDataService: AnonLayoutWrapperDataService, - private cdRef: ChangeDetectorRef, + private configService: ConfigService, ) {} - get expirationDate() { - if (this.send == null || this.send.expirationDate == null) { - return null; - } - return this.send.expirationDate; - } - - get creatorIdentifier() { - if (this.send == null || this.send.creatorIdentifier == null) { - return null; - } - return this.send.creatorIdentifier; - } - - async ngOnInit() { - await this.load(); + ngOnInit() { + void this.load(); } private async load() { - this.unavailable = false; - this.error = false; - this.hideEmail = false; - this.loading = true; - - let response = this.sendResponse(); + this.loading.set(true); + this.unavailable.set(false); + this.error.set(false); try { - if (!response) { - response = await this.sendApiService.postSendAccess(this.id(), this.accessRequest()); + const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP); + let response: SendAccessResponse; + if (sendEmailOtp) { + const accessToken = this.accessToken(); + if (!accessToken) { + this.authRequired.emit(); + return; + } + response = await this.sendApiService.postSendAccessV2(accessToken); + } else { + const sendResponse = this.sendResponse(); + if (!sendResponse) { + this.authRequired.emit(); + return; + } + response = sendResponse; } - const keyArray = Utils.fromUrlB64ToArray(this.key()); const sendAccess = new SendAccess(response); this.decKey = await this.keyService.makeSendKey(keyArray); - this.send = await sendAccess.decrypt(this.decKey); + const decSend = await sendAccess.decrypt(this.decKey); + this.send.set(decSend); } catch (e) { + this.send.set(null); if (e instanceof ErrorResponse) { if (e.statusCode === 401) { this.authRequired.emit(); } else if (e.statusCode === 404) { - this.unavailable = true; + this.unavailable.set(true); } else if (e.statusCode === 400) { this.toastService.showToast({ variant: "error", @@ -104,28 +114,23 @@ export class SendViewComponent implements OnInit { message: e.message, }); } else { - this.error = true; + this.error.set(true); } } else { - this.error = true; + this.error.set(true); } + } finally { + this.loading.set(false); } - this.loading = false; - this.hideEmail = - this.creatorIdentifier == null && !this.loading && !this.unavailable && !response; - - this.hideEmail = this.send != null && this.creatorIdentifier == null; - - if (this.creatorIdentifier != null) { + const creatorIdentifier = this.creatorIdentifier(); + if (creatorIdentifier != null) { this.layoutWrapperDataService.setAnonLayoutWrapperData({ pageSubtitle: { key: "sendAccessCreatorIdentifier", - placeholders: [this.creatorIdentifier], + placeholders: [creatorIdentifier], }, }); } - - this.cdRef.markForCheck(); } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 932b58cf22a..a01e0b91e71 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12699,5 +12699,8 @@ }, "emailProtected": { "message": "Email protected" + }, + "invalidSendPassword": { + "message": "Invalid Send password" } } diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index 80c4410af11..a7e36d8c8b1 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -1,3 +1,5 @@ +import { SendAccessToken } from "@bitwarden/common/auth/send-access"; + import { ListResponse } from "../../../models/response/list.response"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { Send } from "../models/domain/send"; @@ -16,6 +18,10 @@ export abstract class SendApiService { request: SendAccessRequest, apiUrl?: string, ): Promise; + abstract postSendAccessV2( + accessToken: SendAccessToken, + apiUrl?: string, + ): Promise; abstract getSends(): Promise>; abstract postSend(request: SendRequest): Promise; abstract postFileTypeSend(request: SendRequest): Promise; @@ -28,6 +34,11 @@ export abstract class SendApiService { request: SendAccessRequest, apiUrl?: string, ): Promise; + abstract getSendFileDownloadDataV2( + send: SendAccessView, + accessToken: SendAccessToken, + apiUrl?: string, + ): Promise; abstract renewSendFileUploadUrl( sendId: string, fileId: string, diff --git a/libs/common/src/tools/send/services/send-api.service.ts b/libs/common/src/tools/send/services/send-api.service.ts index 1c931b7ad98..f09117316d8 100644 --- a/libs/common/src/tools/send/services/send-api.service.ts +++ b/libs/common/src/tools/send/services/send-api.service.ts @@ -1,3 +1,5 @@ +import { SendAccessToken } from "@bitwarden/common/auth/send-access"; + import { ApiService } from "../../../abstractions/api.service"; import { ErrorResponse } from "../../../models/response/error.response"; import { ListResponse } from "../../../models/response/list.response"; @@ -52,6 +54,25 @@ export class SendApiService implements SendApiServiceAbstraction { return new SendAccessResponse(r); } + async postSendAccessV2( + accessToken: SendAccessToken, + apiUrl?: string, + ): Promise { + const setAuthTokenHeader = (headers: Headers) => { + headers.set("Authorization", "Bearer " + accessToken.token); + }; + const r = await this.apiService.send( + "POST", + "/sends/access", + null, + false, + true, + apiUrl, + setAuthTokenHeader, + ); + return new SendAccessResponse(r); + } + async getSendFileDownloadData( send: SendAccessView, request: SendAccessRequest, @@ -72,6 +93,26 @@ export class SendApiService implements SendApiServiceAbstraction { return new SendFileDownloadDataResponse(r); } + async getSendFileDownloadDataV2( + send: SendAccessView, + accessToken: SendAccessToken, + apiUrl?: string, + ): Promise { + const setAuthTokenHeader = (headers: Headers) => { + headers.set("Authorization", "Bearer " + accessToken.token); + }; + const r = await this.apiService.send( + "POST", + "/sends/access/file/" + send.file.id, + null, + true, + true, + apiUrl, + setAuthTokenHeader, + ); + return new SendFileDownloadDataResponse(r); + } + async getSends(): Promise> { const r = await this.apiService.send("GET", "/sends", null, true, true); return new ListResponse(r, SendResponse);