From f42c6476c3e255c441740316e5b67682aed55a0f Mon Sep 17 00:00:00 2001 From: Maciej Koperdowski <85642918+mkoper02@users.noreply.github.com> Date: Sun, 23 Jun 2024 13:28:15 +0200 Subject: [PATCH] [RSN-60] - Implement required methods in MeController and UserController (#58) * feat: add jwt and exceptions handlers Simplify the logic on the controllers' side, custom exceptions and a library for validation were added. The implemented handlers are allow to catch those exceptions and return the corresponding statuses and detailed response. Additionally, mappers have been created to more easily convert entities into the corresponding DTOs, fixed enum conversion when it comes to UserRole and export postgres port on the development environment. * test: add unit tests for jwt, handlers and validators * feat: add placeholders for endpoints * chore: add missing controllers placeholders * Created controllers * Created controllers * Finished controllers and updated services * Update UserService tests * Create and update image methods update * Image controllers update * Update controller and service * Added Address to UserDto * Controller update and service changes Changed Image and Participant service (and participant tests), controller now use new service methods * Methods name changes --------- Co-authored-by: raczu --- .../Services/ParticipantServiceTests.cs | 153 +++++++--------- .../Services/UserServiceTests.cs | 34 +++- .../ReasnAPI/Controllers/MeController.cs | 171 +++++++++++++----- .../ReasnAPI/Controllers/UsersController.cs | 75 ++++++-- .../ReasnAPI/Mappers/ParticipantMapper.cs | 14 +- .../ReasnAPI/Mappers/UserInterestMapper.cs | 5 +- .../ReasnAPI/ReasnAPI/Mappers/UserMapper.cs | 1 + .../ReasnAPI/Models/DTOs/ParticipantDto.cs | 4 +- .../ReasnAPI/ReasnAPI/Models/DTOs/UserDto.cs | 1 + Server/ReasnAPI/ReasnAPI/Program.cs | 16 +- .../ReasnAPI/Services/EventService.cs | 2 +- .../ReasnAPI/Services/ImageService.cs | 14 +- .../ReasnAPI/Services/ParameterService.cs | 2 +- .../ReasnAPI/Services/ParticipantService.cs | 63 +++---- .../ReasnAPI/ReasnAPI/Services/UserService.cs | 52 ++++-- 15 files changed, 361 insertions(+), 246 deletions(-) diff --git a/Server/ReasnAPI/ReasnAPI.Tests/Services/ParticipantServiceTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/Services/ParticipantServiceTests.cs index 80057add..8537a802 100644 --- a/Server/ReasnAPI/ReasnAPI.Tests/Services/ParticipantServiceTests.cs +++ b/Server/ReasnAPI/ReasnAPI.Tests/Services/ParticipantServiceTests.cs @@ -11,62 +11,6 @@ namespace ReasnAPI.Tests.Services; [TestClass] public class ParticipantServiceTests { - [TestMethod] - public void GetParticipantById_ParticipantExists_ParticipantReturned() - { - var mockContext = new Mock(); - - var event1 = new Event - { - Id = 1, - Name = "Event", - Description = "Description" - }; - - var user = new User - { - Id = 1, - Username = "User", - Email = "Email", - Password = "Password" - }; - - var participant = new Participant - { - Id = 1, - EventId = event1.Id, - UserId = user.Id, - Status = ParticipantStatus.Interested - }; - - var fakeParticipant = new FakeDbSet { participant }; - var fakeEvent = new FakeDbSet { event1 }; - var fakeUser = new FakeDbSet { user }; - - mockContext.Setup(c => c.Events).Returns(fakeEvent); - mockContext.Setup(c => c.Users).Returns(fakeUser); - mockContext.Setup(c => c.Participants).Returns(fakeParticipant); - - var participantService = new ParticipantService(mockContext.Object); - - var result = participantService.GetParticipantById(1); - - Assert.IsNotNull(result); - Assert.AreEqual(1, result.EventId); - Assert.AreEqual(1, result.UserId); - Assert.AreEqual(ParticipantStatus.Interested, result.Status); - } - - [TestMethod] - public void GetParticipantById_ParticipantDoesNotExist_NullReturned() - { - var mockContext = new Mock(); - mockContext.Setup(c => c.Participants).ReturnsDbSet([]); - - var participantService = new ParticipantService(mockContext.Object); - - Assert.ThrowsException(() => participantService.GetParticipantById(1)); - } [TestMethod] public void GetAllParticipants_ParticipantsExist_ParticipantsReturned() @@ -99,16 +43,16 @@ public void GetAllParticipants_ParticipantsExist_ParticipantsReturned() var participant1 = new Participant { Id = 1, - EventId = event1.Id, - UserId = user1.Id, + Event = event1, + User = user1, Status = ParticipantStatus.Interested }; var participant2 = new Participant { Id = 2, - EventId = event1.Id, - UserId = user2.Id, + Event = event1, + User = user2, Status = ParticipantStatus.Participating }; @@ -169,16 +113,18 @@ public void GetParticipantsByFilter_ParticipantsExist_ParticipantsReturned() var participant1 = new Participant { Id = 1, + Event = event1, EventId = event1.Id, - UserId = user1.Id, + User = user1, Status = ParticipantStatus.Interested }; var participant2 = new Participant { Id = 2, + Event = event1, EventId = event1.Id, - UserId = user2.Id, + User = user2, Status = ParticipantStatus.Participating }; @@ -217,13 +163,14 @@ public void CreateParticipant_ParticipantCreated_ParticipantReturned() { Id = 1, Name = "Event", - Description = "Description" + Description = "Description", + Slug = "Event-Slug" }; var user = new User { Id = 1, - Username = "User", + Username = "Username", Email = "Email", Password = "Password" }; @@ -236,16 +183,16 @@ public void CreateParticipant_ParticipantCreated_ParticipantReturned() var participantDto = new ParticipantDto { - EventId = 1, - UserId = 1, + EventSlug = "Event-Slug", + Username = "Username", Status = ParticipantStatus.Interested }; - var result = participantService.CreateParticipant(participantDto); + var result = participantService.CreateOrUpdateParticipant(participantDto); Assert.IsNotNull(result); - Assert.AreEqual(1, result.EventId); - Assert.AreEqual(1, result.UserId); + Assert.AreEqual("Event-Slug", result.EventSlug); + Assert.AreEqual("Username", result.Username); Assert.AreEqual(ParticipantStatus.Interested, result.Status); } @@ -257,7 +204,7 @@ public void CreateParticipant_ParticipantDtoIsNull_NullReturned() var participantService = new ParticipantService(mockContext.Object); - Assert.ThrowsException(() => participantService.CreateParticipant(null)); + Assert.ThrowsException(() => participantService.CreateOrUpdateParticipant(null)); } [TestMethod] @@ -269,13 +216,14 @@ public void UpdateParticipant_ParticipantExists_ParticipantReturned() { Id = 1, Name = "Event", - Description = "Description" + Description = "Description", + Slug = "Event-Slug" }; var user = new User { Id = 1, - Username = "User", + Username = "Username", Email = "Email", Password = "Password" }; @@ -283,8 +231,8 @@ public void UpdateParticipant_ParticipantExists_ParticipantReturned() var participant = new Participant { Id = 1, - EventId = event1.Id, - UserId = user.Id, + Event = event1, + User = user, Status = ParticipantStatus.Interested }; @@ -294,16 +242,16 @@ public void UpdateParticipant_ParticipantExists_ParticipantReturned() var participantService = new ParticipantService(mockContext.Object); - var result = participantService.UpdateParticipant(1, new ParticipantDto + var result = participantService.CreateOrUpdateParticipant(new ParticipantDto { - EventId = 2, - UserId = 2, + EventSlug = "Event-Slug", + Username = "Username", Status = ParticipantStatus.Participating }); Assert.IsNotNull(result); - Assert.AreEqual(1, result.EventId); - Assert.AreEqual(1, result.UserId); + Assert.AreEqual("Event-Slug", result.EventSlug); + Assert.AreEqual("Username", result.Username); Assert.AreEqual(ParticipantStatus.Participating, result.Status); } @@ -311,14 +259,16 @@ public void UpdateParticipant_ParticipantExists_ParticipantReturned() public void UpdateParticipant_ParticipantDoesNotExist_NullReturned() { var mockContext = new Mock(); + mockContext.Setup(c => c.Events).ReturnsDbSet([]); + mockContext.Setup(c => c.Users).ReturnsDbSet([]); mockContext.Setup(c => c.Participants).ReturnsDbSet([]); var participantService = new ParticipantService(mockContext.Object); - Assert.ThrowsException(() => participantService.UpdateParticipant(1, new ParticipantDto + Assert.ThrowsException(() => participantService.CreateOrUpdateParticipant(new ParticipantDto { - EventId = 2, - UserId = 2, + EventSlug = "Event-Slug", + Username = "Username", Status = ParticipantStatus.Participating })); } @@ -333,20 +283,45 @@ public void UpdateParticipant_ParticipantDtoIsNull_NullReturned() var participantService = new ParticipantService(mockContext.Object); - Assert.ThrowsException(() => participantService.UpdateParticipant(1, null)); + Assert.ThrowsException(() => participantService.CreateOrUpdateParticipant(null)); } [TestMethod] public void DeleteParticipant_ParticipantExists_ParticipantDeleted() { var mockContext = new Mock(); - mockContext.Setup(c => c.Participants).ReturnsDbSet([ - new() { Id = 1, EventId = 1, UserId = 1, Status = ParticipantStatus.Participating } - ]); + + var event1 = new Event + { + Id = 1, + Name = "Event", + Description = "Description", + Slug = "Event-Slug" + }; + + var user = new User + { + Id = 1, + Username = "Username", + Email = "Email", + Password = "Password" + }; + + var participant = new Participant + { + Id = 1, + Event = event1, + User = user, + Status = ParticipantStatus.Participating + }; + + mockContext.Setup(c => c.Participants).ReturnsDbSet([participant]); + mockContext.Setup(c => c.Events).ReturnsDbSet([event1]); + mockContext.Setup(c => c.Users).ReturnsDbSet([user]); var participantService = new ParticipantService(mockContext.Object); - participantService.DeleteParticipant(1); + participantService.DeleteParticipant(user.Id, event1.Slug); mockContext.Verify(c => c.SaveChanges(), Times.Once); } @@ -356,10 +331,12 @@ public void DeleteParticipant_ParticipantDoesNotExist_NothingDeleted() { var mockContext = new Mock(); mockContext.Setup(c => c.Participants).ReturnsDbSet([]); + mockContext.Setup(c => c.Events).ReturnsDbSet([]); + mockContext.Setup(c => c.Users).ReturnsDbSet([]); var participantService = new ParticipantService(mockContext.Object); - Assert.ThrowsException(() => participantService.DeleteParticipant(1)); + Assert.ThrowsException(() => participantService.DeleteParticipant(1, "Event-Slug")); mockContext.Verify(c => c.SaveChanges(), Times.Never); } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI.Tests/Services/UserServiceTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/Services/UserServiceTests.cs index a7758b7e..9638d623 100644 --- a/Server/ReasnAPI/ReasnAPI.Tests/Services/UserServiceTests.cs +++ b/Server/ReasnAPI/ReasnAPI.Tests/Services/UserServiceTests.cs @@ -27,16 +27,28 @@ public void GetUserById_UserExists_UserReturned() { var mockContext = new Mock(); + var address = new Address + { + Id = 1, + City = "City", + Country = "Country", + Street = "Street", + State = "State", + ZipCode = "ZipCode" + }; + var user = new User { Id = 1, Name = "John", Surname = "Doe", Username = "Username", - Email = "Email" + Email = "Email", + Address = address, }; mockContext.Setup(c => c.Users).ReturnsDbSet([user]); + mockContext.Setup(c => c.Addresses).ReturnsDbSet([address]); var userService = new UserService(mockContext.Object); @@ -65,13 +77,24 @@ public void GetUserByUsername_UserExists_UserReturned() { var mockContext = new Mock(); + var address = new Address + { + Id = 1, + City = "City", + Country = "Country", + Street = "Street", + State = "State", + ZipCode = "ZipCode" + }; + var user = new User { Id = 1, Name = "John", Surname = "Doe", Username = "Username", - Email = "Email" + Email = "Email", + Address = address, }; mockContext.Setup(c => c.Users).ReturnsDbSet([user]); @@ -143,6 +166,7 @@ public void UpdateUser_UserUpdated_UserReturned() mockContext.Setup(c => c.Addresses).ReturnsDbSet([address]); mockContext.Setup(c => c.Users).ReturnsDbSet([user]); + mockContext.Setup(c => c.UserInterests).ReturnsDbSet([]); var userService = new UserService(mockContext.Object); @@ -157,7 +181,7 @@ public void UpdateUser_UserUpdated_UserReturned() Role = UserRole.User }; - var result = userService.UpdateUser(1, userDto); + var result = userService.UpdateUser("Username", userDto); Assert.IsNotNull(result); Assert.AreEqual("Jane", result.Name); @@ -187,7 +211,7 @@ public void UpdateUser_UserDtoIsNull_NullReturned() var userService = new UserService(mockContext.Object); - Assert.ThrowsException(() => userService.UpdateUser(1, null)); + Assert.ThrowsException(() => userService.UpdateUser("Username", null)); } [TestMethod] @@ -209,6 +233,6 @@ public void UpdateUser_UserDoesNotExist_NullReturned() AddressId = 1 }; - Assert.ThrowsException(() => userService.UpdateUser(1, userDto)); + Assert.ThrowsException(() => userService.UpdateUser("Username", userDto)); } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/MeController.cs b/Server/ReasnAPI/ReasnAPI/Controllers/MeController.cs index 825b1860..66ef6037 100644 --- a/Server/ReasnAPI/ReasnAPI/Controllers/MeController.cs +++ b/Server/ReasnAPI/ReasnAPI/Controllers/MeController.cs @@ -1,7 +1,8 @@ +using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using ReasnAPI.Mappers; using ReasnAPI.Models.DTOs; +using ReasnAPI.Models.Enums; using ReasnAPI.Services; namespace ReasnAPI.Controllers; @@ -9,90 +10,176 @@ namespace ReasnAPI.Controllers; [ApiController] [Authorize] [Route("[controller]")] -public class MeController : ControllerBase +public class MeController(UserService userService, EventService eventService, ParticipantService participantService, ImageService imageService) : ControllerBase { - private readonly UserService _userService; - - public MeController(UserService userService) - { - _userService = userService; - } + private readonly UserService _userService = userService; + private readonly EventService _eventService = eventService; + private readonly ParticipantService _participantService = participantService; + private readonly ImageService _imageService = imageService; [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public IActionResult GetCurrentUser() { - var user = _userService.GetCurrentUser(); - return Ok(user.ToDto()); + var username = _userService.GetCurrentUser().Username; + var user = _userService.GetUserByUsername(username); + + return Ok(user); } [HttpPut] - public IActionResult UpdateCurrentUser() + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult UpdateCurrentUser( + [FromBody] UserDto userDto, + [FromServices] IValidator validator) { - throw new NotImplementedException(); + validator.ValidateAndThrow(userDto); + + var user = _userService.GetCurrentUser(); + + // Users cant change their role in this endpoint + if (user.Role != userDto.Role) + { + return Forbid(); + } + + var updatedUser = _userService.UpdateUser(user.Username, userDto); + + return Ok(updatedUser); } - [HttpPost] + [HttpGet] [Route("image")] - public IActionResult AddCurrentUserImage() - { - throw new NotImplementedException(); + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult GetCurrentUserImage() + { + var user = _userService.GetCurrentUser(); + var image = _imageService.GetImageByUserId(user.Id); + + if (image is null) + { + return NotFound(); + } + + return File(image.ImageData, "image/jpeg"); } - [HttpPut] + [HttpPost] [Route("image")] - public IActionResult UpdateCurrentUserImage() + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task AddCurrentUserImage([FromForm] List images) { - throw new NotImplementedException(); + var userId = _userService.GetCurrentUser().Id; + + foreach (var image in images.Where(image => image.Length > 0)) + { + using var ms = new MemoryStream(); + await image.CopyToAsync(ms); + var fileBytes = ms.ToArray(); + + var imageDto = new ImageDto + { + ObjectId = userId, + ObjectType = ObjectType.User, + ImageData = fileBytes + }; + + _imageService.CreateImages([imageDto]); + } + + return Ok(); } - [HttpDelete] + [HttpPut] [Route("image")] - public IActionResult DeleteCurrentUserImage() + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task UpdateCurrentUserImage([FromForm] List images) { - throw new NotImplementedException(); - } + var userId = _userService.GetCurrentUser().Id; - [HttpGet] - [Route("interests")] - public IActionResult GetCurrentUserInterests() - { - throw new NotImplementedException(); - } + foreach (var image in images.Where(image => image.Length > 0)) + { + using var ms = new MemoryStream(); + await image.CopyToAsync(ms); + var fileBytes = ms.ToArray(); - [HttpPost] - [Route("interests")] - public IActionResult AddCurrentUserInterest() - { - throw new NotImplementedException(); + var imageDto = new ImageDto + { + ObjectId = userId, + ObjectType = ObjectType.User, + ImageData = fileBytes + }; + + _imageService.UpdateImageForUser(userId, imageDto); + } + + return Ok(); } [HttpDelete] - [Route("interests/{interestId:int}")] - public IActionResult DeleteCurrentUserInterest(int interestId) + [Route("image")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public IActionResult DeleteCurrentUserImage() { - throw new NotImplementedException(); + var userId = _userService.GetCurrentUser().Id; + _imageService.DeleteImageByObjectIdAndType(userId, ObjectType.User); + + return NoContent(); } [HttpGet] [Route("events")] + [ProducesResponseType>(StatusCodes.Status200OK)] public IActionResult GetCurrentUserEvents() { - throw new NotImplementedException(); + var user = _userService.GetCurrentUser(); + var events = _eventService.GetUserEvents(user.Username); + + if (user.Role == UserRole.Organizer) + { + var organizerEvents = _eventService.GetEventsByFilter(e => e.OrganizerId == user.Id); + events = events.Concat(organizerEvents); + } + + return Ok(events); } [HttpPost] [Route("events/{slug}/enroll")] - public IActionResult EnrollCurrentUserInEvent(string slug) + [ProducesResponseType(StatusCodes.Status201Created)] + public IActionResult EnrollCurrentUserInEvent([FromRoute] string slug) { - throw new NotImplementedException(); + var user = _userService.GetCurrentUser(); + + var participant = _participantService.CreateOrUpdateParticipant(new ParticipantDto { EventSlug = slug, Username = user.Username, Status = ParticipantStatus.Interested }); + + var location = Url.Action( + action: nameof(GetCurrentUserEvents), + controller: "Me"); + + return Created(location, participant); } [HttpPost] [Route("events/{slug}/confirm")] - public IActionResult ConfirmCurrentUserEventAttendance(string slug) + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult ConfirmCurrentUserEventAttendance([FromRoute] string slug) { - throw new NotImplementedException(); + var user = _userService.GetCurrentUser(); + var participant = _participantService.CreateOrUpdateParticipant(new ParticipantDto { EventSlug = slug, Username = user.Username, Status = ParticipantStatus.Participating }); + + return Ok(participant); + } + + [HttpDelete] + [Route("events/{slug}/cancel")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public IActionResult CancelCurrentUserEventAttendance([FromRoute] string slug) + { + var userId = _userService.GetCurrentUser().Id; + _participantService.DeleteParticipant(userId, slug); + + return NoContent(); } [HttpGet] diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs b/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs index cb2849ab..7cda9226 100644 --- a/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs +++ b/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs @@ -1,47 +1,84 @@ +using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using ReasnAPI.Models.DTOs; +using ReasnAPI.Models.Enums; +using ReasnAPI.Services; namespace ReasnAPI.Controllers; [ApiController] [Route("[controller]")] -public class UsersController : ControllerBase +public class UsersController(UserService userService, InterestService interestService) : ControllerBase { + private readonly UserService _userService = userService; + private readonly InterestService _interestService = interestService; + [HttpGet] [Authorize(Roles = "Admin")] + [ProducesResponseType>(StatusCodes.Status200OK)] public IActionResult GetUsers() { - throw new NotImplementedException(); + var users = _userService.GetUsersByFilter(u => u.IsActive && u.Role != UserRole.Admin); + return Ok(users); } [HttpGet] [Route("{username}")] - public IActionResult GetUserByUsername(string username) + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult GetUserByUsername([FromRoute] string username) { - throw new NotImplementedException(); + var user = _userService.GetUserByUsername(username); + + if (user.Role == UserRole.Admin) + { + return Forbid(); + } + + return Ok(user); } [HttpPut] [Authorize] [Route("{username}")] - public IActionResult UpdateUser(string username) + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult UpdateUser( + [FromBody] UserDto userDto, + [FromRoute] string username, + [FromServices] IValidator validator) { - throw new NotImplementedException(); - } + validator.ValidateAndThrow(userDto); - [HttpGet] - [Authorize] - [Route("interests")] - public IActionResult GetUsersInterests(string username) - { - throw new NotImplementedException(); - } + var currentUser = _userService.GetCurrentUser(); - [HttpDelete] - [Authorize(Roles = "Admin")] - [Route("interests/{interestId:int}")] - public IActionResult DeleteUserInterest(int interestId) + // Only admins can update other users from this endpoint + if (currentUser.Role != UserRole.Admin) + { + return Forbid(); + } + + var updatedUser = _userService.UpdateUser(username, userDto); + + return Ok(updatedUser); + } + + [HttpGet] + [Authorize] + [Route("interests")] + [ProducesResponseType>(StatusCodes.Status200OK)] + public IActionResult GetUsersInterests() + { + var interests = _interestService.GetAllInterests(); + return Ok(interests); + } + + [HttpDelete] + [Authorize(Roles = "Admin")] + [Route("interests/{interestId:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public IActionResult DeleteUserInterest([FromRoute] int interestId) { - throw new NotImplementedException(); + _interestService.DeleteInterest(interestId); + return NoContent(); } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Mappers/ParticipantMapper.cs b/Server/ReasnAPI/ReasnAPI/Mappers/ParticipantMapper.cs index 66fee4e6..23e03520 100644 --- a/Server/ReasnAPI/ReasnAPI/Mappers/ParticipantMapper.cs +++ b/Server/ReasnAPI/ReasnAPI/Mappers/ParticipantMapper.cs @@ -8,8 +8,8 @@ public static ParticipantDto ToDto(this Participant participant) { return new ParticipantDto { - EventId = participant.EventId, - UserId = participant.UserId, + EventSlug = participant.Event.Slug, + Username = participant.User.Username, Status = participant.Status }; } @@ -18,14 +18,4 @@ public static List ToDtoList(this IEnumerable parti { return participants.Select(ToDto).ToList(); } - - public static Participant ToEntity(this ParticipantDto participantDto) - { - return new Participant - { - EventId = participantDto.EventId, - UserId = participantDto.UserId, - Status = participantDto.Status - }; - } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs b/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs index 19ac06bb..42c9325b 100644 --- a/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs +++ b/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs @@ -19,11 +19,12 @@ public static List ToDtoList(this IEnumerable use return userInterests.Select(ToDto).ToList(); } - public static UserInterest ToEntity(this UserInterestDto userInterestDto) + public static UserInterest ToEntity(this UserInterestDto userInterestDto, int userId, int interestId) { return new UserInterest { - Interest = userInterestDto.Interest.ToEntity(), + UserId = userId, + InterestId = interestId, Level = userInterestDto.Level }; } diff --git a/Server/ReasnAPI/ReasnAPI/Mappers/UserMapper.cs b/Server/ReasnAPI/ReasnAPI/Mappers/UserMapper.cs index 512756b8..1cd31cbc 100644 --- a/Server/ReasnAPI/ReasnAPI/Mappers/UserMapper.cs +++ b/Server/ReasnAPI/ReasnAPI/Mappers/UserMapper.cs @@ -16,6 +16,7 @@ public static UserDto ToDto(this User user) Phone = user.Phone, Role = user.Role, AddressId = user.AddressId, + Address = user.Address.ToDto(), Interests = user.UserInterests.ToDtoList() }; } diff --git a/Server/ReasnAPI/ReasnAPI/Models/DTOs/ParticipantDto.cs b/Server/ReasnAPI/ReasnAPI/Models/DTOs/ParticipantDto.cs index 55a0b167..910ceab1 100644 --- a/Server/ReasnAPI/ReasnAPI/Models/DTOs/ParticipantDto.cs +++ b/Server/ReasnAPI/ReasnAPI/Models/DTOs/ParticipantDto.cs @@ -4,8 +4,8 @@ namespace ReasnAPI.Models.DTOs { public class ParticipantDto { - public int EventId { get; set; } - public int UserId { get; set; } + public string EventSlug { get; set; } = null!; + public string Username { get; set; } = null!; public ParticipantStatus Status { get; set; } } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Models/DTOs/UserDto.cs b/Server/ReasnAPI/ReasnAPI/Models/DTOs/UserDto.cs index 3d16dc47..d3642802 100644 --- a/Server/ReasnAPI/ReasnAPI/Models/DTOs/UserDto.cs +++ b/Server/ReasnAPI/ReasnAPI/Models/DTOs/UserDto.cs @@ -11,6 +11,7 @@ public class UserDto public string? Phone { get; set; } public UserRole Role { get; set; } public int AddressId { get; set; } + public AddressDto Address { get; set; } = null!; public List? Interests { get; set; } } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Program.cs b/Server/ReasnAPI/ReasnAPI/Program.cs index f09cfa4a..7599e5d2 100644 --- a/Server/ReasnAPI/ReasnAPI/Program.cs +++ b/Server/ReasnAPI/ReasnAPI/Program.cs @@ -13,7 +13,6 @@ using ReasnAPI.Services; using ReasnAPI.Services.Authentication; using ReasnAPI.Validators; -using ReasnAPI.Services; using Npgsql; using ReasnAPI.Models.Enums; using Microsoft.Extensions.Configuration; @@ -58,6 +57,14 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddValidatorsFromAssemblyContaining(); @@ -72,13 +79,6 @@ options.UseNpgsql(dataSource) .EnableDetailedErrors()); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - builder.Services.AddControllers(); builder.Services.AddSwaggerGen(options => diff --git a/Server/ReasnAPI/ReasnAPI/Services/EventService.cs b/Server/ReasnAPI/ReasnAPI/Services/EventService.cs index 3e3d3709..0efacdb3 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/EventService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/EventService.cs @@ -21,7 +21,7 @@ public EventDto CreateEvent(EventDto eventDto) newEvent.CreatedAt = createdTime; newEvent.UpdatedAt = createdTime; newEvent.Slug = CreateSlug(eventDto); - + context.Events.Add(newEvent); diff --git a/Server/ReasnAPI/ReasnAPI/Services/ImageService.cs b/Server/ReasnAPI/ReasnAPI/Services/ImageService.cs index 59d688d4..f911fe13 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/ImageService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/ImageService.cs @@ -174,20 +174,16 @@ public ImageDto GetImageById(int id) return imageDto; } - public IEnumerable GetImagesByUserId(int userId) + public ImageDto GetImageByUserId(int userId) { - var images = context.Images - .Where(image => image.ObjectId == userId && image.ObjectType == ObjectType.User) - .ToList(); + var image = context.Images.FirstOrDefault(image => image.ObjectId == userId && image.ObjectType == ObjectType.User); - if (!images.Any()) + if (image is null) { - throw new NotFoundException("Images not found"); + throw new NotFoundException("Image not found"); } - var imageDtos = images.ToDtoList().AsEnumerable(); - - return imageDtos; + return image.ToDto(); } public IEnumerable GetAllImages() diff --git a/Server/ReasnAPI/ReasnAPI/Services/ParameterService.cs b/Server/ReasnAPI/ReasnAPI/Services/ParameterService.cs index f38c8534..9372a992 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/ParameterService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/ParameterService.cs @@ -35,7 +35,7 @@ public ParameterDto UpdateParameter(int parameterId, ParameterDto parameterDto) var parameterCheck = parameters.FirstOrDefault(r => r.Parameters.Any(p => p.Id == parameterId)); - if (parameterCheck is not null) + if (parameterCheck is not null) { throw new BadRequestException("Parameter is associated with an event"); } diff --git a/Server/ReasnAPI/ReasnAPI/Services/ParticipantService.cs b/Server/ReasnAPI/ReasnAPI/Services/ParticipantService.cs index 2b568ddb..481bcb5b 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/ParticipantService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/ParticipantService.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore; using ReasnAPI.Exceptions; using ReasnAPI.Mappers; using ReasnAPI.Models.Database; @@ -8,84 +9,68 @@ namespace ReasnAPI.Services; public class ParticipantService(ReasnContext context) { - public ParticipantDto CreateParticipant(ParticipantDto participantDto) + public ParticipantDto CreateOrUpdateParticipant(ParticipantDto participantDto) { ArgumentNullException.ThrowIfNull(participantDto); - var participantDb = context.Participants.FirstOrDefault(r => r.Event.Id == participantDto.EventId && r.User.Id == participantDto.UserId); + var eventDb = context.Events.FirstOrDefault(e => e.Slug == participantDto.EventSlug); - if (participantDb is not null) + if (eventDb is null) { - throw new BadRequestException("Participant already exists"); + throw new NotFoundException("Event not found"); } - var userInDb = context.Users.FirstOrDefault(r => r.Id == participantDto.UserId); + var userDb = context.Users.FirstOrDefault(u => u.Username == participantDto.Username); - if (userInDb is null) + if (userDb is null) { throw new NotFoundException("User not found"); } - var eventInDb = context.Events.FirstOrDefault(r => r.Id == participantDto.EventId); + var participant = context.Participants.FirstOrDefault(r => r.Event.Id == eventDb.Id && r.User.Id == userDb.Id); - if (eventInDb is null) + if (participant is null) { - throw new NotFoundException("Event not found"); + context.Participants.Add(new Participant { EventId = eventDb.Id, UserId = userDb.Id, Status = participantDto.Status }); } - context.Participants.Add(participantDto.ToEntity()); - context.SaveChanges(); - - return participantDto; - } - - public ParticipantDto UpdateParticipant(int participantId, ParticipantDto participantDto) - { - ArgumentNullException.ThrowIfNull(participantDto); - - var participant = context.Participants.FirstOrDefault(r => r.Id == participantId); - if (participant is null) + else { - throw new NotFoundException("Participant not found"); + participant.Status = participantDto.Status; + context.Participants.Update(participant); } - participant.Status = participantDto.Status; - - context.Participants.Update(participant); context.SaveChanges(); - return participant.ToDto(); + return participantDto; } - public void DeleteParticipant(int participantId) + public void DeleteParticipant(int userId, string eventSlug) { - var participant = context.Participants.FirstOrDefault(r => r.Id == participantId); + var eventDb = context.Events.FirstOrDefault(e => e.Slug == eventSlug); - if (participant is null) + if (eventDb is null) { - throw new NotFoundException("Participant not found"); + throw new NotFoundException("Event not found"); } - context.Participants.Remove(participant); - context.SaveChanges(); - } - - public ParticipantDto GetParticipantById(int participantId) - { - var participant = context.Participants.Find(participantId); + var participant = context.Participants.FirstOrDefault(r => r.Event.Id == eventDb.Id && r.User.Id == userId); if (participant is null) { throw new NotFoundException("Participant not found"); } - return participant.ToDto(); + context.Participants.Remove(participant); + context.SaveChanges(); } public IEnumerable GetParticipantsByFilter(Expression> filter) { return context.Participants .Where(filter) + .Include(p => p.Event) + .Include(p => p.User) .ToDtoList() .AsEnumerable(); } @@ -93,6 +78,8 @@ public IEnumerable GetParticipantsByFilter(Expression GetAllParticipants() { return context.Participants + .Include(p => p.Event) + .Include(p => p.User) .ToDtoList() .AsEnumerable(); } diff --git a/Server/ReasnAPI/ReasnAPI/Services/UserService.cs b/Server/ReasnAPI/ReasnAPI/Services/UserService.cs index b1b49b88..0981a436 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/UserService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/UserService.cs @@ -3,6 +3,7 @@ using ReasnAPI.Mappers; using ReasnAPI.Models.Database; using ReasnAPI.Models.DTOs; +using Serilog; using System.Linq.Expressions; using System.Security.Claims; using System.Transactions; @@ -50,7 +51,7 @@ public User GetCurrentUser() return user; } - public UserDto UpdateUser(int userId, UserDto userDto) + public UserDto UpdateUser(string username, UserDto userDto) { using (var scope = new TransactionScope()) { @@ -59,46 +60,50 @@ public UserDto UpdateUser(int userId, UserDto userDto) var user = _context.Users .Include(u => u.UserInterests) .ThenInclude(ui => ui.Interest) - .FirstOrDefault(r => r.Id == userId); + .FirstOrDefault(r => r.Username == username); if (user is null) { throw new NotFoundException("User not found"); } - var usernameExists = _context.Users.Any(r => r.Username == userDto.Username && r.Id != userId); + var usernameExists = _context.Users.Any(r => r.Username == userDto.Username && r.Id != user.Id); if (usernameExists) { throw new BadRequestException("User with given username already exists"); } - var emailExists = _context.Users.Any(r => r.Email == userDto.Email && r.Id != userId); + var emailExists = _context.Users.Any(r => r.Email == userDto.Email && r.Id != user.Id); if (emailExists) { throw new BadRequestException("User with given email already exists"); } - var phoneExists = _context.Users.Any(r => r.Phone == userDto.Phone && r.Id != userId); + var phoneExists = _context.Users.Any(r => r.Phone == userDto.Phone && r.Id != user.Id); if (phoneExists) { throw new BadRequestException("User with given phone number already exists"); } - user.Username = userDto.Username; user.Name = userDto.Name; user.Surname = userDto.Surname; user.Username = userDto.Username; user.Email = userDto.Email; user.Phone = userDto.Phone; user.Role = userDto.Role; - user.AddressId = userDto.AddressId; user.UpdatedAt = DateTime.UtcNow; _context.Users.Update(user); + // Get list of interests to remove + var interestsToRemove = user.UserInterests + .Where(ui => !userDto.Interests!.Exists(uid => uid.Interest.Name == ui.Interest.Name)); + + _context.UserInterests.RemoveRange(interestsToRemove); + if (userDto.Interests is null || userDto.Interests.Count == 0) { _context.SaveChanges(); @@ -106,18 +111,7 @@ public UserDto UpdateUser(int userId, UserDto userDto) return userDto; } - var interestsToRemove = user.UserInterests - .Where(ui => !userDto.Interests.Exists(uid => uid.Interest.Name == ui.Interest.Name)); - - _context.UserInterests.RemoveRange(interestsToRemove); - - var interestsToAdd = userDto.Interests - .Where(uid => !user.UserInterests.Any(ui => ui.Interest.Name == uid.Interest.Name)) - .Select(uid => uid.ToEntity()) - .ToList(); - - _context.UserInterests.AddRange(interestsToAdd); - + // Get list of interests to update var interestsToUpdate = user.UserInterests .Where(ui => userDto.Interests.Exists(uid => uid.Interest.Name == ui.Interest.Name)) .ToList(); @@ -135,6 +129,20 @@ public UserDto UpdateUser(int userId, UserDto userDto) _context.UserInterests.Update(interest); } + // Get list of existing interests in the database + var existingInterests = _context.Interests.ToList(); + + // Get list of interests to add + // Look for interests that are not already in the user's interests + var interestsToAdd = userDto.Interests + .Where(uid => !user.UserInterests.Any(ui => ui.Interest.Name == uid.Interest.Name)) + .Select(uid => uid.ToEntity(user.Id, existingInterests.Find(ei => ei.Name == uid.Interest.Name)!.Id)) + .ToList(); + + // Update interests for + interestsToAdd.ForEach(user.UserInterests.Add); + _context.Users.Update(user); + _context.SaveChanges(); scope.Complete(); } @@ -145,7 +153,9 @@ public UserDto UpdateUser(int userId, UserDto userDto) public UserDto GetUserById(int userId) { var user = _context.Users + .Include(a => a.Address) .Include(u => u.UserInterests) + .ThenInclude(ui => ui.Interest) .FirstOrDefault(u => u.Id == userId); if (user is null) @@ -159,7 +169,9 @@ public UserDto GetUserById(int userId) public UserDto GetUserByUsername(string username) { var user = _context.Users + .Include(a => a.Address) .Include(u => u.UserInterests) + .ThenInclude(ui => ui.Interest) .FirstOrDefault(u => u.Username == username); if (user is null) @@ -173,6 +185,7 @@ public UserDto GetUserByUsername(string username) public IEnumerable GetUsersByFilter(Expression> filter) { return _context.Users + .Include(a => a.Address) .Include(u => u.UserInterests) .ThenInclude(ui => ui.Interest) .Where(filter) @@ -183,6 +196,7 @@ public IEnumerable GetUsersByFilter(Expression> filter public IEnumerable GetAllUsers() { var users = _context.Users + .Include(a => a.Address) .Include(u => u.UserInterests) .ThenInclude(ui => ui.Interest) .ToList();