mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +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:
@@ -13,6 +13,11 @@ public partial class BTCPayServerClient
|
|||||||
return await SendHttpRequest<ApplicationUserData>("api/v1/users/me", null, HttpMethod.Get, token);
|
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)
|
public virtual async Task<ApplicationUserData> CreateUser(CreateApplicationUserRequest request, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return await SendHttpRequest<ApplicationUserData>("api/v1/users", request, HttpMethod.Post, token);
|
return await SendHttpRequest<ApplicationUserData>("api/v1/users", request, HttpMethod.Post, token);
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ namespace BTCPayServer.Client.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Email { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Whether the user has verified their email
|
/// Whether the user has verified their email
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ namespace BTCPayServer.Client.Models
|
|||||||
{
|
{
|
||||||
public class CreateApplicationUserRequest
|
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>
|
/// <summary>
|
||||||
/// the email AND username of the new user
|
/// the email AND username of the new user
|
||||||
/// </summary>
|
/// </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.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
@@ -45,5 +44,7 @@ namespace BTCPayServer.Data
|
|||||||
public class UserBlob
|
public class UserBlob
|
||||||
{
|
{
|
||||||
public bool ShowInvoiceStatusChangeHint { get; set; }
|
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();
|
acc = tester.NewAccount();
|
||||||
await acc.GrantAccessAsync(isAdmin: true);
|
await acc.GrantAccessAsync(isAdmin: true);
|
||||||
unrestricted = await acc.CreateClient();
|
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()
|
var newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Id, new CreateApiKeyRequest()
|
||||||
{
|
{
|
||||||
Label = "Hello world",
|
Label = "Hello world",
|
||||||
@@ -208,6 +214,8 @@ namespace BTCPayServer.Tests
|
|||||||
});
|
});
|
||||||
var newUserClient = acc.CreateClientFromAPIKey(newUserAPIKey.ApiKey);
|
var newUserClient = acc.CreateClientFromAPIKey(newUserAPIKey.ApiKey);
|
||||||
Assert.Equal(newUser.Id, (await newUserClient.GetCurrentUser()).Id);
|
Assert.Equal(newUser.Id, (await newUserClient.GetCurrentUser()).Id);
|
||||||
|
Assert.Equal("New User", newUser.Name);
|
||||||
|
Assert.Equal("avatar.jpg", newUser.ImageUrl);
|
||||||
// Admin delete it
|
// Admin delete it
|
||||||
await unrestricted.RevokeAPIKey(newUser.Id, newUserAPIKey.ApiKey);
|
await unrestricted.RevokeAPIKey(newUser.Id, newUserAPIKey.ApiKey);
|
||||||
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetCurrentUser());
|
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetCurrentUser());
|
||||||
@@ -846,6 +854,63 @@ namespace BTCPayServer.Tests
|
|||||||
new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" });
|
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]
|
[Fact]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanUsePullPaymentViaAPI()
|
public async Task CanUsePullPaymentViaAPI()
|
||||||
|
|||||||
@@ -329,12 +329,27 @@
|
|||||||
<span>Account</span>
|
<span>Account</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
|
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
|
||||||
<li class="p-3 border-bottom">
|
<li class="p-3 border-bottom d-flex align-items-center gap-2">
|
||||||
<strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong>
|
@if (!string.IsNullOrEmpty(Model.UserImageUrl))
|
||||||
@if (User.IsInRole(Roles.ServerAdmin))
|
|
||||||
{
|
{
|
||||||
<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>
|
</li>
|
||||||
@if (!Theme.CustomTheme)
|
@if (!Theme.CustomTheme)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Lightning;
|
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
@@ -15,10 +14,8 @@ using BTCPayServer.Services.Invoices;
|
|||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.Secp256k1;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Components.MainNav
|
namespace BTCPayServer.Components.MainNav
|
||||||
{
|
{
|
||||||
@@ -31,6 +28,7 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||||
private readonly SettingsRepository _settingsRepository;
|
private readonly SettingsRepository _settingsRepository;
|
||||||
|
private readonly UriResolver _uriResolver;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
public PoliciesSettings PoliciesSettings { get; }
|
public PoliciesSettings PoliciesSettings { get; }
|
||||||
@@ -44,6 +42,7 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||||
SettingsRepository settingsRepository,
|
SettingsRepository settingsRepository,
|
||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
|
UriResolver uriResolver,
|
||||||
PoliciesSettings policiesSettings)
|
PoliciesSettings policiesSettings)
|
||||||
{
|
{
|
||||||
_storeRepo = storeRepo;
|
_storeRepo = storeRepo;
|
||||||
@@ -53,6 +52,7 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
_storesController = storesController;
|
_storesController = storesController;
|
||||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||||
_settingsRepository = settingsRepository;
|
_settingsRepository = settingsRepository;
|
||||||
|
_uriResolver = uriResolver;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
PoliciesSettings = policiesSettings;
|
PoliciesSettings = policiesSettings;
|
||||||
}
|
}
|
||||||
@@ -125,6 +125,16 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
|
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);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
public bool AltcoinsBuild { get; set; }
|
public bool AltcoinsBuild { get; set; }
|
||||||
public int ArchivedAppsCount { get; set; }
|
public int ArchivedAppsCount { get; set; }
|
||||||
public string ContactUrl { get; set; }
|
public string ContactUrl { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string UserImageUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StoreApp
|
public class StoreApp
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Linq;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
@@ -39,6 +40,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
private readonly BTCPayServerOptions _options;
|
private readonly BTCPayServerOptions _options;
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly UserService _userService;
|
private readonly UserService _userService;
|
||||||
|
private readonly UriResolver _uriResolver;
|
||||||
|
|
||||||
public GreenfieldUsersController(UserManager<ApplicationUser> userManager,
|
public GreenfieldUsersController(UserManager<ApplicationUser> userManager,
|
||||||
RoleManager<IdentityRole> roleManager,
|
RoleManager<IdentityRole> roleManager,
|
||||||
@@ -50,6 +52,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
BTCPayServerOptions options,
|
BTCPayServerOptions options,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
|
UriResolver uriResolver,
|
||||||
Logs logs)
|
Logs logs)
|
||||||
{
|
{
|
||||||
this.Logs = logs;
|
this.Logs = logs;
|
||||||
@@ -63,6 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
_options = options;
|
_options = options;
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_uriResolver = uriResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
@@ -127,6 +131,99 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
return await FromModel(user!);
|
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)]
|
[Authorize(Policy = Policies.CanDeleteUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
[HttpDelete("~/api/v1/users/me")]
|
[HttpDelete("~/api/v1/users/me")]
|
||||||
public async Task<IActionResult> DeleteCurrentUser()
|
public async Task<IActionResult> DeleteCurrentUser()
|
||||||
@@ -187,6 +284,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
Created = DateTimeOffset.UtcNow,
|
Created = DateTimeOffset.UtcNow,
|
||||||
Approved = isAdmin // auto-approve first admin and users created by an admin
|
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);
|
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
|
||||||
if (!passwordValidation.Succeeded)
|
if (!passwordValidation.Succeeded)
|
||||||
{
|
{
|
||||||
@@ -286,7 +387,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
private async Task<ApplicationUserData> FromModel(ApplicationUser data)
|
private async Task<ApplicationUserData> FromModel(ApplicationUser data)
|
||||||
{
|
{
|
||||||
var roles = (await _userManager.GetRolesAsync(data)).ToArray();
|
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));
|
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)
|
public override async Task DeleteCurrentUser(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
HandleActionResult(await GetController<GreenfieldUsersController>().DeleteCurrentUser());
|
HandleActionResult(await GetController<GreenfieldUsersController>().DeleteCurrentUser());
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System;
|
|||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Fido2;
|
using BTCPayServer.Fido2;
|
||||||
@@ -41,6 +43,8 @@ namespace BTCPayServer.Controllers
|
|||||||
private readonly UserLoginCodeService _userLoginCodeService;
|
private readonly UserLoginCodeService _userLoginCodeService;
|
||||||
private readonly IHtmlHelper Html;
|
private readonly IHtmlHelper Html;
|
||||||
private readonly UserService _userService;
|
private readonly UserService _userService;
|
||||||
|
private readonly UriResolver _uriResolver;
|
||||||
|
private readonly IFileService _fileService;
|
||||||
readonly StoreRepository _StoreRepository;
|
readonly StoreRepository _StoreRepository;
|
||||||
|
|
||||||
public UIManageController(
|
public UIManageController(
|
||||||
@@ -56,6 +60,8 @@ namespace BTCPayServer.Controllers
|
|||||||
Fido2Service fido2Service,
|
Fido2Service fido2Service,
|
||||||
LinkGenerator linkGenerator,
|
LinkGenerator linkGenerator,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
|
UriResolver uriResolver,
|
||||||
|
IFileService fileService,
|
||||||
UserLoginCodeService userLoginCodeService,
|
UserLoginCodeService userLoginCodeService,
|
||||||
IHtmlHelper htmlHelper
|
IHtmlHelper htmlHelper
|
||||||
)
|
)
|
||||||
@@ -73,6 +79,8 @@ namespace BTCPayServer.Controllers
|
|||||||
_userLoginCodeService = userLoginCodeService;
|
_userLoginCodeService = userLoginCodeService;
|
||||||
Html = htmlHelper;
|
Html = htmlHelper;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_uriResolver = uriResolver;
|
||||||
|
_fileService = fileService;
|
||||||
_StoreRepository = storeRepository;
|
_StoreRepository = storeRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,12 +92,14 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
}
|
}
|
||||||
|
var blob = user.GetBlob() ?? new();
|
||||||
var model = new IndexViewModel
|
var model = new IndexViewModel
|
||||||
{
|
{
|
||||||
Username = user.UserName,
|
|
||||||
Email = user.Email,
|
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);
|
return View(model);
|
||||||
}
|
}
|
||||||
@@ -105,7 +115,7 @@ namespace BTCPayServer.Controllers
|
|||||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var blob = user.GetBlob();
|
var blob = user.GetBlob() ?? new();
|
||||||
blob.ShowInvoiceStatusChangeHint = false;
|
blob.ShowInvoiceStatusChangeHint = false;
|
||||||
user.SetBlob(blob);
|
user.SetBlob(blob);
|
||||||
await _userManager.UpdateAsync(user);
|
await _userManager.UpdateAsync(user);
|
||||||
@@ -114,19 +124,15 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[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);
|
var user = await _userManager.GetUserAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool needUpdate = false;
|
||||||
var email = user.Email;
|
var email = user.Email;
|
||||||
if (model.Email != 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}'.");
|
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));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,15 +51,17 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
model.Roles = roleManager.Roles.ToDictionary(role => role.Id, role => role.Name);
|
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.UserRoles)
|
||||||
.Include(user => user.UserStores)
|
.Include(user => user.UserStores)
|
||||||
.ThenInclude(data => data.StoreData)
|
.ThenInclude(data => data.StoreData)
|
||||||
.Skip(model.Skip)
|
.Skip(model.Skip)
|
||||||
.Take(model.Count)
|
.Take(model.Count)
|
||||||
|
.ToListAsync())
|
||||||
.Select(u => new UsersViewModel.UserViewModel
|
.Select(u => new UsersViewModel.UserViewModel
|
||||||
{
|
{
|
||||||
Name = u.UserName,
|
Name = u.GetBlob()?.Name,
|
||||||
|
ImageUrl = u.GetBlob()?.ImageUrl,
|
||||||
Email = u.Email,
|
Email = u.Email,
|
||||||
Id = u.Id,
|
Id = u.Id,
|
||||||
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
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,
|
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime,
|
||||||
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
|
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToList();
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +82,13 @@ namespace BTCPayServer.Controllers
|
|||||||
if (user == null)
|
if (user == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var roles = await _UserManager.GetRolesAsync(user);
|
var roles = await _UserManager.GetRolesAsync(user);
|
||||||
|
var blob = user.GetBlob();
|
||||||
var model = new UsersViewModel.UserViewModel
|
var model = new UsersViewModel.UserViewModel
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Email = user.Email,
|
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,
|
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
|
||||||
Approved = user.RequiresApproval ? user.Approved : null,
|
Approved = user.RequiresApproval ? user.Approved : null,
|
||||||
IsAdmin = Roles.HasServerAdmin(roles)
|
IsAdmin = Roles.HasServerAdmin(roles)
|
||||||
@@ -93,7 +97,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("server/users/{userId}")]
|
[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);
|
var user = await _UserManager.FindByIdAsync(userId);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@@ -113,6 +117,54 @@ namespace BTCPayServer.Controllers
|
|||||||
propertiesChanged = true;
|
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 admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||||
var roles = await _UserManager.GetRolesAsync(user);
|
var roles = await _UserManager.GetRolesAsync(user);
|
||||||
var wasAdmin = Roles.HasServerAdmin(roles);
|
var wasAdmin = Roles.HasServerAdmin(roles);
|
||||||
|
|||||||
@@ -137,9 +137,11 @@ public partial class UIStoresController
|
|||||||
{
|
{
|
||||||
var users = await _storeRepo.GetStoreUsers(CurrentStore.Id);
|
var users = await _storeRepo.GetStoreUsers(CurrentStore.Id);
|
||||||
vm.StoreId = 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,
|
Email = u.Email,
|
||||||
|
Name = u.UserBlob.Name,
|
||||||
|
ImageUrl = u.UserBlob.ImageUrl,
|
||||||
Id = u.Id,
|
Id = u.Id,
|
||||||
Role = u.StoreRole.Role
|
Role = u.StoreRole.Role
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.ManageViewModels
|
namespace BTCPayServer.Models.ManageViewModels
|
||||||
{
|
{
|
||||||
public class IndexViewModel
|
public class IndexViewModel
|
||||||
{
|
{
|
||||||
public string Username { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string Email
|
public string Email { get; set; }
|
||||||
{
|
public bool EmailConfirmed { get; set; }
|
||||||
get; set;
|
public bool RequiresEmailConfirmation { get; set; }
|
||||||
}
|
public string Name { get; set; }
|
||||||
|
|
||||||
public bool IsEmailConfirmed { get; set; }
|
|
||||||
|
|
||||||
|
[Display(Name = "Profile Picture")]
|
||||||
|
public IFormFile ImageFile { get; set; }
|
||||||
|
public string ImageUrl { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.ServerViewModels
|
namespace BTCPayServer.Models.ServerViewModels
|
||||||
{
|
{
|
||||||
@@ -10,8 +12,12 @@ namespace BTCPayServer.Models.ServerViewModels
|
|||||||
public class UserViewModel
|
public class UserViewModel
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Name { get; set; }
|
|
||||||
public string Email { 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? EmailConfirmed { get; set; }
|
||||||
public bool? Approved { get; set; }
|
public bool? Approved { get; set; }
|
||||||
public bool Disabled { get; set; }
|
public bool Disabled { get; set; }
|
||||||
@@ -20,13 +26,13 @@ namespace BTCPayServer.Models.ServerViewModels
|
|||||||
public IEnumerable<string> Roles { get; set; }
|
public IEnumerable<string> Roles { get; set; }
|
||||||
public IEnumerable<UserStore> Stores { 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 override int CurrentPageCount => Users.Count;
|
||||||
public Dictionary<string, string> Roles { get; set; }
|
public Dictionary<string, string> Roles { get; set; }
|
||||||
}
|
}
|
||||||
public class RolesViewModel : BasePagingViewModel
|
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 string DefaultRole { get; set; }
|
||||||
public override int CurrentPageCount => Roles.Count;
|
public override int CurrentPageCount => Roles.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
{
|
{
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string ImageUrl { get; set; }
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
}
|
}
|
||||||
[Required]
|
[Required]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection.Metadata;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
@@ -61,6 +62,7 @@ namespace BTCPayServer.Services.Stores
|
|||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public StoreRole StoreRole { get; set; }
|
public StoreRole StoreRole { get; set; }
|
||||||
|
public UserBlob UserBlob { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StoreRole
|
public class StoreRole
|
||||||
@@ -188,7 +190,7 @@ namespace BTCPayServer.Services.Stores
|
|||||||
.Select(u => new
|
.Select(u => new
|
||||||
{
|
{
|
||||||
Id = u.ApplicationUserId,
|
Id = u.ApplicationUserId,
|
||||||
u.ApplicationUser.Email,
|
u.ApplicationUser,
|
||||||
u.StoreRole
|
u.StoreRole
|
||||||
})
|
})
|
||||||
.Where(u => roles == null || roles.Contains(u.StoreRole.Id))
|
.Where(u => roles == null || roles.Contains(u.StoreRole.Id))
|
||||||
@@ -196,7 +198,8 @@ namespace BTCPayServer.Services.Stores
|
|||||||
{
|
{
|
||||||
StoreRole = ToStoreRole(arg.StoreRole),
|
StoreRole = ToStoreRole(arg.StoreRole),
|
||||||
Id = arg.Id,
|
Id = arg.Id,
|
||||||
Email = arg.Email
|
Email = arg.ApplicationUser.Email,
|
||||||
|
UserBlob = arg.ApplicationUser.GetBlob() ?? new()
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ namespace BTCPayServer.Services
|
|||||||
|
|
||||||
public static ApplicationUserData FromModel(ApplicationUser data, string?[] roles)
|
public static ApplicationUserData FromModel(ApplicationUser data, string?[] roles)
|
||||||
{
|
{
|
||||||
|
var blob = data.GetBlob() ?? new();
|
||||||
return new ApplicationUserData
|
return new ApplicationUserData
|
||||||
{
|
{
|
||||||
Id = data.Id,
|
Id = data.Id,
|
||||||
@@ -58,6 +59,8 @@ namespace BTCPayServer.Services
|
|||||||
Approved = data.Approved,
|
Approved = data.Approved,
|
||||||
RequiresApproval = data.RequiresApproval,
|
RequiresApproval = data.RequiresApproval,
|
||||||
Created = data.Created,
|
Created = data.Created,
|
||||||
|
Name = blob.Name,
|
||||||
|
ImageUrl = blob.ImageUrl,
|
||||||
Roles = roles,
|
Roles = roles,
|
||||||
Disabled = data.LockoutEnabled && data.LockoutEnd is not null && DateTimeOffset.UtcNow < data.LockoutEnd.Value.UtcDateTime
|
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 BTCPayServer.Abstractions.Models
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@inject IFileService FileService
|
||||||
@model IndexViewModel
|
@model IndexViewModel
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePage(ManageNavPages.Index, "Update your account");
|
ViewData.SetActivePage(ManageNavPages.Index, "Update your account");
|
||||||
|
var canUpload = await FileService.IsAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
<form method="post">
|
|
||||||
<div class="sticky-header">
|
<div class="sticky-header">
|
||||||
<h2>@ViewData["Title"]</h2>
|
<h2>@ViewData["Title"]</h2>
|
||||||
<button type="submit" id="save" class="btn btn-primary">Save</button>
|
<button type="submit" id="save" class="btn btn-primary">Save</button>
|
||||||
@@ -14,34 +17,56 @@
|
|||||||
<div class="col-xxl-constrain col-xl-8">
|
<div class="col-xxl-constrain col-xl-8">
|
||||||
@if (!ViewContext.ModelState.IsValid)
|
@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="form-group">
|
||||||
<div class="col-md-6">
|
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||||
<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">
|
|
||||||
<label asp-for="Email" class="form-label"></label>
|
<label asp-for="Email" class="form-label"></label>
|
||||||
<input asp-for="Email" class="form-control" />
|
@if (Model.RequiresEmailConfirmation)
|
||||||
<span asp-validation-for="Email" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 d-flex align-items-end">
|
|
||||||
@if (Model.IsEmailConfirmed)
|
|
||||||
{
|
{
|
||||||
<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" />
|
<vc:icon symbol="checkmark" />
|
||||||
confirmed
|
confirmed
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
<button asp-action="SendVerificationEmail" class="btn btn-secondary">Send verification email</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
<input asp-for="Email" class="form-control" />
|
||||||
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
</div>
|
</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>
|
<h3 class="mt-5 mb-4">Delete Account</h3>
|
||||||
<div id="danger-zone">
|
<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>
|
<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);
|
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">
|
<div class="sticky-header">
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
@@ -17,13 +21,45 @@
|
|||||||
<button name="command" type="submit" class="btn btn-primary" value="Save" id="SaveUser">Save</button>
|
<button name="command" type="submit" class="btn btn-primary" value="Save" id="SaveUser">Save</button>
|
||||||
</div>
|
</div>
|
||||||
<partial name="_StatusMessage" />
|
<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" />
|
<input asp-for="IsAdmin" type="checkbox" class="form-check-input" />
|
||||||
<label asp-for="IsAdmin" class="form-check-label">User is admin</label>
|
<label asp-for="IsAdmin" class="form-check-label">User is admin</label>
|
||||||
</div>
|
</div>
|
||||||
@if (Model.Approved.HasValue)
|
@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" : "") />
|
<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>
|
<label for="Approved" class="form-check-label">User is approved</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,7 +67,7 @@
|
|||||||
}
|
}
|
||||||
@if (Model.EmailConfirmed.HasValue)
|
@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" : "") />
|
<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>
|
<label for="EmailConfirmed" class="form-check-label">Email address is confirmed</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
|
<th>Name</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
<th class="actions-col" permission="@Policies.CanModifyStoreSettings">Actions</th>
|
<th class="actions-col" permission="@Policies.CanModifyStoreSettings">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>@user.Email</td>
|
<td>@user.Email</td>
|
||||||
|
<td>@user.Name</td>
|
||||||
<td>@user.Role</td>
|
<td>@user.Role</td>
|
||||||
<td class="actions-col" permission="@Policies.CanModifyStoreSettings">
|
<td class="actions-col" permission="@Policies.CanModifyStoreSettings">
|
||||||
<div class="d-inline-flex align-items-center gap-3">
|
<div class="d-inline-flex align-items-center gap-3">
|
||||||
@@ -114,7 +116,6 @@
|
|||||||
})()
|
})()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@section PageFootContent {
|
@section PageFootContent {
|
||||||
<partial name="_ValidationScriptsPartial" />
|
<partial name="_ValidationScriptsPartial" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,13 @@
|
|||||||
padding-left: 0;
|
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 */
|
/* General and site-wide Bootstrap modifications */
|
||||||
p {
|
p {
|
||||||
margin-bottom: 1.5rem;
|
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": {
|
"delete": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Users"
|
"Users"
|
||||||
@@ -106,6 +177,16 @@
|
|||||||
"description": "The email of the new user",
|
"description": "The email of the new user",
|
||||||
"nullable": false
|
"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": {
|
"password": {
|
||||||
"type": "string",
|
"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)",
|
"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": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "User found"
|
"description": "User found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApplicationUserData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Missing authorization for loading the user"
|
"description": "Missing authorization for loading the user"
|
||||||
@@ -372,6 +460,16 @@
|
|||||||
"description": "The email of the user",
|
"description": "The email of the user",
|
||||||
"nullable": false
|
"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": {
|
"emailConfirmed": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "True if the email has been confirmed by the user"
|
"description": "True if the email has been confirmed by the user"
|
||||||
|
|||||||
Reference in New Issue
Block a user