From e9273ff79abe130ae8c29812d8990e2e960ef542 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Nov 2020 14:49:08 -0500 Subject: [PATCH] Send initial implementation (#687) * send work * Bump version to 2.16.2 (#668) * [SSO] New User Provision flow jslib update (f30d6f8 -> d84d6da) (#672) * Update jslib (f30d6f8 -> d84d6da) * Updated imports/constructor to super * OnlyOrg Policy (#669) * added localization strings needed for the OnlyOrg policy * added deprecation warning to policies page * allowed OnlyOrg policy configuration * blocked creating new orgs if already in an org with OnlyOrg enabled * code review cleanup for onlyOrg * removed a blank line * code review cleanup for onlyOrg * send listing actions * updates * access id * update jslib * re-work key and password derivation * update jslib * makeSendKey * update access path * store max access count * update jslib * l10n work * l10n for access page * l10n and cleanup * fix l10n Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Addison Beck --- jslib | 2 +- src/app/app-routing.module.ts | 9 + src/app/app.module.ts | 15 +- src/app/layouts/navbar.component.html | 3 + src/app/send/access.component.html | 59 ++++++ src/app/send/access.component.ts | 139 +++++++++++++ src/app/send/add-edit.component.html | 128 ++++++++++++ src/app/send/add-edit.component.ts | 276 ++++++++++++++++++++++++++ src/app/send/send.component.html | 102 ++++++++++ src/app/send/send.component.ts | 152 ++++++++++++++ src/locales/en/messages.json | 95 +++++++++ src/scss/styles.scss | 2 +- 12 files changed, 978 insertions(+), 4 deletions(-) create mode 100644 src/app/send/access.component.html create mode 100644 src/app/send/access.component.ts create mode 100644 src/app/send/add-edit.component.html create mode 100644 src/app/send/add-edit.component.ts create mode 100644 src/app/send/send.component.html create mode 100644 src/app/send/send.component.ts 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 @@ + 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 @@ +
+
+
+

Bitwarden Send

+
+
+ + {{'loading' | i18n}} +
+
+

{{'sendProtectedPassword' | i18n}}

+

{{'sendProtectedPasswordDontKnow' | i18n}}

+
+ + +
+
+ +
+
+
+

{{send.name}}

+
+ + + {{'sendHiddenByDefault' | i18n}} +
+ +
+ + +
+ + +

{{send.file.fileName}}

+ +
+
+
+
+
+
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 @@ +
+
+
+
+
+ {{'filters' | 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;