Greenfield: Add file endpoints and upload (#6075)

* Greenfield: Add file endpoints and upload

- Endpoints for server files
- File upload using `multipart/form-data`

Closes #6074.

Can also be tested using cURL:

- `curl --location 'https://localhost:14142/api/v1/files' --header 'Authorization: token MY_API_TOKEN' --form 'file=@"LOCAL_FILEPATH"'`
- `curl --location 'https://localhost:14142/api/v1/users/me/picture' --header 'Authorization: token MY_API_TOKEN' --form 'file=@"LOCAL_FILEPATH"'`

* Revert UnresolvedUri changes

* Add upload for store logo
This commit is contained in:
d11n 2024-07-11 02:28:24 +02:00 committed by GitHub
parent 249b991185
commit 25ae6df095
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 852 additions and 52 deletions

View file

@ -0,0 +1,29 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client;
public partial class BTCPayServerClient
{
public virtual async Task<FileData[]> GetFiles(CancellationToken token = default)
{
return await SendHttpRequest<FileData[]>("api/v1/files", null, HttpMethod.Get, token);
}
public virtual async Task<FileData> GetFile(string fileId, CancellationToken token = default)
{
return await SendHttpRequest<FileData>($"api/v1/files/{fileId}", null, HttpMethod.Get, token);
}
public virtual async Task<FileData> UploadFile(string filePath, string mimeType, CancellationToken token = default)
{
return await UploadFileRequest<FileData>("api/v1/files", filePath, mimeType, "file", HttpMethod.Post, token);
}
public virtual async Task DeleteFile(string fileId, CancellationToken token = default)
{
await SendHttpRequest($"api/v1/files/{fileId}", null, HttpMethod.Delete, token);
}
}

View file

@ -37,4 +37,13 @@ public partial class BTCPayServerClient
return await SendHttpRequest<StoreData>($"api/v1/stores/{storeId}", request, HttpMethod.Put, token);
}
public virtual async Task<StoreData> UploadStoreLogo(string storeId, string filePath, string mimeType, CancellationToken token = default)
{
return await UploadFileRequest<StoreData>($"api/v1/stores/{storeId}/logo", filePath, mimeType, "file", HttpMethod.Post, token);
}
public virtual async Task DeleteStoreLogo(string storeId, CancellationToken token = default)
{
await SendHttpRequest($"api/v1/stores/{storeId}/logo", null, HttpMethod.Delete, token);
}
}

View file

@ -18,6 +18,16 @@ public partial class BTCPayServerClient
return await SendHttpRequest<ApplicationUserData>("api/v1/users/me", request, HttpMethod.Put, token);
}
public virtual async Task<ApplicationUserData> UploadCurrentUserProfilePicture(string filePath, string mimeType, CancellationToken token = default)
{
return await UploadFileRequest<ApplicationUserData>("api/v1/users/me/picture", filePath, mimeType, "file", HttpMethod.Post, token);
}
public virtual async Task DeleteCurrentUserProfilePicture(CancellationToken token = default)
{
await SendHttpRequest("api/v1/users/me/picture", null, HttpMethod.Delete, token);
}
public virtual async Task<ApplicationUserData> CreateUser(CreateApplicationUserRequest request, CancellationToken token = default)
{
return await SendHttpRequest<ApplicationUserData>("api/v1/users", request, HttpMethod.Post, token);

View file

@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
@ -152,6 +153,19 @@ public partial class BTCPayServerClient
return request;
}
protected virtual async Task<T> UploadFileRequest<T>(string apiPath, string filePath, string mimeType, string formFieldName, HttpMethod method = null, CancellationToken token = default)
{
using MultipartFormDataContent multipartContent = new();
var fileContent = new StreamContent(File.OpenRead(filePath));
var fileName = Path.GetFileName(filePath);
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType);
multipartContent.Add(fileContent, formFieldName, fileName);
var req = CreateHttpRequest(apiPath, null, method ?? HttpMethod.Post);
req.Content = multipartContent;
using var resp = await _httpClient.SendAsync(req, token);
return await HandleResponse<T>(resp);
}
public static void AppendPayloadToQuery(UriBuilder uri, KeyValuePair<string, object> keyValuePair)
{
if (uri.Query.Length > 1)

View file

@ -0,0 +1,16 @@
using System;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class FileData
{
public string Id { get; set; }
public string UserId { get; set; }
public string Uri { get; set; }
public string Url { get; set; }
public string OriginalName { get; set; }
public string StorageName { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? CreatedAt { get; set; }
}

View file

@ -239,6 +239,76 @@ namespace BTCPayServer.Tests
await newUserClient.GetInvoices(store.Id);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateReadAndDeleteFiles()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.MakeAdmin();
var client = await user.CreateClient();
// List
Assert.Empty(await client.GetFiles());
// Upload
var filePath = TestUtils.GetTestDataFullPath("OldInvoices.csv");
var upload = await client.UploadFile(filePath, "text/csv");
Assert.Equal("OldInvoices.csv", upload.OriginalName);
Assert.NotNull(upload.Uri);
Assert.NotNull(upload.Url);
// Re-check list
Assert.Single(await client.GetFiles());
// Single file endpoint
var singleFile = await client.GetFile(upload.Id);
Assert.Equal("OldInvoices.csv", singleFile.OriginalName);
Assert.NotNull(singleFile.Uri);
Assert.NotNull(singleFile.Url);
// Delete
await client.DeleteFile(upload.Id);
Assert.Empty(await client.GetFiles());
// Profile image
await AssertValidationError(["file"],
async () => await client.UploadCurrentUserProfilePicture(filePath, "text/csv")
);
var profilePath = TestUtils.GetTestDataFullPath("logo.png");
var currentUser = await client.UploadCurrentUserProfilePicture(profilePath, "image/png");
var files = await client.GetFiles();
Assert.Single(files);
Assert.Equal("logo.png", files[0].OriginalName);
Assert.Equal(files[0].Url, currentUser.ImageUrl);
await client.DeleteCurrentUserProfilePicture();
Assert.Empty(await client.GetFiles());
currentUser = await client.GetCurrentUser();
Assert.Null(currentUser.ImageUrl);
// Store logo
var store = await client.CreateStore(new CreateStoreRequest { Name = "mystore" });
await AssertValidationError(["file"],
async () => await client.UploadStoreLogo(store.Id, filePath, "text/csv")
);
var logoPath = TestUtils.GetTestDataFullPath("logo.png");
var storeData = await client.UploadStoreLogo(store.Id, logoPath, "image/png");
files = await client.GetFiles();
Assert.Single(files);
Assert.Equal("logo.png", files[0].OriginalName);
Assert.Equal(files[0].Url, storeData.LogoUrl);
await client.DeleteStoreLogo(store.Id);
Assert.Empty(await client.GetFiles());
storeData = await client.GetStore(store.Id);
Assert.Null(storeData.LogoUrl);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateReadUpdateAndDeletePointOfSaleApp()

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -161,10 +161,14 @@ namespace BTCPayServer.Tests
{
errors.Remove(validationError);
}
valid = !errors.Any();
if (errors.Any())
{
foreach (ValidationError error in errors)
{
TestLogs.LogInformation($"Error Type: {error.ErrorType} - {error.Path}: {error.Message} - Value: {error.Value}");
}
}
Assert.Empty(errors);
Assert.True(valid);
}
[Fact]

View file

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Storage.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.Greenfield;
[ApiController]
[EnableCors(CorsPolicies.All)]
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenfieldFilesController(
UserManager<ApplicationUser> userManager,
IFileService fileService,
StoredFileRepository fileRepository)
: Controller
{
[HttpGet("~/api/v1/files")]
public async Task<IActionResult> GetFiles()
{
var storedFiles = await fileRepository.GetFiles();
var files = new List<FileData>();
foreach (var file in storedFiles)
files.Add(await ToFileData(file));
return Ok(files);
}
[HttpGet("~/api/v1/files/{fileId}")]
public async Task<IActionResult> GetFile(string fileId)
{
var file = await fileRepository.GetFile(fileId);
return file == null
? this.CreateAPIError(404, "file-not-found", "The file does not exist.")
: Ok(await ToFileData(file));
}
[HttpPost("~/api/v1/files")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
if (file is null)
ModelState.AddModelError(nameof(file), "Invalid file");
else if (!file.FileName.IsValidFileName())
ModelState.AddModelError(nameof(file.FileName), "Invalid filename");
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
try
{
var userId = userManager.GetUserId(User)!;
var newFile = await fileService.AddFile(file!, userId);
return Ok(await ToFileData(newFile));
}
catch (Exception e)
{
return this.CreateAPIError(404, "file-upload-failed", e.Message);
}
}
[HttpDelete("~/api/v1/files/{fileId}")]
public async Task<IActionResult> DeleteFile(string fileId)
{
var file = await fileRepository.GetFile(fileId);
if (file == null) return this.CreateAPIError(404, "file-not-found", "The file does not exist.");
await fileRepository.RemoveFile(file);
return Ok();
}
private async Task<FileData> ToFileData(IStoredFile file)
{
return new FileData
{
Id = file.Id,
UserId = file.ApplicationUserId,
Uri = new UnresolvedUri.FileIdUri(file.Id).ToString(),
Url = await fileService.GetFileUrl(Request.GetAbsoluteRootUri(), file.Id),
OriginalName = file.FileName,
StorageName = file.StorageFileName,
CreatedAt = file.Timestamp
};
}
}

View file

@ -3,12 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@ -27,31 +28,40 @@ namespace BTCPayServer.Controllers.Greenfield
{
private readonly StoreRepository _storeRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IFileService _fileService;
private readonly UriResolver _uriResolver;
public GreenfieldStoresController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
public GreenfieldStoresController(
StoreRepository storeRepository,
UserManager<ApplicationUser> userManager,
IFileService fileService,
UriResolver uriResolver)
{
_storeRepository = storeRepository;
_userManager = userManager;
_fileService = fileService;
_uriResolver = uriResolver;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores")]
public Task<ActionResult<IEnumerable<Client.Models.StoreData>>> GetStores()
public async Task<ActionResult<IEnumerable<Client.Models.StoreData>>> GetStores()
{
var stores = HttpContext.GetStoresData();
return Task.FromResult<ActionResult<IEnumerable<Client.Models.StoreData>>>(Ok(stores.Select(FromModel)));
var storesData = HttpContext.GetStoresData();
var stores = new List<Client.Models.StoreData>();
foreach (var storeData in storesData)
{
stores.Add(await FromModel(storeData));
}
return Ok(stores);
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}")]
public IActionResult GetStore(string storeId)
public async Task<IActionResult> GetStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
return Ok(FromModel(store));
return store == null ? StoreNotFound() : Ok(await FromModel(store));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -59,10 +69,8 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> RemoveStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
if (store == null) return StoreNotFound();
await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User));
return Ok();
}
@ -72,17 +80,13 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> CreateStore(CreateStoreRequest request)
{
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
var store = new Data.StoreData();
if (validationResult != null) return validationResult;
var store = new StoreData();
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId);
ToModel(request, store, defaultPaymentMethodId);
await _storeRepository.CreateStore(_userManager.GetUserId(User), store);
return Ok(FromModel(store));
return Ok(await FromModel(store));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -90,24 +94,78 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> UpdateStore(string storeId, UpdateStoreRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
if (store == null) return StoreNotFound();
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
if (validationResult != null) return validationResult;
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId);
ToModel(request, store, defaultPaymentMethodId);
await _storeRepository.UpdateStore(store);
return Ok(FromModel(store));
return Ok(await FromModel(store));
}
internal static Client.Models.StoreData FromModel(StoreData data)
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/logo")]
public async Task<IActionResult> UploadStoreLogo(string storeId, IFormFile file)
{
var store = HttpContext.GetStoreData();
if (store == null) return StoreNotFound();
if (file is null)
ModelState.AddModelError(nameof(file), "Invalid file");
else if (file.Length > 1_000_000)
ModelState.AddModelError(nameof(file), "The uploaded image file should be less than 1MB");
else if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
ModelState.AddModelError(nameof(file), "The uploaded file needs to be an image");
else if (!file.FileName.IsValidFileName())
ModelState.AddModelError(nameof(file.FileName), "Invalid filename");
else
{
var formFile = await file.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
ModelState.AddModelError(nameof(file), "The uploaded file needs to be an image");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
try
{
var userId = _userManager.GetUserId(User)!;
var storedFile = await _fileService.AddFile(file!, userId);
var blob = store.GetStoreBlob();
blob.LogoUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
store.SetStoreBlob(blob);
await _storeRepository.UpdateStore(store);
return Ok(await FromModel(store));
}
catch (Exception e)
{
return this.CreateAPIError(404, "file-upload-failed", e.Message);
}
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/logo")]
public async Task<IActionResult> DeleteStoreLogo(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null) return StoreNotFound();
var blob = store.GetStoreBlob();
var fileId = (blob.LogoUrl as UnresolvedUri.FileIdUri)?.FileId;
if (!string.IsNullOrEmpty(fileId))
{
var userId = _userManager.GetUserId(User)!;
await _fileService.RemoveFile(fileId, userId);
blob.LogoUrl = null;
store.SetStoreBlob(blob);
await _storeRepository.UpdateStore(store);
}
return Ok();
}
internal async Task<Client.Models.StoreData> FromModel(StoreData data)
{
var storeBlob = data.GetStoreBlob();
return new Client.Models.StoreData
@ -117,9 +175,9 @@ namespace BTCPayServer.Controllers.Greenfield
Website = data.StoreWebsite,
Archived = data.Archived,
BrandColor = storeBlob.BrandColor,
CssUrl = storeBlob.CssUrl?.ToString(),
LogoUrl = storeBlob.LogoUrl?.ToString(),
PaymentSoundUrl = storeBlob.PaymentSoundUrl?.ToString(),
CssUrl = storeBlob.CssUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.CssUrl),
LogoUrl = storeBlob.LogoUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.LogoUrl),
PaymentSoundUrl = storeBlob.PaymentSoundUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.PaymentSoundUrl),
SupportUrl = storeBlob.StoreSupportUrl,
SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToString(),

View file

@ -1,10 +1,8 @@
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
@ -22,14 +20,14 @@ namespace BTCPayServer.Controllers.Greenfield
public class GreenfieldTestApiKeyController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly StoreRepository _storeRepository;
private readonly BTCPayServerClient _localBTCPayServerClient;
private readonly GreenfieldStoresController _greenfieldStoresController;
public GreenfieldTestApiKeyController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository, BTCPayServerClient localBTCPayServerClient)
public GreenfieldTestApiKeyController(
UserManager<ApplicationUser> userManager,
GreenfieldStoresController greenfieldStoresController)
{
_userManager = userManager;
_storeRepository = storeRepository;
_localBTCPayServerClient = localBTCPayServerClient;
_greenfieldStoresController = greenfieldStoresController;
}
[HttpGet("me/id")]
@ -55,9 +53,15 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpGet("me/stores")]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public BTCPayServer.Client.Models.StoreData[] GetCurrentUserStores()
public async Task<BTCPayServer.Client.Models.StoreData[]> GetCurrentUserStores()
{
return this.HttpContext.GetStoresData().Select(Greenfield.GreenfieldStoresController.FromModel).ToArray();
var storesData = HttpContext.GetStoresData();
var stores = new List<Client.Models.StoreData>();
foreach (var storeData in storesData)
{
stores.Add(await _greenfieldStoresController.FromModel(storeData));
}
return stores.ToArray();
}
[HttpGet("me/stores/{storeId}/can-view")]

View file

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
@ -17,6 +18,7 @@ using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NicolasDorier.RateLimits;
@ -41,6 +43,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly IAuthorizationService _authorizationService;
private readonly UserService _userService;
private readonly UriResolver _uriResolver;
private readonly IFileService _fileService;
public GreenfieldUsersController(UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
@ -53,6 +56,7 @@ namespace BTCPayServer.Controllers.Greenfield
IAuthorizationService authorizationService,
UserService userService,
UriResolver uriResolver,
IFileService fileService,
Logs logs)
{
this.Logs = logs;
@ -67,6 +71,7 @@ namespace BTCPayServer.Controllers.Greenfield
_authorizationService = authorizationService;
_userService = userService;
_uriResolver = uriResolver;
_fileService = fileService;
}
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -203,7 +208,7 @@ namespace BTCPayServer.Controllers.Greenfield
user.SetBlob(blob);
if (ModelState.IsValid && needUpdate)
{
{
var identityResult = await _userManager.UpdateAsync(user);
if (!identityResult.Succeeded)
{
@ -224,6 +229,68 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(model);
}
[Authorize(Policy = Policies.CanModifyProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/users/me/picture")]
public async Task<IActionResult> UploadCurrentUserProfilePicture(IFormFile? file)
{
if (file is null)
ModelState.AddModelError(nameof(file), "Invalid file");
else if (file.Length > 1_000_000)
ModelState.AddModelError(nameof(file), "The uploaded image file should be less than 1MB");
else if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
ModelState.AddModelError(nameof(file), "The uploaded file needs to be an image");
else if (!file.FileName.IsValidFileName())
ModelState.AddModelError(nameof(file.FileName), "Invalid filename");
else
{
var formFile = await file.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
ModelState.AddModelError(nameof(file), "The uploaded file needs to be an image");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
try
{
var user = await _userManager.GetUserAsync(User);
var storedFile = await _fileService.AddFile(file!, user!.Id);
var blob = user.GetBlob() ?? new UserBlob();
var fileIdUri = new UnresolvedUri.FileIdUri(storedFile.Id);
blob.ImageUrl = fileIdUri.ToString();
user.SetBlob(blob);
await _userManager.UpdateAsync(user);
var model = await FromModel(user);
return Ok(model);
}
catch (Exception e)
{
return this.CreateAPIError(404, "file-upload-failed", e.Message);
}
}
[Authorize(Policy = Policies.CanModifyProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/users/me/picture")]
public async Task<IActionResult> DeleteCurrentUserProfilePicture()
{
var user = await _userManager.GetUserAsync(User);
if (user is null)
{
return this.UserNotFound();
}
var blob = user.GetBlob() ?? new UserBlob();
if (!string.IsNullOrEmpty(blob.ImageUrl))
{
var fileId = (UnresolvedUri.Create(blob.ImageUrl) as UnresolvedUri.FileIdUri)?.FileId;
if (!string.IsNullOrEmpty(fileId)) await _fileService.RemoveFile(fileId, user.Id);
blob.ImageUrl = null;
user.SetBlob(blob);
await _userManager.UpdateAsync(user);
}
return Ok();
}
[Authorize(Policy = Policies.CanDeleteUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/users/me")]
public async Task<IActionResult> DeleteCurrentUser()

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
@ -19,6 +20,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NBitcoin;
@ -772,9 +774,9 @@ namespace BTCPayServer.Controllers.Greenfield
return GetFromActionResult(await GetController<GreenfieldStoresController>().GetStores());
}
public override Task<StoreData> GetStore(string storeId, CancellationToken token = default)
public override async Task<StoreData> GetStore(string storeId, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<StoreData>(GetController<GreenfieldStoresController>().GetStore(storeId)));
return GetFromActionResult<StoreData>(await GetController<GreenfieldStoresController>().GetStore(storeId));
}
public override async Task RemoveStore(string storeId, CancellationToken token = default)
@ -793,6 +795,17 @@ namespace BTCPayServer.Controllers.Greenfield
return GetFromActionResult<StoreData>(await GetController<GreenfieldStoresController>().UpdateStore(storeId, request));
}
public override async Task<StoreData> UploadStoreLogo(string storeId, string filePath, string mimeType, CancellationToken token = default)
{
var file = GetFormFile(filePath, mimeType);
return GetFromActionResult<StoreData>(await GetController<GreenfieldStoresController>().UploadStoreLogo(storeId, file));
}
public override async Task DeleteStoreLogo(string storeId, CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldStoresController>().DeleteStoreLogo(storeId));
}
public override async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string[] orderId = null,
InvoiceStatus[] status = null,
DateTimeOffset? startDate = null,
@ -880,6 +893,17 @@ namespace BTCPayServer.Controllers.Greenfield
return GetFromActionResult<ApplicationUserData>(await GetController<GreenfieldUsersController>().UpdateCurrentUser(request, token));
}
public override async Task<ApplicationUserData> UploadCurrentUserProfilePicture(string filePath, string mimeType, CancellationToken token = default)
{
var file = GetFormFile(filePath, mimeType);
return GetFromActionResult<ApplicationUserData>(await GetController<GreenfieldUsersController>().UploadCurrentUserProfilePicture(file));
}
public override async Task DeleteCurrentUserProfilePicture(CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldUsersController>().DeleteCurrentUserProfilePicture());
}
public override async Task DeleteCurrentUser(CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldUsersController>().DeleteCurrentUser());
@ -1251,5 +1275,37 @@ namespace BTCPayServer.Controllers.Greenfield
{
return GetFromActionResult<List<RoleData>>(await GetController<GreenfieldStoreRolesController>().GetStoreRoles(storeId));
}
public override async Task<FileData[]> GetFiles(CancellationToken token = default)
{
return GetFromActionResult<FileData[]>(await GetController<GreenfieldFilesController>().GetFiles());
}
public override async Task<FileData> GetFile(string fileId, CancellationToken token = default)
{
return GetFromActionResult<FileData>(await GetController<GreenfieldFilesController>().GetFile(fileId));
}
public override async Task<FileData> UploadFile(string filePath, string mimeType, CancellationToken token = default)
{
var file = GetFormFile(filePath, mimeType);
return GetFromActionResult<FileData>(await GetController<GreenfieldFilesController>().UploadFile(file));
}
public override async Task DeleteFile(string fileId, CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldFilesController>().DeleteFile(fileId));
}
private IFormFile GetFormFile(string filePath, string mimeType)
{
var fileName = Path.GetFileName(filePath);
var fs = File.OpenRead(filePath);
return new FormFile(fs, 0, fs.Length, fileName, fileName)
{
Headers = new HeaderDictionary(),
ContentType = mimeType
};
}
}
}

View file

@ -1,3 +1,4 @@
#nullable enable
using System;
namespace BTCPayServer

View file

@ -0,0 +1,223 @@
{
"paths": {
"/api/v1/files": {
"get": {
"operationId": "Files_GetFiles",
"tags": [
"Files"
],
"summary": "Get all files",
"description": "Load all files that exist.",
"parameters": [],
"responses": {
"200": {
"description": "Files found",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileData"
}
}
}
}
},
"401": {
"description": "Missing authorization for loading the files"
}
},
"security": [
{
"API_Key": [
"btcpay.server.canmodifyserversettings"
],
"Basic": []
}
]
},
"post": {
"tags": [
"Files"
],
"summary": "Uploads a file",
"description": "Uploads a file",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"file": {
"type": "string",
"description": "The profile picture",
"format": "binary"
}
}
}
}
}
},
"operationId": "Files_UploadFile",
"responses": {
"200": {
"description": "Uploads a file",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileData"
}
}
}
},
"415": {
"description": "The upload did not work"
}
},
"security": [
{
"API_Key": [
"btcpay.server.canmodifyserversettings"
],
"Basic": []
}
]
}
},
"/api/v1/files/{fileId}": {
"get": {
"operationId": "Files_GetFile",
"tags": [
"Files"
],
"summary": "Get file",
"description": "View information about the specified file",
"parameters": [
{
"name": "fileId",
"in": "path",
"required": true,
"description": "The file information to fetch",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "File found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileData"
}
}
}
},
"401": {
"description": "Missing authorization for loading the file"
}
},
"security": [
{
"API_Key": [
"btcpay.server.canmodifyserversettings"
],
"Basic": []
}
]
},
"delete": {
"tags": [
"Files"
],
"summary": "Delete file",
"description": "Deletes the file",
"operationId": "Files_DeleteFile",
"parameters": [
{
"name": "fileId",
"in": "path",
"required": true,
"description": "The file to delete",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "File deleted successfully"
},
"404": {
"description": "The file could not be found"
}
},
"security": [
{
"API_Key": [
"btcpay.server.canmodifyserversettings"
],
"Basic": []
}
]
}
}
},
"components": {
"schemas": {
"FileData": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "The id of the file",
"nullable": false
},
"userId": {
"type": "string",
"description": "The id of the user that uploaded the file",
"nullable": false
},
"uri": {
"type": "string",
"description": "The internal URI of the file",
"nullable": false
},
"url": {
"type": "string",
"description": "The full URL of the file",
"nullable": true
},
"originalName": {
"type": "string",
"description": "The original name of the file",
"nullable": true
},
"storageName": {
"type": "string",
"description": "The storage name of the file",
"nullable": true
},
"created": {
"nullable": true,
"description": "The creation date of the file as a unix timestamp",
"allOf": [
{
"$ref": "#/components/schemas/UnixTimestamp"
}
]
}
}
}
}
},
"tags": [
{
"name": "Files",
"description": "File operations"
}
]
}

View file

@ -255,6 +255,80 @@
]
}
},
"/api/v1/stores/{storeId}/logo": {
"post": {
"tags": [
"Stores"
],
"summary": "Uploads a logo for the store",
"description": "Uploads a logo for the store",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"file": {
"type": "string",
"description": "The logo",
"format": "binary"
}
}
}
}
}
},
"operationId": "Stores_UploadStoreLogo",
"responses": {
"200": {
"description": "Uploads a logo for the store",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUserData"
}
}
}
},
"404": {
"description": "The store could not be found"
}
},
"security": [
{
"API_Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
},
"delete": {
"tags": [
"Stores"
],
"summary": "Deletes the store logo",
"description": "Delete the store's logo",
"operationId": "Stores_DeleteStoreLogo",
"responses": {
"200": {
"description": "Store logo deleted successfully"
},
"404": {
"description": "The store could not be found"
}
},
"security": [
{
"API_Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
}
},
"/api/v1/stores/{storeId}/roles": {
"get": {
"tags": [

View file

@ -128,6 +128,80 @@
]
}
},
"/api/v1/users/me/picture": {
"post": {
"tags": [
"Users"
],
"summary": "Uploads a profile picture for the current user",
"description": "Uploads a profile picture for the current user",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"file": {
"type": "string",
"description": "The profile picture",
"format": "binary"
}
}
}
}
}
},
"operationId": "Users_UploadCurrentUserProfilePicture",
"responses": {
"200": {
"description": "Uploads a profile picture for the current user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUserData"
}
}
}
},
"404": {
"description": "The user could not be found"
}
},
"security": [
{
"API_Key": [
"btcpay.user.canmodifyprofile"
],
"Basic": []
}
]
},
"delete": {
"tags": [
"Users"
],
"summary": "Deletes user profile picture",
"description": "Deletes the user profile picture",
"operationId": "Users_DeleteCurrentUserProfilePicture",
"responses": {
"200": {
"description": "Profile picture deleted successfully"
},
"404": {
"description": "The user could not be found"
}
},
"security": [
{
"API_Key": [
"btcpay.user.canmodifyprofile"
],
"Basic": []
}
]
}
},
"/api/v1/users": {
"get": {
"operationId": "Users_GetUsers",