[Greenfield] Allow passing email instead of user id in API (#4732)

This commit is contained in:
Nicolas Dorier
2023-03-03 21:24:27 +09:00
committed by GitHub
parent 0406b420c8
commit 5caa0e0722
9 changed files with 56 additions and 37 deletions

View File

@@ -7,6 +7,10 @@ namespace BTCPayServer.Abstractions.Extensions;
public static class GreenfieldExtensions public static class GreenfieldExtensions
{ {
public static IActionResult UserNotFound(this ControllerBase ctrl)
{
return ctrl.CreateAPIError(404, "user-not-found", "The user was not found");
}
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState) public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
{ {
return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError()); return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError());

View File

@@ -212,7 +212,7 @@ namespace BTCPayServer.Tests
var store = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "Pouet lol" }); var store = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "Pouet lol" });
// Grant right to another user // Grant right to another user
newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Id, new CreateApiKeyRequest() newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Email, new CreateApiKeyRequest()
{ {
Label = "Hello world", Label = "Hello world",
Permissions = new Permission[] { Permission.Create(Policies.CanViewInvoices, store.Id) }, Permissions = new Permission[] { Permission.Create(Policies.CanViewInvoices, store.Id) },

View File

@@ -50,12 +50,16 @@ namespace BTCPayServer.Controllers.Greenfield
return CreateUserAPIKey(_userManager.GetUserId(User), request); return CreateUserAPIKey(_userManager.GetUserId(User), request);
} }
[HttpPost("~/api/v1/users/{userId}/api-keys")] [HttpPost("~/api/v1/users/{idOrEmail}/api-keys")]
[Authorize(Policy = Policies.CanManageUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanManageUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CreateUserAPIKey(string userId, CreateApiKeyRequest request) public async Task<IActionResult> CreateUserAPIKey(string idOrEmail, CreateApiKeyRequest request)
{ {
request ??= new CreateApiKeyRequest(); request ??= new CreateApiKeyRequest();
request.Permissions ??= System.Array.Empty<Permission>(); request.Permissions ??= System.Array.Empty<Permission>();
var userId = (await _userManager.FindByIdOrEmail(idOrEmail))?.Id;
if (userId is null)
return this.UserNotFound();
var key = new APIKeyData() var key = new APIKeyData()
{ {
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)), Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
@@ -67,14 +71,7 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
Permissions = request.Permissions.Select(p => p.ToString()).Distinct().ToArray() Permissions = request.Permissions.Select(p => p.ToString()).Distinct().ToArray()
}); });
try await _apiKeyRepository.CreateKey(key);
{
await _apiKeyRepository.CreateKey(key);
}
catch (DbUpdateException)
{
return this.CreateAPIError("user-not-found", "This user does not exists");
}
return Ok(FromModel(key)); return Ok(FromModel(key));
} }
@@ -96,10 +93,13 @@ namespace BTCPayServer.Controllers.Greenfield
return RevokeAPIKey(_userManager.GetUserId(User), apikey); return RevokeAPIKey(_userManager.GetUserId(User), apikey);
} }
[HttpDelete("~/api/v1/users/{userId}/api-keys/{apikey}", Order = 1)] [HttpDelete("~/api/v1/users/{idOrEmail}/api-keys/{apikey}", Order = 1)]
[Authorize(Policy = Policies.CanManageUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanManageUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> RevokeAPIKey(string userId, string apikey) public async Task<IActionResult> RevokeAPIKey(string idOrEmail, string apikey)
{ {
var userId = (await _userManager.FindByIdOrEmail(idOrEmail))?.Id;
if (userId is null)
return this.UserNotFound();
if (!string.IsNullOrEmpty(apikey) && if (!string.IsNullOrEmpty(apikey) &&
await _apiKeyRepository.Remove(apikey, userId)) await _apiKeyRepository.Remove(apikey, userId))
return Ok(); return Ok();

View File

@@ -20,10 +20,12 @@ namespace BTCPayServer.Controllers.Greenfield
public class GreenfieldStoreUsersController : ControllerBase public class GreenfieldStoreUsersController : ControllerBase
{ {
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly UserManager<ApplicationUser> _userManager;
public GreenfieldStoreUsersController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager) public GreenfieldStoreUsersController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
{ {
_storeRepository = storeRepository; _storeRepository = storeRepository;
_userManager = userManager;
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/users")] [HttpGet("~/api/v1/stores/{storeId}/users")]
@@ -34,8 +36,8 @@ namespace BTCPayServer.Controllers.Greenfield
return store == null ? StoreNotFound() : Ok(FromModel(store)); return store == null ? StoreNotFound() : Ok(FromModel(store));
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/users/{userId}")] [HttpDelete("~/api/v1/stores/{storeId}/users/{idOrEmail}")]
public async Task<IActionResult> RemoveStoreUser(string storeId, string userId) public async Task<IActionResult> RemoveStoreUser(string storeId, string idOrEmail)
{ {
var store = HttpContext.GetStoreData(); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
@@ -43,9 +45,9 @@ namespace BTCPayServer.Controllers.Greenfield
return StoreNotFound(); return StoreNotFound();
} }
if (await _storeRepository.RemoveStoreUser(storeId, userId)) var userId = await _userManager.FindByIdOrEmail(idOrEmail);
if (userId != null && await _storeRepository.RemoveStoreUser(storeId, idOrEmail))
{ {
return Ok(); return Ok();
} }

View File

@@ -69,22 +69,22 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpGet("~/api/v1/users/{idOrEmail}")] [HttpGet("~/api/v1/users/{idOrEmail}")]
public async Task<IActionResult> GetUser(string idOrEmail) public async Task<IActionResult> GetUser(string idOrEmail)
{ {
var user = (await _userManager.FindByIdAsync(idOrEmail)) ?? await _userManager.FindByEmailAsync(idOrEmail); var user = await _userManager.FindByIdOrEmail(idOrEmail);
if (user != null) if (user != null)
{ {
return Ok(await FromModel(user)); return Ok(await FromModel(user));
} }
return UserNotFound(); return this.UserNotFound();
} }
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/users/{idOrEmail}/lock")] [HttpPost("~/api/v1/users/{idOrEmail}/lock")]
public async Task<IActionResult> LockUser(string idOrEmail, LockUserRequest request) public async Task<IActionResult> LockUser(string idOrEmail, LockUserRequest request)
{ {
var user = await _userManager.FindByIdAsync(idOrEmail) ?? await _userManager.FindByEmailAsync(idOrEmail); var user = await _userManager.FindByIdOrEmail(idOrEmail);
if (user is null) if (user is null)
{ {
return UserNotFound(); return this.UserNotFound();
} }
var success = await _userService.ToggleUser(user.Id, request.Locked ? DateTimeOffset.MaxValue : null); var success = await _userService.ToggleUser(user.Id, request.Locked ? DateTimeOffset.MaxValue : null);
@@ -223,7 +223,7 @@ namespace BTCPayServer.Controllers.Greenfield
var user = await _userManager.FindByIdAsync(userId); var user = await _userManager.FindByIdAsync(userId);
if (user == null) if (user == null)
{ {
return UserNotFound(); return this.UserNotFound();
} }
// We can safely delete the user if it's not an admin user // We can safely delete the user if it's not an admin user
@@ -251,12 +251,5 @@ namespace BTCPayServer.Controllers.Greenfield
var roles = (await _userManager.GetRolesAsync(data)).ToArray(); var roles = (await _userManager.GetRolesAsync(data)).ToArray();
return UserService.FromModel(data, roles); return UserService.FromModel(data, roles);
} }
private IActionResult UserNotFound()
{
return this.CreateAPIError(404, "user-not-found", "The user was not found");
}
} }
} }

View File

@@ -0,0 +1,19 @@
#nullable enable
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
namespace BTCPayServer
{
public static class UserManagerExtensions
{
public async static Task<TUser?> FindByIdOrEmail<TUser>(this UserManager<TUser> userManager, string? idOrEmail) where TUser : class
{
if (string.IsNullOrEmpty(idOrEmail))
return null;
if (idOrEmail.Contains('@'))
return await userManager.FindByEmailAsync(idOrEmail);
else
return await userManager.FindByIdAsync(idOrEmail);
}
}
}

View File

@@ -33,7 +33,7 @@
] ]
} }
}, },
"/api/v1/users/{userId}/api-keys/{apikey}": { "/api/v1/users/{idOrEmail}/api-keys/{apikey}": {
"delete": { "delete": {
"operationId": "ApiKeys_DeleteUserApiKey", "operationId": "ApiKeys_DeleteUserApiKey",
"tags": [ "tags": [
@@ -43,10 +43,10 @@
"description": "Revoke the API key of a target user so that it cannot be used anymore", "description": "Revoke the API key of a target user so that it cannot be used anymore",
"parameters": [ "parameters": [
{ {
"name": "userId", "name": "idOrEmail",
"in": "path", "in": "path",
"required": true, "required": true,
"description": "The target user", "description": "The target user's id or email",
"schema": { "type": "string" } "schema": { "type": "string" }
}, },
{ {
@@ -220,7 +220,7 @@
] ]
} }
}, },
"/api/v1/users/{userId}/api-keys": { "/api/v1/users/{idOrEmail}/api-keys": {
"post": { "post": {
"operationId": "ApiKeys_CreateUserApiKey", "operationId": "ApiKeys_CreateUserApiKey",
"tags": [ "tags": [
@@ -230,10 +230,10 @@
"description": "Create a new API Key for a user", "description": "Create a new API Key for a user",
"parameters": [ "parameters": [
{ {
"name": "userId", "name": "idOrEmail",
"in": "path", "in": "path",
"required": true, "required": true,
"description": "The target user", "description": "The target user's id or email",
"schema": { "type": "string" } "schema": { "type": "string" }
} }
], ],

View File

@@ -114,7 +114,7 @@
] ]
} }
}, },
"/api/v1/stores/{storeId}/users/{userId}": { "/api/v1/stores/{storeId}/users/{idOrEmail}": {
"delete": { "delete": {
"tags": [ "tags": [
"Stores (Users)" "Stores (Users)"
@@ -136,7 +136,7 @@
"name": "userId", "name": "userId",
"in": "path", "in": "path",
"required": true, "required": true,
"description": "The user", "description": "The user's id or email",
"schema": { "schema": {
"type": "string" "type": "string"
} }

View File

@@ -15,6 +15,7 @@
* Remove superflous punctuation in some translations * Remove superflous punctuation in some translations
* Update Polski translation * Update Polski translation
* Greenfield: Routes accepting a userId can now also accept userEmail (#4732)
## 1.8.0 ## 1.8.0