mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[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:
parent
5b1717fd41
commit
3952af058c
@ -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: [],
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
11
apps/web/src/app/tools/send/icons/expired-send.icon.ts
Normal file
11
apps/web/src/app/tools/send/icons/expired-send.icon.ts
Normal 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>
|
||||
`;
|
@ -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>
|
67
apps/web/src/app/tools/send/send-access-file.component.ts
Normal file
67
apps/web/src/app/tools/send/send-access-file.component.ts
Normal 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"));
|
||||
}
|
||||
};
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
26
apps/web/src/app/tools/send/send-access-text.component.html
Normal file
26
apps/web/src/app/tools/send/send-access-text.component.html
Normal 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>
|
59
apps/web/src/app/tools/send/send-access-text.component.ts
Normal file
59
apps/web/src/app/tools/send/send-access-text.component.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user