diff --git a/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs index 522af81..301cf24 100644 --- a/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs @@ -45,6 +45,7 @@ public static class ConfigurationProgramExtensions public static void AddServices(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Govor.API/Controllers/ProfileController.cs b/Govor.API/Controllers/ProfileController.cs index fe2d327..f5091ee 100644 --- a/Govor.API/Controllers/ProfileController.cs +++ b/Govor.API/Controllers/ProfileController.cs @@ -41,7 +41,6 @@ public class ProfileController : ControllerBase _mapper = mapper; } - [RequestSizeLimit(40_000)] [HttpPost("avatar")] // api/profile/avatar public async Task UploadAvatar([FromForm] AvatarUploadRequest request) { @@ -96,7 +95,7 @@ public class ProfileController : ControllerBase return ms.ToArray(); } - [HttpGet("dowload/me")] + [HttpGet("download/me")] public async Task DownloadProfile() { try diff --git a/Govor.Application.Tests/Services/Authentication/JwtTokenHasherTests.cs b/Govor.Application.Tests/Services/Authentication/JwtTokenHasherTests.cs new file mode 100644 index 0000000..9e62e54 --- /dev/null +++ b/Govor.Application.Tests/Services/Authentication/JwtTokenHasherTests.cs @@ -0,0 +1,51 @@ +using AutoFixture; +using Govor.Application.Interfaces.Authentication; +using Govor.Application.Services.Authentication; + +namespace Govor.Application.Tests.Services.Authentication; + +[TestFixture] +public class JwtTokenHasherTests +{ + private IJwtTokenHasher _jwtTokenHasher; + private Fixture _fixture; + + [SetUp] + public void StarUp() + { + _fixture = new Fixture(); + _jwtTokenHasher = new JwtTokenHasher(); + } + + [Test] + public void Given_Token_When_Hash_Then_Hash_And_Verify_Then_Result_Should_Be_True() + { + // Arrange + string token = _fixture.Create(); + + // Act + string hash = _jwtTokenHasher.HashToken(token); + + var result = _jwtTokenHasher.VerifyToken(token, hash); + + // Assert + Assert.That(hash, Is.Not.EqualTo(token)); + Assert.That(result, Is.True); + } + + [Test] + public void Given_Token_NotToken_When_Hash_Should_Not_Be_True_Then_Result_Should_Be_False() + { + // Arrange + string token = _fixture.Create(); + string notToken = _fixture.Create(); + + // Act + string hash = _jwtTokenHasher.HashToken(token); + + var result = _jwtTokenHasher.VerifyToken(notToken, hash); + + // Assert + Assert.That(result, Is.False); + } +} \ No newline at end of file diff --git a/Govor.Application.Tests/Services/UserSessions/UserSessionOpenerTests.cs b/Govor.Application.Tests/Services/UserSessions/UserSessionOpenerTests.cs index b08cc73..e09c519 100644 --- a/Govor.Application.Tests/Services/UserSessions/UserSessionOpenerTests.cs +++ b/Govor.Application.Tests/Services/UserSessions/UserSessionOpenerTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Options; using Moq; using Govor.Application.Interfaces.Authentication; using Govor.Application.Services.Authentication; +using Govor.Data.Repositories.Exceptions; namespace Govor.Application.Tests.Services.UserSessions; @@ -14,138 +15,141 @@ public class UserSessionOpenerTests { private Mock _repositoryMock; private Mock _jwtServiceMock; - private Mock> _loggerMock; - private IOptions _options; + private Mock _jwtTokenHasherMock; private UserSessionOpener _service; private User _user; - private Guid _sessionId; + private const string DeviceInfo = "Chrome on Windows"; - private const string GeneratedToken = "new-refresh-token"; - private const string NewAccessToken = "new-access-token"; + private const string RefreshToken = "new-refresh-token"; + private const string AccessToken = "new-access-token"; + private const string TokenHash = "hashed-refresh-token"; [SetUp] public void Setup() { _repositoryMock = new Mock(); _jwtServiceMock = new Mock(); - _loggerMock = new Mock>(); - _options = Options.Create(new JwtRefreshOption { RefreshTokenLifetimeDays = 30 }); - - _sessionId = Guid.NewGuid(); + var loggerMock = new Mock>(); + _jwtTokenHasherMock = new Mock(); + var options = Options.Create(new JwtRefreshOption { RefreshTokenLifetimeDays = 30 }); _user = new User { Id = Guid.NewGuid(), Username = "test", - PasswordHash = "hashed", + Description = "some description", + PasswordHash = "hashed-password", IconId = Guid.NewGuid(), - CreatedOn = DateOnly.FromDateTime(DateTime.UtcNow), - WasOnline = DateTime.UtcNow, - InviteId = Guid.NewGuid() + CreatedOn = DateOnly.FromDateTime(DateTime.Now), + WasOnline = DateTime.Now, + InviteId = Guid.NewGuid(), + // ... }; + + _jwtServiceMock.Setup(j => j.GenerateRefreshTokenAsync(_user)).ReturnsAsync(RefreshToken); + _jwtTokenHasherMock.Setup(h => h.HashToken(RefreshToken)).Returns(TokenHash); + + _jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, It.IsAny())).ReturnsAsync(AccessToken); - _jwtServiceMock.Setup(j => j.GenerateRefreshTokenAsync(_user)).ReturnsAsync(GeneratedToken); - _jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, _sessionId)).ReturnsAsync(NewAccessToken); _service = new UserSessionOpener( _repositoryMock.Object, _jwtServiceMock.Object, - _options, - _loggerMock.Object + _jwtTokenHasherMock.Object, + options, + loggerMock.Object ); } - + + // UpdateExistingSessionAsync [Test] - public async Task OpenSessionAsync_ShouldReturnExistingToken_IfSessionValid() + public async Task OpenSessionAsync_ShouldUpdateExistingSession_WhenFoundByDeviceInfo() { // Arrange - var session = new UserSession + var existingSessionId = Guid.NewGuid(); + var existingSession = new UserSession { - Id = _sessionId, + Id = existingSessionId, UserId = _user.Id, DeviceInfo = DeviceInfo, - RefreshToken = "valid-token", + RefreshTokenHash = "old-hash", CreatedAt = DateTime.UtcNow.AddDays(-10), ExpiresAt = DateTime.UtcNow.AddDays(10), - IsRevoked = false + IsRevoked = true }; - _repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List { session }); - - // Act - var result = await _service.OpenSessionAsync(_user, DeviceInfo); - - // Assert - Assert.That(result.refreshToken, Is.EqualTo(GeneratedToken)); - Assert.That(result.accessToken, Is.EqualTo(NewAccessToken)); - _repositoryMock.Verify(r => r.UpdateAsync(It.Is(s => - s.RefreshToken == GeneratedToken && - s.IsRevoked == false)), Times.Once); - } - - [Test] - public async Task OpenSessionAsync_ShouldUpdateSession_IfExpiredOrRevoked() - { - // Arrange - var session = new UserSession - { - Id = _sessionId, - UserId = _user.Id, - DeviceInfo = DeviceInfo, - RefreshToken = "old-token", - CreatedAt = DateTime.UtcNow.AddDays(-40), - ExpiresAt = DateTime.UtcNow.AddDays(-1), - IsRevoked = false - }; - - _repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List { session }); - - // Act - var result = await _service.OpenSessionAsync(_user, DeviceInfo); - - // Assert - Assert.That(result.refreshToken, Is.EqualTo(GeneratedToken)); - Assert.That(result.accessToken, Is.EqualTo(NewAccessToken)); - _repositoryMock.Verify(r => r.UpdateAsync(It.Is(s => - s.RefreshToken == GeneratedToken && - s.IsRevoked == false)), Times.Once); - } - - [Test] - public async Task OpenSessionAsync_ShouldCreateNewSession_IfNoneExists() - { - // Arrange - _repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List()); - _jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, It.IsAny())).ReturnsAsync(NewAccessToken); + _repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List { existingSession }); // Act var result = await _service.OpenSessionAsync(_user, DeviceInfo); // Assert - Assert.That(result.refreshToken, Is.EqualTo(GeneratedToken)); - Assert.That(result.accessToken, Is.EqualTo(NewAccessToken)); + Assert.Multiple(() => + { + Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken."); + Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken."); + }); + + _repositoryMock.Verify(r => r.UpdateAsync(It.Is(s => + s.Id == existingSessionId && + s.RefreshTokenHash == TokenHash && + s.IsRevoked == false && + s.ExpiresAt > DateTime.UtcNow + )), Times.Once, "Должен быть вызван метод UpdateAsync с новыми данными."); + + _repositoryMock.Verify(r => r.AddAsync(It.IsAny()), Times.Never); + } + + // CreateNewSessionAsync + + [Test] + public async Task OpenSessionAsync_ShouldCreateNewSession_IfNoneFound() + { + // Arrange + _repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List()); + + // Act + var result = await _service.OpenSessionAsync(_user, DeviceInfo); + + // Assert + Assert.Multiple(() => + { + Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken."); + Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken."); + }); + _repositoryMock.Verify(r => r.AddAsync(It.Is(s => s.UserId == _user.Id && s.DeviceInfo == DeviceInfo && - s.RefreshToken == GeneratedToken - )), Times.Once); + s.RefreshTokenHash == TokenHash && + s.IsRevoked == false + )), Times.Once, "Должен быть вызван метод AddAsync для создания новой сессии."); + + _repositoryMock.Verify(r => r.UpdateAsync(It.IsAny()), Times.Never); } [Test] - public async Task OpenSessionAsync_ShouldCreateNewSession_WhenNotFoundByKeyExceptionThrown() + public async Task OpenSessionAsync_ShouldCreateNewSession_WhenRepositoryThrowsNotFoundByKeyException() { // Arrange _repositoryMock .Setup(r => r.GetByUserIdAsync(_user.Id)) - .ThrowsAsync(new Govor.Data.Repositories.Exceptions.NotFoundByKeyException(_user.Id, "userId")); - _jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, It.IsAny())).ReturnsAsync(NewAccessToken); + .ThrowsAsync(new NotFoundByKeyException(_user.Id, "userId")); // Act var result = await _service.OpenSessionAsync(_user, DeviceInfo); // Assert - Assert.That(result.refreshToken, Is.EqualTo(GeneratedToken)); - Assert.That(result.accessToken, Is.EqualTo(NewAccessToken)); - _repositoryMock.Verify(r => r.AddAsync(It.IsAny()), Times.Once); + Assert.Multiple(() => + { + Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken."); + Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken."); + }); + + _repositoryMock.Verify(r => r.AddAsync(It.Is(s => + s.RefreshTokenHash == TokenHash + )), Times.Once, "Должен быть вызван AddAsync после перехвата исключения."); + + _repositoryMock.Verify(r => r.UpdateAsync(It.IsAny()), Times.Never); } } diff --git a/Govor.Application.Tests/Services/UserSessions/UserSessionRefresherTests.cs b/Govor.Application.Tests/Services/UserSessions/UserSessionRefresherTests.cs index 38fcacc..b86877c 100644 --- a/Govor.Application.Tests/Services/UserSessions/UserSessionRefresherTests.cs +++ b/Govor.Application.Tests/Services/UserSessions/UserSessionRefresherTests.cs @@ -58,7 +58,7 @@ public class UserSessionRefresherTests _session = new UserSession { - RefreshToken = OldRefreshToken, + RefreshTokenHash = OldRefreshToken, UserId = _user.Id, DeviceInfo = "Chrome", CreatedAt = DateTime.UtcNow.AddDays(-5), @@ -71,7 +71,7 @@ public class UserSessionRefresherTests public async Task RefreshTokenAsync_ValidToken_ReturnsNewTokensAndCreatesNewSession() { // Arrange - _sessionsRepoMock.Setup(r => r.GetByRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session); + _sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session); _usersRepoMock.Setup(r => r.FindByIdAsync(_user.Id)).ReturnsAsync(_user); _jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, _session.Id)).ReturnsAsync(NewAccessToken); _jwtServiceMock.Setup(j => j.GenerateRefreshTokenAsync(_user)).ReturnsAsync(NewRefreshToken); @@ -87,7 +87,7 @@ public class UserSessionRefresherTests _sessionsRepoMock.Verify(r => r.UpdateAsync(_session), Times.Once); _sessionsRepoMock.Verify(r => r.AddAsync(It.Is(s => s.UserId == _user.Id && - s.RefreshToken == NewRefreshToken && + s.RefreshTokenHash == NewRefreshToken && s.DeviceInfo == _session.DeviceInfo)), Times.Once); } @@ -96,7 +96,7 @@ public class UserSessionRefresherTests { // Arrange _session.IsRevoked = true; - _sessionsRepoMock.Setup(r => r.GetByRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session); + _sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session); // Act & Assert var ex = Assert.ThrowsAsync(async () => @@ -110,7 +110,7 @@ public class UserSessionRefresherTests { // Arrange _session.ExpiresAt = DateTime.UtcNow.AddMinutes(-1); - _sessionsRepoMock.Setup(r => r.GetByRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session); + _sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session); // Act & Assert var ex = Assert.ThrowsAsync(async () => @@ -123,7 +123,7 @@ public class UserSessionRefresherTests public void RefreshTokenAsync_TokenNotFound_ThrowsUnauthorizedAccessException() { // Arrange - _sessionsRepoMock.Setup(r => r.GetByRefreshTokenAsync(OldRefreshToken)) + _sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)) .ThrowsAsync(new NotFoundByKeyException("token", OldRefreshToken)); // Act & Assert var ex = Assert.ThrowsAsync(async () => diff --git a/Govor.Application/Interfaces/Authentication/IJwtTokenHasher.cs b/Govor.Application/Interfaces/Authentication/IJwtTokenHasher.cs new file mode 100644 index 0000000..439e87b --- /dev/null +++ b/Govor.Application/Interfaces/Authentication/IJwtTokenHasher.cs @@ -0,0 +1,7 @@ +namespace Govor.Application.Interfaces.Authentication; + +public interface IJwtTokenHasher +{ + string HashToken(string token); + bool VerifyToken(string token, string storedHash); +} \ No newline at end of file diff --git a/Govor.Application/Services/Authentication/JwtTokenHasher.cs b/Govor.Application/Services/Authentication/JwtTokenHasher.cs new file mode 100644 index 0000000..0e26f59 --- /dev/null +++ b/Govor.Application/Services/Authentication/JwtTokenHasher.cs @@ -0,0 +1,16 @@ +using Govor.Application.Interfaces.Authentication; + +namespace Govor.Application.Services.Authentication; + +public class JwtTokenHasher : IJwtTokenHasher +{ + public string HashToken(string token) + { + return BCrypt.Net.BCrypt.HashPassword(token, workFactor: 13); + } + + public bool VerifyToken(string token, string storedHash) + { + return BCrypt.Net.BCrypt.Verify(token, storedHash); + } +} \ No newline at end of file diff --git a/Govor.Application/Services/UserSessions/UserSessionOpener.cs b/Govor.Application/Services/UserSessions/UserSessionOpener.cs index 0838eff..7dda30f 100644 --- a/Govor.Application/Services/UserSessions/UserSessionOpener.cs +++ b/Govor.Application/Services/UserSessions/UserSessionOpener.cs @@ -7,86 +7,97 @@ using Govor.Data.Repositories.Exceptions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Govor.Application.Services.UserSessions +namespace Govor.Application.Services.UserSessions; + +public class UserSessionOpener : IUserSessionOpener { - public class UserSessionOpener : IUserSessionOpener + private readonly IUserSessionsRepository _repository; + private readonly ILogger _logger; + private readonly IJwtTokenHasher _jwtTokenHasher; + private readonly JwtRefreshOption _options; + private readonly IJwtService _jwtService; + + public UserSessionOpener( + IUserSessionsRepository repository, + IJwtService jwtService, + IJwtTokenHasher jwtTokenHasher, + IOptions options, + ILogger logger) { - private readonly IUserSessionsRepository _repository; - private readonly ILogger _logger; - private readonly JwtRefreshOption _options; - private readonly IJwtService _jwtService; - - public UserSessionOpener( - IUserSessionsRepository repository, - IJwtService jwtService, - IOptions options, - ILogger logger) - { - _jwtService = jwtService; - _repository = repository; - _logger = logger; - _options = options.Value; - } - - public async Task OpenSessionAsync(User user, string deviceInfo) - { - _logger.LogInformation($"Opening session for user {user.Id} on device '{deviceInfo}'"); - - try - { - var sessions = await _repository.GetByUserIdAsync(user.Id); - var session = sessions.FirstOrDefault(s => s.DeviceInfo == deviceInfo); - - if (session is not null) - { - var newExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays); - var accessToken = await _jwtService.GenerateAccessTokenAsync(user, session.Id); - var newRefreshToken = await _jwtService.GenerateRefreshTokenAsync(user); - - session.RefreshToken = newRefreshToken; - session.ExpiresAt = newExpiresAt; - session.CreatedAt = DateTime.UtcNow; - session.IsRevoked = false; - - await _repository.UpdateAsync(session); - _logger.LogInformation($"Updated session for user {user.Id} on device '{deviceInfo}'"); - - return new RefreshResult(session.RefreshToken, accessToken); - } - - return await OpenNewSession(); - } - catch (NotFoundByKeyException ex) - { - return await OpenNewSession(); - } - - async Task OpenNewSession() - { - var sessionId = Guid.NewGuid(); - - var accessToken = await _jwtService.GenerateAccessTokenAsync(user, sessionId); - var refreshToken = await _jwtService.GenerateRefreshTokenAsync(user); - - var newSession = new UserSession - { - Id = sessionId, - UserId = user.Id, - DeviceInfo = deviceInfo, - RefreshToken = refreshToken, - CreatedAt = DateTime.UtcNow, - ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays), - IsRevoked = false - }; - - await _repository.AddAsync(newSession); - - _logger.LogInformation($"Created new session {sessionId} for user {user.Id} on device '{deviceInfo}'"); - - return new RefreshResult(refreshToken, accessToken); - } - - } + _jwtService = jwtService; + _repository = repository; + _jwtTokenHasher = jwtTokenHasher; + _logger = logger; + _options = options.Value; } -} + public async Task OpenSessionAsync(User user, string deviceInfo) + { + _logger.LogInformation($"Opening session for user {user.Id} on device '{deviceInfo}'"); + + try + { + var sessions = await _repository.GetByUserIdAsync(user.Id); + var existingSession = sessions.FirstOrDefault(s => s.DeviceInfo == deviceInfo); + + if (existingSession is not null) + return await UpdateExistingSessionAsync(user, deviceInfo, existingSession); + } + catch (NotFoundByKeyException ex) + { + + } + + return await CreateNewSessionAsync(user, deviceInfo); + } + + private async Task UpdateExistingSessionAsync(User user, string deviceInfo, UserSession session) + { + var (accessToken, refreshToken) = await GenerateTokensAsync(user, session.Id); + + var newTokenHash = _jwtTokenHasher.HashToken(refreshToken); + var newExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays); + + session.RefreshTokenHash = newTokenHash; + session.ExpiresAt = newExpiresAt; + session.CreatedAt = DateTime.UtcNow; + session.IsRevoked = false; + + await _repository.UpdateAsync(session); + _logger.LogInformation($"Updated session for user {user.Id} on device '{deviceInfo}'"); + + return new RefreshResult(refreshToken, accessToken); + } + + private async Task CreateNewSessionAsync(User user, string deviceInfo) + { + var sessionId = Guid.NewGuid(); + var (accessToken, refreshToken) = await GenerateTokensAsync(user, sessionId); + + var refreshTokenHash = _jwtTokenHasher.HashToken(refreshToken); + + var newSession = new UserSession + { + Id = sessionId, + UserId = user.Id, + DeviceInfo = deviceInfo, + RefreshTokenHash = refreshTokenHash, + CreatedAt = DateTime.UtcNow, + ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays), + IsRevoked = false + }; + + await _repository.AddAsync(newSession); + + _logger.LogInformation($"Created new session {sessionId} for user {user.Id} on device '{deviceInfo}'"); + + return new RefreshResult(refreshToken, accessToken); + } + + private async Task<(string accessToken, string refreshToken)> GenerateTokensAsync(User user, Guid sessionId) + { + var accessToken = await _jwtService.GenerateAccessTokenAsync(user, sessionId); + var refreshToken = await _jwtService.GenerateRefreshTokenAsync(user); + return (accessToken, refreshToken); + } +} \ No newline at end of file diff --git a/Govor.Application/Services/UserSessions/UserSessionRefresher.cs b/Govor.Application/Services/UserSessions/UserSessionRefresher.cs index 701bd13..10f359b 100644 --- a/Govor.Application/Services/UserSessions/UserSessionRefresher.cs +++ b/Govor.Application/Services/UserSessions/UserSessionRefresher.cs @@ -36,7 +36,7 @@ public class UserSessionRefresher : IUserSessionRefresher { try { - var session = await _sessionsRepository.GetByRefreshTokenAsync(refreshToken); + var session = await _sessionsRepository.GetByHashedRefreshTokenAsync(refreshToken); if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow) throw new UnauthorizedAccessException("Refresh token is invalid or expired"); @@ -55,7 +55,7 @@ public class UserSessionRefresher : IUserSessionRefresher var newSession = new UserSession { UserId = user.Id, - RefreshToken = newRefreshToken, + RefreshTokenHash = newRefreshToken, DeviceInfo = session.DeviceInfo, CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays) diff --git a/Govor.Core/Models/Users/UserSession.cs b/Govor.Core/Models/Users/UserSession.cs index 23a85a5..dcd448b 100644 --- a/Govor.Core/Models/Users/UserSession.cs +++ b/Govor.Core/Models/Users/UserSession.cs @@ -6,7 +6,7 @@ public class UserSession { public Guid Id { get; set; } = Guid.NewGuid(); public Guid UserId { get; set; } - public string RefreshToken { get; set; } = string.Empty; + public string RefreshTokenHash { get; set; } = string.Empty; public string DeviceInfo { get; set; } = string.Empty; // "Chrome on Windows" public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime ExpiresAt { get; set; } @@ -21,7 +21,7 @@ public class UserSession return Id == userSession.Id && UserId == userSession.UserId && - RefreshToken == userSession.RefreshToken && + RefreshTokenHash == userSession.RefreshTokenHash && DeviceInfo == userSession.DeviceInfo && CreatedAt == userSession.CreatedAt && ExpiresAt == userSession.ExpiresAt && diff --git a/Govor.Core/Repositories/UserSessionsRepository/IUserSessionsExist.cs b/Govor.Core/Repositories/UserSessionsRepository/IUserSessionsExist.cs index 9d4ff9e..1655ee5 100644 --- a/Govor.Core/Repositories/UserSessionsRepository/IUserSessionsExist.cs +++ b/Govor.Core/Repositories/UserSessionsRepository/IUserSessionsExist.cs @@ -5,6 +5,6 @@ namespace Govor.Core.Repositories.UserSessionsRepository; public interface IUserSessionsExist { public bool Exist(Guid sessionId); - public bool Exist(string refresh); + public bool Exist(string hashedToken); public bool Exist(UserSession userSession); } \ No newline at end of file diff --git a/Govor.Core/Repositories/UserSessionsRepository/IUserSessionsReader.cs b/Govor.Core/Repositories/UserSessionsRepository/IUserSessionsReader.cs index b86be83..851569c 100644 --- a/Govor.Core/Repositories/UserSessionsRepository/IUserSessionsReader.cs +++ b/Govor.Core/Repositories/UserSessionsRepository/IUserSessionsReader.cs @@ -10,5 +10,5 @@ public interface IUserSessionsReader public Task> GetByCreatedAtAsync(DateTime createdAt); public Task> GetByExpiresAtAsync(DateTime createdAt); public Task> GetByRevokedAsync(bool isRevoked); - public Task GetByRefreshTokenAsync(string refreshToken); + public Task GetByHashedRefreshTokenAsync(string hash); } \ No newline at end of file diff --git a/Govor.Data.Tests/Repositories/UserSessionsRepositoryTests.cs b/Govor.Data.Tests/Repositories/UserSessionsRepositoryTests.cs index 6871a63..234e21b 100644 --- a/Govor.Data.Tests/Repositories/UserSessionsRepositoryTests.cs +++ b/Govor.Data.Tests/Repositories/UserSessionsRepositoryTests.cs @@ -212,7 +212,7 @@ public class UserSessionsRepositoryTests .With(f => f.IsRevoked, true) .CreateMany(random.Next(2, 10)).ToList(); - var token = sessions.First().RefreshToken; + var token = sessions.First().RefreshTokenHash; await using var context = new GovorDbContext(_options); var repository = new UserSessionsRepository(context); @@ -221,7 +221,7 @@ public class UserSessionsRepositoryTests await context.SaveChangesAsync(); // Act - var result = await repository.GetByRefreshTokenAsync(token); + var result = await repository.GetByHashedRefreshTokenAsync(token); // Assert Assert.That(result, Is.Not.Null); @@ -235,7 +235,7 @@ public class UserSessionsRepositoryTests using var context = new GovorDbContext(_options); var repository = new UserSessionsRepository(context); // Act & Assert - Assert.ThrowsAsync>(async () => await repository.GetByRefreshTokenAsync(_fixture.Create())); + Assert.ThrowsAsync>(async () => await repository.GetByHashedRefreshTokenAsync(_fixture.Create())); } [Test] @@ -281,7 +281,7 @@ public class UserSessionsRepositoryTests // Act var result1 = repository.Exist(session); var result2 = repository.Exist(session.Id); - var result3 = repository.Exist(session.RefreshToken); + var result3 = repository.Exist(session.RefreshTokenHash); // Assert Assert.That(result1, Is.True); @@ -301,7 +301,7 @@ public class UserSessionsRepositoryTests // Act var result1 = repository.Exist(session); var result2 = repository.Exist(session.Id); - var result3 = repository.Exist(session.RefreshToken); + var result3 = repository.Exist(session.RefreshTokenHash); // Assert Assert.That(result1, Is.False); Assert.That(result2, Is.False); diff --git a/Govor.Data/Configurations/UserSessionConfiguration.cs b/Govor.Data/Configurations/UserSessionConfiguration.cs index d81b485..bb5cf69 100644 --- a/Govor.Data/Configurations/UserSessionConfiguration.cs +++ b/Govor.Data/Configurations/UserSessionConfiguration.cs @@ -12,7 +12,7 @@ public class UserSessionConfiguration : IEntityTypeConfiguration { builder.HasKey(us => us.Id); - builder.Property(us => us.RefreshToken) + builder.Property(us => us.RefreshTokenHash) .IsRequired(); builder.Property(us => us.DeviceInfo) diff --git a/Govor.Data/Migrations/20251214065852_update_sessions_model.Designer.cs b/Govor.Data/Migrations/20251214065852_update_sessions_model.Designer.cs new file mode 100644 index 0000000..7a52bfb --- /dev/null +++ b/Govor.Data/Migrations/20251214065852_update_sessions_model.Designer.cs @@ -0,0 +1,774 @@ +// +using System; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Govor.Data.Migrations +{ + [DbContext(typeof(GovorDbContext))] + [Migration("20251214065852_update_sessions_model")] + partial class update_sessions_model + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ImageId") + .HasColumnType("uuid"); + + b.Property("IsChannel") + .HasColumnType("boolean"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.ToTable("ChatGroups"); + }); + + modelBuilder.Entity("Govor.Core.Models.Friendship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddresseeId") + .HasColumnType("uuid"); + + b.Property("RequesterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AddresseeId"); + + b.HasIndex("RequesterId"); + + b.ToTable("Friendships"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupAdmins"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InvitationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MaxParticipants") + .HasColumnType("integer"); + + b.Property("UserMakerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("UserMakerId"); + + b.ToTable("GroupInvitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InvitationId") + .HasColumnType("uuid"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("MemberSince") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("InvitationId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("MaxParticipants") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Invitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("text"); + + b.Property("MineType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("OwnerType") + .HasColumnType("integer"); + + b.Property("UploaderId") + .HasColumnType("uuid"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MediaFileId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MediaFileId"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatGroupId") + .HasColumnType("uuid"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEdited") + .HasColumnType("boolean"); + + b.Property("PrivateChatId") + .HasColumnType("uuid"); + + b.Property("RecipientId") + .HasColumnType("uuid"); + + b.Property("RecipientType") + .HasColumnType("integer"); + + b.Property("ReplyToMessageId") + .HasColumnType("uuid"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatGroupId"); + + b.HasIndex("PrivateChatId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReactionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageReactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageViews"); + }); + + modelBuilder.Entity("Govor.Core.Models.PrivateChat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("UserAId") + .HasColumnType("uuid"); + + b.Property("UserBId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PrivateChats"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.OneTimePreKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserCryptoSessionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UploadedAt"); + + b.HasIndex("UserCryptoSessionId", "IsUsed"); + + b.ToTable("OneTimePreKeys"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.SignedPreKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PublicSignedPreKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("SignedPreKeySignature") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("UserCryptoSessionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserCryptoSessionId") + .IsUnique(); + + b.ToTable("SignedPreKeys"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PublicIdentityKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("UserSessionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserSessionId") + .IsUnique(); + + b.ToTable("UserCryptoSessions"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("date"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IconId") + .HasColumnType("uuid"); + + b.Property("InviteId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WasOnline") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InviteId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRevoked") + .HasColumnType("boolean"); + + b.Property("RefreshTokenHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Govor.Core.Models.Friendship", b => + { + b.HasOne("Govor.Core.Models.Users.User", "Addressee") + .WithMany("ReceivedFriendRequests") + .HasForeignKey("AddresseeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Users.User", "Requester") + .WithMany("SentFriendRequests") + .HasForeignKey("RequesterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Addressee"); + + b.Navigation("Requester"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Admins") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("InviteCodes") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Users.User", "UserMaker") + .WithMany() + .HasForeignKey("UserMakerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("UserMaker"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.GroupInvitation", null) + .WithMany() + .HasForeignKey("InvitationId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b => + { + b.HasOne("Govor.Core.Models.MediaFile", "MediaFile") + .WithMany() + .HasForeignKey("MediaFileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Messages.Message", "Message") + .WithMany("MediaAttachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaFile"); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Messages") + .HasForeignKey("ChatGroupId"); + + b.HasOne("Govor.Core.Models.PrivateChat", null) + .WithMany("Messages") + .HasForeignKey("PrivateChatId"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b => + { + b.HasOne("Govor.Core.Models.Messages.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b => + { + b.HasOne("Govor.Core.Models.Messages.Message", null) + .WithMany("MessageViews") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => + { + b.HasOne("Govor.Core.Models.Users.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Users.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.OneTimePreKey", b => + { + b.HasOne("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserCryptoSession") + .WithMany("OneTimePreKeys") + .HasForeignKey("UserCryptoSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserCryptoSession"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.SignedPreKey", b => + { + b.HasOne("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserCryptoSession") + .WithOne("SignedPreKey") + .HasForeignKey("Govor.Core.Models.Users.Crypto.SignedPreKey", "UserCryptoSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserCryptoSession"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.HasOne("Govor.Core.Models.Users.UserSession", "UserSession") + .WithOne("CryptoSession") + .HasForeignKey("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserSession"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => + { + b.HasOne("Govor.Core.Models.Invitation", "Invite") + .WithMany("Users") + .HasForeignKey("InviteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invite"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.HasOne("Govor.Core.Models.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Navigation("Admins"); + + b.Navigation("InviteCodes"); + + b.Navigation("Members"); + + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.Navigation("MediaAttachments"); + + b.Navigation("MessageViews"); + + b.Navigation("Reactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.PrivateChat", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.Navigation("OneTimePreKeys"); + + b.Navigation("SignedPreKey") + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => + { + b.Navigation("ReceivedFriendRequests"); + + b.Navigation("SentFriendRequests"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.Navigation("CryptoSession") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Govor.Data/Migrations/20251214065852_update_sessions_model.cs b/Govor.Data/Migrations/20251214065852_update_sessions_model.cs new file mode 100644 index 0000000..9e7a7f9 --- /dev/null +++ b/Govor.Data/Migrations/20251214065852_update_sessions_model.cs @@ -0,0 +1,94 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Govor.Data.Migrations +{ + /// + public partial class update_sessions_model : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "RefreshToken", + table: "UserSessions", + newName: "RefreshTokenHash"); + + migrationBuilder.AlterColumn( + name: "Username", + table: "Users", + type: "character varying(50)", + maxLength: 50, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "PasswordHash", + table: "Users", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "Users", + type: "character varying(500)", + maxLength: 500, + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Users_Username", + table: "Users"); + + migrationBuilder.RenameColumn( + name: "RefreshTokenHash", + table: "UserSessions", + newName: "RefreshToken"); + + migrationBuilder.AlterColumn( + name: "Username", + table: "Users", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(50)", + oldMaxLength: 50); + + migrationBuilder.AlterColumn( + name: "PasswordHash", + table: "Users", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "Description", + table: "Users", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(500)", + oldMaxLength: 500, + oldNullable: true); + } + } +} diff --git a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs index 0b916df..6217647 100644 --- a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs +++ b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs @@ -468,8 +468,8 @@ namespace Govor.Data.Migrations .HasColumnType("date"); b.Property("Description") - .IsRequired() - .HasColumnType("text"); + .HasMaxLength(500) + .HasColumnType("character varying(500)"); b.Property("IconId") .HasColumnType("uuid"); @@ -479,11 +479,13 @@ namespace Govor.Data.Migrations b.Property("PasswordHash") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Username") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(50) + .HasColumnType("character varying(50)"); b.Property("WasOnline") .HasColumnType("timestamp with time zone"); @@ -492,6 +494,9 @@ namespace Govor.Data.Migrations b.HasIndex("InviteId"); + b.HasIndex("Username") + .IsUnique(); + b.ToTable("Users"); }); @@ -515,7 +520,7 @@ namespace Govor.Data.Migrations b.Property("IsRevoked") .HasColumnType("boolean"); - b.Property("RefreshToken") + b.Property("RefreshTokenHash") .IsRequired() .HasColumnType("text"); diff --git a/Govor.Data/Repositories/UserSessionsRepository.cs b/Govor.Data/Repositories/UserSessionsRepository.cs index 897b9bc..1355425 100644 --- a/Govor.Data/Repositories/UserSessionsRepository.cs +++ b/Govor.Data/Repositories/UserSessionsRepository.cs @@ -62,12 +62,12 @@ public class UserSessionsRepository : IUserSessionsRepository .ToListOrThrowIfEmpty(new NotFoundByKeyException(isRevoked, "Sessions is revoked does not exist")); } - public async Task GetByRefreshTokenAsync(string refreshToken) + public async Task GetByHashedRefreshTokenAsync(string hash) { return await _context.UserSessions .AsNoTracking() - .FirstOrDefaultAsync(session => session.RefreshToken == refreshToken) - ?? throw new NotFoundByKeyException(refreshToken, "Session with given refresh token does not exist"); + .FirstOrDefaultAsync(session => session.RefreshTokenHash == hash) + ?? throw new NotFoundByKeyException(hash, "Session with given refresh token does not exist"); } public async Task AddAsync(UserSession userSession) @@ -93,7 +93,7 @@ public class UserSessionsRepository : IUserSessionsRepository .Where(u => u.Id == userSession.Id) .ExecuteUpdateAsync(u => u .SetProperty(a => a.UserId, userSession.UserId) - .SetProperty(u => u.RefreshToken, userSession.RefreshToken) + .SetProperty(u => u.RefreshTokenHash, userSession.RefreshTokenHash) .SetProperty(u => u.DeviceInfo, userSession.DeviceInfo) .SetProperty(u => u.CreatedAt, userSession.CreatedAt) .SetProperty(u => u.ExpiresAt, userSession.ExpiresAt) @@ -154,15 +154,15 @@ public class UserSessionsRepository : IUserSessionsRepository return _context.UserSessions.Any(session => session.Id == sessionId); } - public bool Exist(string refresh) + public bool Exist(string hashedToken) { - return _context.UserSessions.Any(session => session.RefreshToken == refresh); + return _context.UserSessions.Any(session => session.RefreshTokenHash == hashedToken); } public bool Exist(UserSession userSession) { return _context.UserSessions.Any(session => session.Id == userSession.Id && - session.RefreshToken == userSession.RefreshToken && + session.RefreshTokenHash == userSession.RefreshTokenHash && session.IsRevoked == userSession.IsRevoked && session.CreatedAt == userSession.CreatedAt && session.ExpiresAt == userSession.ExpiresAt &&