diff --git a/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs index 6a1602a..8233328 100644 --- a/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs @@ -87,6 +87,8 @@ public static class ConfigurationProgramExtensions services.AddAutoMapper(typeof(MappingProfile)); services.AddScoped(); + + services.AddScoped(); } public static void AddRepositories(this IServiceCollection services) diff --git a/Govor.API/Common/Mapping/MappingProfile.cs b/Govor.API/Common/Mapping/MappingProfile.cs index 5b15e9e..9f94bda 100644 --- a/Govor.API/Common/Mapping/MappingProfile.cs +++ b/Govor.API/Common/Mapping/MappingProfile.cs @@ -21,5 +21,7 @@ public class MappingProfile : Profile .AfterMap(); CreateMap(); + + CreateMap(); } } \ No newline at end of file diff --git a/Govor.API/Controllers/SessionController.cs b/Govor.API/Controllers/SessionController.cs new file mode 100644 index 0000000..ae600ee --- /dev/null +++ b/Govor.API/Controllers/SessionController.cs @@ -0,0 +1,109 @@ +using AutoMapper; +using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Application.Interfaces.UserSession; +using Govor.Contracts.DTOs; +using Govor.Data.Repositories.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Govor.API.Controllers; + +[ApiController] +[Authorize(Roles = "Admin,User")] +[Route("api/[controller]")] +public class SessionController : Controller +{ + private readonly ILogger _logger; + private readonly ICurrentUserService _currentUserService; + private readonly IUserSessionReader _userSessionReader; + private readonly IUserSessionRevoker _userSessionRevoker; + private readonly IMapper _mapper; + + public SessionController( + ILogger logger, + ICurrentUserService currentUserService, + IUserSessionReader userSessionReader, + IUserSessionRevoker userSessionRevoker, + IMapper mapper) + { + _logger = logger; + _currentUserService = currentUserService; + _userSessionReader = userSessionReader; + _userSessionRevoker = userSessionRevoker; + _mapper = mapper; + } + + [HttpGet("all")] + public async Task GetAllSessions() + { + try + { + var sessions = await _userSessionReader.GetAllSessionsAsync(_currentUserService.GetCurrentUserId()); + return Ok(_mapper.Map>(sessions)); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex, ex.Message); + return Forbid(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, "Unexpected Error! Please try again later."); + } + } + + [HttpDelete("close/{sessionId}")] + public async Task CloseSession(Guid sessionId) + { + try + { + if (sessionId == Guid.Empty) + return BadRequest("Invalid sessionId."); + + await _userSessionRevoker.CloseSessionByIdAsync(sessionId, + _currentUserService.GetCurrentUserId()); + return Ok(); + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, ex.Message); + return BadRequest(ex.Message); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex, ex.Message); + return Forbid(ex.Message); + } + catch (NotFoundException ex) + { + _logger.LogWarning(ex, ex.Message); + return NotFound(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, "Unexpected Error! Please try again later."); + } + } + + [HttpDelete("close/all")] + public async Task CloseAllSessions() + { + try + { + await _userSessionRevoker.CloseAllSessionsAsync(_currentUserService.GetCurrentUserId()); + return Ok(); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex, ex.Message); + return Forbid(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, "Unexpected Error! Please try again later."); + } + } +} \ No newline at end of file diff --git a/Govor.Application.Tests/Services/UserSessions/UserSessionReaderTests.cs b/Govor.Application.Tests/Services/UserSessions/UserSessionReaderTests.cs new file mode 100644 index 0000000..145c2d6 --- /dev/null +++ b/Govor.Application.Tests/Services/UserSessions/UserSessionReaderTests.cs @@ -0,0 +1,66 @@ +using AutoFixture; +using Govor.Application.Interfaces.UserSession; +using Govor.Application.Services.UserSessions; +using Govor.Core.Models; +using Govor.Core.Repositories.UserSessionsRepository; +using Govor.Data.Repositories.Exceptions; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Govor.Application.Tests.Services.UserSessions; + +[TestFixture] +[TestOf(typeof(UserSessionReader))] +public class UserSessionReaderTests +{ + private Mock _mockUserSessionsRepository; + private Mock> _mockLogger; + private Fixture _fixture; + private IUserSessionReader _userSessionReader; + + [SetUp] + public void SetUp() + { + _fixture = new Fixture(); + + _mockUserSessionsRepository = new Mock(); + _mockLogger = new Mock>(); + + _userSessionReader = new UserSessionReader(_mockUserSessionsRepository.Object, + _mockLogger.Object); + } + + [Test] + public async Task GetAllUserSessionsAsync_ShouldReturnAllUserSessions() + { + // Arrange + var sessios = _fixture.CreateMany().ToList(); + var userId = Guid.NewGuid(); + + _mockUserSessionsRepository.Setup(f => f.GetByUserIdAsync(userId)) + .ReturnsAsync(sessios); + // Act + var result = await _userSessionReader.GetAllSessionsAsync(userId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EquivalentTo(sessios)); + } + + [Test] + public async Task GetAllUserSessionsAsync_ThrowsNotFoundByKeyException_ReturnsEmptyList() + { + // Arrange + var userId = Guid.NewGuid(); + + _mockUserSessionsRepository.Setup(f => f.GetByUserIdAsync(userId)) + .ThrowsAsync(new NotFoundByKeyException(userId)); + + // Act + var result = await _userSessionReader.GetAllSessionsAsync(userId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/UserSession/IUserSessionRevoker.cs b/Govor.Application/Interfaces/UserSession/IUserSessionRevoker.cs index 6106db5..af256fa 100644 --- a/Govor.Application/Interfaces/UserSession/IUserSessionRevoker.cs +++ b/Govor.Application/Interfaces/UserSession/IUserSessionRevoker.cs @@ -3,6 +3,6 @@ namespace Govor.Application.Interfaces.UserSession; public interface IUserSessionRevoker { - Task CloseSessionByRefreshTokenAsync(string refreshToken); + Task CloseSessionByIdAsync(Guid sessionId, Guid userId); Task CloseAllSessionsAsync(Guid userId); } diff --git a/Govor.Application/Services/UserSessions/UserSessionReader.cs b/Govor.Application/Services/UserSessions/UserSessionReader.cs new file mode 100644 index 0000000..32c9600 --- /dev/null +++ b/Govor.Application/Services/UserSessions/UserSessionReader.cs @@ -0,0 +1,34 @@ +using Govor.Application.Interfaces.UserSession; +using Govor.Core.Models; +using Govor.Core.Repositories.UserSessionsRepository; +using Govor.Data.Repositories.Exceptions; +using Microsoft.Extensions.Logging; + +namespace Govor.Application.Services.UserSessions; + +public class UserSessionReader : IUserSessionReader +{ + private readonly IUserSessionsRepository _repository; + private readonly ILogger _logger; + + public UserSessionReader(IUserSessionsRepository repository, ILogger logger) + { + _repository = repository; + _logger = logger; + } + + public async Task> GetAllSessionsAsync(Guid userId) + { + try + { + _logger.LogInformation($"Getting all sessions for user {userId}"); + var sessions = await _repository.GetByUserIdAsync(userId); + return sessions; + } + catch (NotFoundByKeyException ex) + { + _logger.LogWarning("The user has no active sessions."); + return []; + } + } +} \ No newline at end of file diff --git a/Govor.Contracts/DTOs/SessionDto.cs b/Govor.Contracts/DTOs/SessionDto.cs new file mode 100644 index 0000000..f0b8a8d --- /dev/null +++ b/Govor.Contracts/DTOs/SessionDto.cs @@ -0,0 +1,10 @@ +namespace Govor.Contracts.DTOs; + +public record SessionDto +{ + public Guid Id { get; init; } + public string DeviceInfo { get; init; } = string.Empty; + public DateTime CreatedAt { get; init; } + public DateTime ExpiresAt { get; init; } + public bool IsRevoked { get; init; } +}