[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 <djsmith@web.de>
This commit is contained in:
aj-rosado 2023-11-17 16:06:59 +00:00 committed by GitHub
parent 5b1717fd41
commit 3952af058c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 365 additions and 233 deletions

View File

@ -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: [],
})

View File

@ -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,

View File

@ -1,150 +1,84 @@
<form #form (ngSubmit)="load()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-center mt-5">
<div class="col-12">
<h1 class="lead text-center mb-4">Bitwarden Send</h1>
<form [formGroup]="formGroup" [bitSubmit]="load">
<div
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-xl tw-flex-col tw-items-center tw-justify-center tw-p-8"
>
<img class="logo logo-themed" alt="Bitwarden" />
<div class="tw-mt-5 tw-w-full">
<h2 bitTypography="h2" class="tw-mb-4 tw-text-center">View Send</h2>
</div>
<div class="col-12 text-center" *ngIf="creatorIdentifier != null">
<p>{{ "sendCreatorIdentifier" | i18n : creatorIdentifier }}</p>
<div class="tw-w-full tw-text-center" *ngIf="creatorIdentifier != null">
<p>{{ "sendAccessCreatorIdentifier" | i18n : creatorIdentifier }}</p>
</div>
<div class="col-8" *ngIf="hideEmail">
<app-callout type="warning" title="{{ 'warning' | i18n }}">
{{ "viewSendHiddenEmailWarning" | i18n }}
<a href="https://bitwarden.com/help/receive-send/" target="_blank">{{
"learnMore" | i18n
}}</a
>.
</app-callout>
</div>
</div>
<div class="row justify-content-center">
<div class="col-5">
<div class="card d-block">
<div class="card-body" *ngIf="loading" class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="card-body" *ngIf="!loading && passwordRequired">
<p>{{ "sendProtectedPassword" | i18n }}</p>
<p>{{ "sendProtectedPasswordDontKnow" | i18n }}</p>
<div class="form-group">
<label for="password">{{ "password" | i18n }}</label>
<input
id="password"
type="password"
name="Password"
class="text-monospace form-control"
[(ngModel)]="password"
required
appInputVerbatim
appAutofocus
/>
</div>
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span>
<i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
<div class="card-body" *ngIf="!loading && unavailable">
{{ "sendAccessUnavailable" | i18n }}
</div>
<div class="card-body" *ngIf="!loading && error">
{{ "unexpectedError" | i18n }}
</div>
<div class="card-body" *ngIf="!loading && !passwordRequired && send">
<p class="text-center">
<bit-callout *ngIf="hideEmail" type="warning" title="{{ 'warning' | i18n }}">
{{ "viewSendHiddenEmailWarning" | i18n }}
<a
bitLink
href="https://bitwarden.com/help/receive-send/"
target="_blank"
rel="noopener noreferrer"
>{{ "learnMore" | i18n }}</a
>.
</bit-callout>
<div
class="tw-mt-3 tw-w-10/12 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<ng-container *ngIf="!loading; else spinner">
<app-send-access-password
(setPasswordEvent)="setPassword($event)"
*ngIf="passwordRequired && !error"
></app-send-access-password>
<bit-no-items [icon]="expiredSendIcon" class="tw-text-main" *ngIf="unavailable">
<ng-container slot="description">{{ "sendAccessUnavailable" | i18n }}</ng-container>
</bit-no-items>
<bit-no-items [icon]="expiredSendIcon" class="tw-text-main" *ngIf="error">
<ng-container slot="description">{{ "unexpectedErrorSend" | i18n }}</ng-container>
</bit-no-items>
<div *ngIf="!passwordRequired && send && !error && !unavailable">
<p class="tw-text-center">
<b>{{ send.name }}</b>
</p>
<hr />
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<app-callout *ngIf="send.text.hidden" type="tip">{{
"sendHiddenByDefault" | i18n
}}</app-callout>
<div class="form-group">
<textarea
id="text"
rows="8"
name="Text"
[ngModel]="sendText"
class="form-control"
readonly
></textarea>
</div>
<button
class="btn btn-block btn-link"
type="button"
(click)="toggleText()"
*ngIf="send.text.hidden"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showText, 'bwi-eye-slash': showText }"
></i>
{{ "toggleVisibility" | i18n }}
</button>
<button class="btn btn-block btn-link" type="button" (click)="copyText()">
<i class="bwi bwi-copy" aria-hidden="true"></i> {{ "copyValue" | i18n }}
</button>
<app-send-access-text [send]="send"></app-send-access-text>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<p>{{ send.file.fileName }}</p>
<button
class="btn btn-primary btn-block"
type="button"
(click)="download()"
*ngIf="!downloading"
>
<i class="bwi bwi-download" aria-hidden="true"></i>
{{ "downloadFile" | i18n }} ({{ send.file.sizeName }})
</button>
<button
class="btn btn-primary btn-block"
type="button"
*ngIf="downloading"
disabled="true"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<app-send-access-file
[send]="send"
[decKey]="decKey"
[accessRequest]="accessRequest"
></app-send-access-file>
</ng-container>
<p *ngIf="expirationDate" class="text-center text-muted">
<p *ngIf="expirationDate" class="tw-text-center tw-text-muted">
Expires: {{ expirationDate | date : "medium" }}
</p>
</div>
</div>
</ng-container>
<ng-template #spinner>
<div class="tw-text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
</ng-template>
</div>
<div class="col-12 text-center mt-5 text-muted">
<p class="mb-0">
{{ "sendAccessTaglineProductDesc" | i18n }}<br />
<div class="tw-mt-5 tw-w-10/12 tw-text-center tw-text-muted">
<p bitTypography="body2" class="tw-mb-0">
{{ "sendAccessTaglineProductDesc" | i18n }}
{{ "sendAccessTaglineLearnMore" | i18n }}
<a href="https://www.bitwarden.com/products/send?source=web-vault" target="_blank"
<a
bitLink
href="https://www.bitwarden.com/products/send?source=web-vault"
target="_blank"
rel="noopener noreferrer"
>Bitwarden Send</a
>
{{ "sendAccessTaglineOr" | i18n }}
<a href="https://vault.bitwarden.com/#/register" target="_blank">{{
"sendAccessTaglineSignUp" | i18n
}}</a>
<a bitLink routerLink="/register" target="_blank">{{ "sendAccessTaglineSignUp" | i18n }}</a>
{{ "sendAccessTaglineTryToday" | i18n }}
</p>
</div>

View File

@ -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<SendAccessResponse>;
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<SendAccessResponse>;
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;
}
}

View File

@ -0,0 +1,11 @@
import { svgIcon } from "@bitwarden/components";
export const ExpiredSend = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="130" height="130" fill="none">
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M22.75 29.695c0-4.991 4.074-9.037 9.1-9.037h14.3v2.582h-14.3c-3.59 0-6.5 2.89-6.5 6.455v68.428h-2.6V29.696Zm75.4 76.175V68.428h2.6v37.442c0 4.991-4.074 9.038-9.1 9.038h-53.3v-2.582h53.3c3.59 0 6.5-2.891 6.5-6.456Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M43.55 37.441c0-17.113 13.969-30.986 31.2-30.986s31.2 13.873 31.2 30.986c0 17.114-13.969 30.987-31.2 30.987s-31.2-13.873-31.2-30.986Zm31.2-33.568c-18.667 0-33.8 15.03-33.8 33.569S56.083 71.01 74.75 71.01c18.668 0 33.8-15.03 33.8-33.569S93.418 3.873 74.75 3.873Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M73.972 65.2c0 .357.291.646.65.646 15.968 0 28.925-12.71 28.925-28.404a.648.648 0 0 0-.65-.646.648.648 0 0 0-.65.646c0 14.967-12.36 27.113-27.625 27.113a.648.648 0 0 0-.65.645ZM46.347 38.087c.36 0 .65-.289.65-.645 0-14.968 12.361-27.113 27.625-27.113.36 0 .65-.29.65-.646a.648.648 0 0 0-.65-.646c-15.968 0-28.925 12.71-28.925 28.405 0 .356.291.645.65.645Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M123.729 81.869a1.926 1.926 0 0 1 0 2.739l-1.439 1.43a1.96 1.96 0 0 1-2.758 0L95.577 62.245a1.306 1.306 0 0 0-1.839 0 1.285 1.285 0 0 0 0 1.826l23.956 23.791a4.571 4.571 0 0 0 6.434 0l1.44-1.43a4.497 4.497 0 0 0 0-6.39l-23.956-23.791a1.306 1.306 0 0 0-1.838 0 1.285 1.285 0 0 0 0 1.825l23.955 23.792ZM34.45 36.797c0-.714.582-1.292 1.3-1.292h5.85c.718 0 1.3.578 1.3 1.291 0 .714-.582 1.292-1.3 1.292h-5.85c-.718 0-1.3-.578-1.3-1.291Zm0 10.973c0-.713.582-1.29 1.3-1.29h7.8c.718 0 1.3.578 1.3 1.29 0 .714-.582 1.292-1.3 1.292h-7.8c-.718 0-1.3-.578-1.3-1.291Zm0 10.975c0-.713.582-1.291 1.3-1.291H49.4c.718 0 1.3.578 1.3 1.29 0 .714-.582 1.292-1.3 1.292H35.75c-.718 0-1.3-.578-1.3-1.291Zm0 10.975c0-.714.582-1.292 1.3-1.292H72.8c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H35.75c-.718 0-1.3-.578-1.3-1.29Zm0 10.973c0-.713.582-1.29 1.3-1.29h27.3c.718 0 1.3.577 1.3 1.29 0 .713-.582 1.291-1.3 1.291h-27.3c-.718 0-1.3-.578-1.3-1.29Zm6.5 10.975c0-.713.582-1.291 1.3-1.291H88.4c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H42.25c-.718 0-1.3-.578-1.3-1.291Zm0 10.974c0-.713.582-1.291 1.3-1.291H88.4c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H42.25c-.718 0-1.3-.578-1.3-1.291Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M43.664 86.742c.412.292.617.794.524 1.289l-6.366 33.964a1.305 1.305 0 0 1-1.745.968l-9.692-3.707-4.914 5.689c-.355.41-.928.557-1.438.37a1.292 1.292 0 0 1-.849-1.211v-8.444c0-.305.108-.599.306-.832l14.73-17.357a1.306 1.306 0 0 1 1.831-.156c.549.46.619 1.275.156 1.82L21.784 116.13v4.485l3.225-3.733c.358-.414.94-.56 1.454-.364l9.089 3.476 5.567-29.698-32.42 18.385 6.813 3.082c.653.296.941 1.061.643 1.71a1.303 1.303 0 0 1-1.722.64l-9.122-4.128a1.289 1.289 0 0 1-.106-2.296l37.06-21.017c.44-.249.986-.222 1.399.07Z" clip-rule="evenodd"/>
</svg>
`;

View File

@ -0,0 +1,5 @@
<p>{{ send.file.fileName }}</p>
<button bitButton type="button" buttonType="primary" [bitAction]="download" [block]="true">
<i class="bwi bwi-download" aria-hidden="true"></i>
{{ "downloadAttachments" | i18n }} ({{ send.file.sizeName }})
</button>

View File

@ -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"));
}
};
}

View File

@ -0,0 +1,28 @@
<p bitTypography="body1">{{ "sendProtectedPassword" | i18n }}</p>
<p bitTypography="body1">{{ "sendProtectedPasswordDontKnow" | i18n }}</p>
<div class="tw-mb-3" [formGroup]="formGroup">
<bit-form-field>
<bit-label>{{ "password" | i18n }}</bit-label>
<input
bitInput
type="password"
formControlName="password"
required
appInputVerbatim
appAutofocus
/>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<div class="tw-flex">
<button
bitButton
bitFormButton
type="submit"
buttonType="primary"
[loading]="loading"
[block]="true"
>
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }} </span>
</button>
</div>
</div>

View File

@ -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<void>();
protected formGroup = this.formBuilder.group({
password: ["", [Validators.required]],
});
@Input() loading: boolean;
@Output() setPasswordEvent = new EventEmitter<string>();
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();
}
}

View File

@ -0,0 +1,26 @@
<bit-callout *ngIf="send.text.hidden" type="info">{{ "sendHiddenByDefault" | i18n }}</bit-callout>
<bit-form-field [formGroup]="formGroup">
<textarea id="text" bitInput rows="8" name="Text" formControlName="sendText" readonly></textarea>
</bit-form-field>
<div class="tw-mb-3">
<button
bitButton
type="button"
buttonType="secondary"
[block]="true"
(click)="toggleText()"
*ngIf="send.text.hidden"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showText, 'bwi-eye-slash': showText }"
></i>
{{ "toggleVisibility" | i18n }}
</button>
</div>
<div class="tw-mb-3">
<button bitButton type="button" buttonType="primary" [block]="true" (click)="copyText()">
<i class="bwi bwi-clone" aria-hidden="true"></i> {{ "copyValue" | i18n }}
</button>
</div>

View File

@ -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;
}
}

View File

@ -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."
}
}