mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 22:11:48 +01:00
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:
parent
249b991185
commit
25ae6df095
17 changed files with 852 additions and 52 deletions
29
BTCPayServer.Client/BTCPayServerClient.Files.cs
Normal file
29
BTCPayServer.Client/BTCPayServerClient.Files.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
16
BTCPayServer.Client/Models/FileData.cs
Normal file
16
BTCPayServer.Client/Models/FileData.cs
Normal 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; }
|
||||
}
|
|
@ -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()
|
||||
|
|
BIN
BTCPayServer.Tests/TestData/logo.png
Normal file
BIN
BTCPayServer.Tests/TestData/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer
|
||||
|
|
223
BTCPayServer/wwwroot/swagger/v1/swagger.template.files.json
Normal file
223
BTCPayServer/wwwroot/swagger/v1/swagger.template.files.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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": [
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue