mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
User: Add name and image URL (#6008)
* User: Add name and image URL More personalization options, prerequisite for btcpayserver/app#3. Additionally: - Remove ambigious and read-only username from manage view. - Improve email verification conditions and display. - Greenfield: Update current user. Prerequisite for btcpayserver/app#13. * Refactor UpdateCurrentUser * Replace new columns by UserBlob * Update email check and add test case for mailbox addresses --------- Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
1ba7b67e70
commit
bf66b54c9a
24 changed files with 632 additions and 71 deletions
|
@ -13,6 +13,11 @@ public partial class BTCPayServerClient
|
|||
return await SendHttpRequest<ApplicationUserData>("api/v1/users/me", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<ApplicationUserData> UpdateCurrentUser(UpdateApplicationUserRequest request, CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<ApplicationUserData>("api/v1/users/me", request, HttpMethod.Put, token);
|
||||
}
|
||||
|
||||
public virtual async Task<ApplicationUserData> CreateUser(CreateApplicationUserRequest request, CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<ApplicationUserData>("api/v1/users", request, HttpMethod.Post, token);
|
||||
|
|
|
@ -14,6 +14,16 @@ namespace BTCPayServer.Client.Models
|
|||
/// the email AND username of the user
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the name of the user
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the image url of the user
|
||||
/// </summary>
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user has verified their email
|
||||
|
|
|
@ -2,6 +2,16 @@ namespace BTCPayServer.Client.Models
|
|||
{
|
||||
public class CreateApplicationUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// the name of the new user
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the image url of the new user
|
||||
/// </summary>
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the email AND username of the new user
|
||||
/// </summary>
|
||||
|
|
29
BTCPayServer.Client/Models/UpdateApplicationUserRequest.cs
Normal file
29
BTCPayServer.Client/Models/UpdateApplicationUserRequest.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class UpdateApplicationUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// the name of the user
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the image url of the user
|
||||
/// </summary>
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the email AND username of the user
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// current password of the user
|
||||
/// </summary>
|
||||
public string CurrentPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// new password of the user
|
||||
/// </summary>
|
||||
public string NewPassword { get; set; }
|
||||
}
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
|
@ -19,7 +18,7 @@ namespace BTCPayServer.Data
|
|||
public List<APIKeyData> APIKeys { get; set; }
|
||||
public DateTimeOffset? Created { get; set; }
|
||||
public string DisabledNotifications { get; set; }
|
||||
|
||||
|
||||
public List<NotificationData> Notifications { get; set; }
|
||||
public List<UserStore> UserStores { get; set; }
|
||||
public List<Fido2Credential> Fido2Credentials { get; set; }
|
||||
|
@ -45,5 +44,7 @@ namespace BTCPayServer.Data
|
|||
public class UserBlob
|
||||
{
|
||||
public bool ShowInvoiceStatusChangeHint { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,7 +200,13 @@ namespace BTCPayServer.Tests
|
|||
acc = tester.NewAccount();
|
||||
await acc.GrantAccessAsync(isAdmin: true);
|
||||
unrestricted = await acc.CreateClient();
|
||||
var newUser = await unrestricted.CreateUser(new CreateApplicationUserRequest() { Email = Utils.GenerateEmail(), Password = "Kitten0@" });
|
||||
var newUser = await unrestricted.CreateUser(new CreateApplicationUserRequest
|
||||
{
|
||||
Email = Utils.GenerateEmail(),
|
||||
Password = "Kitten0@",
|
||||
Name = "New User",
|
||||
ImageUrl = "avatar.jpg"
|
||||
});
|
||||
var newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Id, new CreateApiKeyRequest()
|
||||
{
|
||||
Label = "Hello world",
|
||||
|
@ -208,6 +214,8 @@ namespace BTCPayServer.Tests
|
|||
});
|
||||
var newUserClient = acc.CreateClientFromAPIKey(newUserAPIKey.ApiKey);
|
||||
Assert.Equal(newUser.Id, (await newUserClient.GetCurrentUser()).Id);
|
||||
Assert.Equal("New User", newUser.Name);
|
||||
Assert.Equal("avatar.jpg", newUser.ImageUrl);
|
||||
// Admin delete it
|
||||
await unrestricted.RevokeAPIKey(newUser.Id, newUserAPIKey.ApiKey);
|
||||
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetCurrentUser());
|
||||
|
@ -846,6 +854,63 @@ namespace BTCPayServer.Tests
|
|||
new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" });
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUpdateUsersViaAPI()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
tester.PayTester.DisableRegistration = true;
|
||||
await tester.StartAsync();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
|
||||
// We have no admin, so it should work
|
||||
var user = await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest { Email = "test@gmail.com", Password = "abceudhqw" });
|
||||
Assert.Empty(user.Roles);
|
||||
|
||||
// We have no admin, so it should work
|
||||
var admin = await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest { Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true });
|
||||
Assert.Contains("ServerAdmin", admin.Roles);
|
||||
|
||||
var adminAcc = tester.NewAccount();
|
||||
adminAcc.UserId = admin.Id;
|
||||
adminAcc.IsAdmin = true;
|
||||
var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile);
|
||||
|
||||
// Invalid email
|
||||
await AssertValidationError(["Email"],
|
||||
async () => await adminClient.UpdateCurrentUser(
|
||||
new UpdateApplicationUserRequest { Email = "test@" }));
|
||||
await AssertValidationError(["Email"],
|
||||
async () => await adminClient.UpdateCurrentUser(
|
||||
new UpdateApplicationUserRequest { Email = "Firstname Lastname <blah@example.com>" }));
|
||||
|
||||
// Duplicate email
|
||||
await AssertValidationError(["Email"],
|
||||
async () => await adminClient.UpdateCurrentUser(
|
||||
new UpdateApplicationUserRequest { Email = "test@gmail.com" }));
|
||||
|
||||
// Invalid current password
|
||||
await AssertValidationError(["CurrentPassword"],
|
||||
async () => await adminClient.UpdateCurrentUser(
|
||||
new UpdateApplicationUserRequest { Email = "test@gmail.com", CurrentPassword = "123", NewPassword = "abceudhqw123"}));
|
||||
|
||||
// Change properties with valid state
|
||||
var changed = await adminClient.UpdateCurrentUser(
|
||||
new UpdateApplicationUserRequest
|
||||
{
|
||||
Email = "administrator@gmail.com",
|
||||
CurrentPassword = "abceudhqw",
|
||||
NewPassword = "abceudhqw123",
|
||||
Name = "Changed Admin",
|
||||
ImageUrl = "avatar.jpg"
|
||||
});
|
||||
Assert.Equal("administrator@gmail.com", changed.Email);
|
||||
Assert.Equal("Changed Admin", changed.Name);
|
||||
Assert.Equal("avatar.jpg", changed.ImageUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePullPaymentViaAPI()
|
||||
|
|
|
@ -329,12 +329,27 @@
|
|||
<span>Account</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
|
||||
<li class="p-3 border-bottom">
|
||||
<strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong>
|
||||
@if (User.IsInRole(Roles.ServerAdmin))
|
||||
<li class="p-3 border-bottom d-flex align-items-center gap-2">
|
||||
@if (!string.IsNullOrEmpty(Model.UserImageUrl))
|
||||
{
|
||||
<div class="text-secondary">Administrator</div>
|
||||
<img src="@Model.UserImageUrl" alt="Profile picture" class="profile-picture"/>
|
||||
}
|
||||
<div>
|
||||
<strong class="d-block text-truncate" style="max-width:@(string.IsNullOrEmpty(Model.UserImageUrl) ? "195px" : "160px")">
|
||||
@if (string.IsNullOrEmpty(Model.UserName))
|
||||
{
|
||||
@(User.Identity.Name)
|
||||
}
|
||||
else
|
||||
{
|
||||
@($"{Model.UserName} ({User.Identity.Name})")
|
||||
}
|
||||
</strong>
|
||||
@if (User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
<div class="text-secondary">Administrator</div>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
@if (!Theme.CustomTheme)
|
||||
{
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
|
@ -15,10 +14,8 @@ using BTCPayServer.Services.Invoices;
|
|||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Secp256k1;
|
||||
|
||||
namespace BTCPayServer.Components.MainNav
|
||||
{
|
||||
|
@ -31,6 +28,7 @@ namespace BTCPayServer.Components.MainNav
|
|||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
|
@ -44,6 +42,7 @@ namespace BTCPayServer.Components.MainNav
|
|||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
SettingsRepository settingsRepository,
|
||||
IMemoryCache cache,
|
||||
UriResolver uriResolver,
|
||||
PoliciesSettings policiesSettings)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
|
@ -53,6 +52,7 @@ namespace BTCPayServer.Components.MainNav
|
|||
_storesController = storesController;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_settingsRepository = settingsRepository;
|
||||
_uriResolver = uriResolver;
|
||||
_cache = cache;
|
||||
PoliciesSettings = policiesSettings;
|
||||
}
|
||||
|
@ -124,6 +124,16 @@ namespace BTCPayServer.Components.MainNav
|
|||
|
||||
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(HttpContext.User);
|
||||
if (user != null)
|
||||
{
|
||||
var blob = user.GetBlob();
|
||||
vm.UserName = blob?.Name;
|
||||
vm.UserImageUrl = string.IsNullOrEmpty(blob?.ImageUrl)
|
||||
? null
|
||||
: await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob?.ImageUrl));
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ namespace BTCPayServer.Components.MainNav
|
|||
public bool AltcoinsBuild { get; set; }
|
||||
public int ArchivedAppsCount { get; set; }
|
||||
public string ContactUrl { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserImageUrl { get; set; }
|
||||
}
|
||||
|
||||
public class StoreApp
|
||||
|
|
|
@ -3,6 +3,7 @@ using System;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
|
@ -39,6 +40,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
private readonly BTCPayServerOptions _options;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly UserService _userService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
|
||||
public GreenfieldUsersController(UserManager<ApplicationUser> userManager,
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
|
@ -50,6 +52,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
BTCPayServerOptions options,
|
||||
IAuthorizationService authorizationService,
|
||||
UserService userService,
|
||||
UriResolver uriResolver,
|
||||
Logs logs)
|
||||
{
|
||||
this.Logs = logs;
|
||||
|
@ -63,6 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
_options = options;
|
||||
_authorizationService = authorizationService;
|
||||
_userService = userService;
|
||||
_uriResolver = uriResolver;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
|
@ -127,6 +131,99 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
return await FromModel(user!);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/users/me")]
|
||||
public async Task<IActionResult> UpdateCurrentUser(UpdateApplicationUserRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (User.Identity is null || user is null)
|
||||
return this.CreateAPIError(401, "unauthenticated", "User is not authenticated");
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Email) && !request.Email.IsValidEmail())
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Email), "Invalid email");
|
||||
}
|
||||
|
||||
bool needUpdate = false;
|
||||
var setNewPassword = !string.IsNullOrEmpty(request.NewPassword);
|
||||
if (setNewPassword)
|
||||
{
|
||||
if (!await _userManager.CheckPasswordAsync(user, request.CurrentPassword))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.CurrentPassword), "The current password is not correct.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var passwordValidation = await _passwordValidator.ValidateAsync(_userManager, user, request.NewPassword);
|
||||
if (passwordValidation.Succeeded)
|
||||
{
|
||||
var setUserResult = await _userManager.ChangePasswordAsync(user, request.CurrentPassword, request.NewPassword);
|
||||
if (!setUserResult.Succeeded)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Email), "Unexpected error occurred setting password for user.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var error in passwordValidation.Errors)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.NewPassword), error.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var email = user.Email;
|
||||
if (!string.IsNullOrEmpty(request.Email) && request.Email != email)
|
||||
{
|
||||
var setUserResult = await _userManager.SetUserNameAsync(user, request.Email);
|
||||
if (!setUserResult.Succeeded)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Email), "Unexpected error occurred setting email for user.");
|
||||
}
|
||||
var setEmailResult = await _userManager.SetEmailAsync(user, request.Email);
|
||||
if (!setEmailResult.Succeeded)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Email), "Unexpected error occurred setting email for user.");
|
||||
}
|
||||
}
|
||||
|
||||
var blob = user.GetBlob() ?? new();
|
||||
if (request.Name is not null && request.Name != blob.Name)
|
||||
{
|
||||
blob.Name = request.Name;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (request.ImageUrl is not null && request.ImageUrl != blob.ImageUrl)
|
||||
{
|
||||
blob.ImageUrl = request.ImageUrl;
|
||||
needUpdate = true;
|
||||
}
|
||||
user.SetBlob(blob);
|
||||
|
||||
if (ModelState.IsValid && needUpdate)
|
||||
{
|
||||
var identityResult = await _userManager.UpdateAsync(user);
|
||||
if (!identityResult.Succeeded)
|
||||
{
|
||||
foreach (var error in identityResult.Errors)
|
||||
{
|
||||
if (error.Code == "DuplicateUserName")
|
||||
ModelState.AddModelError(nameof(request.Email), error.Description);
|
||||
else
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var model = await FromModel(user);
|
||||
return Ok(model);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanDeleteUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/users/me")]
|
||||
public async Task<IActionResult> DeleteCurrentUser()
|
||||
|
@ -187,6 +284,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
Created = DateTimeOffset.UtcNow,
|
||||
Approved = isAdmin // auto-approve first admin and users created by an admin
|
||||
};
|
||||
var blob = user.GetBlob() ?? new();
|
||||
blob.Name = request.Name;
|
||||
blob.ImageUrl = request.ImageUrl;
|
||||
user.SetBlob(blob);
|
||||
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
|
||||
if (!passwordValidation.Succeeded)
|
||||
{
|
||||
|
@ -286,7 +387,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
private async Task<ApplicationUserData> FromModel(ApplicationUser data)
|
||||
{
|
||||
var roles = (await _userManager.GetRolesAsync(data)).ToArray();
|
||||
return UserService.FromModel(data, roles);
|
||||
var model = UserService.FromModel(data, roles);
|
||||
model.ImageUrl = string.IsNullOrEmpty(model.ImageUrl)
|
||||
? null
|
||||
: await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(model.ImageUrl));
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -875,6 +875,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
await GetController<GreenfieldStoreOnChainWalletsController>().GetOnChainFeeRate(storeId, cryptoCode, blockTarget));
|
||||
}
|
||||
|
||||
public override async Task<ApplicationUserData> UpdateCurrentUser(UpdateApplicationUserRequest request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<ApplicationUserData>(await GetController<GreenfieldUsersController>().UpdateCurrentUser(request, token));
|
||||
}
|
||||
|
||||
public override async Task DeleteCurrentUser(CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldUsersController>().DeleteCurrentUser());
|
||||
|
|
|
@ -2,6 +2,8 @@ using System;
|
|||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Fido2;
|
||||
|
@ -41,6 +43,8 @@ namespace BTCPayServer.Controllers
|
|||
private readonly UserLoginCodeService _userLoginCodeService;
|
||||
private readonly IHtmlHelper Html;
|
||||
private readonly UserService _userService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly IFileService _fileService;
|
||||
readonly StoreRepository _StoreRepository;
|
||||
|
||||
public UIManageController(
|
||||
|
@ -56,6 +60,8 @@ namespace BTCPayServer.Controllers
|
|||
Fido2Service fido2Service,
|
||||
LinkGenerator linkGenerator,
|
||||
UserService userService,
|
||||
UriResolver uriResolver,
|
||||
IFileService fileService,
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
IHtmlHelper htmlHelper
|
||||
)
|
||||
|
@ -73,6 +79,8 @@ namespace BTCPayServer.Controllers
|
|||
_userLoginCodeService = userLoginCodeService;
|
||||
Html = htmlHelper;
|
||||
_userService = userService;
|
||||
_uriResolver = uriResolver;
|
||||
_fileService = fileService;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
|
@ -84,12 +92,14 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var blob = user.GetBlob() ?? new();
|
||||
var model = new IndexViewModel
|
||||
{
|
||||
Username = user.UserName,
|
||||
Email = user.Email,
|
||||
IsEmailConfirmed = user.EmailConfirmed
|
||||
Name = blob.Name,
|
||||
ImageUrl = string.IsNullOrEmpty(blob.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)),
|
||||
EmailConfirmed = user.EmailConfirmed,
|
||||
RequiresEmailConfirmation = user.RequiresEmailConfirmation
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
@ -105,7 +115,7 @@ namespace BTCPayServer.Controllers
|
|||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var blob = user.GetBlob();
|
||||
var blob = user.GetBlob() ?? new();
|
||||
blob.ShowInvoiceStatusChangeHint = false;
|
||||
user.SetBlob(blob);
|
||||
await _userManager.UpdateAsync(user);
|
||||
|
@ -114,19 +124,15 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Index(IndexViewModel model)
|
||||
public async Task<IActionResult> Index(IndexViewModel model, [FromForm] bool RemoveImageFile = false)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
bool needUpdate = false;
|
||||
var email = user.Email;
|
||||
if (model.Email != email)
|
||||
{
|
||||
|
@ -145,8 +151,72 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
|
||||
}
|
||||
needUpdate = true;
|
||||
}
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Your profile has been updated";
|
||||
|
||||
var blob = user.GetBlob() ?? new();
|
||||
if (blob.Name != model.Name)
|
||||
{
|
||||
blob.Name = model.Name;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (model.ImageFile != null)
|
||||
{
|
||||
if (model.ImageFile.Length > 1_000_000)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ImageFile), "The uploaded image file should be less than 1MB");
|
||||
}
|
||||
else if (!model.ImageFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ImageFile), "The uploaded file needs to be an image");
|
||||
}
|
||||
else
|
||||
{
|
||||
var formFile = await model.ImageFile.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ImageFile), "The uploaded file needs to be an image");
|
||||
}
|
||||
else
|
||||
{
|
||||
model.ImageFile = formFile;
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.ImageFile, user.Id);
|
||||
var fileIdUri = new UnresolvedUri.FileIdUri(storedFile.Id);
|
||||
blob.ImageUrl = fileIdUri.ToString();
|
||||
needUpdate = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ImageFile), $"Could not save image: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RemoveImageFile && !string.IsNullOrEmpty(blob.ImageUrl))
|
||||
{
|
||||
blob.ImageUrl = null;
|
||||
needUpdate = true;
|
||||
}
|
||||
user.SetBlob(blob);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (needUpdate is true)
|
||||
{
|
||||
needUpdate = await _userManager.UpdateAsync(user) is { Succeeded: true };
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Your profile has been updated";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Error updating profile";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
|
|
|
@ -51,15 +51,17 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
|
||||
model.Roles = roleManager.Roles.ToDictionary(role => role.Id, role => role.Name);
|
||||
model.Users = await usersQuery
|
||||
model.Users = (await usersQuery
|
||||
.Include(user => user.UserRoles)
|
||||
.Include(user => user.UserStores)
|
||||
.ThenInclude(data => data.StoreData)
|
||||
.Skip(model.Skip)
|
||||
.Take(model.Count)
|
||||
.ToListAsync())
|
||||
.Select(u => new UsersViewModel.UserViewModel
|
||||
{
|
||||
Name = u.UserName,
|
||||
Name = u.GetBlob()?.Name,
|
||||
ImageUrl = u.GetBlob()?.ImageUrl,
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
||||
|
@ -69,8 +71,7 @@ namespace BTCPayServer.Controllers
|
|||
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime,
|
||||
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
.ToList();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
@ -81,10 +82,13 @@ namespace BTCPayServer.Controllers
|
|||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var blob = user.GetBlob();
|
||||
var model = new UsersViewModel.UserViewModel
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Name = blob?.Name,
|
||||
ImageUrl = string.IsNullOrEmpty(blob?.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)),
|
||||
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
|
||||
Approved = user.RequiresApproval ? user.Approved : null,
|
||||
IsAdmin = Roles.HasServerAdmin(roles)
|
||||
|
@ -93,7 +97,7 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
|
||||
[HttpPost("server/users/{userId}")]
|
||||
public new async Task<IActionResult> User(string userId, UsersViewModel.UserViewModel viewModel)
|
||||
public new async Task<IActionResult> User(string userId, UsersViewModel.UserViewModel viewModel, [FromForm] bool RemoveImageFile = false)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
|
@ -113,6 +117,54 @@ namespace BTCPayServer.Controllers
|
|||
propertiesChanged = true;
|
||||
}
|
||||
|
||||
var blob = user.GetBlob() ?? new();
|
||||
if (blob.Name != viewModel.Name)
|
||||
{
|
||||
blob.Name = viewModel.Name;
|
||||
propertiesChanged = true;
|
||||
}
|
||||
|
||||
if (viewModel.ImageFile != null)
|
||||
{
|
||||
if (viewModel.ImageFile.Length > 1_000_000)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.ImageFile), "The uploaded image file should be less than 1MB");
|
||||
}
|
||||
else if (!viewModel.ImageFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.ImageFile), "The uploaded file needs to be an image");
|
||||
}
|
||||
else
|
||||
{
|
||||
var formFile = await viewModel.ImageFile.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.ImageFile), "The uploaded file needs to be an image");
|
||||
}
|
||||
else
|
||||
{
|
||||
viewModel.ImageFile = formFile;
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(viewModel.ImageFile, userId);
|
||||
var fileIdUri = new UnresolvedUri.FileIdUri(storedFile.Id);
|
||||
blob.ImageUrl = fileIdUri.ToString();
|
||||
propertiesChanged = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.ImageFile), $"Could not save image: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RemoveImageFile && !string.IsNullOrEmpty(blob.ImageUrl))
|
||||
{
|
||||
blob.ImageUrl = null;
|
||||
propertiesChanged = true;
|
||||
}
|
||||
user.SetBlob(blob);
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var wasAdmin = Roles.HasServerAdmin(roles);
|
||||
|
|
|
@ -137,9 +137,11 @@ public partial class UIStoresController
|
|||
{
|
||||
var users = await _storeRepo.GetStoreUsers(CurrentStore.Id);
|
||||
vm.StoreId = CurrentStore.Id;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel
|
||||
{
|
||||
Email = u.Email,
|
||||
Name = u.UserBlob.Name,
|
||||
ImageUrl = u.UserBlob.ImageUrl,
|
||||
Id = u.Id,
|
||||
Role = u.StoreRole.Role
|
||||
}).ToList();
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class IndexViewModel
|
||||
{
|
||||
public string Username { get; set; }
|
||||
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[MaxLength(50)]
|
||||
public string Email
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool IsEmailConfirmed { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool EmailConfirmed { get; set; }
|
||||
public bool RequiresEmailConfirmation { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
[Display(Name = "Profile Picture")]
|
||||
public IFormFile ImageFile { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
|
@ -10,8 +12,12 @@ namespace BTCPayServer.Models.ServerViewModels
|
|||
public class UserViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
[Display(Name = "Image")]
|
||||
public IFormFile ImageFile { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public bool? EmailConfirmed { get; set; }
|
||||
public bool? Approved { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
|
@ -20,13 +26,13 @@ namespace BTCPayServer.Models.ServerViewModels
|
|||
public IEnumerable<string> Roles { get; set; }
|
||||
public IEnumerable<UserStore> Stores { get; set; }
|
||||
}
|
||||
public List<UserViewModel> Users { get; set; } = new List<UserViewModel>();
|
||||
public List<UserViewModel> Users { get; set; } = [];
|
||||
public override int CurrentPageCount => Users.Count;
|
||||
public Dictionary<string, string> Roles { get; set; }
|
||||
}
|
||||
public class RolesViewModel : BasePagingViewModel
|
||||
{
|
||||
public List<StoreRepository.StoreRole> Roles { get; set; } = new List<StoreRepository.StoreRole>();
|
||||
public List<StoreRepository.StoreRole> Roles { get; set; } = [];
|
||||
public string DefaultRole { get; set; }
|
||||
public override int CurrentPageCount => Roles.Count;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
{
|
||||
public string Email { get; set; }
|
||||
public string Role { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
[Required]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
|
@ -61,6 +62,7 @@ namespace BTCPayServer.Services.Stores
|
|||
public string Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
public StoreRole StoreRole { get; set; }
|
||||
public UserBlob UserBlob { get; set; }
|
||||
}
|
||||
|
||||
public class StoreRole
|
||||
|
@ -188,7 +190,7 @@ namespace BTCPayServer.Services.Stores
|
|||
.Select(u => new
|
||||
{
|
||||
Id = u.ApplicationUserId,
|
||||
u.ApplicationUser.Email,
|
||||
u.ApplicationUser,
|
||||
u.StoreRole
|
||||
})
|
||||
.Where(u => roles == null || roles.Contains(u.StoreRole.Id))
|
||||
|
@ -196,7 +198,8 @@ namespace BTCPayServer.Services.Stores
|
|||
{
|
||||
StoreRole = ToStoreRole(arg.StoreRole),
|
||||
Id = arg.Id,
|
||||
Email = arg.Email
|
||||
Email = arg.ApplicationUser.Email,
|
||||
UserBlob = arg.ApplicationUser.GetBlob() ?? new()
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ namespace BTCPayServer.Services
|
|||
|
||||
public static ApplicationUserData FromModel(ApplicationUser data, string?[] roles)
|
||||
{
|
||||
var blob = data.GetBlob() ?? new();
|
||||
return new ApplicationUserData
|
||||
{
|
||||
Id = data.Id,
|
||||
|
@ -58,6 +59,8 @@ namespace BTCPayServer.Services
|
|||
Approved = data.Approved,
|
||||
RequiresApproval = data.RequiresApproval,
|
||||
Created = data.Created,
|
||||
Name = blob.Name,
|
||||
ImageUrl = blob.ImageUrl,
|
||||
Roles = roles,
|
||||
Disabled = data.LockoutEnabled && data.LockoutEnd is not null && DateTimeOffset.UtcNow < data.LockoutEnd.Value.UtcDateTime
|
||||
};
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Abstractions.Models
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject IFileService FileService
|
||||
@model IndexViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ManageNavPages.Index, "Update your account");
|
||||
var canUpload = await FileService.IsAvailable();
|
||||
}
|
||||
|
||||
|
||||
<form method="post">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" id="save" class="btn btn-primary">Save</button>
|
||||
|
@ -14,34 +17,56 @@
|
|||
<div class="col-xxl-constrain col-xl-8">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All"></div>
|
||||
<div asp-validation-summary="All" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Username" class="form-label"></label>
|
||||
<input asp-for="Username" class="form-control" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-end">
|
||||
@if (Model.IsEmailConfirmed)
|
||||
@if (Model.RequiresEmailConfirmation)
|
||||
{
|
||||
<span class="badge bg-success p-2 my-1">
|
||||
<button asp-action="SendVerificationEmail" class="d-inline-flex align-items-center gap-1 btn btn-link p-0">
|
||||
<vc:icon symbol="actions-email" />
|
||||
Send verification email
|
||||
</button>
|
||||
}
|
||||
else if (Model.EmailConfirmed)
|
||||
{
|
||||
<span class="d-inline-flex align-items-center gap-1 text-success">
|
||||
<vc:icon symbol="checkmark" />
|
||||
confirmed
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button asp-action="SendVerificationEmail" class="btn btn-secondary">Send verification email</button>
|
||||
}
|
||||
</div>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="form-label"></label>
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
@if (canUpload)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<label asp-for="ImageFile" class="form-label"></label>
|
||||
@if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveImageFile" value="true">
|
||||
<vc:icon symbol="cross" /> Remove
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<input asp-for="ImageFile" type="file" class="form-control flex-grow">
|
||||
@if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
<img src="@Model.ImageUrl" alt="Profile picture" class="profile-picture"/>
|
||||
}
|
||||
</div>
|
||||
<span asp-validation-for="ImageFile" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
<h3 class="mt-5 mb-4">Delete Account</h3>
|
||||
<div id="danger-zone">
|
||||
<a id="delete-user" class="btn btn-outline-danger mb-5" data-confirm-input="DELETE" data-bs-toggle="modal" data-bs-target="#ConfirmModal" asp-action="DeleteUserPost" data-description="This action will also delete all stores, invoices, apps and data associated with the user.">Delete Account</a>
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
@model UsersViewModel.UserViewModel
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model UsersViewModel.UserViewModel
|
||||
@inject IFileService FileService
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Users, Model.Email);
|
||||
var canUpload = await FileService.IsAvailable();
|
||||
}
|
||||
|
||||
<form method="post">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="sticky-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
|
@ -17,13 +21,45 @@
|
|||
<button name="command" type="submit" class="btn btn-primary" value="Save" id="SaveUser">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="form-check">
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="form-label"></label>
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<label asp-for="ImageFile" class="form-label"></label>
|
||||
@if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveImageFile" value="true">
|
||||
<vc:icon symbol="cross" /> Remove
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (canUpload)
|
||||
{
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<input asp-for="ImageFile" type="file" class="form-control flex-grow">
|
||||
@if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
<img src="@Model.ImageUrl" alt="Profile picture" class="profile-picture" />
|
||||
}
|
||||
</div>
|
||||
<span asp-validation-for="ImageFile" class="text-danger"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input asp-for="ImageFile" type="file" class="form-control" disabled>
|
||||
<div class="form-text">In order to upload an image, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</div>
|
||||
}
|
||||
</div>
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="IsAdmin" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="IsAdmin" class="form-check-label">User is admin</label>
|
||||
</div>
|
||||
@if (Model.Approved.HasValue)
|
||||
{
|
||||
<div class="form-check">
|
||||
<div class="form-check my-3">
|
||||
<input id="Approved" name="Approved" type="checkbox" value="true" class="form-check-input" @(Model.Approved.Value ? "checked" : "") />
|
||||
<label for="Approved" class="form-check-label">User is approved</label>
|
||||
</div>
|
||||
|
@ -31,7 +67,7 @@
|
|||
}
|
||||
@if (Model.EmailConfirmed.HasValue)
|
||||
{
|
||||
<div class="form-check">
|
||||
<div class="form-check my-3">
|
||||
<input id="EmailConfirmed" name="EmailConfirmed" value="true" type="checkbox" class="form-check-input" @(Model.EmailConfirmed.Value ? "checked" : "") />
|
||||
<label for="EmailConfirmed" class="form-check-label">Email address is confirmed</label>
|
||||
</div>
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th class="actions-col" permission="@Policies.CanModifyStoreSettings">Actions</th>
|
||||
</tr>
|
||||
|
@ -54,6 +55,7 @@
|
|||
{
|
||||
<tr>
|
||||
<td>@user.Email</td>
|
||||
<td>@user.Name</td>
|
||||
<td>@user.Role</td>
|
||||
<td class="actions-col" permission="@Policies.CanModifyStoreSettings">
|
||||
<div class="d-inline-flex align-items-center gap-3">
|
||||
|
@ -114,7 +116,6 @@
|
|||
})()
|
||||
</script>
|
||||
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
|
|
|
@ -47,6 +47,13 @@
|
|||
padding-left: 0;
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
height: var(--profile-picture-size, 2.1rem);
|
||||
width: var(--profile-picture-size, 2.1rem);
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* General and site-wide Bootstrap modifications */
|
||||
p {
|
||||
margin-bottom: 1.5rem;
|
||||
|
|
|
@ -32,6 +32,77 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Update current user information",
|
||||
"description": "Update the current user",
|
||||
"requestBody": {
|
||||
"x-name": "request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "The email of the user",
|
||||
"nullable": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the user",
|
||||
"nullable": true
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": "string",
|
||||
"description": "The profile picture URL of the user",
|
||||
"nullable": true
|
||||
},
|
||||
"currentPassword": {
|
||||
"type": "string",
|
||||
"description": "The current password of the user",
|
||||
"nullable": true
|
||||
},
|
||||
"newPassword": {
|
||||
"type": "string",
|
||||
"description": "The new password of the user",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true,
|
||||
"x-position": 1
|
||||
},
|
||||
"operationId": "Users_UpdateCurrentUser",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Information about 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"
|
||||
|
@ -106,6 +177,16 @@
|
|||
"description": "The email of the new user",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the new user",
|
||||
"nullable": true
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": "string",
|
||||
"description": "The profile picture URL of the new user",
|
||||
"nullable": true
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "The password of the new user (if no password is set, an email will be sent to the user requiring him to set the password)",
|
||||
|
@ -186,7 +267,14 @@
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User found"
|
||||
"description": "User found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApplicationUserData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing authorization for loading the user"
|
||||
|
@ -372,6 +460,16 @@
|
|||
"description": "The email of the user",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the user",
|
||||
"nullable": true
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": "string",
|
||||
"description": "The profile picture URL of the user",
|
||||
"nullable": true
|
||||
},
|
||||
"emailConfirmed": {
|
||||
"type": "boolean",
|
||||
"description": "True if the email has been confirmed by the user"
|
||||
|
|
Loading…
Add table
Reference in a new issue