File upload for store logo and user profile picture

This commit is contained in:
Dennis Reimann 2024-07-01 19:51:01 +02:00
parent 908b0d8a37
commit 85643b8b2e
No known key found for this signature in database
GPG Key ID: 5009E1797F03F8D0
3 changed files with 185 additions and 7 deletions

View File

@ -1,10 +1,13 @@
@attribute [Route(Routes.Store)]
@using BTCPayApp.Core.Auth
@using BTCPayApp.Core.Contracts
@using BTCPayApp.UI.Components.Layout
@using BTCPayApp.UI.Features
@using BTCPayServer.Client
@using BTCPayServer.Client.Models
@using Microsoft.AspNetCore.StaticFiles
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IDataDirectoryProvider DataDirectoryProvider
@inject IAccountManager AccountManager
@inject NavigationManager NavigationManager
@inject IState<StoreState> StoreState
@ -46,7 +49,7 @@
</div>
@if (_canView is true)
{
<ValidationEditContext @ref="_validationEditContext" Model="Model" OnValidSubmit="HandleValidSubmit" SuccessMessage="@_successMessage" ErrorMessage="@Error">
<ValidationEditContext @ref="_validationEditContext" Model="Model" OnValidSubmit="HandleValidSubmit" SuccessMessage="@_successMessage" ErrorMessage="@Error" class="mb-5">
<DataAnnotationsValidator/>
<fieldset disabled="@(_canModify is not true)">
<fieldset class="box">
@ -62,6 +65,34 @@
</div>
</fieldset>
<h2>Branding</h2>
<fieldset class="box">
<div class="form-group">
<label for="BrandColor" class="form-label">Brand Color</label>
<div class="input-group">
<InputText @bind-Value="Model.BrandColor" class="form-control form-control-color flex-grow-0" id="BrandColor" type="color" style="width:3rem" aria-describedby="BrandColorValue"/>
<InputText @bind-Value="Model.BrandColor" class="form-control form-control-color flex-grow-0 font-monospace" pattern="^#[0-9a-fA-F]{6}$" style="width:5.5rem;font-size:0.9rem"/>
</div>
<ValidationMessage For="@(() => Model.BrandColor)"/>
</div>
<div class="d-flex align-items-center justify-content-between gap-2">
<label for="Logo" class="form-label">Logo</label>
@if (!string.IsNullOrEmpty(Model.LogoUrl))
{
<button type="button" class="btn btn-link p-0 text-danger" @onclick="UnsetLogo">
<Icon Symbol="cross" /> Remove
</button>
}
</div>
<div class="d-flex align-items-center gap-3">
<InputFile OnChange="LoadLogo" @key="@_inputFileId" id="Logo" class="form-control"/>
@if (!string.IsNullOrEmpty(Model.LogoUrl))
{
<img src="@Model.LogoUrl" alt="Logo" class="logo"/>
}
</div>
</fieldset>
<h2>Payment</h2>
<fieldset class="box">
<div class="form-group">
@ -179,8 +210,10 @@
private ValidationEditContext? _validationEditContext;
private StoreModel? Model { get; set; }
private string? _successMessage;
private string? _errorMessage;
private bool? _canView;
private bool? _canModify;
private Guid _inputFileId = Guid.NewGuid();
protected override async Task OnInitializedAsync()
{
@ -233,10 +266,29 @@
_successMessage = null;
var store = Store!;
if (!string.IsNullOrEmpty(Model!.LogoPath))
{
var path = Model.LogoPath;
var mimeType = GetContentType(path);
var upload = await AccountManager.GetClient().UploadStoreLogo(store.Id, path, mimeType);
Model.LogoUrl = upload.LogoUrl;
// cleanup
File.Delete(path);
Model.LogoPath = null;
_inputFileId = Guid.NewGuid();
}
else if (string.IsNullOrEmpty(Model!.LogoUrl))
{
await AccountManager.GetClient().DeleteStoreLogo(store.Id);
}
Dispatcher.Dispatch(new StoreState.UpdateStore(StoreId!, new UpdateStoreRequest
{
Name = Model!.Name,
Website = Model.Website,
BrandColor = Model.BrandColor,
LogoUrl = Model.LogoUrl,
DefaultCurrency = Model.DefaultCurrency,
AnyoneCanCreateInvoice = Model.AnyoneCanCreateInvoice,
NetworkFeeMode = Model.NetworkFeeMode,
@ -249,8 +301,6 @@
OnChainWithLnInvoiceFallback = Model.OnChainWithLnInvoiceFallback,
PlaySoundOnPayment = Model.PlaySoundOnPayment,
// pass these attributes to not reset them
BrandColor = store.BrandColor,
LogoUrl = store.LogoUrl,
CssUrl = store.CssUrl,
PaymentSoundUrl = store.PaymentSoundUrl,
SupportUrl = store.SupportUrl,
@ -281,6 +331,8 @@
{
Name = data.Name,
Website = data.Website,
BrandColor = data.BrandColor,
LogoUrl = data.LogoUrl,
DefaultCurrency = data.DefaultCurrency,
AnyoneCanCreateInvoice = data.AnyoneCanCreateInvoice,
NetworkFeeMode = data.NetworkFeeMode,
@ -298,7 +350,46 @@
private StoreData? Store => StoreState.Value.Store?.Data;
private bool Loading => StoreState.Value.Store?.Loading is true;
private bool Sending => StoreState.Value.Store?.Sending is true;
private string? Error => StoreState.Value.Store?.Error;
private string? Error => _errorMessage ?? StoreState.Value.Store?.Error;
private async Task LoadLogo(InputFileChangeEventArgs e)
{
try
{
var appData = await DataDirectoryProvider.GetAppDataDirectory();
var fileName = e.File.Name;
var dirPath = Path.Combine(appData, "tmp");
var filePath = Path.Combine(dirPath, fileName);
Directory.CreateDirectory(dirPath);
await using FileStream fs = new(filePath, FileMode.Create);
await e.File.OpenReadStream().CopyToAsync(fs);
await fs.FlushAsync();
Model!.LogoPath = filePath;
_errorMessage = null;
}
catch (Exception ex)
{
_errorMessage = $"Profile picture could not be applied: {ex.Message}";
}
}
private void UnsetLogo()
{
Model!.LogoUrl = null;
Model.LogoPath = null;
_inputFileId = Guid.NewGuid();
}
private static string GetContentType(string filePath)
{
var mimeProvider = new FileExtensionContentTypeProvider();
if (!mimeProvider.TryGetContentType(filePath, out var contentType))
{
contentType = "application/octet-stream";
}
return contentType;
}
private class StoreModel
{
@ -307,6 +398,12 @@
[Url]
public string? Website { get; set; }
// Branding
public string? BrandColor { get; set; }
[Url]
public string? LogoUrl { get; set; }
public string? LogoPath { get; set; }
// Payment
[Required]
public string? DefaultCurrency { get; set; }

View File

@ -0,0 +1,6 @@
.logo {
height: var(--profile-picture-size, 2.1rem);
width: var(--profile-picture-size, 2.1rem);
border-radius: 50%;
object-fit: cover;
}

View File

@ -3,8 +3,11 @@
@using BTCPayApp.CommonServer.Models
@using BTCPayApp.Core
@using BTCPayApp.Core.Auth
@using BTCPayApp.Core.Contracts
@using BTCPayApp.UI.Components.Layout
@using Microsoft.AspNetCore.StaticFiles
@inject IAccountManager AccountManager
@inject IDataDirectoryProvider DataDirectoryProvider
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
<PageTitle>User</PageTitle>
@ -28,7 +31,7 @@
}
</div>
<div class="box">
<ValidationEditContext Model="Account" OnValidSubmit="HandleValidAccountSubmit" SuccessMessage="@_accountSuccessMessage" ErrorMessage="@_accountErrorMessage">
<ValidationEditContext Model="Account" OnValidSubmit="HandleValidAccountSubmit" SuccessMessage="@_accountSuccessMessage" ErrorMessage="@_accountErrorMessage" enctype="multipart/form-data">
<DataAnnotationsValidator />
<div class="form-group">
<label for="Email" class="form-label" data-required>Email</label>
@ -41,10 +44,25 @@
<ValidationMessage For="@(() => Account.Name)" />
</div>
<div class="form-group">
<label for="ImageUrl" class="form-label">Image Url</label>
<InputText @bind-Value="Account.ImageUrl" id="ImageUrl" type="url" class="form-control"/>
<div class="d-flex align-items-center justify-content-between gap-2">
<label for="ProfilePicture" class="form-label">Profile Picture</label>
@if (!string.IsNullOrEmpty(Account.ImageUrl))
{
<button type="button" class="btn btn-link p-0 text-danger" @onclick="UnsetProfilePicture">
<Icon Symbol="cross" /> Remove
</button>
}
</div>
<div class="d-flex align-items-center gap-3">
<InputFile OnChange="LoadProfilePicture" @key="@_inputFileId" id="ProfilePicture" class="form-control"/>
@if (!string.IsNullOrEmpty(Account.ImageUrl))
{
<img src="@Account.ImageUrl" alt="Profile picture" class="profile-picture"/>
}
</div>
<ValidationMessage For="@(() => Account.ImageUrl)" />
</div>
<button type="submit" class="btn btn-primary w-100" disabled="@(_accountSending)">
@if (_accountSending)
{
@ -115,6 +133,7 @@
private bool _passwordSending;
private string? _passwordErrorMessage;
private string? _passwordSuccessMessage;
private Guid _inputFileId = Guid.NewGuid();
protected override async Task OnInitializedAsync()
{
@ -148,6 +167,22 @@
_accountErrorMessage = _accountSuccessMessage = null;
_accountSending = true;
if (!string.IsNullOrEmpty(Account!.ImagePath))
{
var path = Account.ImagePath;
var mimeType = GetContentType(path);
var upload = await AccountManager.GetClient().UploadCurrentUserProfilePicture(path, mimeType);
Account.ImageUrl = upload.ImageUrl;
// cleanup
File.Delete(path);
Account.ImagePath = null;
_inputFileId = Guid.NewGuid();
}
else if (string.IsNullOrEmpty(Account!.ImageUrl))
{
await AccountManager.GetClient().DeleteCurrentUserProfilePicture();
}
var result = await AccountManager.ChangeAccountInfo(Account.Email!, Account.Name, Account.ImageUrl);
_accountSending = false;
@ -187,6 +222,45 @@
}
}
private async Task LoadProfilePicture(InputFileChangeEventArgs e)
{
try
{
var appData = await DataDirectoryProvider.GetAppDataDirectory();
var fileName = e.File.Name;
var dirPath = Path.Combine(appData, "tmp");
var filePath = Path.Combine(dirPath, fileName);
Directory.CreateDirectory(dirPath);
await using FileStream fs = new(filePath, FileMode.Create);
await e.File.OpenReadStream().CopyToAsync(fs);
await fs.FlushAsync();
Account!.ImagePath = filePath;
_accountErrorMessage = null;
}
catch (Exception ex)
{
_accountErrorMessage = $"Profile picture could not be applied: {ex.Message}";
}
}
private void UnsetProfilePicture()
{
Account!.ImageUrl = null;
Account.ImagePath = null;
_inputFileId = Guid.NewGuid();
}
private static string GetContentType(string filePath)
{
var mimeProvider = new FileExtensionContentTypeProvider();
if (!mimeProvider.TryGetContentType(filePath, out var contentType))
{
contentType = "application/octet-stream";
}
return contentType;
}
private class AccountModel
{
[Required, EmailAddress]
@ -194,6 +268,7 @@
public string? Name { get; set; }
[Url]
public string? ImageUrl { get; set; }
public string? ImagePath { get; set; }
}
private class PasswordModel