diff --git a/BTCPayServer.Abstractions/Extensions/GreenfieldExtensions.cs b/BTCPayServer.Abstractions/Extensions/GreenfieldExtensions.cs index ab772d235..deee34e34 100644 --- a/BTCPayServer.Abstractions/Extensions/GreenfieldExtensions.cs +++ b/BTCPayServer.Abstractions/Extensions/GreenfieldExtensions.cs @@ -7,6 +7,10 @@ namespace BTCPayServer.Abstractions.Extensions; 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) { return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError()); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 034ee9bed..7367ddb2b 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -212,7 +212,7 @@ namespace BTCPayServer.Tests var store = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "Pouet lol" }); // Grant right to another user - newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Id, new CreateApiKeyRequest() + newUserAPIKey = await unrestricted.CreateAPIKey(newUser.Email, new CreateApiKeyRequest() { Label = "Hello world", Permissions = new Permission[] { Permission.Create(Policies.CanViewInvoices, store.Id) }, diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldApiKeysController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldApiKeysController.cs index f0c7e8651..c6e2f80be 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldApiKeysController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldApiKeysController.cs @@ -50,12 +50,16 @@ namespace BTCPayServer.Controllers.Greenfield 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)] - public async Task CreateUserAPIKey(string userId, CreateApiKeyRequest request) + public async Task CreateUserAPIKey(string idOrEmail, CreateApiKeyRequest request) { request ??= new CreateApiKeyRequest(); request.Permissions ??= System.Array.Empty(); + + var userId = (await _userManager.FindByIdOrEmail(idOrEmail))?.Id; + if (userId is null) + return this.UserNotFound(); var key = new APIKeyData() { Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)), @@ -67,14 +71,7 @@ namespace BTCPayServer.Controllers.Greenfield { Permissions = request.Permissions.Select(p => p.ToString()).Distinct().ToArray() }); - try - { - await _apiKeyRepository.CreateKey(key); - } - catch (DbUpdateException) - { - return this.CreateAPIError("user-not-found", "This user does not exists"); - } + await _apiKeyRepository.CreateKey(key); return Ok(FromModel(key)); } @@ -96,10 +93,13 @@ namespace BTCPayServer.Controllers.Greenfield 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)] - public async Task RevokeAPIKey(string userId, string apikey) + public async Task RevokeAPIKey(string idOrEmail, string apikey) { + var userId = (await _userManager.FindByIdOrEmail(idOrEmail))?.Id; + if (userId is null) + return this.UserNotFound(); if (!string.IsNullOrEmpty(apikey) && await _apiKeyRepository.Remove(apikey, userId)) return Ok(); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreUsersController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreUsersController.cs index e94491f10..cba0c46cc 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreUsersController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreUsersController.cs @@ -20,10 +20,12 @@ namespace BTCPayServer.Controllers.Greenfield public class GreenfieldStoreUsersController : ControllerBase { private readonly StoreRepository _storeRepository; + private readonly UserManager _userManager; public GreenfieldStoreUsersController(StoreRepository storeRepository, UserManager userManager) { _storeRepository = storeRepository; + _userManager = userManager; } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/users")] @@ -34,8 +36,8 @@ namespace BTCPayServer.Controllers.Greenfield return store == null ? StoreNotFound() : Ok(FromModel(store)); } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - [HttpDelete("~/api/v1/stores/{storeId}/users/{userId}")] - public async Task RemoveStoreUser(string storeId, string userId) + [HttpDelete("~/api/v1/stores/{storeId}/users/{idOrEmail}")] + public async Task RemoveStoreUser(string storeId, string idOrEmail) { var store = HttpContext.GetStoreData(); if (store == null) @@ -43,9 +45,9 @@ namespace BTCPayServer.Controllers.Greenfield return StoreNotFound(); } - if (await _storeRepository.RemoveStoreUser(storeId, userId)) + var userId = await _userManager.FindByIdOrEmail(idOrEmail); + if (userId != null && await _storeRepository.RemoveStoreUser(storeId, idOrEmail)) { - return Ok(); } diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs index 5651f894d..31419fbb9 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs @@ -69,22 +69,22 @@ namespace BTCPayServer.Controllers.Greenfield [HttpGet("~/api/v1/users/{idOrEmail}")] public async Task GetUser(string idOrEmail) { - var user = (await _userManager.FindByIdAsync(idOrEmail)) ?? await _userManager.FindByEmailAsync(idOrEmail); + var user = await _userManager.FindByIdOrEmail(idOrEmail); if (user != null) { return Ok(await FromModel(user)); } - return UserNotFound(); + return this.UserNotFound(); } [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPost("~/api/v1/users/{idOrEmail}/lock")] public async Task 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) { - return UserNotFound(); + return this.UserNotFound(); } 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); if (user == null) { - return UserNotFound(); + return this.UserNotFound(); } // 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(); return UserService.FromModel(data, roles); } - - - - private IActionResult UserNotFound() - { - return this.CreateAPIError(404, "user-not-found", "The user was not found"); - } } } diff --git a/BTCPayServer/UserManagerExtensions.cs b/BTCPayServer/UserManagerExtensions.cs new file mode 100644 index 000000000..de6750d8a --- /dev/null +++ b/BTCPayServer/UserManagerExtensions.cs @@ -0,0 +1,19 @@ +#nullable enable +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace BTCPayServer +{ + public static class UserManagerExtensions + { + public async static Task FindByIdOrEmail(this UserManager 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); + } + } +} diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.api-keys.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.api-keys.json index 46c71d128..8fb444643 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.api-keys.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.api-keys.json @@ -33,7 +33,7 @@ ] } }, - "/api/v1/users/{userId}/api-keys/{apikey}": { + "/api/v1/users/{idOrEmail}/api-keys/{apikey}": { "delete": { "operationId": "ApiKeys_DeleteUserApiKey", "tags": [ @@ -43,10 +43,10 @@ "description": "Revoke the API key of a target user so that it cannot be used anymore", "parameters": [ { - "name": "userId", + "name": "idOrEmail", "in": "path", "required": true, - "description": "The target user", + "description": "The target user's id or email", "schema": { "type": "string" } }, { @@ -220,7 +220,7 @@ ] } }, - "/api/v1/users/{userId}/api-keys": { + "/api/v1/users/{idOrEmail}/api-keys": { "post": { "operationId": "ApiKeys_CreateUserApiKey", "tags": [ @@ -230,10 +230,10 @@ "description": "Create a new API Key for a user", "parameters": [ { - "name": "userId", + "name": "idOrEmail", "in": "path", "required": true, - "description": "The target user", + "description": "The target user's id or email", "schema": { "type": "string" } } ], diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-users.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-users.json index ea2823aa7..628f9901c 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-users.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-users.json @@ -114,7 +114,7 @@ ] } }, - "/api/v1/stores/{storeId}/users/{userId}": { + "/api/v1/stores/{storeId}/users/{idOrEmail}": { "delete": { "tags": [ "Stores (Users)" @@ -136,7 +136,7 @@ "name": "userId", "in": "path", "required": true, - "description": "The user", + "description": "The user's id or email", "schema": { "type": "string" } diff --git a/Changelog.md b/Changelog.md index 3da06c502..73fe4b17c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,7 @@ * Remove superflous punctuation in some translations * Update Polski translation +* Greenfield: Routes accepting a userId can now also accept userEmail (#4732) ## 1.8.0