Greenfield: Expand user data (#6649)

This commit is contained in:
d11n
2025-04-04 09:15:33 +02:00
committed by GitHub
parent 65629f40b3
commit a916ae81be
11 changed files with 135 additions and 94 deletions

View File

@@ -50,6 +50,11 @@ namespace BTCPayServer.Client.Models
/// </summary> /// </summary>
public string[] Roles { get; set; } public string[] Roles { get; set; }
/// <summary>
/// the invitation url of the user
/// </summary>
public string InvitationUrl { get; set; }
/// <summary> /// <summary>
/// the date the user was created. Null if created before v1.0.5.6. /// the date the user was created. Null if created before v1.0.5.6.
/// </summary> /// </summary>

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models namespace BTCPayServer.Client.Models
{ {
@@ -10,32 +12,24 @@ namespace BTCPayServer.Client.Models
public string Id { get; set; } public string Id { get; set; }
} }
public class StoreUserData public class StoreUserData : ApplicationUserData
{ {
/// <summary> /// <summary>
/// the id of the user /// the id of the user
/// </summary> /// </summary>
[Obsolete("Use Id instead")]
public string UserId { get; set; } public string UserId { get; set; }
/// <summary> /// <summary>
/// the store role of the user /// the store role of the user
/// </summary> /// </summary>
public string StoreRole { get; set; }
/// <summary>
/// the store role of the user
/// </summary>
[Obsolete("Use StoreRole instead")]
public string Role { get; set; } public string Role { get; set; }
/// <summary>
/// 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; }
} }
public class RoleData public class RoleData

View File

@@ -248,7 +248,7 @@ namespace BTCPayServer.Tests
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id)); await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
// if user is a guest or owner, then it should be ok // if user is a guest or owner, then it should be ok
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id }); await unrestricted.AddStoreUser(store.Id, new StoreUserData() { Id = newUser.Id });
await newUserClient.GetInvoices(store.Id); await newUserClient.GetInvoices(store.Id);
} }
@@ -4017,8 +4017,8 @@ namespace BTCPayServer.Tests
#pragma warning restore CS0618 #pragma warning restore CS0618
var users = await client.GetStoreUsers(user.StoreId); var users = await client.GetStoreUsers(user.StoreId);
var storeUser = Assert.Single(users); var storeUser = Assert.Single(users);
Assert.Equal(user.UserId, storeUser.UserId); Assert.Equal(user.UserId, storeUser.Id);
Assert.Equal(ownerRole.Id, storeUser.Role); Assert.Equal(ownerRole.Id, storeUser.StoreRole);
Assert.Equal(user.Email, storeUser.Email); Assert.Equal(user.Email, storeUser.Email);
Assert.Equal("The Admin", storeUser.Name); Assert.Equal("The Admin", storeUser.Name);
Assert.Equal("avatar.jpg", storeUser.ImageUrl); Assert.Equal("avatar.jpg", storeUser.ImageUrl);
@@ -4050,15 +4050,15 @@ namespace BTCPayServer.Tests
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.RemoveStoreUser(user.StoreId, user.UserId)); await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.RemoveStoreUser(user.StoreId, user.UserId));
// add users to store // add users to store
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = manager.UserId }); await client.AddStoreUser(user.StoreId, new StoreUserData { StoreRole = managerRole.Id, Id = manager.UserId });
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = employeeRole.Id, UserId = employee.UserId }); await client.AddStoreUser(user.StoreId, new StoreUserData { StoreRole = employeeRole.Id, Id = employee.UserId });
// add with email // add with email
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.Email }); await client.AddStoreUser(user.StoreId, new StoreUserData { StoreRole = guestRole.Id, Id = guest.Email });
// test unknown user // test unknown user
await AssertAPIError("user-not-found", async () => await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = "unknown" })); await AssertAPIError("user-not-found", async () => await client.AddStoreUser(user.StoreId, new StoreUserData { StoreRole = managerRole.Id, Id = "unknown" }));
await AssertAPIError("user-not-found", async () => await client.UpdateStoreUser(user.StoreId, "unknown", new StoreUserData { Role = ownerRole.Id })); await AssertAPIError("user-not-found", async () => await client.UpdateStoreUser(user.StoreId, "unknown", new StoreUserData { StoreRole = ownerRole.Id }));
await AssertAPIError("user-not-found", async () => await client.RemoveStoreUser(user.StoreId, "unknown")); await AssertAPIError("user-not-found", async () => await client.RemoveStoreUser(user.StoreId, "unknown"));
//test no access to api for employee //test no access to api for employee
@@ -4080,7 +4080,7 @@ namespace BTCPayServer.Tests
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId)); await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId));
// updates // updates
await client.UpdateStoreUser(user.StoreId, employee.UserId, new StoreUserData { Role = ownerRole.Id }); await client.UpdateStoreUser(user.StoreId, employee.UserId, new StoreUserData { StoreRole = ownerRole.Id });
await employeeClient.GetStore(user.StoreId); await employeeClient.GetStore(user.StoreId);
// remove // remove
@@ -4088,9 +4088,9 @@ namespace BTCPayServer.Tests
await AssertHttpError(403, async () => await employeeClient.GetStore(user.StoreId)); await AssertHttpError(403, async () => await employeeClient.GetStore(user.StoreId));
// test duplicate add // test duplicate add
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId }); await client.AddStoreUser(user.StoreId, new StoreUserData { StoreRole = ownerRole.Id, Id = employee.UserId });
await AssertAPIError("duplicate-store-user-role", async () => await AssertAPIError("duplicate-store-user-role", async () =>
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId })); await client.AddStoreUser(user.StoreId, new StoreUserData { StoreRole = ownerRole.Id, Id = employee.UserId }));
await employeeClient.RemoveStoreUser(user.StoreId, user.UserId); await employeeClient.RemoveStoreUser(user.StoreId, user.UserId);
//test no access to api when unrelated to store at all //test no access to api when unrelated to store at all

View File

@@ -194,5 +194,5 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores-payment-methods_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions> <ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores-payment-methods_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores-users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
</Project> </Project>

View File

@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using System.Reflection.Metadata;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
@@ -11,6 +14,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NicolasDorier.RateLimits;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.Greenfield namespace BTCPayServer.Controllers.Greenfield
@@ -22,15 +26,18 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly CallbackGenerator _callbackGenerator;
private readonly UriResolver _uriResolver; private readonly UriResolver _uriResolver;
public GreenfieldStoreUsersController( public GreenfieldStoreUsersController(
StoreRepository storeRepository, StoreRepository storeRepository,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
CallbackGenerator callbackGenerator,
UriResolver uriResolver) UriResolver uriResolver)
{ {
_storeRepository = storeRepository; _storeRepository = storeRepository;
_userManager = userManager; _userManager = userManager;
_callbackGenerator = callbackGenerator;
_uriResolver = uriResolver; _uriResolver = uriResolver;
} }
@@ -47,10 +54,12 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> RemoveStoreUser(string storeId, string idOrEmail) public async Task<IActionResult> RemoveStoreUser(string storeId, string idOrEmail)
{ {
var store = HttpContext.GetStoreData(); var store = HttpContext.GetStoreData();
if (store == null) return StoreNotFound(); if (store == null)
return StoreNotFound();
var user = await _userManager.FindByIdOrEmail(idOrEmail); var user = await _userManager.FindByIdOrEmail(idOrEmail);
if (user == null) return UserNotFound(); if (user == null)
return UserNotFound();
return await _storeRepository.RemoveStoreUser(storeId, user.Id) return await _storeRepository.RemoveStoreUser(storeId, user.Id)
? Ok() ? Ok()
@@ -63,17 +72,23 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> AddOrUpdateStoreUser(string storeId, StoreUserData request, string idOrEmail = null) public async Task<IActionResult> AddOrUpdateStoreUser(string storeId, StoreUserData request, string idOrEmail = null)
{ {
var store = HttpContext.GetStoreData(); var store = HttpContext.GetStoreData();
if (store == null) return StoreNotFound(); if (store == null)
return StoreNotFound();
#pragma warning disable CS0618 // Type or member is obsolete
request.StoreRole ??= request.Role;
request.Id ??= request.UserId;
#pragma warning restore CS0618 // Type or member is obsolete
var user = await _userManager.FindByIdOrEmail(idOrEmail ?? request.UserId); var user = await _userManager.FindByIdOrEmail(idOrEmail ?? request.Id);
if (user == null) return UserNotFound(); if (user == null)
return UserNotFound();
StoreRoleId roleId = null; StoreRoleId roleId = null;
if (request.Role is not null) if (request.StoreRole is not null)
{ {
roleId = await _storeRepository.ResolveStoreRoleId(storeId, request.Role); roleId = await _storeRepository.ResolveStoreRoleId(storeId, request.StoreRole);
if (roleId is null) if (roleId is null)
ModelState.AddModelError(nameof(request.Role), "The role id provided does not exist"); ModelState.AddModelError(nameof(request.StoreRole), "The role id provided does not exist");
} }
if (!ModelState.IsValid) if (!ModelState.IsValid)
@@ -87,21 +102,21 @@ namespace BTCPayServer.Controllers.Greenfield
: this.CreateAPIError(409, "duplicate-store-user-role", "The user is already added to the store"); : this.CreateAPIError(409, "duplicate-store-user-role", "The user is already added to the store");
} }
private async Task<IEnumerable<StoreUserData>> FromModel(StoreData data) private async Task<IEnumerable<StoreUserData>> FromModel(StoreData store)
{ {
var storeUsers = new List<StoreUserData>(); var storeUsers = new List<StoreUserData>();
foreach (var storeUser in data.UserStores) foreach (var storeUser in store.UserStores)
{ {
var user = await _userManager.FindByIdOrEmail(storeUser.ApplicationUserId); var user = await _userManager.FindByIdOrEmail(storeUser.ApplicationUserId);
var blob = user?.GetBlob(); if (user == null)
storeUsers.Add(new StoreUserData continue;
{ var data = await UserService.ForAPI<StoreUserData>(user, [], _callbackGenerator, _uriResolver, Request);
UserId = storeUser.ApplicationUserId, data.StoreRole = storeUser.StoreRoleId;
Role = storeUser.StoreRoleId, #pragma warning disable CS0618 // Type or member is obsolete
Email = user?.Email, data.UserId = storeUser.ApplicationUserId;
Name = blob?.Name, data.Role = storeUser.StoreRoleId;
ImageUrl = blob?.ImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)) #pragma warning restore CS0618 // Type or member is obsolete
}); storeUsers.Add(data);
} }
return storeUsers; return storeUsers;
} }

View File

@@ -1,5 +1,6 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -84,7 +85,7 @@ namespace BTCPayServer.Controllers.Greenfield
var user = await _userManager.FindByIdOrEmail(idOrEmail); var user = await _userManager.FindByIdOrEmail(idOrEmail);
if (user != null) if (user != null)
{ {
return Ok(await FromModel(user)); return Ok(await ForAPI(user));
} }
return this.UserNotFound(); return this.UserNotFound();
} }
@@ -128,7 +129,13 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpGet("~/api/v1/users/")] [HttpGet("~/api/v1/users/")]
public async Task<ActionResult<ApplicationUserData[]>> GetUsers() public async Task<ActionResult<ApplicationUserData[]>> GetUsers()
{ {
return Ok(await _userService.GetUsersWithRoles()); var usersWithRoles = await _userService.GetUsersWithRoles();
List<ApplicationUserData> users = [];
foreach (var user in usersWithRoles)
{
users.Add(await UserService.ForAPI<ApplicationUserData>(user.User, user.Roles, _callbackGenerator, _uriResolver, Request));
}
return Ok(users);
} }
[Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@@ -136,7 +143,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<ActionResult<ApplicationUserData>> GetCurrentUser() public async Task<ActionResult<ApplicationUserData>> GetCurrentUser()
{ {
var user = await _userManager.GetUserAsync(User); var user = await _userManager.GetUserAsync(User);
return await FromModel(user!); return await ForAPI(user!);
} }
[Authorize(Policy = Policies.CanModifyProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanModifyProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@@ -232,7 +239,7 @@ namespace BTCPayServer.Controllers.Greenfield
if (!ModelState.IsValid) if (!ModelState.IsValid)
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
var model = await FromModel(user); var model = await ForAPI(user);
return Ok(model); return Ok(model);
} }
@@ -264,7 +271,7 @@ namespace BTCPayServer.Controllers.Greenfield
user.SetBlob(blob); user.SetBlob(blob);
await _userManager.UpdateAsync(user); await _userManager.UpdateAsync(user);
_eventAggregator.Publish(new UserEvent.Updated(user)); _eventAggregator.Publish(new UserEvent.Updated(user));
var model = await FromModel(user); var model = await ForAPI(user);
return Ok(model); return Ok(model);
} }
catch (Exception e) catch (Exception e)
@@ -414,7 +421,7 @@ namespace BTCPayServer.Controllers.Greenfield
}; };
_eventAggregator.Publish(userEvent); _eventAggregator.Publish(userEvent);
var model = await FromModel(user); var model = await ForAPI(user);
return CreatedAtAction(string.Empty, model); return CreatedAtAction(string.Empty, model);
} }
@@ -449,14 +456,11 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(); return Ok();
} }
private async Task<ApplicationUserData> FromModel(ApplicationUser data) private async Task<ApplicationUserData> ForAPI(ApplicationUser data)
{ {
var roles = (await _userManager.GetRolesAsync(data)).ToArray(); var roles = (await _userManager.GetRolesAsync(data)).ToArray();
var model = UserService.FromModel(data, roles); var blob = data.GetBlob();
model.ImageUrl = string.IsNullOrEmpty(model.ImageUrl) return await UserService.ForAPI<ApplicationUserData>(data, roles, _callbackGenerator, _uriResolver, Request);
? null
: await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(model.ImageUrl));
return model;
} }
} }
} }

View File

@@ -70,7 +70,7 @@ namespace BTCPayServer.Controllers
InvitationUrl = InvitationUrl =
string.IsNullOrEmpty(blob?.InvitationToken) string.IsNullOrEmpty(blob?.InvitationToken)
? null ? null
: _callbackGenerator.ForInvitation(u, blob.InvitationToken, Request), : _callbackGenerator.ForInvitation(u.Id, blob.InvitationToken, Request),
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null, EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
Approved = u.RequiresApproval ? u.Approved : null, Approved = u.RequiresApproval ? u.Approved : null,
Created = u.Created, Created = u.Created,
@@ -97,7 +97,7 @@ namespace BTCPayServer.Controllers
Id = user.Id, Id = user.Id,
Email = user.Email, Email = user.Email,
Name = blob?.Name, Name = blob?.Name,
InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null : _callbackGenerator.ForInvitation(user, blob.InvitationToken, Request), InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null : _callbackGenerator.ForInvitation(user.Id, blob.InvitationToken, Request),
ImageUrl = string.IsNullOrEmpty(blob?.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)), 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,

View File

@@ -49,12 +49,12 @@ namespace BTCPayServer.Services
public async Task<string> ForInvitation(ApplicationUser user, HttpRequest request) public async Task<string> ForInvitation(ApplicationUser user, HttpRequest request)
{ {
var code = await UserManager.GenerateInvitationTokenAsync<ApplicationUser>(user.Id) ?? throw Bug(); var code = await UserManager.GenerateInvitationTokenAsync<ApplicationUser>(user.Id) ?? throw Bug();
return ForInvitation(user, code, request); return ForInvitation(user.Id, code, request);
} }
public string ForInvitation(ApplicationUser user, string code, HttpRequest request) public string ForInvitation(string userId, string code, HttpRequest request)
{ {
return LinkGenerator.GetUriByAction(nameof(UIAccountController.AcceptInvite), "UIAccount", return LinkGenerator.GetUriByAction(nameof(UIAccountController.AcceptInvite), "UIAccount",
new { userId = user.Id, code }, request.Scheme, request.Host, request.PathBase) ?? throw Bug(); new { userId, code }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
} }
public async Task<string> ForPasswordReset(ApplicationUser user, HttpRequest request) public async Task<string> ForPasswordReset(ApplicationUser user, HttpRequest request)
{ {

View File

@@ -4,10 +4,12 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Storage.Services; using BTCPayServer.Storage.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -40,17 +42,30 @@ namespace BTCPayServer.Services
_logger = logger; _logger = logger;
} }
public async Task<List<ApplicationUserData>> GetUsersWithRoles() public record ApplicationUserWithRoles(ApplicationUser User, string[] Roles);
public async Task<List<ApplicationUserWithRoles>> GetUsersWithRoles()
{ {
await using var context = _applicationDbContextFactory.CreateContext(); await using var context = _applicationDbContextFactory.CreateContext();
return await (context.Users.Select(p => FromModel(p, p.UserRoles.Join(context.Roles, userRole => userRole.RoleId, role => role.Id, var res = await context.Users.Select(p =>
(userRole, role) => role.Name).ToArray()))).ToListAsync(); new
{
User = p,
Roles = p.UserRoles.Join(context.Roles, userRole => userRole.RoleId,
role => role.Id, (userRole, role) => role.Name).ToArray()
})
.ToListAsync();
return res.Select(p => new ApplicationUserWithRoles(p.User, (p.Roles ?? [])!)).ToList();
} }
public static ApplicationUserData FromModel(ApplicationUser data, string?[] roles) public static async Task<T> ForAPI<T>(
ApplicationUser data,
string?[] roles,
CallbackGenerator callbackGenerator,
UriResolver uriResolver,
HttpRequest request) where T : ApplicationUserData, new()
{ {
var blob = data.GetBlob() ?? new(); var blob = data.GetBlob() ?? new UserBlob();
return new ApplicationUserData return new T
{ {
Id = data.Id, Id = data.Id,
Email = data.Email, Email = data.Email,
@@ -60,9 +75,13 @@ namespace BTCPayServer.Services
RequiresApproval = data.RequiresApproval, RequiresApproval = data.RequiresApproval,
Created = data.Created, Created = data.Created,
Name = blob.Name, 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 = IsDisabled(data),
ImageUrl = string.IsNullOrEmpty(blob.ImageUrl)
? null
: await uriResolver.Resolve(request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)),
InvitationUrl = string.IsNullOrEmpty(blob.InvitationToken) ? null
: callbackGenerator.ForInvitation(data.Id, blob.InvitationToken, request)
}; };
} }
@@ -78,8 +97,8 @@ namespace BTCPayServer.Services
private static bool IsDisabled(ApplicationUser user) private static bool IsDisabled(ApplicationUser user)
{ {
return user.LockoutEnabled && user.LockoutEnd is not null && return user is { LockoutEnabled: true, LockoutEnd: {} lockoutEnd } &&
DateTimeOffset.UtcNow < user.LockoutEnd.Value.UtcDateTime; DateTimeOffset.UtcNow < lockoutEnd.UtcDateTime;
} }
public static bool TryCanLogin([NotNullWhen(true)] ApplicationUser? user, [MaybeNullWhen(true)] out string error) public static bool TryCanLogin([NotNullWhen(true)] ApplicationUser? user, [MaybeNullWhen(true)] out string error)

View File

@@ -235,30 +235,25 @@
"properties": { "properties": {
"userId": { "userId": {
"type": "string", "type": "string",
"description": "The id of the user", "description": "The id of the user (Deprecated, use `id` instead)",
"nullable": false "nullable": false,
"deprecated": true
}, },
"role": { "role": {
"type": "string",
"description": "The role of the user. Default roles are `Owner`, `Manager`, `Employee` and `Guest` (Deprecated, use `storeRole` instead)",
"nullable": false,
"deprecated": true
},
"storeRole": {
"type": "string", "type": "string",
"description": "The role of the user. Default roles are `Owner`, `Manager`, `Employee` and `Guest`", "description": "The role of the user. Default roles are `Owner`, `Manager`, `Employee` and `Guest`",
"nullable": false "nullable": false
},
"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
} }
} }
},
{
"$ref": "#/components/schemas/ApplicationUserData"
} }
] ]
} }

View File

@@ -520,6 +520,11 @@
"description": "The profile picture URL of the user", "description": "The profile picture URL of the user",
"nullable": true "nullable": true
}, },
"invitationUrl": {
"type": "string",
"description": "The pending invitation 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"
@@ -546,6 +551,10 @@
} }
] ]
}, },
"disabled": {
"type": "boolean",
"description": "True if an admin has disabled the user"
},
"roles": { "roles": {
"type": "array", "type": "array",
"nullable": false, "nullable": false,