From ccceec9da253ffa13dbb232bda0bc0af6365809a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20W=C3=B6rndle?= Date: Wed, 7 Dec 2022 20:51:55 +0100 Subject: [PATCH 1/2] feat: user management endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stefan Pfahler Signed-off-by: Philipp Wörndle --- .../Deskstar/Controllers/UserController.cs | 100 +++++++++++++--- .../Deskstar/Core/RequestInteractions.cs | 16 +++ .../Deskstar/Models/UserProfileDto.cs | 2 + .../Deskstar/Usecases/UserUsecases.cs | 30 +++++ .../Teststar.Tests/Tests/UserUsecasesTests.cs | 112 +++++++++++++++--- 5 files changed, 229 insertions(+), 31 deletions(-) create mode 100644 src/deskstar-backend/Deskstar/Core/RequestInteractions.cs diff --git a/src/deskstar-backend/Deskstar/Controllers/UserController.cs b/src/deskstar-backend/Deskstar/Controllers/UserController.cs index a4f81938..e23e2d5f 100644 --- a/src/deskstar-backend/Deskstar/Controllers/UserController.cs +++ b/src/deskstar-backend/Deskstar/Controllers/UserController.cs @@ -1,9 +1,9 @@ -using System.IdentityModel.Tokens.Jwt; +using Deskstar.Core; +using Deskstar.Entities; using Deskstar.Models; using Deskstar.Usecases; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; namespace Deskstar.Controllers; @@ -24,6 +24,46 @@ public UserController(ILogger logger, IUserUsecases userUsecases _autoMapperConfiguration = autoMapperConfiguration; } /// + /// Returns all users for a specific company + /// + /// List of user information in JSON Format + /// + /// Sample request: + /// Get /users with JWT Token + /// + /// + /// List of user information in JSON Format + /// Internal Server Error + /// Bad Request + [HttpGet] + [Authorize(Policy = "Admin")] + [ProducesResponseType(typeof(UserProfileDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Produces("application/json")] + public IActionResult Get() + { + var userId = RequestInteractions.ExtractIdFromRequest(Request); + try + { + var entities = _userUsecases.ReadAllUsers(userId); + var mapper = _autoMapperConfiguration.GetConfiguration().CreateMapper(); + var users = entities.Select(user => mapper.Map(user)).ToList(); + + return Ok(users); + } + catch (ArgumentException e) + { + _logger.LogError(e, e.Message); + return Problem(detail: e.Message, statusCode: 400); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return Problem(statusCode: 500); + } + } + /// /// Returns user specific information /// /// User information in JSON Format @@ -43,10 +83,7 @@ public UserController(ILogger logger, IUserUsecases userUsecases [Produces("application/json")] public IActionResult GetMe() { - var accessToken = Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", string.Empty); - var handler = new JwtSecurityTokenHandler(); - var jwtSecurityToken = handler.ReadJwtToken(accessToken); - var userId = new Guid(jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.NameId).Value); + var userId = RequestInteractions.ExtractIdFromRequest(Request); try { @@ -67,7 +104,7 @@ public IActionResult GetMe() } } - /// + /// /// Lets an admin approve a users registration for their company /// /// empty response @@ -86,10 +123,7 @@ public IActionResult GetMe() [ProducesResponseType(StatusCodes.Status500InternalServerError)] public IActionResult ApproveUser(string userId) { - var accessToken = Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", string.Empty); - var handler = new JwtSecurityTokenHandler(); - var jwtSecurityToken = handler.ReadJwtToken(accessToken); - var adminId = new Guid(jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.NameId).Value); + var adminId = RequestInteractions.ExtractIdFromRequest(Request); try { @@ -127,10 +161,7 @@ public IActionResult ApproveUser(string userId) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public IActionResult DeclineUser(string userId) { - var accessToken = Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", string.Empty); - var handler = new JwtSecurityTokenHandler(); - var jwtSecurityToken = handler.ReadJwtToken(accessToken); - var adminId = new Guid(jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.NameId).Value); + var adminId = RequestInteractions.ExtractIdFromRequest(Request); try { @@ -148,4 +179,43 @@ public IActionResult DeclineUser(string userId) return Problem(statusCode: 500); } } + /// + /// Update user information + /// + /// empty response + /// + /// Sample request: + /// Post /users/me + /// + /// + /// Empty Response + /// Internal Server Error + /// Bad Request + [HttpPost("me")] + [Authorize] + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult UpdateUser(UserProfileDto userDto) + { + var userId = RequestInteractions.ExtractIdFromRequest(Request); + + try + { + var mapper = _autoMapperConfiguration.GetConfiguration().CreateMapper(); + var user = mapper.Map(userDto); + _userUsecases.UpdateUser(user); + return Ok(); + } + catch (ArgumentException e) + { + _logger.LogError(e, e.Message); + return Problem(detail: e.Message, statusCode: 400); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return Problem(statusCode: 500); + } + } } \ No newline at end of file diff --git a/src/deskstar-backend/Deskstar/Core/RequestInteractions.cs b/src/deskstar-backend/Deskstar/Core/RequestInteractions.cs new file mode 100644 index 00000000..e2047394 --- /dev/null +++ b/src/deskstar-backend/Deskstar/Core/RequestInteractions.cs @@ -0,0 +1,16 @@ +using System.IdentityModel.Tokens.Jwt; +using Microsoft.Net.Http.Headers; + +namespace Deskstar.Core; + +public class RequestInteractions +{ + public static Guid ExtractIdFromRequest(HttpRequest request) + { + var accessToken = request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", string.Empty); + var handler = new JwtSecurityTokenHandler(); + var jwtSecurityToken = handler.ReadJwtToken(accessToken); + return new Guid(jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.NameId).Value); + + } +} \ No newline at end of file diff --git a/src/deskstar-backend/Deskstar/Models/UserProfileDto.cs b/src/deskstar-backend/Deskstar/Models/UserProfileDto.cs index ec02ef02..57824fe5 100644 --- a/src/deskstar-backend/Deskstar/Models/UserProfileDto.cs +++ b/src/deskstar-backend/Deskstar/Models/UserProfileDto.cs @@ -15,6 +15,8 @@ public static void createMappings(IMapperConfigurationExpression cfg) { cfg.CreateMap() .ForMember(dest => dest.Company, act => act.MapFrom(src => src.Company)); + cfg.CreateMap() + .ForMember(dest => dest.Company, act => act.MapFrom(src => src.Company)); } public Guid UserId { get; set; } diff --git a/src/deskstar-backend/Deskstar/Usecases/UserUsecases.cs b/src/deskstar-backend/Deskstar/Usecases/UserUsecases.cs index 911b873c..36001b34 100644 --- a/src/deskstar-backend/Deskstar/Usecases/UserUsecases.cs +++ b/src/deskstar-backend/Deskstar/Usecases/UserUsecases.cs @@ -6,6 +6,8 @@ namespace Deskstar.Usecases; public interface IUserUsecases { + public List ReadAllUsers(Guid adminId); + public Guid UpdateUser(User user); public User ReadSpecificUser(Guid userId); public Guid ApproveUser(Guid adminId, string userId); public Guid DeclineUser(Guid adminId, string userId); @@ -86,4 +88,32 @@ public Guid DeclineUser(Guid adminId, string userId) throw new ArgumentException($"There is no user with id '{userId}'"); } } + + public List ReadAllUsers(Guid adminId) + { + try + { + var admin = _context.Users.Single(user => user.UserId == adminId); + return _context.Users.Where(user => user.CompanyId == admin.CompanyId).ToList(); + } + catch (InvalidOperationException) + { + throw new ArgumentException($"There is no admin with id '{adminId}'"); + } + } + + public Guid UpdateUser(User user) + { + try + { + _context.Users.Single(u => u.UserId == user.UserId); + _context.Users.Update(user); + _context.SaveChanges(); + return user.UserId; + } + catch (InvalidOperationException) + { + throw new ArgumentException($"There is no user with id '{user.UserId}'"); + } + } } \ No newline at end of file diff --git a/src/deskstar-backend/Teststar.Tests/Tests/UserUsecasesTests.cs b/src/deskstar-backend/Teststar.Tests/Tests/UserUsecasesTests.cs index 90c23e15..5f557c0c 100644 --- a/src/deskstar-backend/Teststar.Tests/Tests/UserUsecasesTests.cs +++ b/src/deskstar-backend/Teststar.Tests/Tests/UserUsecasesTests.cs @@ -64,10 +64,10 @@ public void ApproveUser_ShouldApproveUser_WhenValidUserIdIsGiven() //arrange var logger = new Mock>(); - var adminUsecases = new UserUsecases(logger.Object, db); + var userUsecases = new UserUsecases(logger.Object, db); //act - var result = adminUsecases.ApproveUser(admin.UserId, user.UserId.ToString()); + var result = userUsecases.ApproveUser(admin.UserId, user.UserId.ToString()); //assert Assert.That(result == user.UserId); @@ -87,10 +87,10 @@ public void ApproveUser_ShouldThrowArgumentException_WhenInvalidUserIdIsGiven() //arrange var logger = new Mock>(); - var adminUsecases = new UserUsecases(logger.Object, db); + var userUsecases = new UserUsecases(logger.Object, db); //act + assert - Assert.Throws(() => adminUsecases.ApproveUser(admin.UserId, "invalidguid")); + Assert.Throws(() => userUsecases.ApproveUser(admin.UserId, "invalidguid")); //cleanup db.Database.EnsureDeleted(); @@ -106,10 +106,10 @@ public void ApproveUser_ShouldThrowArgumentException_WhenThereIsNoUserWithGivenI //arrange var logger = new Mock>(); - var adminUsecases = new UserUsecases(logger.Object, db); + var userUsecases = new UserUsecases(logger.Object, db); //act + assert - Assert.Throws(() => adminUsecases.ApproveUser(admin.UserId, (new Guid()).ToString())); + Assert.Throws(() => userUsecases.ApproveUser(admin.UserId, (new Guid()).ToString())); //cleanup db.Database.EnsureDeleted(); @@ -125,10 +125,10 @@ public void ApproveUser_ShouldThrowArgumentException_WhenValidUserIsGivenButAdmi //arrange var logger = new Mock>(); - var adminUsecases = new UserUsecases(logger.Object, db); + var userUsecases = new UserUsecases(logger.Object, db); //act + assert - Assert.Throws(() => adminUsecases.ApproveUser(admin.UserId, user.UserId.ToString())); + Assert.Throws(() => userUsecases.ApproveUser(admin.UserId, user.UserId.ToString())); //cleanup db.Database.EnsureDeleted(); @@ -144,10 +144,10 @@ public void DeclineUser_ShouldThrowArgumentException_WhenValidUserIsGivenButAdmi //arrange var logger = new Mock>(); - var adminUsecases = new UserUsecases(logger.Object, db); + var userUsecases = new UserUsecases(logger.Object, db); //act + assert - Assert.Throws(() => adminUsecases.DeclineUser(admin.UserId, user.UserId.ToString())); + Assert.Throws(() => userUsecases.DeclineUser(admin.UserId, user.UserId.ToString())); //cleanup db.Database.EnsureDeleted(); @@ -164,10 +164,10 @@ public void DeclineUser_ShouldThrowArgumentException_WhenUserIsAlreadyApproved() //arrange var logger = new Mock>(); - var adminUsecases = new UserUsecases(logger.Object, db); + var userUsecases = new UserUsecases(logger.Object, db); //act + assert - Assert.Throws(() => adminUsecases.DeclineUser(admin.UserId, user.UserId.ToString())); + Assert.Throws(() => userUsecases.DeclineUser(admin.UserId, user.UserId.ToString())); //cleanup db.Database.EnsureDeleted(); @@ -183,10 +183,10 @@ public void DeclineUser_ShouldThrowArgumentException_WhenNoUserExistsWithGivenId //arrange var logger = new Mock>(); - var adminUsecases = new UserUsecases(logger.Object, db); + var userUsecases = new UserUsecases(logger.Object, db); //act + assert - Assert.Throws(() => adminUsecases.DeclineUser(admin.UserId, (new Guid()).ToString())); + Assert.Throws(() => userUsecases.DeclineUser(admin.UserId, (new Guid()).ToString())); //cleanup db.Database.EnsureDeleted(); @@ -204,10 +204,10 @@ public void DeclineUser_ShouldRemoveUser_WhenValidUserIdIsGiven() //arrange var logger = new Mock>(); - var adminUsecases = new UserUsecases(logger.Object, db); + var userUsecases = new UserUsecases(logger.Object, db); //act - var result = adminUsecases.DeclineUser(admin.UserId, user.UserId.ToString()); + var result = userUsecases.DeclineUser(admin.UserId, user.UserId.ToString()); //assert var userWasRemoved = !db.Users.Contains(user); @@ -218,6 +218,86 @@ public void DeclineUser_ShouldRemoveUser_WhenValidUserIdIsGiven() db.Database.EnsureDeleted(); } + [Test] + public void UpdateUser_WhenValidUserIsProvided_ShouldUpdateUser() + { + //setup + using var db = new DataContext(); + User user, admin; + bool isUserApproved = true; + + SetupSingleUser(db, isUserApproved, out user, out admin); + + //arrange + var logger = new Mock>(); + var userUsecases = new UserUsecases(logger.Object, db); + + //act + var result = userUsecases.UpdateUser(user); + + //assert + Assert.That(result == user.UserId); + + } + [Test] + public void UpdateUser_WhenInvalidUserIsProvided_ShouldThrowArgumentException() + { + //setup + using var db = new DataContext(); + User user, admin; + bool isUserApproved = true; + + SetupSingleUser(db, isUserApproved, out user, out admin); + + //arrange + var logger = new Mock>(); + var userUsecases = new UserUsecases(logger.Object, db); + + //act + assert + Assert.Throws(() => userUsecases.UpdateUser(new User())); + + } + [Test] + public void ReadAllUsers_WhenValidUserIdIsProvided_ShouldReturnListOfAllUsersInTheSameCompany() + { + //setup + using var db = new DataContext(); + User user, admin; + bool isUserApproved = true; + + SetupSingleUser(db, isUserApproved, out user, out admin); + + //arrange + var logger = new Mock>(); + var userUsecases = new UserUsecases(logger.Object, db); + + //act + var result = userUsecases.ReadAllUsers(admin.UserId); + + //assert + Assert.That(result.Count != 0); + Assert.That(result.Contains(user)); + + } + [Test] + public void ReadAllUsers_WhenInvalidUserIdIsProvided_ShouldThrowArgumentException() + { + //setup + using var db = new DataContext(); + User user, admin; + bool isUserApproved = true; + + SetupSingleUser(db, isUserApproved, out user, out admin); + + //arrange + var logger = new Mock>(); + var userUsecases = new UserUsecases(logger.Object, db); + + //act + assert + Assert.Throws(() => userUsecases.ReadAllUsers(new Guid())); + + } + private void SetupSingleUser(DataContext db, bool userApproved, out User user, out User admin) { var hasher = new PasswordHasher(); From 9dad378780e53323e41c285a317fb1ee20118c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20W=C3=B6rndle?= Date: Mon, 12 Dec 2022 09:30:58 +0100 Subject: [PATCH 2/2] chore: remove newline --- src/deskstar-backend/Deskstar/Core/RequestInteractions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/deskstar-backend/Deskstar/Core/RequestInteractions.cs b/src/deskstar-backend/Deskstar/Core/RequestInteractions.cs index e2047394..ab1e096b 100644 --- a/src/deskstar-backend/Deskstar/Core/RequestInteractions.cs +++ b/src/deskstar-backend/Deskstar/Core/RequestInteractions.cs @@ -11,6 +11,5 @@ public static Guid ExtractIdFromRequest(HttpRequest request) var handler = new JwtSecurityTokenHandler(); var jwtSecurityToken = handler.ReadJwtToken(accessToken); return new Guid(jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.NameId).Value); - } } \ No newline at end of file