Apps: Add direct file upload in item editor (#5140)

This commit is contained in:
d11n 2023-07-06 04:01:36 +02:00 committed by GitHub
parent e998340387
commit 966e598f10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 6 deletions

View file

@ -1,6 +1,9 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
@ -8,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -21,17 +25,20 @@ namespace BTCPayServer.Controllers
public UIAppsController(
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository,
IFileService fileService,
AppService appService,
IHtmlHelper html)
{
_userManager = userManager;
_storeRepository = storeRepository;
_fileService = fileService;
_appService = appService;
Html = html;
}
private readonly UserManager<ApplicationUser> _userManager;
private readonly StoreRepository _storeRepository;
private readonly IFileService _fileService;
private readonly AppService _appService;
public string CreatedAppId { get; set; }
@ -184,13 +191,50 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/upload-file")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> FileUpload(IFormFile file)
{
var app = GetCurrentApp();
var userId = GetUserId();
if (app is null || userId is null)
return NotFound();
if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
return Json(new { error = "The file needs to be an image" });
}
if (file.Length > 500_000)
{
return Json(new { error = "The image file size should be less than 0.5MB" });
}
var formFile = await file.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
{
return Json(new { error = "The file needs to be an image" });
}
try
{
var storedFile = await _fileService.AddFile(file, userId);
var fileId = storedFile.Id;
var fileUrl = await _fileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
return Json(new { fileId, fileUrl });
}
catch (Exception e)
{
return Json(new { error = $"Could not save file: {e.Message}" });
}
}
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
{
if (string.IsNullOrWhiteSpace(currency))
{
currency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency;
var store = await _storeRepository.FindStore(storeId);
currency = store?.GetStoreBlob().DefaultCurrency;
}
return currency.Trim().ToUpperInvariant();
return currency?.Trim().ToUpperInvariant();
}
private string GetUserId() => _userManager.GetUserId(User);

View file

@ -17,7 +17,6 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
@ -267,6 +266,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
if (app == null)
return NotFound();
vm.AppId = app.Id;
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");

View file

@ -566,6 +566,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
if (app == null)
return NotFound();
vm.Id = app.Id;
if (!ModelState.IsValid)
return View("PointOfSale/UpdatePointOfSale", vm);

View file

@ -43,8 +43,13 @@
</div>
</div>
<div class="form-group">
<label for="EditorImageUrl" class="form-label">Image Url</label>
<label for="EditorImage" class="form-label">Image Url</label>
<input id="EditorImageUrl" class="form-control mb-2" pattern="[^\*#]+" v-model="editingItem && editingItem.image" ref="txtImage" />
<div class="d-flex align-items-center gap-2">
<input id="EditorImage" type="file" class="form-control" ref="editorImage" v-on:change="uploadFileChanged">
<button class="btn btn-primary" type="button" id="EditorUploadButton" v-on:click="uploadFile" :disabled="uploadDisabled">Upload</button>
</div>
<span v-if="uploadError" v-text="uploadError" class="text-danger"></span>
</div>
<div class="form-group">
<label for="EditorDescription" class="form-label">Description</label>
@ -142,6 +147,7 @@
<script src="~/vendor/tom-select/tom-select.complete.min.js" asp-append-version="true"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const fileUploadUrl = @Safe.Json(Url.Action("FileUpload", "UIApps", new { appId = Context.GetRouteValue("appId") }));
const parseConfig = str => {
try {
return JSON.parse(str)
@ -166,7 +172,9 @@ document.addEventListener("DOMContentLoaded", () => {
{ text: 'Custom', value: 'Topup' },
],
categoriesSelect: null,
productModal: null
productModal: null,
uploadDisabled: true,
uploadError: null
}
},
mounted() {
@ -259,7 +267,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (this.editingItem.description.startsWith("- ")){
this.errors.push("Description cannot start with \"- \"");
}
if (!this.$refs.txtImage.checkValidity()) {
if (!this.$refs.editorImage.checkValidity()) {
this.errors.push("Image cannot have * or #");
}
if (this.editingItem.image.startsWith("- ")){
@ -288,6 +296,32 @@ document.addEventListener("DOMContentLoaded", () => {
this.categoriesSelect.setValue(this.editingItem.categories);
this.productModal.show();
}
},
uploadFileChanged () {
this.uploadDisabled = !this.$refs.editorImage || this.$refs.editorImage.files.length === 0;
},
async uploadFile() {
const file = this.$refs.editorImage.files[0];
if (!file) return this.uploadError = 'No file selected';
this.uploadError = null;
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch(fileUploadUrl, { method: 'POST', body: formData });
if (response.ok) {
const { error, fileUrl } = await response.json();
if (error) return this.uploadError = error;
this.editingItem.image = fileUrl;
this.$refs.editorImage.value = null;
this.uploadDisabled = true;
return;
}
} catch (e) {
console.error(e);
}
this.uploadError = 'Upload failed';
}
}
});