From 3952af058c9107ccceef9a787ddabb495e7b6675 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:06:59 +0000 Subject: [PATCH] [PM-2806] Migrate send access to Component Library (#6139) * Remove unneeded ApiService * Extract SendAccess for sends of type text * Migrate form and card-body * Migrate callout * Extract SendAccess for sends of type file * Converted SendAccess component to standalone * Migrated bottom message to CL * Added Send Access Password Component * Added No item component, password component and changed bootstrap classes * Updated send texts and added layout for unexpected error * Changed SendAccessTextComponent to standalone * Moved AccessComponent to oss.module.ts and removed unnecessary components from app.module * Properly set access modifiers * Using async action on download button * Updated links * Using tailwind classes * Using ng-template and ng-container * Added validation to check if status code is from a wrong password * Using Component Library Forms * using subscriber to update password on send access * Using reactive forms to show the text on send access * Updated message.json keys for changed values * Removed unnecessary components and changed classes to tailwind ones * added margin bottom on send-access-password to keep consistent with other send-access layouts * removed duplicated message key * Added error toast message on wrong password --------- Co-authored-by: Daniel James Smith --- apps/web/src/app/oss.module.ts | 3 + .../src/app/shared/loose-components.module.ts | 3 - .../src/app/tools/send/access.component.html | 190 ++++++------------ .../src/app/tools/send/access.component.ts | 159 ++++++--------- .../app/tools/send/icons/expired-send.icon.ts | 11 + .../send/send-access-file.component.html | 5 + .../tools/send/send-access-file.component.ts | 67 ++++++ .../send/send-access-password.component.html | 28 +++ .../send/send-access-password.component.ts | 36 ++++ .../send/send-access-text.component.html | 26 +++ .../tools/send/send-access-text.component.ts | 59 ++++++ apps/web/src/locales/en/messages.json | 11 +- 12 files changed, 365 insertions(+), 233 deletions(-) create mode 100644 apps/web/src/app/tools/send/icons/expired-send.icon.ts create mode 100644 apps/web/src/app/tools/send/send-access-file.component.html create mode 100644 apps/web/src/app/tools/send/send-access-file.component.ts create mode 100644 apps/web/src/app/tools/send/send-access-password.component.html create mode 100644 apps/web/src/app/tools/send/send-access-password.component.ts create mode 100644 apps/web/src/app/tools/send/send-access-text.component.html create mode 100644 apps/web/src/app/tools/send/send-access-text.component.ts diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index a68b681dca3..73c03fd5dc8 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -5,6 +5,7 @@ import { AuthModule } from "./auth"; import { LoginModule } from "./auth/login/login.module"; import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module"; import { LooseComponentsModule, SharedModule } from "./shared"; +import { AccessComponent } from "./tools/send/access.component"; import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module"; import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-filter.module"; @@ -18,6 +19,7 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f OrganizationUserModule, LoginModule, AuthModule, + AccessComponent, ], exports: [ SharedModule, @@ -26,6 +28,7 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f VaultFilterModule, OrganizationBadgeModule, LoginModule, + AccessComponent, ], bootstrap: [], }) diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 091f8ec68b0..767d9cadc35 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -73,7 +73,6 @@ import { SettingsComponent } from "../settings/settings.component"; import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component"; import { GeneratorComponent } from "../tools/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/password-generator-history.component"; -import { AccessComponent } from "../tools/send/access.component"; import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component"; import { ToolsComponent } from "../tools/tools.component"; import { PremiumBadgeComponent } from "../vault/components/premium-badge.component"; @@ -108,7 +107,6 @@ import { SharedModule } from "./shared.module"; declarations: [ AcceptFamilySponsorshipComponent, AcceptOrganizationComponent, - AccessComponent, AccountComponent, AddEditComponent, AddEditCustomFieldsComponent, @@ -191,7 +189,6 @@ import { SharedModule } from "./shared.module"; UserVerificationModule, PremiumBadgeComponent, AcceptOrganizationComponent, - AccessComponent, AccountComponent, AddEditComponent, AddEditCustomFieldsComponent, diff --git a/apps/web/src/app/tools/send/access.component.html b/apps/web/src/app/tools/send/access.component.html index 634d0bbf40b..7b891f4f6f4 100644 --- a/apps/web/src/app/tools/send/access.component.html +++ b/apps/web/src/app/tools/send/access.component.html @@ -1,150 +1,84 @@ -
-
-
-

Bitwarden Send

+ +
+ +
+

View Send

-
-

{{ "sendCreatorIdentifier" | i18n : creatorIdentifier }}

+
+

{{ "sendAccessCreatorIdentifier" | i18n : creatorIdentifier }}

-
- - {{ "viewSendHiddenEmailWarning" | i18n }} - {{ - "learnMore" | i18n - }}. - -
-
-
-
-
-
- - {{ "loading" | i18n }} -
-
-

{{ "sendProtectedPassword" | i18n }}

-

{{ "sendProtectedPasswordDontKnow" | i18n }}

-
- - -
-
- -
-
-
- {{ "sendAccessUnavailable" | i18n }} -
-
- {{ "unexpectedError" | i18n }} -
-
-

+ + {{ "viewSendHiddenEmailWarning" | i18n }} + {{ "learnMore" | i18n }}. + +

+ + + + {{ "sendAccessUnavailable" | i18n }} + + + {{ "unexpectedErrorSend" | i18n }} + +
+

{{ send.name }}


- {{ - "sendHiddenByDefault" | i18n - }} -
- -
- - +
-

{{ send.file.fileName }}

- - +
-

+

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

-
+ + +
+ + {{ "loading" | i18n }} +
+
-
-

- {{ "sendAccessTaglineProductDesc" | i18n }}
+

+

+ {{ "sendAccessTaglineProductDesc" | i18n }} {{ "sendAccessTaglineLearnMore" | i18n }} - Bitwarden Send {{ "sendAccessTaglineOr" | i18n }} - {{ - "sendAccessTaglineSignUp" | i18n - }} + {{ "sendAccessTaglineSignUp" | i18n }} {{ "sendAccessTaglineTryToday" | i18n }}

diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/access.component.ts index 180acfd69f7..d01bc9c52ff 100644 --- a/apps/web/src/app/tools/send/access.component.ts +++ b/apps/web/src/app/tools/send/access.component.ts @@ -1,16 +1,14 @@ import { Component, OnInit } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access"; @@ -18,56 +16,65 @@ import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/s import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { NoItemsModule } from "@bitwarden/components"; + +import { SharedModule } from "../../shared"; + +import { ExpiredSend } from "./icons/expired-send.icon"; +import { SendAccessFileComponent } from "./send-access-file.component"; +import { SendAccessPasswordComponent } from "./send-access-password.component"; +import { SendAccessTextComponent } from "./send-access-text.component"; @Component({ selector: "app-send-access", templateUrl: "access.component.html", + standalone: true, + imports: [ + SendAccessFileComponent, + SendAccessTextComponent, + SendAccessPasswordComponent, + SharedModule, + NoItemsModule, + ], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class AccessComponent implements OnInit { - send: SendAccessView; - sendType = SendType; - downloading = false; - loading = true; - passwordRequired = false; - formPromise: Promise; - password: string; - showText = false; - unavailable = false; - error = false; - hideEmail = false; + protected send: SendAccessView; + protected sendType = SendType; + protected loading = true; + protected passwordRequired = false; + protected formPromise: Promise; + protected password: string; + protected unavailable = false; + protected error = false; + protected hideEmail = false; + protected decKey: SymmetricCryptoKey; + protected accessRequest: SendAccessRequest; + protected expiredSendIcon = ExpiredSend; + + protected formGroup = this.formBuilder.group({}); private id: string; private key: string; - private decKey: SymmetricCryptoKey; - private accessRequest: SendAccessRequest; constructor( - private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService, - private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, private route: ActivatedRoute, private cryptoService: CryptoService, - private fileDownloadService: FileDownloadService, - private sendApiService: SendApiService + private sendApiService: SendApiService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + protected formBuilder: FormBuilder ) {} - get sendText() { - if (this.send == null || this.send.text == null) { - return null; - } - return this.showText ? this.send.text.text : this.send.text.maskedText; - } - - get expirationDate() { + protected get expirationDate() { if (this.send == null || this.send.expirationDate == null) { return null; } return this.send.expirationDate; } - get creatorIdentifier() { + protected get creatorIdentifier() { if (this.send == null || this.send.creatorIdentifier == null) { return null; } @@ -86,77 +93,22 @@ export class AccessComponent implements OnInit { }); } - async download() { - if (this.send == null || this.decKey == null) { - return; - } - - if (this.downloading) { - return; - } - - const downloadData = await this.sendApiService.getSendFileDownloadData( - this.send, - this.accessRequest - ); - - if (Utils.isNullOrWhitespace(downloadData.url)) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("missingSendFile")); - return; - } - - this.downloading = true; - const response = await fetch(new Request(downloadData.url, { cache: "no-store" })); - if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); - this.downloading = false; - return; - } - - try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const decBuf = await this.cryptoService.decryptFromBytes(encBuf, this.decKey); - this.fileDownloadService.download({ - fileName: this.send.file.fileName, - blobData: decBuf, - downloadMethod: "save", - }); - } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); - } - - this.downloading = false; - } - - copyText() { - this.platformUtilsService.copyToClipboard(this.send.text.text); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("valueCopied", this.i18nService.t("sendTypeText")) - ); - } - - toggleText() { - this.showText = !this.showText; - } - - async load() { + protected load = async () => { this.unavailable = false; this.error = false; this.hideEmail = false; - const keyArray = Utils.fromUrlB64ToArray(this.key); - this.accessRequest = new SendAccessRequest(); - if (this.password != null) { - const passwordHash = await this.cryptoFunctionService.pbkdf2( - this.password, - keyArray, - "sha256", - SEND_KDF_ITERATIONS - ); - this.accessRequest.password = Utils.fromBufferToB64(passwordHash); - } try { + const keyArray = Utils.fromUrlB64ToArray(this.key); + this.accessRequest = new SendAccessRequest(); + if (this.password != null) { + const passwordHash = await this.cryptoFunctionService.pbkdf2( + this.password, + keyArray, + "sha256", + SEND_KDF_ITERATIONS + ); + this.accessRequest.password = Utils.fromBufferToB64(passwordHash); + } let sendResponse: SendAccessResponse = null; if (this.loading) { sendResponse = await this.sendApiService.postSendAccess(this.id, this.accessRequest); @@ -168,16 +120,23 @@ export class AccessComponent implements OnInit { const sendAccess = new SendAccess(sendResponse); this.decKey = await this.cryptoService.makeSendKey(keyArray); this.send = await sendAccess.decrypt(this.decKey); - this.showText = this.send.text != null ? !this.send.text.hidden : true; } catch (e) { if (e instanceof ErrorResponse) { if (e.statusCode === 401) { this.passwordRequired = true; } else if (e.statusCode === 404) { this.unavailable = true; + } else if (e.statusCode === 400) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + e.message + ); } else { this.error = true; } + } else { + this.error = true; } } this.loading = false; @@ -186,5 +145,9 @@ export class AccessComponent implements OnInit { !this.passwordRequired && !this.loading && !this.unavailable; + }; + + protected setPassword(password: string) { + this.password = password; } } diff --git a/apps/web/src/app/tools/send/icons/expired-send.icon.ts b/apps/web/src/app/tools/send/icons/expired-send.icon.ts new file mode 100644 index 00000000000..b39cdca797d --- /dev/null +++ b/apps/web/src/app/tools/send/icons/expired-send.icon.ts @@ -0,0 +1,11 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ExpiredSend = svgIcon` + + + + + + + +`; diff --git a/apps/web/src/app/tools/send/send-access-file.component.html b/apps/web/src/app/tools/send/send-access-file.component.html new file mode 100644 index 00000000000..82880407809 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-file.component.html @@ -0,0 +1,5 @@ +

{{ send.file.fileName }}

+ diff --git a/apps/web/src/app/tools/send/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access-file.component.ts new file mode 100644 index 00000000000..c4e71a51629 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-file.component.ts @@ -0,0 +1,67 @@ +import { Component, Input } from "@angular/core"; + +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; +import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; + +import { SharedModule } from "../../shared"; + +@Component({ + selector: "app-send-access-file", + templateUrl: "send-access-file.component.html", + imports: [SharedModule], + standalone: true, +}) +export class SendAccessFileComponent { + @Input() send: SendAccessView; + @Input() decKey: SymmetricCryptoKey; + @Input() accessRequest: SendAccessRequest; + constructor( + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private cryptoService: CryptoService, + private fileDownloadService: FileDownloadService, + private sendApiService: SendApiService + ) {} + + protected download = async () => { + if (this.send == null || this.decKey == null) { + return; + } + + const downloadData = await this.sendApiService.getSendFileDownloadData( + this.send, + this.accessRequest + ); + + if (Utils.isNullOrWhitespace(downloadData.url)) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("missingSendFile")); + return; + } + + const response = await fetch(new Request(downloadData.url, { cache: "no-store" })); + if (response.status !== 200) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + return; + } + + try { + const encBuf = await EncArrayBuffer.fromResponse(response); + const decBuf = await this.cryptoService.decryptFromBytes(encBuf, this.decKey); + this.fileDownloadService.download({ + fileName: this.send.file.fileName, + blobData: decBuf, + downloadMethod: "save", + }); + } catch (e) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + } + }; +} diff --git a/apps/web/src/app/tools/send/send-access-password.component.html b/apps/web/src/app/tools/send/send-access-password.component.html new file mode 100644 index 00000000000..8bb2c306010 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-password.component.html @@ -0,0 +1,28 @@ +

{{ "sendProtectedPassword" | i18n }}

+

{{ "sendProtectedPasswordDontKnow" | i18n }}

+
+ + {{ "password" | i18n }} + + + +
+ +
+
diff --git a/apps/web/src/app/tools/send/send-access-password.component.ts b/apps/web/src/app/tools/send/send-access-password.component.ts new file mode 100644 index 00000000000..07a08fda7cd --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-password.component.ts @@ -0,0 +1,36 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; + +import { SharedModule } from "../../shared"; + +@Component({ + selector: "app-send-access-password", + templateUrl: "send-access-password.component.html", + imports: [SharedModule], + standalone: true, +}) +export class SendAccessPasswordComponent { + private destroy$ = new Subject(); + protected formGroup = this.formBuilder.group({ + password: ["", [Validators.required]], + }); + + @Input() loading: boolean; + @Output() setPasswordEvent = new EventEmitter(); + + constructor(private formBuilder: FormBuilder) {} + + async ngOnInit() { + this.formGroup.controls.password.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((val) => { + this.setPasswordEvent.emit(val); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/apps/web/src/app/tools/send/send-access-text.component.html b/apps/web/src/app/tools/send/send-access-text.component.html new file mode 100644 index 00000000000..ca772251146 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-text.component.html @@ -0,0 +1,26 @@ +{{ "sendHiddenByDefault" | i18n }} + + + +
+ +
+
+ +
diff --git a/apps/web/src/app/tools/send/send-access-text.component.ts b/apps/web/src/app/tools/send/send-access-text.component.ts new file mode 100644 index 00000000000..4c3c9c89675 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-text.component.ts @@ -0,0 +1,59 @@ +import { Component, Input } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; + +import { SharedModule } from "../../shared"; + +@Component({ + selector: "app-send-access-text", + templateUrl: "send-access-text.component.html", + imports: [SharedModule], + standalone: true, +}) +export class SendAccessTextComponent { + private _send: SendAccessView = null; + protected showText = false; + + protected formGroup = this.formBuilder.group({ + sendText: [""], + }); + + constructor( + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private formBuilder: FormBuilder + ) {} + + get send(): SendAccessView { + return this._send; + } + + @Input() set send(value: SendAccessView) { + this._send = value; + this.showText = this.send.text != null ? !this.send.text.hidden : true; + + if (this.send == null || this.send.text == null) { + return; + } + + this.formGroup.controls.sendText.patchValue( + this.showText ? this.send.text.text : this.send.text.maskedText + ); + } + + protected copyText() { + this.platformUtilsService.copyToClipboard(this.send.text.text); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("valueCopied", this.i18nService.t("sendTypeText")) + ); + } + + protected toggleText() { + this.showText = !this.showText; + } +} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 546cd7209cd..7330e5053cd 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7395,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } }