mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 22:11:48 +01:00
Apps: Add direct file upload in item editor (#5140)
This commit is contained in:
parent
e998340387
commit
966e598f10
4 changed files with 85 additions and 6 deletions
|
@ -1,6 +1,9 @@
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
@ -8,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
@ -21,17 +25,20 @@ namespace BTCPayServer.Controllers
|
||||||
public UIAppsController(
|
public UIAppsController(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
|
IFileService fileService,
|
||||||
AppService appService,
|
AppService appService,
|
||||||
IHtmlHelper html)
|
IHtmlHelper html)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_storeRepository = storeRepository;
|
_storeRepository = storeRepository;
|
||||||
|
_fileService = fileService;
|
||||||
_appService = appService;
|
_appService = appService;
|
||||||
Html = html;
|
Html = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly StoreRepository _storeRepository;
|
private readonly StoreRepository _storeRepository;
|
||||||
|
private readonly IFileService _fileService;
|
||||||
private readonly AppService _appService;
|
private readonly AppService _appService;
|
||||||
|
|
||||||
public string CreatedAppId { get; set; }
|
public string CreatedAppId { get; set; }
|
||||||
|
@ -184,13 +191,50 @@ namespace BTCPayServer.Controllers
|
||||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
|
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)
|
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(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);
|
private string GetUserId() => _userManager.GetUserId(User);
|
||||||
|
|
|
@ -17,7 +17,6 @@ using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
|
@ -267,6 +266,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
vm.AppId = app.Id;
|
||||||
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
|
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
|
||||||
if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
|
if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||||
|
|
|
@ -566,6 +566,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
vm.Id = app.Id;
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
return View("PointOfSale/UpdatePointOfSale", vm);
|
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<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" />
|
<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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="EditorDescription" class="form-label">Description</label>
|
<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 src="~/vendor/tom-select/tom-select.complete.min.js" asp-append-version="true"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const fileUploadUrl = @Safe.Json(Url.Action("FileUpload", "UIApps", new { appId = Context.GetRouteValue("appId") }));
|
||||||
const parseConfig = str => {
|
const parseConfig = str => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str)
|
return JSON.parse(str)
|
||||||
|
@ -166,7 +172,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
{ text: 'Custom', value: 'Topup' },
|
{ text: 'Custom', value: 'Topup' },
|
||||||
],
|
],
|
||||||
categoriesSelect: null,
|
categoriesSelect: null,
|
||||||
productModal: null
|
productModal: null,
|
||||||
|
uploadDisabled: true,
|
||||||
|
uploadError: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -259,7 +267,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (this.editingItem.description.startsWith("- ")){
|
if (this.editingItem.description.startsWith("- ")){
|
||||||
this.errors.push("Description cannot start with \"- \"");
|
this.errors.push("Description cannot start with \"- \"");
|
||||||
}
|
}
|
||||||
if (!this.$refs.txtImage.checkValidity()) {
|
if (!this.$refs.editorImage.checkValidity()) {
|
||||||
this.errors.push("Image cannot have * or #");
|
this.errors.push("Image cannot have * or #");
|
||||||
}
|
}
|
||||||
if (this.editingItem.image.startsWith("- ")){
|
if (this.editingItem.image.startsWith("- ")){
|
||||||
|
@ -288,6 +296,32 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
this.categoriesSelect.setValue(this.editingItem.categories);
|
this.categoriesSelect.setValue(this.editingItem.categories);
|
||||||
this.productModal.show();
|
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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue