1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 11:13:46 +00:00

[PM-328] Move Send to Tools (#5104)

* Move send in libs/common

* Move send in libs/angular

* Move send in browser

* Move send in cli

* Move send in desktop

* Move send in web
This commit is contained in:
Daniel James Smith
2023-03-29 16:23:37 +02:00
committed by GitHub
parent e645688f8a
commit e238ea20a9
105 changed files with 328 additions and 321 deletions

View File

@@ -0,0 +1,152 @@
<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>
</div>
<div class="col-12 text-center" *ngIf="creatorIdentifier != null">
<p>{{ "sendCreatorIdentifier" | 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">
<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>
</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>
</ng-container>
<p *ngIf="expirationDate" class="text-center text-muted">
Expires: {{ expirationDate | date : "medium" }}
</p>
</div>
</div>
</div>
<div class="col-12 text-center mt-5 text-muted">
<p class="mb-0">
{{ "sendAccessTaglineProductDesc" | i18n }}<br />
{{ "sendAccessTaglineLearnMore" | i18n }}
<a href="https://www.bitwarden.com/products/send?source=web-vault" target="_blank"
>Bitwarden Send</a
>
{{ "sendAccessTaglineOr" | i18n }}
<a href="https://vault.bitwarden.com/#/register" target="_blank">{{
"sendAccessTaglineSignUp" | i18n
}}</a>
{{ "sendAccessTaglineTryToday" | i18n }}
</p>
</div>
</div>
</form>

View File

@@ -0,0 +1,190 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums/kdfType";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncArrayBuffer } from "@bitwarden/common/models/domain/enc-array-buffer";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access";
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
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";
@Component({
selector: "app-send-access",
templateUrl: "access.component.html",
})
// 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;
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
) {}
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() {
if (this.send == null || this.send.expirationDate == null) {
return null;
}
return this.send.expirationDate;
}
get creatorIdentifier() {
if (this.send == null || this.send.creatorIdentifier == null) {
return null;
}
return this.send.creatorIdentifier;
}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
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;
}
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() {
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 {
let sendResponse: SendAccessResponse = null;
if (this.loading) {
sendResponse = await this.sendApiService.postSendAccess(this.id, this.accessRequest);
} else {
this.formPromise = this.sendApiService.postSendAccess(this.id, this.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;
} else if (e.statusCode === 404) {
this.unavailable = true;
} else {
this.error = true;
}
}
}
this.loading = false;
this.hideEmail =
this.creatorIdentifier == null &&
!this.passwordRequired &&
!this.loading &&
!this.unavailable;
}
}

View File

@@ -0,0 +1,305 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="sendAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
>
<div class="modal-header">
<h1 class="modal-title" id="sendAddEditTitle">{{ title }}</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="send">
<app-callout *ngIf="disableSend">
<span>{{ "sendDisabledWarning" | i18n }}</span>
</app-callout>
<app-callout *ngIf="!disableSend && disableHideEmail">
<span>{{ "sendOptionsPolicyInEffect" | i18n }}</span>
<ul class="mb-0">
<li>{{ "sendDisableHideEmailInEffect" | i18n }}</li>
</ul>
</app-callout>
<div class="row">
<div class="col-6 form-group">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="send.name"
required
[readOnly]="disableSend"
/>
<small class="form-text text-muted">{{ "sendNameDesc" | i18n }}</small>
</div>
</div>
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label>{{ "whatTypeOfSend" | i18n }}</label>
<div class="form-check" *ngFor="let o of typeOptions">
<input
class="form-check-input"
type="radio"
[(ngModel)]="send.type"
name="Type_{{ o.value }}"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged()"
[checked]="send.type === o.value"
/>
<label class="form-check-label" for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<div class="form-group">
<label for="text">{{ "sendTypeText" | i18n }}</label>
<textarea
id="text"
name="Text.Text"
rows="6"
[(ngModel)]="send.text.text"
class="form-control"
[readOnly]="disableSend"
></textarea>
<small class="form-text text-muted">{{ "sendTextDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[(ngModel)]="send.text.hidden"
id="text-hidden"
name="Text.Hidden"
[disabled]="disableSend"
/>
<label class="form-check-label" for="text-hidden">{{
"textHiddenByDefault" | i18n
}}</label>
</div>
</div>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<div class="form-group">
<div *ngIf="editMode">
<strong class="d-block">{{ "file" | i18n }}</strong>
{{ send.file.fileName }} ({{ send.file.sizeName }})
</div>
<div *ngIf="!editMode">
<label for="file">{{ "file" | i18n }}</label>
<input
type="file"
id="file"
class="form-control-file"
name="file"
required
[disabled]="disableSend"
/>
<small class="form-text text-muted"
>{{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }}</small
>
</div>
</div>
</ng-container>
<h3 class="mt-5">{{ "share" | i18n }}</h3>
<div class="form-group" *ngIf="link">
<label for="link">{{ "sendLinkLabel" | i18n }}</label>
<input type="text" readonly id="link" name="Link" [ngModel]="link" class="form-control" />
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[(ngModel)]="copyLink"
id="copy-link"
name="CopyLink"
/>
<label class="form-check-label" for="copy-link">{{
"copySendLinkOnSave" | i18n
}}</label>
</div>
</div>
<div
id="options-header"
class="section-header d-flex flex-row align-items-center mt-5"
(click)="toggleOptions()"
>
<h3 class="mb-0 mr-2">
<button appStopClick class="header-expandable">
<i
class="bwi"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
></i>
{{ "options" | i18n }}
</button>
</h3>
</div>
<div id="options" [hidden]="!showOptions">
<app-send-efflux-dates
[initialDeletionDate]="send.deletionDate"
[initialExpirationDate]="send.expirationDate"
[editMode]="editMode"
[disabled]="disableSend"
(datesChanged)="setDates($event)"
>
</app-send-efflux-dates>
<div class="row">
<div class="col-6 form-group">
<label for="maxAccessCount">{{ "maxAccessCount" | i18n }}</label>
<input
id="maxAccessCount"
class="form-control"
type="number"
name="MaxAccessCount"
[(ngModel)]="send.maxAccessCount"
min="1"
[readOnly]="disableSend"
/>
<div class="form-text text-muted small">{{ "maxAccessCountDesc" | i18n }}</div>
</div>
<div class="col-6 form-group" *ngIf="editMode">
<label for="accessCount">{{ "currentAccessCount" | i18n }}</label>
<input
id="accessCount"
class="form-control"
type="text"
name="AccessCount"
readonly
[(ngModel)]="send.accessCount"
/>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="password" *ngIf="!hasPassword">{{ "password" | i18n }}</label>
<label for="password" *ngIf="hasPassword">{{ "newPassword" | i18n }}</label>
<div class="input-group">
<input
id="password"
class="form-control text-monospace"
type="{{ showPassword ? 'text' : 'password' }}"
name="Password"
[(ngModel)]="password"
[readOnly]="disableSend"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePasswordVisible()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-text text-muted small">{{ "sendPasswordDesc" | i18n }}</div>
</div>
</div>
<div class="form-group">
<label for="notes">{{ "notes" | i18n }}</label>
<textarea
id="notes"
name="Notes"
rows="6"
[(ngModel)]="send.notes"
class="form-control"
[readOnly]="disableSend"
></textarea>
<div class="form-text text-muted small">{{ "sendNotesDesc" | i18n }}</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[(ngModel)]="send.hideEmail"
id="hideEmail"
name="HideEmail"
[disabled]="(disableHideEmail && !send.hideEmail) || disableSend"
/>
<label class="form-check-label" for="hideEmail">
{{ "hideEmail" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[(ngModel)]="send.disabled"
id="disabled"
name="Disabled"
[disabled]="disableSend"
/>
<label class="form-check-label" for="disabled">{{ "disableThisSend" | i18n }}</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit manual"
[ngClass]="{ loading: form.loading }"
[disabled]="form.loading || disableSend"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto" *ngIf="send">
<button
#deleteBtn
type="button"
(click)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="$any(deleteBtn).loading"
[appApiAction]="deletePromise"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,55 @@
import { DatePipe } from "@angular/common";
import { Component } from "@angular/core";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
@Component({
selector: "app-send-add-edit",
templateUrl: "add-edit.component.html",
})
export class AddEditComponent extends BaseAddEditComponent {
override componentName = "app-send-add-edit";
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
datePipe: DatePipe,
sendService: SendService,
stateService: StateService,
messagingService: MessagingService,
policyService: PolicyService,
logService: LogService,
sendApiService: SendApiService
) {
super(
i18nService,
platformUtilsService,
environmentService,
datePipe,
sendService,
messagingService,
policyService,
logService,
stateService,
sendApiService
);
}
async copyLinkToClipboard(link: string): Promise<void | boolean> {
// Copy function on web depends on the modal being open or not. Since this event occurs during a transition
// of the modal closing we need to add a small delay to make sure state of the DOM is consistent.
return new Promise((resolve) => {
window.setTimeout(() => resolve(super.copyLinkToClipboard(link)), 500);
});
}
}

View File

@@ -0,0 +1,188 @@
<div class="row" [formGroup]="datesForm">
<div class="col-6 form-group">
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<ng-template #deletionDateCustom>
<ng-container [ngSwitch]="browserPath">
<ng-container *ngSwitchCase="'firefox'">
<div class="d-flex justify-content-around">
<input
id="deletionDateCustomFallback"
class="form-control mt-1"
type="date"
name="DeletionDateFallback"
formControlName="fallbackDeletionDate"
required
placeholder="MM/DD/YYYY"
data-date-format="mm/dd/yyyy"
/>
<input
id="deletionTimeCustomFallback"
class="form-control mt-1 ml-1"
type="time"
name="DeletionTimeDate"
formControlName="fallbackDeletionTime"
required
placeholder="HH:MM AM/PM"
/>
</div>
</ng-container>
<ng-container *ngSwitchCase="'safari'">
<div class="d-flex justify-content-around">
<input
id="deletionDateCustomFallback"
class="form-control mt-1"
type="date"
name="DeletionDateFallback"
formControlName="fallbackDeletionDate"
required
placeholder="MM/DD/YYYY"
data-date-format="mm/dd/yyyy"
/>
<select
id="deletionTimeCustomFallback"
class="form-control mt-1 ml-1"
[required]="!editMode"
formControlName="fallbackDeletionTime"
name="SafariDeletionTime"
>
<option
*ngFor="let o of safariDeletionTimePresetOptions"
[ngValue]="o.twentyFourHour"
>
{{ o.twelveHour }}
</option>
</select>
</div>
</ng-container>
<ng-container *ngSwitchDefault>
<input
id="deletionDateCustom"
class="form-control mt-1"
type="datetime-local"
name="DeletionDate"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disabled"
/>
</ng-container>
</ng-container>
</ng-template>
<div *ngIf="!editMode">
<select
id="deletionDate"
name="SelectedDeletionDatePreset"
formControlName="selectedDeletionDatePreset"
class="form-control"
required
>
<option *ngFor="let o of deletionDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
<ng-container *ngIf="selectedDeletionDatePreset.value === 0">
<ng-container *ngTemplateOutlet="deletionDateCustom"> </ng-container>
</ng-container>
</div>
<div *ngIf="editMode">
<ng-container *ngTemplateOutlet="deletionDateCustom"> </ng-container>
</div>
<div class="form-text text-muted small">{{ "deletionDateDesc" | i18n }}</div>
</div>
<div class="col-6 form-group">
<div class="d-flex">
<label for="expirationDate">{{ "expirationDate" | i18n }}</label>
<a
href="#"
appStopClick
(click)="clearExpiration()"
class="ml-auto"
*ngIf="editMode && !disabled"
>
{{ "clear" | i18n }}
</a>
</div>
<ng-template #expirationDateCustom>
<ng-container [ngSwitch]="browserPath">
<div *ngSwitchCase="'firefox'" class="d-flex justify-content-around">
<input
id="expirationDateCustomFallback"
class="form-control mt-1"
type="date"
name="ExpirationDateFallback"
formControlName="fallbackExpirationDate"
[required]="!editMode"
placeholder="MM/DD/YYYY"
[readOnly]="disabled"
data-date-format="mm/dd/yyyy"
/>
<input
id="expirationTimeCustomFallback"
class="form-control mt-1 ml-1"
type="time"
name="ExpirationTimeFallback"
formControlName="fallbackExpirationTime"
[required]="!editMode"
placeholder="HH:MM AM/PM"
[readOnly]="disabled"
/>
</div>
<!-- non-default cases are not showing up -->
<div *ngSwitchCase="'safari'" class="d-flex justify-content-around">
<input
id="expirationDateCustomFallback"
class="form-control mt-1"
type="date"
name="ExpirationDateFallback"
formControlName="fallbackExpirationDate"
[required]="!editMode"
placeholder="MM/DD/YYYY"
[readOnly]="disabled"
data-date-format="mm/dd/yyyy"
/>
<select
id="expirationTimeCustomFallback"
class="form-control mt-1 ml-1"
[required]="!editMode"
formControlName="fallbackExpirationTime"
name="SafariExpirationTime"
>
<option
*ngFor="let o of safariExpirationTimePresetOptions"
[ngValue]="o.twentyFourHour"
>
{{ o.twelveHour }}
</option>
</select>
</div>
<ng-container *ngSwitchDefault>
<input
id="expirationDateCustom"
class="form-control mt-1"
type="datetime-local"
name="ExpirationDate"
formControlName="defaultExpirationDateTime"
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disabled"
/>
</ng-container>
</ng-container>
</ng-template>
<div *ngIf="!editMode">
<select
id="expirationDate"
name="SelectedExpirationDatePreset"
formControlName="selectedExpirationDatePreset"
class="form-control"
required
>
<option *ngFor="let o of expirationDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
<ng-container *ngIf="selectedExpirationDatePreset.value === 0">
<ng-container *ngTemplateOutlet="expirationDateCustom"> </ng-container>
</ng-container>
</div>
<div *ngIf="editMode">
<ng-container *ngTemplateOutlet="expirationDateCustom"> </ng-container>
</div>
<div class="form-text text-muted small">{{ "expirationDateDesc" | i18n }}</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
import { DatePipe } from "@angular/common";
import { Component } from "@angular/core";
import { ControlContainer, NgForm } from "@angular/forms";
import { EffluxDatesComponent as BaseEffluxDatesComponent } from "@bitwarden/angular/tools/send/efflux-dates.component";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({
selector: "app-send-efflux-dates",
templateUrl: "efflux-dates.component.html",
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
})
export class EffluxDatesComponent extends BaseEffluxDatesComponent {
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected datePipe: DatePipe
) {
super(i18nService, platformUtilsService, datePipe);
}
}

View File

@@ -0,0 +1,194 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="container page-content">
<app-callout type="warning" title="{{ 'sendDisabled' | i18n }}" *ngIf="disableSend">
<span>{{ "sendDisabledWarning" | i18n }}</span>
</app-callout>
<div class="row">
<div class="col-3 groupings">
<div class="card vault-filters">
<div class="card-header d-flex">
{{ "filters" | i18n }}
</div>
<div class="card-body">
<input
type="search"
placeholder="{{ searchPlaceholder || ('searchSends' | i18n) }}"
id="search"
class="form-control"
[(ngModel)]="searchText"
(input)="searchTextChanged()"
autocomplete="off"
appAutofocus
/>
<div class="filter">
<ul class="filter-options">
<li class="filter-option" [ngClass]="{ active: selectedAll }">
<span class="filter-buttons">
<button class="filter-button" appStopClick (click)="selectAll()">
<i class="bwi bwi-fw bwi-filter"></i>{{ "allSends" | i18n }}
</button>
</span>
</li>
</ul>
</div>
<div class="filter">
<div class="filter-heading">
<h3>{{ "types" | i18n }}</h3>
</div>
<ul class="filter-options">
<li class="filter-option" [ngClass]="{ active: selectedType === sendType.Text }">
<span class="filter-buttons">
<button class="filter-button" appStopClick (click)="selectType(sendType.Text)">
<i class="bwi bwi-fw bwi-file-text"></i>{{ "sendTypeText" | i18n }}
</button>
</span>
</li>
<li class="filter-option" [ngClass]="{ active: selectedType === sendType.File }">
<span class="filter-buttons">
<button class="filter-button" appStopClick (click)="selectType(sendType.File)">
<i class="bwi bwi-fw bwi-file"></i>{{ "sendTypeFile" | i18n }}
</button>
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="col-9">
<div class="page-header d-flex">
<h1>
{{ "send" | i18n }}
<small #actionSpinner [appApiAction]="actionPromise">
<ng-container *ngIf="$any(actionSpinner).loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</small>
</h1>
<div class="ml-auto d-flex">
<button
type="button"
class="btn btn-outline-primary btn-sm"
(click)="addSend()"
[disabled]="disableSend"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "createSend" | i18n }}
</button>
</div>
</div>
<!--Listing Table-->
<table class="table table-hover table-list" *ngIf="filteredSends && filteredSends.length">
<tbody>
<tr *ngFor="let s of filteredSends">
<td class="table-list-icon">
<div class="icon" aria-hidden="true">
<i class="bwi bwi-fw bwi-lg bwi-file" *ngIf="s.type == sendType.File"></i>
<i class="bwi bwi-fw bwi-lg bwi-file-text" *ngIf="s.type == sendType.Text"></i>
</div>
</td>
<td class="reduced-lh wrap">
<a href="#" appStopClick appStopProp (click)="editSend(s)">{{ s.name }}</a>
<ng-container *ngIf="s.disabled">
<i
class="bwi bwi-exclamation-triangle"
appStopProp
title="{{ 'disabled' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "disabled" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.password">
<i
class="bwi bwi-key"
appStopProp
title="{{ 'password' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "password" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.maxAccessCountReached">
<i
class="bwi bwi-ban"
appStopProp
title="{{ 'maxAccessCountReached' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "maxAccessCountReached" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i
class="bwi bwi-clock"
appStopProp
title="{{ 'expired' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "expired" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
<i
class="bwi bwi-trash"
appStopProp
title="{{ 'pendingDeletion' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "pendingDeletion" | i18n }}</span>
</ng-container>
<br />
<small appStopProp>{{ s.deletionDate | date : "medium" }}</small>
</td>
<td class="table-list-options">
<button
[bitMenuTriggerFor]="sendOptions"
class="tw-border-none tw-bg-transparent tw-text-main"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #sendOptions>
<button bitMenuItem (click)="copy(s)">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ "copySendLink" | i18n }}
</button>
<button bitMenuItem (click)="removePassword(s)" *ngIf="s.password && !disableSend">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "removePassword" | i18n }}
</button>
<button bitMenuItem (click)="delete(s)">
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
</tbody>
</table>
<div class="no-items" *ngIf="filteredSends && !filteredSends.length">
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="loaded">
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
<p>{{ "noSendsInList" | i18n }}</p>
<button (click)="addSend()" class="btn btn-outline-primary" [disabled]="disableSend">
<i class="bwi bwi-plus bwi-fw"></i>{{ "createSend" | i18n }}
</button>
</ng-container>
</div>
</div>
</div>
</div>
<ng-template #sendAddEdit></ng-template>

View File

@@ -0,0 +1,108 @@
import { Component, NgZone, ViewChild, ViewContainerRef } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { Icons } from "@bitwarden/components";
import { AddEditComponent } from "./add-edit.component";
const BroadcasterSubscriptionId = "SendComponent";
@Component({
selector: "app-send",
templateUrl: "send.component.html",
})
export class SendComponent extends BaseSendComponent {
@ViewChild("sendAddEdit", { read: ViewContainerRef, static: true })
sendAddEditModalRef: ViewContainerRef;
noItemIcon = Icons.Search;
constructor(
sendService: SendService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
ngZone: NgZone,
searchService: SearchService,
policyService: PolicyService,
private modalService: ModalService,
private broadcasterService: BroadcasterService,
logService: LogService,
sendApiService: SendApiService
) {
super(
sendService,
i18nService,
platformUtilsService,
environmentService,
ngZone,
searchService,
policyService,
logService,
sendApiService
);
}
async ngOnInit() {
await super.ngOnInit();
await this.load();
// Broadcaster subscription - load if sync completes in the background
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (message.successfully) {
await this.load();
}
break;
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async addSend() {
if (this.disableSend) {
return;
}
const component = await this.editSend(null);
component.type = this.type;
}
async editSend(send: SendView) {
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.sendAddEditModalRef,
(comp) => {
comp.sendId = send == null ? null : send.id;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onSavedSend.subscribe(async () => {
modal.close();
await this.load();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onDeletedSend.subscribe(async () => {
modal.close();
await this.load();
});
}
);
return childComponent;
}
}