diff --git a/jslib b/jslib
index 5e50aa1a19..0e9e73ce95 160000
--- a/jslib
+++ b/jslib
@@ -1 +1 @@
-Subproject commit 5e50aa1a195bde11fdc14e9bdf71542766fdbb8d
+Subproject commit 0e9e73ce95a321ee05edbb62c50f9e1828f69c5a
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index e005295f92..34f3ba14f7 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -57,6 +57,9 @@ import {
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
+import { AccessComponent } from './send/access.component';
+import { SendComponent } from './send/send.component';
+
import { AccountComponent } from './settings/account.component';
import { CreateOrganizationComponent } from './settings/create-organization.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
@@ -141,6 +144,11 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'deleteAccount' },
},
+ {
+ path: 'send/:sendId/:key',
+ component: AccessComponent,
+ data: { title: 'Bitwarden Send' },
+ },
],
},
{
@@ -149,6 +157,7 @@ const routes: Routes = [
canActivate: [AuthGuardService],
children: [
{ path: 'vault', component: VaultComponent, data: { titleId: 'myVault' } },
+ { path: 'sends', component: SendComponent, data: { title: 'Send' } },
{
path: 'settings',
component: SettingsComponent,
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index fe87b83bb3..5d9ffe2cc8 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -98,6 +98,10 @@ import { CollectionsComponent as OrgCollectionsComponent } from './organizations
import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vault/groupings.component';
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
+import { AccessComponent } from './send/access.component';
+import { AddEditComponent as SendAddEditComponent } from './send/add-edit.component';
+import { SendComponent } from './send/send.component';
+
import { AccountComponent } from './settings/account.component';
import { AddCreditComponent } from './settings/add-credit.component';
import { AdjustPaymentComponent } from './settings/adjust-payment.component';
@@ -179,7 +183,10 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
-import { registerLocaleData } from '@angular/common';
+import {
+ registerLocaleData,
+ DatePipe,
+} from '@angular/common';
import localeCa from '@angular/common/locales/ca';
import localeCs from '@angular/common/locales/cs';
import localeDa from '@angular/common/locales/da';
@@ -252,6 +259,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
declarations: [
A11yTitleDirective,
+ AccessComponent,
AcceptOrganizationComponent,
AccountComponent,
SetPasswordComponent,
@@ -358,6 +366,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
SearchCiphersPipe,
SearchPipe,
SelectCopyDirective,
+ SendAddEditComponent,
+ SendComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
@@ -417,6 +427,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgUserGroupsComponent,
PasswordGeneratorHistoryComponent,
PurgeVaultComponent,
+ SendAddEditComponent,
ShareComponent,
TwoFactorAuthenticatorComponent,
TwoFactorDuoComponent,
@@ -427,7 +438,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
TwoFactorYubiKeyComponent,
UpdateKeyComponent,
],
- providers: [],
+ providers: [DatePipe],
bootstrap: [AppComponent],
})
export class AppModule { }
diff --git a/src/app/layouts/navbar.component.html b/src/app/layouts/navbar.component.html
index 9da6193958..4e07404753 100644
--- a/src/app/layouts/navbar.component.html
+++ b/src/app/layouts/navbar.component.html
@@ -8,6 +8,9 @@
{{'myVault' | i18n}}
+
+ Send
+
{{'tools' | i18n}}
diff --git a/src/app/send/access.component.html b/src/app/send/access.component.html
new file mode 100644
index 0000000000..1c0f5cae5a
--- /dev/null
+++ b/src/app/send/access.component.html
@@ -0,0 +1,59 @@
+
diff --git a/src/app/send/access.component.ts b/src/app/send/access.component.ts
new file mode 100644
index 0000000000..4068656c81
--- /dev/null
+++ b/src/app/send/access.component.ts
@@ -0,0 +1,139 @@
+import {
+ Component,
+ OnInit,
+} from '@angular/core';
+
+import { ActivatedRoute } from '@angular/router';
+
+import { ApiService } from 'jslib/abstractions/api.service';
+import { CryptoService } from 'jslib/abstractions/crypto.service';
+import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
+
+import { Utils } from 'jslib/misc/utils';
+
+import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
+import { SendAccess } from 'jslib/models/domain/sendAccess';
+
+import { SendAccessView } from 'jslib/models/view/sendAccessView';
+
+import { SendType } from 'jslib/enums/sendType';
+import { SendAccessRequest } from 'jslib/models/request/sendAccessRequest';
+import { ErrorResponse } from 'jslib/models/response/errorResponse';
+
+import { SendAccessResponse } from 'jslib/models/response/sendAccessResponse';
+
+@Component({
+ selector: 'app-send-access',
+ templateUrl: 'access.component.html',
+})
+export class AccessComponent implements OnInit {
+ send: SendAccessView;
+ sendType = SendType;
+ downloading = false;
+ loading = true;
+ passwordRequired = false;
+ formPromise: Promise;
+ password: string;
+ showText = false;
+
+ private id: string;
+ private key: string;
+ private decKey: SymmetricCryptoKey;
+
+ constructor(private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService,
+ private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
+ private route: ActivatedRoute, private cryptoService: CryptoService) {
+ }
+
+ get sendText() {
+ if (this.send == null || this.send.text == null) {
+ return null;
+ }
+ return this.showText ? this.send.text.text : this.send.text.maskedText;
+ }
+
+ ngOnInit() {
+ this.route.params.subscribe(async (params) => {
+ this.id = params.sendId;
+ this.key = params.key;
+ if (this.key == null || this.id == null) {
+ return;
+ }
+ await this.load();
+ });
+ }
+
+ async download() {
+ if (this.send == null || this.decKey == null) {
+ return;
+ }
+
+ if (this.downloading) {
+ return;
+ }
+
+ this.downloading = true;
+ const response = await fetch(new Request(this.send.file.url, { cache: 'no-store' }));
+ if (response.status !== 200) {
+ this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
+ this.downloading = false;
+ return;
+ }
+
+ try {
+ const buf = await response.arrayBuffer();
+ const decBuf = await this.cryptoService.decryptFromBytes(buf, this.decKey);
+ this.platformUtilsService.saveFile(window, decBuf, null, this.send.file.fileName);
+ } catch (e) {
+ this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
+ }
+
+ this.downloading = false;
+ }
+
+ selectText() {
+ (document.getElementById('text') as HTMLInputElement).select();
+ }
+
+ 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;
+ }
+
+ private async load() {
+ const keyArray = Utils.fromUrlB64ToArray(this.key);
+ const accessRequest = new SendAccessRequest();
+ if (this.password != null) {
+ const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password, keyArray, 'sha256', 100000);
+ accessRequest.password = Utils.fromBufferToB64(passwordHash);
+ }
+ try {
+ let sendResponse: SendAccessResponse = null;
+ if (this.loading) {
+ sendResponse = await this.apiService.postSendAccess(this.id, accessRequest);
+ } else {
+ this.formPromise = this.apiService.postSendAccess(this.id, accessRequest);
+ sendResponse = await this.formPromise;
+ }
+ this.passwordRequired = false;
+ 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;
+ }
+ }
+ }
+ this.loading = false;
+ }
+}
diff --git a/src/app/send/add-edit.component.html b/src/app/send/add-edit.component.html
new file mode 100644
index 0000000000..a4dcb9cb70
--- /dev/null
+++ b/src/app/send/add-edit.component.html
@@ -0,0 +1,128 @@
+
+
+
+
+
diff --git a/src/app/send/add-edit.component.ts b/src/app/send/add-edit.component.ts
new file mode 100644
index 0000000000..f99e13b1dc
--- /dev/null
+++ b/src/app/send/add-edit.component.ts
@@ -0,0 +1,276 @@
+import { DatePipe } from '@angular/common';
+
+import {
+ EventEmitter,
+ Input,
+ Output,
+} from '@angular/core';
+
+import { Component } from '@angular/core';
+
+import { SendType } from 'jslib/enums/sendType';
+
+import { ApiService } from 'jslib/abstractions/api.service';
+import { CryptoService } from 'jslib/abstractions/crypto.service';
+import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
+import { EnvironmentService } from 'jslib/abstractions/environment.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
+
+import { SendView } from 'jslib/models/view/sendView';
+import { SendFileView } from 'jslib/models/view/sendFileView';
+import { SendTextView } from 'jslib/models/view/sendTextView';
+
+import { Send } from 'jslib/models/domain/send';
+import { SendFile } from 'jslib/models/domain/sendFile';
+import { SendText } from 'jslib/models/domain/sendText';
+
+import { SendData } from 'jslib/models/data/sendData';
+
+import { SendRequest } from 'jslib/models/request/sendRequest';
+
+import { Utils } from 'jslib/misc/utils';
+
+@Component({
+ selector: 'app-send-add-edit',
+ templateUrl: 'add-edit.component.html',
+})
+export class AddEditComponent {
+ @Input() sendId: string;
+ @Input() type: SendType;
+
+ @Output() onSavedSend = new EventEmitter();
+ @Output() onDeletedSend = new EventEmitter();
+ @Output() onCancelled = new EventEmitter();
+
+ editMode: boolean = false;
+ send: SendView;
+ link: string;
+ title: string;
+ deletionDate: string;
+ expirationDate: string;
+ hasPassword: boolean;
+ password: string;
+ formPromise: Promise;
+ deletePromise: Promise;
+ sendType = SendType;
+ typeOptions: any[];
+
+ constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
+ private apiService: ApiService, private cryptoService: CryptoService,
+ private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService,
+ private datePipe: DatePipe) {
+ this.typeOptions = [
+ { name: i18nService.t('sendTypeFile'), value: SendType.File },
+ { name: i18nService.t('sendTypeText'), value: SendType.Text },
+ ];
+ }
+
+ async ngOnInit() {
+ await this.load();
+ }
+
+ async load() {
+ this.editMode = this.sendId != null;
+ if (this.editMode) {
+ this.editMode = true;
+ this.title = this.i18nService.t('editSend');
+ } else {
+ this.title = this.i18nService.t('createSend');
+ }
+
+ if (this.send == null) {
+ if (this.editMode) {
+ const send = await this.loadSend();
+ this.send = await send.decrypt();
+ } else {
+ this.send = new SendView();
+ this.send.type = this.type == null ? SendType.File : this.type;
+ this.send.file = new SendFileView();
+ this.send.text = new SendTextView();
+ this.send.deletionDate = new Date();
+ this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
+ }
+ }
+
+ this.hasPassword = this.send.password != null && this.send.password.trim() !== '';
+
+ // Parse dates
+ this.deletionDate = this.send.deletionDate == null ? null :
+ this.datePipe.transform(this.send.deletionDate, 'yyyy-MM-ddTHH:mm');
+ this.expirationDate = this.send.expirationDate == null ? null :
+ this.datePipe.transform(this.send.expirationDate, 'yyyy-MM-ddTHH:mm');
+
+ if (this.editMode) {
+ let webVaultUrl = this.environmentService.getWebVaultUrl();
+ if (webVaultUrl == null) {
+ webVaultUrl = 'https://vault.bitwarden.com';
+ }
+ this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key;
+ }
+ }
+
+ async submit(): Promise {
+ if (this.send.name == null || this.send.name === '') {
+ this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('nameRequired'));
+ return false;
+ }
+
+ let file: File = null;
+ if (this.send.type === SendType.File && !this.editMode) {
+ const fileEl = document.getElementById('file') as HTMLInputElement;
+ const files = fileEl.files;
+ if (files == null || files.length === 0) {
+ this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('selectFile'));
+ return;
+ }
+
+ file = files[0];
+ if (file.size > 104857600) { // 100 MB
+ this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('maxFileSize'));
+ return;
+ }
+ }
+
+ const encSend = await this.encryptSend(file);
+ try {
+ this.formPromise = this.saveSend(encSend);
+ await this.formPromise;
+ this.send.id = encSend[0].id;
+ this.platformUtilsService.showToast('success', null,
+ this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'));
+ this.onSavedSend.emit(this.send);
+ return true;
+ } catch { }
+
+ return false;
+ }
+
+ clearExpiration() {
+ this.expirationDate = null;
+ }
+
+ async delete(): Promise {
+ if (this.deletePromise != null) {
+ return;
+ }
+ const confirmed = await this.platformUtilsService.showDialog(
+ this.i18nService.t('deleteSendConfirmation'),
+ this.i18nService.t('deleteSend'),
+ this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
+ if (!confirmed) {
+ return;
+ }
+
+ try {
+ this.deletePromise = this.apiService.deleteSend(this.send.id);
+ await this.deletePromise;
+ this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
+ await this.load();
+ this.onDeletedSend.emit(this.send);
+ } catch { }
+ }
+
+ protected async loadSend(): Promise {
+ const response = await this.apiService.getSend(this.sendId);
+ const data = new SendData(response);
+ return new Send(data);
+ }
+
+ protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> {
+ let fileData: ArrayBuffer = null;
+ const send = new Send();
+ send.id = this.send.id;
+ send.type = this.send.type;
+ send.disabled = this.send.disabled;
+ send.maxAccessCount = this.send.maxAccessCount;
+ if (this.send.key == null) {
+ this.send.key = await this.cryptoFunctionService.randomBytes(16);
+ this.send.cryptoKey = await this.cryptoService.makeSendKey(this.send.key);
+ }
+ if (this.password != null) {
+ const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password,
+ this.send.key, 'sha256', 100000);
+ send.password = Utils.fromBufferToB64(passwordHash);
+ }
+ send.key = await this.cryptoService.encrypt(this.send.key, null);
+ send.name = await this.cryptoService.encrypt(this.send.name, this.send.cryptoKey);
+ send.notes = await this.cryptoService.encrypt(this.send.notes, this.send.cryptoKey);
+ if (send.type === SendType.Text) {
+ send.text = new SendText();
+ send.text.text = await this.cryptoService.encrypt(this.send.text.text, this.send.cryptoKey);
+ send.text.hidden = this.send.text.hidden;
+ } else if (send.type === SendType.File) {
+ send.file = new SendFile();
+ if (file != null) {
+ fileData = await this.parseFile(send, file);
+ }
+ }
+
+ // Parse dates
+ try {
+ send.deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
+ } catch {
+ send.deletionDate = null;
+ }
+ try {
+ send.expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate);
+ } catch {
+ send.expirationDate = null;
+ }
+
+ return [send, fileData];
+ }
+
+ protected async saveSend(sendData: [Send, ArrayBuffer]) {
+ const request = new SendRequest(sendData[0]);
+ if (sendData[0].id == null) {
+ if (sendData[0].type === SendType.Text) {
+ await this.apiService.postSend(request);
+ } else {
+ const fd = new FormData();
+ try {
+ const blob = new Blob([sendData[1]], { type: 'application/octet-stream' });
+ fd.append('model', JSON.stringify(request));
+ fd.append('data', blob, sendData[0].file.fileName.encryptedString);
+ } catch (e) {
+ if (Utils.isNode && !Utils.isBrowser) {
+ fd.append('model', JSON.stringify(request));
+ fd.append('data', Buffer.from(sendData[1]) as any, {
+ filepath: sendData[0].file.fileName.encryptedString,
+ contentType: 'application/octet-stream',
+ } as any);
+ } else {
+ throw e;
+ }
+ }
+ await this.apiService.postSendFile(fd);
+ }
+ } else {
+ await this.apiService.putSend(sendData[0].id, request);
+ }
+ }
+
+ private parseFile(send: Send, file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsArrayBuffer(file);
+ reader.onload = async (evt) => {
+ try {
+ send.file.fileName = await this.cryptoService.encrypt(file.name, this.send.cryptoKey);
+ const fileData = await this.cryptoService.encryptToBytes(evt.target.result as ArrayBuffer,
+ this.send.cryptoKey);
+ resolve(fileData);
+ } catch (e) {
+ reject(e);
+ }
+ };
+ reader.onerror = (evt) => {
+ reject('Error reading file.');
+ };
+ });
+ }
+}
diff --git a/src/app/send/send.component.html b/src/app/send/send.component.html
new file mode 100644
index 0000000000..0b58a5e68e
--- /dev/null
+++ b/src/app/send/send.component.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
{{'types' | i18n}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ {{s.name}}
+
+
+ {{'password' | i18n}}
+
+
+ {{s.deletionDate | date:'medium'}}
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
diff --git a/src/app/send/send.component.ts b/src/app/send/send.component.ts
new file mode 100644
index 0000000000..f4ec0670ce
--- /dev/null
+++ b/src/app/send/send.component.ts
@@ -0,0 +1,152 @@
+import {
+ Component,
+ ComponentFactoryResolver,
+ OnInit,
+ ViewChild,
+ ViewContainerRef,
+} from '@angular/core';
+
+import { SendType } from 'jslib/enums/sendType';
+
+import { SendView } from 'jslib/models/view/sendView';
+
+import { AddEditComponent } from './add-edit.component';
+
+import { ModalComponent } from '../modal.component';
+
+import { ApiService } from 'jslib/abstractions/api.service';
+import { EnvironmentService } from 'jslib/abstractions/environment.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
+import { UserService } from 'jslib/abstractions/user.service';
+
+import { SendData } from 'jslib/models/data/sendData';
+
+import { Send } from 'jslib/models/domain/send';
+
+@Component({
+ selector: 'app-send',
+ templateUrl: 'send.component.html',
+})
+export class SendComponent implements OnInit {
+ @ViewChild('sendAddEdit', { read: ViewContainerRef, static: true }) sendAddEditModalRef: ViewContainerRef;
+
+ sendType = SendType;
+ loading = true;
+ expired: boolean = false;
+ type: SendType = null;
+ sends: SendView[] = [];
+
+ modal: ModalComponent = null;
+ actionPromise: any;
+
+ constructor(private apiService: ApiService, private userService: UserService,
+ private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
+ private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService) { }
+
+ async ngOnInit() {
+ await this.load();
+ }
+
+ async load() {
+ this.loading = true;
+ const userId = await this.userService.getUserId();
+ const sends = await this.apiService.getSends();
+ const sendsArr: SendView[] = [];
+ if (sends != null && sends.data != null) {
+ for (const res of sends.data) {
+ const data = new SendData(res, userId);
+ const send = new Send(data);
+ const view = await send.decrypt();
+ sendsArr.push(view);
+ }
+ }
+ this.sends = sendsArr;
+ this.loading = false;
+ }
+
+ addSend() {
+ const component = this.editSend(null);
+ component.type = this.type;
+ }
+
+ editSend(send: SendView) {
+ if (this.modal != null) {
+ this.modal.close();
+ }
+
+ const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
+ this.modal = this.sendAddEditModalRef.createComponent(factory).instance;
+ const childComponent = this.modal.show(
+ AddEditComponent, this.sendAddEditModalRef);
+
+ childComponent.sendId = send == null ? null : send.id;
+ childComponent.onSavedSend.subscribe(async (s: SendView) => {
+ this.modal.close();
+ await this.load();
+ });
+ childComponent.onDeletedSend.subscribe(async (s: SendView) => {
+ this.modal.close();
+ await this.load();
+ });
+
+ this.modal.onClosed.subscribe(() => {
+ this.modal = null;
+ });
+
+ return childComponent;
+ }
+
+ async removePassword(s: SendView): Promise {
+ if (this.actionPromise != null || s.password == null) {
+ return;
+ }
+ const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'),
+ this.i18nService.t('removePassword'),
+ this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
+ if (!confirmed) {
+ return false;
+ }
+
+ try {
+ this.actionPromise = this.apiService.putSendRemovePassword(s.id);
+ await this.actionPromise;
+ this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword'));
+ await this.load();
+ } catch { }
+ this.actionPromise = null;
+ }
+
+ async delete(s: SendView): Promise {
+ if (this.actionPromise != null) {
+ return false;
+ }
+ const confirmed = await this.platformUtilsService.showDialog(
+ this.i18nService.t('deleteSendConfirmation'),
+ this.i18nService.t('deleteSend'),
+ this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
+ if (!confirmed) {
+ return false;
+ }
+
+ try {
+ this.actionPromise = this.apiService.deleteSend(s.id);
+ await this.actionPromise;
+ this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
+ await this.load();
+ } catch { }
+ this.actionPromise = null;
+ return true;
+ }
+
+ copy(s: SendView) {
+ let webVaultUrl = this.environmentService.getWebVaultUrl();
+ if (webVaultUrl == null) {
+ webVaultUrl = 'https://vault.bitwarden.com';
+ }
+ const link = webVaultUrl + '/#/send/' + s.accessId + '/' + s.urlB64Key;
+ this.platformUtilsService.copyToClipboard(link);
+ this.platformUtilsService.showToast('success', null,
+ this.i18nService.t('valueCopied', this.i18nService.t('sendLink')));
+ }
+}
diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json
index 819886adf5..17a5c93fb7 100644
--- a/src/locales/en/messages.json
+++ b/src/locales/en/messages.json
@@ -37,6 +37,9 @@
"password": {
"message": "Password"
},
+ "newPassword": {
+ "message": "New Password"
+ },
"passphrase": {
"message": "Passphrase"
},
@@ -3238,5 +3241,97 @@
},
"requireSsoPolicyReqError": {
"message": "Single Organization policy not enabled."
+ },
+ "sendTypeFile": {
+ "message": "File"
+ },
+ "sendTypeText": {
+ "message": "Text"
+ },
+ "createSend": {
+ "message": "Create New Send",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "editSend": {
+ "message": "Edit Send",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "createdSend": {
+ "message": "Created Send",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "editedSend": {
+ "message": "Edited Send",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "deletedSend": {
+ "message": "Deleted Send",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "deleteSend": {
+ "message": "Delete Send",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "deleteSendConfirmation": {
+ "message": "Are you sure you want to delete this Send?",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "whatTypeOfSend": {
+ "message": "What type of Send is this?",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "deletionDate": {
+ "message": "Deletion Date"
+ },
+ "expirationDate": {
+ "message": "Expiration Date"
+ },
+ "maxAccessCount": {
+ "message": "Maximum Access Count"
+ },
+ "currentAccessCount": {
+ "message": "Current Access Count"
+ },
+ "disabled": {
+ "message": "Disabled"
+ },
+ "sendLink": {
+ "message": "Send Link",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "copySendLink": {
+ "message": "Copy Send Link",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "removePassword": {
+ "message": "Remove Password"
+ },
+ "removedPassword": {
+ "message": "Removed Password"
+ },
+ "removePasswordConfirmation": {
+ "message": "Are you sure you want to remove the password?"
+ },
+ "allSends": {
+ "message": "All Sends"
+ },
+ "searchSends": {
+ "message": "Search Sends",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "sendProtectedPassword": {
+ "message": "This Send is protected with a password. Please type the password below to continue.",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "sendProtectedPasswordDontKnow": {
+ "message": "Don't know the password? Ask the Sender for the password needed to access this Send.",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "sendHiddenByDefault": {
+ "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"
}
}
diff --git a/src/scss/styles.scss b/src/scss/styles.scss
index 676e275fc9..55b3c92c1c 100644
--- a/src/scss/styles.scss
+++ b/src/scss/styles.scss
@@ -462,7 +462,7 @@ input[type="search"]::-webkit-search-cancel-button {
color: #c40800;
}
-app-vault-groupings, app-org-vault-groupings {
+app-vault-groupings, app-org-vault-groupings, .groupings {
.card {
#search {
margin-bottom: 1rem;