mirror of
https://github.com/Govor-team/Govor.git
synced 2026-07-21 03:34:56 +00:00
Implement user session management and JWT refresh tokens
Added user session models, interfaces, repository, and service for managing user sessions and refresh tokens. Refactored authentication flow to return user objects and open sessions with device info, supporting refresh token generation and validation. Updated JWT configuration to separate access and refresh options, and refactored related tests and API contracts. Improved media upload handling and error logging. Migrated dependency references and DI registrations accordingly.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
name: Qodana Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
name: Run Qodana Code Analysis
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Qodana
|
||||
uses: JetBrains/qodana-action@v2024.1
|
||||
with:
|
||||
# Указываем путь к qodana.yaml (если он не в корне)
|
||||
args: --ide QDNET --project-dir .
|
||||
@@ -1,11 +1,12 @@
|
||||
using AutoFixture;
|
||||
using Govor.API.Controllers;
|
||||
using Govor.API.Services.Authentication.Interfaces;
|
||||
using Govor.Application.Exceptions.AuthService;
|
||||
using Govor.Application.Exceptions.InvitesService;
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Govor.Application.Interfaces.UserSession;
|
||||
using Govor.Contracts.Requests;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Models.Users;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
@@ -19,6 +20,7 @@ public class AuthControllerTests
|
||||
private Mock<IAccountService> _accountServiceMock;
|
||||
private Mock<IInvitesService> _invitesServiceMock;
|
||||
private Mock<ILogger<AuthController>> _loggerMock;
|
||||
private Mock<IUserSessionOpener> _userSessionOpenerMock;
|
||||
private AuthController _controller;
|
||||
|
||||
[SetUp]
|
||||
@@ -31,10 +33,12 @@ public class AuthControllerTests
|
||||
_accountServiceMock = new Mock<IAccountService>();
|
||||
_invitesServiceMock = new Mock<IInvitesService>();
|
||||
_loggerMock = new Mock<ILogger<AuthController>>();
|
||||
|
||||
_userSessionOpenerMock = new Mock<IUserSessionOpener>();
|
||||
|
||||
_controller = new AuthController(
|
||||
_accountServiceMock.Object,
|
||||
_invitesServiceMock.Object,
|
||||
_userSessionOpenerMock.Object,
|
||||
_loggerMock.Object
|
||||
);
|
||||
}
|
||||
@@ -47,9 +51,18 @@ public class AuthControllerTests
|
||||
var request = _fixture.Create<RegistrationRequest>();
|
||||
var invitation = _fixture.Create<Invitation>();
|
||||
var token = _fixture.Create<string>();
|
||||
|
||||
|
||||
var user = _fixture.Build<User>()
|
||||
.With(x => x.Username).Create();
|
||||
|
||||
_invitesServiceMock.Setup(s => s.ValidateAsync(request.InviteLink)).ReturnsAsync(invitation);
|
||||
_accountServiceMock.Setup(s => s.RegistrationAsync(request.Name, request.Password, invitation)).ReturnsAsync(token);
|
||||
|
||||
|
||||
_accountServiceMock.Setup(l => l.RegistrationAsync(request.Name, request.Password, invitation))
|
||||
.ReturnsAsync(user);
|
||||
|
||||
_userSessionOpenerMock.Setup(f => f.OpenSessionAsync(user, request.DeviceInfo))
|
||||
.ReturnsAsync(token);
|
||||
|
||||
// Act
|
||||
var result = await _controller.Register(request);
|
||||
@@ -137,7 +150,14 @@ public class AuthControllerTests
|
||||
// Arrange
|
||||
var loginRequest = _fixture.Create<LoginRequest>();
|
||||
var token = _fixture.Create<string>();
|
||||
_accountServiceMock.Setup(l => l.LoginAsync(loginRequest.Name, loginRequest.Password)).ReturnsAsync(token);
|
||||
|
||||
var user = _fixture.Build<User>()
|
||||
.With(x => x.Username).Create();
|
||||
|
||||
_accountServiceMock.Setup(l => l.LoginAsync(loginRequest.Name, loginRequest.Password)).ReturnsAsync(user);
|
||||
|
||||
_userSessionOpenerMock.Setup(f => f.OpenSessionAsync(user, loginRequest.DeviceInfo))
|
||||
.ReturnsAsync(token);
|
||||
|
||||
// Act
|
||||
var result = await _controller.Login(loginRequest);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Govor.API.Services.AdminsStuff.Interfaces;
|
||||
using Govor.Application.Interfaces;
|
||||
using Govor.Contracts.DTOs;
|
||||
using Govor.Contracts.Requests;
|
||||
using Govor.Core.Repositories.Invaites;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Govor.API.Services.AdminsStuff.Interfaces;
|
||||
using Govor.Application.Interfaces;
|
||||
using Govor.Contracts.Responses.Admins;
|
||||
using Govor.Core.Models.Users;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Govor.API.Services.Authentication.Interfaces;
|
||||
using Govor.Application.Exceptions.AuthService;
|
||||
using Govor.Application.Exceptions.InvitesService;
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Govor.Application.Interfaces.UserSession;
|
||||
using Govor.Contracts.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -13,33 +13,38 @@ namespace Govor.API.Controllers;
|
||||
[Route("api/[controller]")]
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private IUserSessionOpener _userSession;
|
||||
private IInvitesService _invitesService;
|
||||
private IAccountService _accountService;
|
||||
private ILogger<AuthController> _logger;
|
||||
|
||||
public AuthController(IAccountService accountService, IInvitesService invitesService, ILogger<AuthController> logger)
|
||||
public AuthController(IAccountService accountService, IInvitesService invitesService,IUserSessionOpener userSessionOpener, ILogger<AuthController> logger)
|
||||
{
|
||||
_userSession = userSessionOpener;
|
||||
_accountService = accountService;
|
||||
_invitesService = invitesService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[RequireHttps]
|
||||
[HttpPost("register")]// api/auth/register
|
||||
//[RequireHttps]
|
||||
public async Task<IActionResult> Register([FromBody] RegistrationRequest registrationRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
var invite = await _invitesService.ValidateAsync(registrationRequest.InviteLink);
|
||||
|
||||
var token = await _accountService.RegistrationAsync(registrationRequest.Name, registrationRequest.Password,
|
||||
var user = await _accountService.RegistrationAsync(registrationRequest.Name, registrationRequest.Password,
|
||||
invite);
|
||||
_logger.LogInformation($"Register request for {registrationRequest.Name} processed successfully");
|
||||
|
||||
_logger.LogInformation($"Register request for {user.Username} with id {user.Id} processed successfully");
|
||||
|
||||
var token = await _userSession.OpenSessionAsync(user, registrationRequest.DeviceInfo);
|
||||
|
||||
_logger.LogInformation($"Session for user {user.Username} with id {user.Id} has been opened");
|
||||
return Ok(new { token });
|
||||
}
|
||||
catch (UserAlreadyExistException ex)
|
||||
@@ -64,35 +69,73 @@ public class AuthController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[RequireHttps]
|
||||
[HttpPost("login")]// api/auth/login
|
||||
//[RequireHttps]
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest userRequest)
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest loginRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
var token = await _accountService.LoginAsync(userRequest.Name, userRequest.Password);
|
||||
_logger.LogInformation($"Login request for {userRequest.Name} processed successfully");
|
||||
var user = await _accountService.LoginAsync(loginRequest.Name, loginRequest.Password);
|
||||
_logger.LogInformation($"Login request for {user.Username} with id {user.Id} processed successfully");
|
||||
|
||||
var token = await _userSession.OpenSessionAsync(user, loginRequest.DeviceInfo);
|
||||
|
||||
_logger.LogInformation($"Session for user {user.Username} with id {user.Id} has been opened");
|
||||
|
||||
return Ok(new { token });
|
||||
}
|
||||
catch (UserNotRegisteredException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Login failed for user {Name}", userRequest.Name);
|
||||
_logger.LogWarning(ex, "Login failed for user {Name}", loginRequest.Name);
|
||||
return BadRequest("Login failed: user does not exist.");
|
||||
}
|
||||
catch (LoginUserException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Login failed for user {Name}", userRequest.Name);
|
||||
_logger.LogWarning(ex, "Login failed for user {Name}", loginRequest.Name);
|
||||
return BadRequest("Login failed: username or password is incorrect.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error during login for user {Name}", userRequest.Name);
|
||||
_logger.LogError(ex, "Unexpected error during login for user {Name}", loginRequest.Name);
|
||||
return StatusCode(500, "An unexpected error occurred. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[RequireHttps]
|
||||
[HttpPost("refresh")]
|
||||
public async Task<IActionResult> Refresh([FromBody] string refreshToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
throw new InvalidOperationException("Refresh token cant be empty.");
|
||||
|
||||
var newAccessToken = await _accountService.RefreshTokenAsync(refreshToken);
|
||||
return Ok(new { accessToken = newAccessToken });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid refresh token");
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Refresh token failed");
|
||||
return Unauthorized("Invalid refresh token");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, ex.Message);
|
||||
return StatusCode(500, "An unexpected error occurred.");
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class ChatLoadController : Controller
|
||||
public async Task<IActionResult> GetChatMessages(
|
||||
[FromQuery] Guid chatId,
|
||||
[FromQuery] Guid? startMessageId,
|
||||
[FromQuery] int pageSize = 20)
|
||||
[FromQuery] int pageSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -29,32 +29,51 @@ public class MediaController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("upload")]
|
||||
[RequestSizeLimit(100_000_000)] // ~100MB
|
||||
[RequestSizeLimit(20_000_000)] // ~20MB
|
||||
public async Task<IActionResult> Upload([FromForm] MediaUploadRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request.FromFile.Length > 20_000_000)
|
||||
return BadRequest("File is too large");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(ModelState);
|
||||
|
||||
// Чтение байт из IFormFile
|
||||
using var memoryStream = new MemoryStream();
|
||||
await request.Data.CopyToAsync(memoryStream);
|
||||
|
||||
await request.FromFile.CopyToAsync(memoryStream);
|
||||
|
||||
byte[] fileBytes = memoryStream.ToArray();
|
||||
|
||||
var media = new Media(
|
||||
_currentUserService.GetCurrentUserId(),
|
||||
DateTime.UtcNow,
|
||||
Path.GetFileName(request.FromFile.FileName),
|
||||
fileBytes,
|
||||
request.FileName,
|
||||
request.Type,
|
||||
request.MimeType,
|
||||
request.EncryptedKey
|
||||
);
|
||||
|
||||
var result = await _mediaService.UploadMediaAsync(media);
|
||||
|
||||
_logger.LogInformation(
|
||||
$"Uploaded file: {Path.GetFileName(request.FromFile.FileName)} from user {_currentUserService.GetCurrentUserId()}");
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return Unauthorized(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error uploading media");
|
||||
@@ -71,7 +90,12 @@ public class MediaController : Controller
|
||||
return BadRequest(ModelState);
|
||||
|
||||
var media = await _mediaService.GetMediaByIdAsync(id);
|
||||
return File(media.Data, media.MineType, media.FileName);
|
||||
return File(media.Data, media.MineType, Path.GetFileName(media.FileName));
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, ex.Message);
|
||||
return NotFound(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using Govor.API.Services.AdminsStuff.Interfaces;
|
||||
using Govor.API.Services.Authentication.Interfaces;
|
||||
using Govor.Application.Infrastructure.AdminsStuff;
|
||||
using Govor.Application.Infrastructure.Extensions;
|
||||
using Govor.Application.Infrastructure.Validators;
|
||||
@@ -9,10 +7,12 @@ using Govor.Application.Interfaces.Friends;
|
||||
using Govor.Application.Interfaces.Infrastructure.Extensions;
|
||||
using Govor.Application.Interfaces.Medias;
|
||||
using Govor.Application.Interfaces.Messages;
|
||||
using Govor.Application.Interfaces.UserSession;
|
||||
using Govor.Application.Services;
|
||||
using Govor.Application.Services.Authentication;
|
||||
using Govor.Application.Services.Friends;
|
||||
using Govor.Application.Services.Messages;
|
||||
using Govor.Application.Services.UserSessions;
|
||||
using Govor.Core.Infrastructure.Extensions;
|
||||
using Govor.Core.Infrastructure.Validators;
|
||||
using Govor.Core.Models;
|
||||
@@ -26,6 +26,7 @@ using Govor.Core.Repositories.MediasAttachments;
|
||||
using Govor.Core.Repositories.Messages;
|
||||
using Govor.Core.Repositories.PrivateChats;
|
||||
using Govor.Core.Repositories.Users;
|
||||
using Govor.Core.Repositories.UserSessionsRepository;
|
||||
using Govor.Data;
|
||||
using Govor.Data.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -67,6 +68,9 @@ public static class ConfigurationProgramExtensions
|
||||
services.AddScoped<IUserGroupsService, UserGroupsService>();
|
||||
services.AddScoped<IMessagesLoader, MessagesLoader>();
|
||||
services.AddScoped<IMediaService, MediaService>();
|
||||
|
||||
// UserSession
|
||||
services.AddScoped<IUserSessionOpener, UserSessionOpener>();
|
||||
|
||||
// Auto Mapper
|
||||
services.AddAutoMapper(typeof(MappingProfile));
|
||||
@@ -82,7 +86,7 @@ public static class ConfigurationProgramExtensions
|
||||
services.AddScoped<IFriendshipsRepository, FriendshipsRepository>();
|
||||
services.AddScoped<IPrivateChatsRepository, PrivateChatsRepository>();
|
||||
services.AddScoped<IGroupsRepository, GroupRepository>();
|
||||
|
||||
services.AddScoped<IUserSessionsRepository, UserSessionsRepository>();
|
||||
// other
|
||||
}
|
||||
|
||||
|
||||
@@ -25,5 +25,9 @@
|
||||
<ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Contracts\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ builder.Services.AddCors(options =>
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.Configure<JwtOption>(configuration.GetSection(nameof(JwtOption)));
|
||||
builder.Services.Configure<JwtAccessOption>(configuration.GetSection(nameof(JwtAccessOption)));
|
||||
|
||||
// Add services
|
||||
builder.Services.AddSignalRConf();// signalR
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
"GovorDbContext": "Server=147.45.255.215;Port=3306;Database=artemy_DB;User=artemy;Password=LoxHuy))228Goy;"
|
||||
},
|
||||
"UseMySql": true,
|
||||
"AllowedHosts": "*",
|
||||
"JwtOption": {
|
||||
"SecretKeу": "MY VERY SECRET KEY asdasdpafjhasofafpajsfj",
|
||||
"Hours": "24"
|
||||
"AllowedHosts": "govor-team-govor-88b3.twc1.net",
|
||||
"JwtAccessOption": {
|
||||
"SecretKey": "Q89eY7zP7C4+TqLmHF4kw9xkF1E8Ru4Zpg+up9wFt9g=",
|
||||
"Minutes": 25
|
||||
},
|
||||
"JwtRefreshOption": {
|
||||
"RefreshTokenLifetimeDays": 30
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using AutoFixture;
|
||||
using Govor.API.Services.AdminsStuff.Interfaces;
|
||||
using Govor.Application.Infrastructure.AdminsStuff;
|
||||
using Govor.Application.Interfaces;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Repositories.Invaites;
|
||||
using Moq;
|
||||
|
||||
@@ -2,10 +2,8 @@ using AutoFixture;
|
||||
using Govor.Core.Infrastructure.Extensions;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Repositories.Users;
|
||||
using Govor.API.Services.Authentication.Interfaces;
|
||||
using Govor.Application.Exceptions.AuthService;
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Govor.Application.Services;
|
||||
using Govor.Application.Services.Authentication;
|
||||
using Govor.Core.Models.Users;
|
||||
using Govor.Core.Repositories.Admins;
|
||||
@@ -52,6 +50,7 @@ public class AuthServiceTests
|
||||
);
|
||||
}
|
||||
|
||||
// Tests for Register action
|
||||
[Test]
|
||||
public void Given_ExistUser_When_Register_Should_Throw_UserAlreadyExistsException()
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using AutoFixture;
|
||||
using Govor.API.Services.Authentication.Interfaces;
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Govor.Application.Services.Authentication;
|
||||
using Govor.Core.Models.Users;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Moq;
|
||||
|
||||
namespace Govor.Application.Tests.Services.Authentication;
|
||||
@@ -13,11 +15,13 @@ namespace Govor.Application.Tests.Services.Authentication;
|
||||
public class JwtServiceTests
|
||||
{
|
||||
private Fixture _fixture;
|
||||
private Mock<IOptions<JwtOption>> _jwtOptionsMock;
|
||||
private Mock<IOptions<JwtAccessOption>> _jwtOptionsMock;
|
||||
private Mock<IOptions<JwtRefreshOption>> _jwtRefreshOptionsMock;
|
||||
private Mock<IInvitesService> _invitesServiceMock;
|
||||
private IJwtService _jwtService;
|
||||
|
||||
private JwtOption _testJwtOptions;
|
||||
private JwtAccessOption _testJwtAccessOptions;
|
||||
private JwtRefreshOption _testJwtRefreshOptions;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
@@ -26,18 +30,29 @@ public class JwtServiceTests
|
||||
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
|
||||
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
|
||||
|
||||
_testJwtOptions = new JwtOption
|
||||
_testJwtAccessOptions = new JwtAccessOption
|
||||
{
|
||||
SecretKeу = "THIS IS A TEST SECRET KEY THAT IS LONG ENOUGH", // Ensure key size is sufficient for HMACSHA256
|
||||
Hours = 1
|
||||
SecretKeу = "THIS_IS_A_TEST_SECRET_KEY_THAT_IS_LONG_ENOUGH_1234", // Ensure key size is sufficient for HMACSHA256
|
||||
Minutes = 5
|
||||
};
|
||||
|
||||
_jwtOptionsMock = new Mock<IOptions<JwtOption>>();
|
||||
_jwtOptionsMock.Setup(o => o.Value).Returns(_testJwtOptions);
|
||||
|
||||
_testJwtRefreshOptions = new JwtRefreshOption()
|
||||
{
|
||||
RefreshTokenLifetimeDays = 30
|
||||
};
|
||||
|
||||
_jwtOptionsMock = new Mock<IOptions<JwtAccessOption>>();
|
||||
_jwtOptionsMock.Setup(o => o.Value).Returns(_testJwtAccessOptions);
|
||||
|
||||
_jwtRefreshOptionsMock = new Mock<IOptions<JwtRefreshOption>>();
|
||||
_jwtRefreshOptionsMock.Setup(o => o.Value).Returns(_testJwtRefreshOptions);
|
||||
|
||||
_invitesServiceMock = new Mock<IInvitesService>();
|
||||
|
||||
_jwtService = new JwtService(_jwtOptionsMock.Object, _invitesServiceMock.Object);
|
||||
_jwtService = new JwtService(
|
||||
_jwtOptionsMock.Object,
|
||||
_jwtRefreshOptionsMock.Object,
|
||||
_invitesServiceMock.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -48,7 +63,7 @@ public class JwtServiceTests
|
||||
var expectedRole = "User";
|
||||
_invitesServiceMock.Setup(s => s.GetRoleAsync(user)).Returns(Task.FromResult(expectedRole));
|
||||
// Act
|
||||
var tokenString = await _jwtService.GenerateJwtTokenAsync(user);
|
||||
var tokenString = await _jwtService.GenerateAccessTokenAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.That(tokenString, Is.Not.Null.And.Not.Empty);
|
||||
@@ -57,4 +72,57 @@ public class JwtServiceTests
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
Assert.DoesNotThrow(() => handler.ReadJwtToken(tokenString));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateRefreshTokenAsync_ReturnsValidRefreshToken()
|
||||
{
|
||||
// Arrange
|
||||
var user = new User { Id = Guid.NewGuid() };
|
||||
|
||||
// Act
|
||||
var token = await _jwtService.GenerateRefreshTokenAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.That(token, Is.Not.Null);
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
|
||||
Assert.That(user.Id.ToString(), Is.EqualTo(jwt.Claims.First(c => c.Type == "userId").Value));
|
||||
Assert.That("refresh",Is.EqualTo(jwt.Claims.First(c => c.Type == "tokenType").Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPrincipalFromExpiredToken_ReturnsValidClaimsPrincipal()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_testJwtAccessOptions.SecretKeу));
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var token = handler.CreateToken(new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim("userId", userId.ToString())
|
||||
}),
|
||||
NotBefore = now.AddSeconds(-10),
|
||||
IssuedAt = now.AddSeconds(-10),
|
||||
Expires = now.AddSeconds(-5),
|
||||
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
|
||||
});
|
||||
|
||||
var expiredToken = handler.WriteToken(token);
|
||||
|
||||
// Act
|
||||
var principal = _jwtService.GetPrincipalFromExpiredToken(expiredToken);
|
||||
|
||||
// Assert
|
||||
Assert.That(principal, Is.Not.Null);
|
||||
var claim = principal.FindFirst("userId");
|
||||
Assert.That(claim, Is.Not.Null);
|
||||
Assert.That(userId.ToString(), Is.EqualTo(claim.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Govor.Application.Services.Authentication;
|
||||
using Govor.Application.Services.UserSessions;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Models.Users;
|
||||
using Govor.Core.Repositories.UserSessionsRepository;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
|
||||
namespace Govor.Application.Tests.Services.UserSessions;
|
||||
|
||||
[TestFixture]
|
||||
public class UserSessionOpenerTests
|
||||
{
|
||||
private Mock<IUserSessionsRepository> _repositoryMock;
|
||||
private Mock<IJwtService> _jwtServiceMock;
|
||||
private Mock<ILogger<UserSessionOpener>> _loggerMock;
|
||||
private IOptions<JwtRefreshOption> _options;
|
||||
private UserSessionOpener _service;
|
||||
private User _user;
|
||||
private const string DeviceInfo = "Chrome on Windows";
|
||||
private const string GeneratedToken = "new-refresh-token";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_repositoryMock = new Mock<IUserSessionsRepository>();
|
||||
_jwtServiceMock = new Mock<IJwtService>();
|
||||
_loggerMock = new Mock<ILogger<UserSessionOpener>>();
|
||||
_options = Options.Create(new JwtRefreshOption { RefreshTokenLifetimeDays = 30 });
|
||||
|
||||
_service = new UserSessionOpener(
|
||||
_repositoryMock.Object,
|
||||
_jwtServiceMock.Object,
|
||||
_options,
|
||||
_loggerMock.Object
|
||||
);
|
||||
|
||||
_user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = "test",
|
||||
PasswordHash = "hashed",
|
||||
IconId = Guid.NewGuid(),
|
||||
CreatedOn = DateOnly.FromDateTime(DateTime.UtcNow),
|
||||
WasOnline = DateTime.UtcNow,
|
||||
InviteId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
_jwtServiceMock
|
||||
.Setup(j => j.GenerateRefreshTokenAsync(_user))
|
||||
.ReturnsAsync(GeneratedToken);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OpenSessionAsync_ShouldReturnExistingToken_IfSessionValid()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Core.Models.UserSession
|
||||
{
|
||||
UserId = _user.Id,
|
||||
DeviceInfo = DeviceInfo,
|
||||
RefreshToken = "valid-token",
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-10),
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(10),
|
||||
IsRevoked = false
|
||||
};
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByUserIdAsync(_user.Id))
|
||||
.ReturnsAsync(new List<UserSession> { session });
|
||||
|
||||
// Act
|
||||
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
|
||||
|
||||
// Asser
|
||||
Assert.That(result, Is.EqualTo("valid-token"));
|
||||
_repositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OpenSessionAsync_ShouldUpdateSession_IfExpiredOrRevoked()
|
||||
{
|
||||
// Arrange
|
||||
var session = new Core.Models.UserSession
|
||||
{
|
||||
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<UserSession> { session });
|
||||
|
||||
// Act
|
||||
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo(GeneratedToken));
|
||||
_repositoryMock.Verify(r => r.UpdateAsync(It.Is<UserSession>(s => s.RefreshToken == GeneratedToken)), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OpenSessionAsync_ShouldCreateNewSession_IfNoneExists()
|
||||
{
|
||||
// Arrange
|
||||
_repositoryMock
|
||||
.Setup(r => r.GetByUserIdAsync(_user.Id))
|
||||
.ReturnsAsync(new List<UserSession>());
|
||||
|
||||
// Act
|
||||
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo(GeneratedToken));
|
||||
_repositoryMock.Verify(r => r.AddAsync(It.Is<UserSession>(s =>
|
||||
s.UserId == _user.Id &&
|
||||
s.DeviceInfo == DeviceInfo &&
|
||||
s.RefreshToken == GeneratedToken
|
||||
)), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Govor.API.Services.AdminsStuff.Interfaces;
|
||||
using Govor.Application.Interfaces;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Repositories.Invaites;
|
||||
|
||||
@@ -19,7 +19,7 @@ public class InvitationGenerator(IInvitesRepository repository) : IInvitationGen
|
||||
IsAdmin = isAdmin
|
||||
};
|
||||
|
||||
await repository.AddAsync(newInvitation);
|
||||
await repository.AddAsync(newInvitation);
|
||||
|
||||
return newInvitation.Code;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Models.Users;
|
||||
|
||||
namespace Govor.Application.Interfaces.Authentication;
|
||||
|
||||
public interface IAccountService
|
||||
{
|
||||
public Task<string> RegistrationAsync(string name, string password, Invitation invitation);
|
||||
public Task<string> LoginAsync(string name, string password);
|
||||
public Task<User> RegistrationAsync(string name, string password, Invitation invitation);
|
||||
public Task<User> LoginAsync(string name, string password);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Models.Users;
|
||||
|
||||
namespace Govor.API.Services.Authentication.Interfaces;
|
||||
namespace Govor.Application.Interfaces.Authentication;
|
||||
|
||||
public interface IInvitesService
|
||||
{
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using System.Security.Claims;
|
||||
using Govor.Core.Models.Users;
|
||||
|
||||
namespace Govor.Application.Interfaces.Authentication;
|
||||
|
||||
public interface IJwtService
|
||||
{
|
||||
Task<string> GenerateJwtTokenAsync(User user);
|
||||
Task<string> GenerateAccessTokenAsync(User user);
|
||||
Task<string> GenerateRefreshTokenAsync(User user);
|
||||
ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Govor.API.Services.AdminsStuff.Interfaces;
|
||||
namespace Govor.Application.Interfaces;
|
||||
|
||||
public interface IInvitationGenerator
|
||||
{
|
||||
|
||||
@@ -12,8 +12,8 @@ public interface IMediaService
|
||||
|
||||
public record Media(Guid UploaderId,
|
||||
DateTime UploadedOn,
|
||||
byte[] Data,
|
||||
string FileName,
|
||||
byte[] Data,
|
||||
MediaType Type,
|
||||
string MineType,
|
||||
string EncryptedKey);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Govor.Core.Models.Users;
|
||||
|
||||
namespace Govor.Application.Interfaces.UserSession;
|
||||
|
||||
|
||||
public interface IUserSessionOpener
|
||||
{
|
||||
Task<string> OpenSessionAsync(User user, string deviceInfo);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Govor.Application.Interfaces.UserSession;
|
||||
|
||||
public interface IUserSessionReader
|
||||
{
|
||||
Task<List<Core.Models.UserSession>> GetAllSessionsAsync(Guid userId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Govor.Application.Interfaces.UserSession;
|
||||
|
||||
|
||||
public interface IUserSessionRevoker
|
||||
{
|
||||
Task CloseSessionByRefreshTokenAsync(string refreshToken);
|
||||
Task CloseAllSessionsAsync(Guid userId);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class AuthService : IAccountService
|
||||
_usernameValidator = usernameValidator;
|
||||
}
|
||||
|
||||
public async Task<string> RegistrationAsync(string name, string password, Invitation invitation)
|
||||
public async Task<User> RegistrationAsync(string name, string password, Invitation invitation)
|
||||
{
|
||||
_usernameValidator.Validate(name);
|
||||
|
||||
@@ -55,11 +55,11 @@ public class AuthService : IAccountService
|
||||
|
||||
await SetRole(user, invitation);
|
||||
|
||||
return await _jwtService.GenerateJwtTokenAsync(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
public async Task<string> LoginAsync(string name, string password)
|
||||
public async Task<User> LoginAsync(string name, string password)
|
||||
{
|
||||
if (await _usersRepository.ExistsUsernameAsync(name) == false)
|
||||
throw new UserNotRegisteredException(name);
|
||||
@@ -69,9 +69,34 @@ public class AuthService : IAccountService
|
||||
if (_passwordHasher.Verify(password, user.PasswordHash) == false)
|
||||
throw new LoginUserException();
|
||||
|
||||
return await _jwtService.GenerateJwtTokenAsync(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/*
|
||||
public async Task<string> RefreshTokenAsync(string refreshToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var principal = _jwtService.GetPrincipalFromExpiredToken(refreshToken);
|
||||
|
||||
var userId = Guid.Parse(principal?.FindFirst("userId")?.Value ?? string.Empty);
|
||||
|
||||
var storedTokens = await _userSessionsRepository.GetUserTokensAsync(userId);
|
||||
|
||||
if (!storedTokens.Contains(refreshToken))
|
||||
throw new UnauthorizedAccessException("Invalid refresh token");
|
||||
|
||||
var user = await _usersRepository.FindByIdAsync(userId);
|
||||
var newAccessToken = await _jwtService.GenerateAccessTokenAsync(user);
|
||||
return newAccessToken;
|
||||
}
|
||||
catch (SecurityTokenException ex)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid refresh token", ex);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
private async Task SetRole(User user, Invitation invitation)
|
||||
{
|
||||
if(invitation.IsAdmin)
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
namespace Govor.Application.Services.Authentication;
|
||||
public class JwtOption
|
||||
public class JwtAccessOption
|
||||
{
|
||||
public string SecretKeу {get; set;}
|
||||
public int Hours { get; set; }
|
||||
public int Minutes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Govor.Application.Services.Authentication;
|
||||
|
||||
public class JwtRefreshOption
|
||||
{
|
||||
public int RefreshTokenLifetimeDays { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Govor.API.Services.Authentication.Interfaces;
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Govor.Core.Models.Users;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -11,16 +10,18 @@ namespace Govor.Application.Services.Authentication;
|
||||
|
||||
public class JwtService : IJwtService
|
||||
{
|
||||
private JwtOption _jwtOption;
|
||||
private JwtAccessOption _jwtAccessOption;
|
||||
private JwtRefreshOption _refreshOptions;
|
||||
private IInvitesService _invitesService;
|
||||
|
||||
public JwtService(IOptions<JwtOption> options, IInvitesService invitesService)
|
||||
public JwtService(IOptions<JwtAccessOption> options, IOptions<JwtRefreshOption> refreshOptions, IInvitesService invitesService)
|
||||
{
|
||||
_jwtOption = options.Value;
|
||||
_refreshOptions = refreshOptions.Value;
|
||||
_jwtAccessOption = options.Value;
|
||||
_invitesService = invitesService;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateJwtTokenAsync(User user)
|
||||
public async Task<string> GenerateAccessTokenAsync(User user)
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
@@ -29,13 +30,55 @@ public class JwtService : IJwtService
|
||||
};
|
||||
|
||||
var singing = new SigningCredentials(
|
||||
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOption.SecretKeу)),
|
||||
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtAccessOption.SecretKeу)),
|
||||
SecurityAlgorithms.HmacSha256Signature);
|
||||
|
||||
var token = new JwtSecurityToken(signingCredentials: singing,
|
||||
expires: DateTime.UtcNow.AddHours(_jwtOption.Hours),
|
||||
var token = new JwtSecurityToken(
|
||||
expires: DateTime.UtcNow.AddMinutes(_jwtAccessOption.Minutes),
|
||||
signingCredentials: singing,
|
||||
claims: claims);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateRefreshTokenAsync(User user)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtAccessOption.SecretKeу));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("userId", user.Id.ToString()),
|
||||
new Claim("tokenType", "refresh")
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
expires: DateTime.UtcNow.AddDays(_refreshOptions.RefreshTokenLifetimeDays),
|
||||
signingCredentials: creds,
|
||||
claims: claims
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
|
||||
{
|
||||
var tokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateAudience = false,
|
||||
ValidateIssuer = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtAccessOption.SecretKeу)),
|
||||
ValidateLifetime = false // << important
|
||||
};
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var principal = handler.ValidateToken(token, tokenValidationParameters, out var securityToken);
|
||||
|
||||
if (securityToken is not JwtSecurityToken jwtToken || jwtToken.Header.Alg != SecurityAlgorithms.HmacSha256)
|
||||
throw new SecurityTokenException("Invalid token");
|
||||
|
||||
return principal;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Govor.API.Services.Authentication.Interfaces;
|
||||
using Govor.Application.Exceptions.InvitesService;
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Models.Users;
|
||||
using Govor.Core.Repositories.Invaites;
|
||||
|
||||
@@ -67,7 +67,7 @@ public class MediaService : IMediaService
|
||||
var mediaFile = await _dbContext.MediaFiles
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == mediaId)
|
||||
?? throw new KeyNotFoundException("No media found");
|
||||
?? throw new KeyNotFoundException($"No media found by given id {mediaId}");
|
||||
|
||||
// Загрузить бинарные данные из хранилища
|
||||
Stream dataStream = await _storageService.LoadAsync(mediaFile.Url);
|
||||
@@ -83,8 +83,8 @@ public class MediaService : IMediaService
|
||||
return new Media(
|
||||
mediaFile.UploaderId,
|
||||
mediaFile.DateCreated,
|
||||
mediaFile.MediaType.ToString(),
|
||||
contentBytes,
|
||||
string.Empty,
|
||||
mediaFile.MediaType,
|
||||
mediaFile.MineType,
|
||||
string.Empty
|
||||
@@ -96,5 +96,4 @@ public class MediaService : IMediaService
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Govor.Application.Interfaces.Authentication;
|
||||
using Govor.Application.Interfaces.UserSession;
|
||||
using Govor.Application.Services.Authentication;
|
||||
using Govor.Core.Models.Users;
|
||||
using Govor.Core.Repositories.UserSessionsRepository;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Govor.Application.Services.UserSessions;
|
||||
|
||||
public class UserSessionOpener : IUserSessionOpener
|
||||
{
|
||||
private readonly IUserSessionsRepository _repository;
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly ILogger<UserSessionOpener> _logger;
|
||||
private readonly JwtRefreshOption _options;
|
||||
|
||||
public UserSessionOpener(IUserSessionsRepository repository, IJwtService jwtService, IOptions<JwtRefreshOption> options, ILogger<UserSessionOpener> logger)
|
||||
{
|
||||
_jwtService = jwtService;
|
||||
_repository = repository;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
}
|
||||
public async Task<string> OpenSessionAsync(User user, string deviceInfo)
|
||||
{
|
||||
_logger.LogInformation($"Opening session for user {user.Id} on device '{deviceInfo}'");
|
||||
|
||||
var sessions = await _repository.GetByUserIdAsync(user.Id);
|
||||
var session = sessions.FirstOrDefault(s => s.DeviceInfo == deviceInfo);
|
||||
|
||||
var newRefreshToken = await _jwtService.GenerateRefreshTokenAsync(user);
|
||||
var newExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenLifetimeDays);
|
||||
|
||||
if (session is not null)
|
||||
{
|
||||
if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow)
|
||||
{
|
||||
// Update Session
|
||||
session.RefreshToken = newRefreshToken;
|
||||
session.ExpiresAt = newExpiresAt;
|
||||
session.CreatedAt = DateTime.UtcNow;
|
||||
session.IsRevoked = false;
|
||||
|
||||
await _repository.UpdateAsync(session);
|
||||
_logger.LogInformation($"Updated expired/revoked session for user {user.Id} on device '{deviceInfo}'");
|
||||
}
|
||||
|
||||
return session.RefreshToken;
|
||||
}
|
||||
|
||||
// New Session
|
||||
var newSession = new Core.Models.UserSession
|
||||
{
|
||||
UserId = user.Id,
|
||||
DeviceInfo = deviceInfo,
|
||||
RefreshToken = newRefreshToken,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ExpiresAt = newExpiresAt,
|
||||
IsRevoked = false
|
||||
};
|
||||
|
||||
await _repository.AddAsync(newSession);
|
||||
_logger.LogInformation($"Created new session for user {user.Id} on device '{deviceInfo}'");
|
||||
|
||||
return newRefreshToken;
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Features">
|
||||
<HintPath>..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.17\Microsoft.AspNetCore.Http.Features.dll</HintPath>
|
||||
</Reference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -13,4 +13,5 @@ public class LoginRequest
|
||||
[Required]
|
||||
[MinLength(8)]
|
||||
public string Password { get; init; }
|
||||
public string DeviceInfo { get; init; }
|
||||
}
|
||||
@@ -7,9 +7,11 @@ namespace Govor.Contracts.Requests;
|
||||
public class MediaUploadRequest
|
||||
{
|
||||
[Required]
|
||||
public IFormFile Data { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public string EncryptedKey { get; set; } = string.Empty;
|
||||
public IFormFile FromFile { get; set; }
|
||||
[Required]
|
||||
public MediaType Type { get; set; }
|
||||
[Required, MaxLength(255)]
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
[Required]
|
||||
public string EncryptedKey { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -15,4 +15,5 @@ public record RegistrationRequest
|
||||
public string Password { get; init; }
|
||||
[MinLength(InvitationValidator.MIN_INVITATION_LENGTH)]
|
||||
public string InviteLink { get; init; }
|
||||
public string DeviceInfo { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Govor.Core.Models;
|
||||
|
||||
public class UserSession
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
public string RefreshToken { 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; }
|
||||
public bool IsRevoked { get; set; } = false;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
UserSession? userSession = obj as UserSession;
|
||||
|
||||
return Id == userSession.Id &&
|
||||
UserId == userSession.UserId &&
|
||||
RefreshToken == userSession.RefreshToken &&
|
||||
DeviceInfo == userSession.DeviceInfo &&
|
||||
CreatedAt == userSession.CreatedAt &&
|
||||
ExpiresAt == userSession.ExpiresAt &&
|
||||
IsRevoked == userSession.IsRevoked;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Govor.Core.Models;
|
||||
|
||||
namespace Govor.Core.Repositories.UserSessionsRepository;
|
||||
|
||||
public interface IUserSessionsExist
|
||||
{
|
||||
public bool Exist(Guid sessionId);
|
||||
public bool Exist(string refresh);
|
||||
public bool Exist(UserSession userSession);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Govor.Core.Models;
|
||||
|
||||
namespace Govor.Core.Repositories.UserSessionsRepository;
|
||||
|
||||
public interface IUserSessionsReader
|
||||
{
|
||||
public Task<List<UserSession>> GetAllAsync();
|
||||
public Task<UserSession> GetByIdAsync(Guid sessionId);
|
||||
public Task<List<UserSession>> GetByUserIdAsync(Guid userId);
|
||||
public Task<List<UserSession>> GetByCreatedAtAsync(DateTime createdAt);
|
||||
public Task<List<UserSession>> GetByExpiresAtAsync(DateTime createdAt);
|
||||
public Task<List<UserSession>> GetByRevokedAsync(bool isRevoked);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Govor.Core.Models;
|
||||
|
||||
namespace Govor.Core.Repositories.UserSessionsRepository;
|
||||
|
||||
public interface IUserSessionsRepository : IUserSessionsReader, IUserSessionsWriter, IUserSessionsExist
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Govor.Core.Models;
|
||||
|
||||
namespace Govor.Core.Repositories.UserSessionsRepository;
|
||||
|
||||
public interface IUserSessionsWriter
|
||||
{
|
||||
Task AddAsync(UserSession userSession);
|
||||
Task UpdateAsync(UserSession userSession);
|
||||
Task RemoveAsync(Guid sessionId);
|
||||
Task RemoveByUserIdAsync(Guid userId);
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using AutoFixture;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Data.Repositories;
|
||||
using Govor.Data.Repositories.Exceptions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Govor.Data.Tests.Repositories;
|
||||
|
||||
[TestFixture]
|
||||
public class UserSessionsRepositoryTests
|
||||
{
|
||||
private Fixture _fixture;
|
||||
private DbContextOptions<GovorDbContext> _options;
|
||||
private int _testIteration = 0;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_testIteration += 1;
|
||||
|
||||
_fixture = new Fixture();
|
||||
|
||||
_fixture.Behaviors
|
||||
.OfType<ThrowingRecursionBehavior>()
|
||||
.ToList()
|
||||
.ForEach(b => _fixture.Behaviors.Remove(b));
|
||||
|
||||
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
|
||||
|
||||
_options = new DbContextOptionsBuilder<GovorDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: $"DbGovor_{nameof(UserSessionsRepositoryTests)}_{_testIteration}")
|
||||
.Options;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllSessions_ReturnsAllSessions()
|
||||
{
|
||||
// Arrange
|
||||
var random = new Random();
|
||||
var sessions = _fixture.CreateMany<UserSession>(random.Next(2, 10)).ToList();
|
||||
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
context.UserSessions.AddRange(sessions);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await repository.GetAllAsync();
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(sessions.Count));
|
||||
Assert.That(result, Is.EquivalentTo(sessions));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Given_EmptySetDb_When_GetAll_Should_Throw_NotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<NotFoundException>(async () => await repository.GetAllAsync());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Given_ValidId_When_GetByIdAsync_ShouldReturnSession()
|
||||
{
|
||||
// Arrange
|
||||
var random = new Random();
|
||||
var sessions = _fixture.CreateMany<UserSession>(random.Next(2, 10)).ToList();
|
||||
|
||||
var id = sessions.First().Id;
|
||||
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
context.UserSessions.AddRange(sessions);
|
||||
await context.SaveChangesAsync();
|
||||
// Act
|
||||
var result = await repository.GetByIdAsync(id);
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Id, Is.EqualTo(id));
|
||||
Assert.That(result, Is.EqualTo(sessions.First()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Given_InvalidId_When_GetByIdAsync_Should_Throw_NotFoundByKeyException()
|
||||
{
|
||||
// Arrange
|
||||
using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<NotFoundByKeyException<Guid>>(async () => await repository.GetByIdAsync(_fixture.Create<Guid>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Given_ValidCreatedAt_When_GetByCreatedAtAsync_ShouldReturnSession()
|
||||
{
|
||||
// Arrange
|
||||
var random = new Random();
|
||||
var sessions = _fixture.CreateMany<UserSession>(random.Next(2, 10)).ToList();
|
||||
|
||||
var created = sessions.First().CreatedAt;
|
||||
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
context.UserSessions.AddRange(sessions);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var list = await repository.GetByCreatedAtAsync(created);
|
||||
|
||||
|
||||
// Assert
|
||||
var result = list.First();
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.CreatedAt, Is.EqualTo(created));
|
||||
Assert.That(result, Is.EqualTo(sessions.First()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Given_ValidCreatedAt_When_GetByCreatedAtAsync_Should_Throw_NotFoundByKeyException()
|
||||
{
|
||||
// Arrange
|
||||
using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<NotFoundByKeyException<DateTime>>(async () => await repository.GetByCreatedAtAsync(_fixture.Create<DateTime>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Given_ValidExpiresAt_When_GetByExpiresAtAsync_ShouldReturnSession()
|
||||
{
|
||||
// Arrange
|
||||
var random = new Random();
|
||||
var sessions = _fixture.CreateMany<UserSession>(random.Next(2, 10)).ToList();
|
||||
|
||||
var expiresAt = sessions.First().ExpiresAt;
|
||||
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
context.UserSessions.AddRange(sessions);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var list = await repository.GetByExpiresAtAsync(expiresAt);
|
||||
|
||||
// Assert
|
||||
var result = list.First();
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.ExpiresAt, Is.EqualTo(expiresAt));
|
||||
Assert.That(result, Is.EqualTo(sessions.First()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Given_ValidExpiresAt_When_GetByExpiresAtAsync_Should_Throw_NotFoundByKeyException()
|
||||
{
|
||||
// Arrange
|
||||
using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<NotFoundByKeyException<DateTime>>(async () => await repository.GetByExpiresAtAsync(_fixture.Create<DateTime>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Given_isRevoked_When_GetByRevokedAsync_ShouldReturnSession()
|
||||
{
|
||||
// Arrange
|
||||
var random = new Random();
|
||||
var sessions = _fixture.Build<UserSession>()
|
||||
.With(f => f.IsRevoked, true)
|
||||
.CreateMany(random.Next(2, 10)).ToList();
|
||||
|
||||
var isRevoke = sessions.First().IsRevoked;
|
||||
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
context.UserSessions.AddRange(sessions);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await repository.GetByRevokedAsync(isRevoke);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.EquivalentTo(sessions));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Given_InvalidRevoked_When_GetByExpiresAtAsync_Should_Throw_NotFoundByKeyException()
|
||||
{
|
||||
// Arrange
|
||||
using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<NotFoundByKeyException<bool>>(async () => await repository.GetByRevokedAsync(false));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Given_ValidUserSessions_When_AddAsync_Then_UserSessionsAdded()
|
||||
{
|
||||
// Arrange
|
||||
var session = _fixture.Create<UserSession>();
|
||||
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
// Act
|
||||
await repository.AddAsync(session);
|
||||
|
||||
// Assert
|
||||
Assert.That(context.UserSessions.Count, Is.EqualTo(1));
|
||||
Assert.That(context.UserSessions.First(), Is.EqualTo(session));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Given_InvalidUserSessions_When_AddAsync_Should_Throw_AdditionException()
|
||||
{
|
||||
// Arrange
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<AdditionException>(async () => await repository.AddAsync(default));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Given_ExistUserSessions_When_Exist_Then_ReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var session = _fixture.Create<UserSession>();
|
||||
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
context.UserSessions.Add(session);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result1 = repository.Exist(session);
|
||||
var result2 = repository.Exist(session.Id);
|
||||
var result3 = repository.Exist(session.RefreshToken);
|
||||
|
||||
// Assert
|
||||
Assert.That(result1, Is.True);
|
||||
Assert.That(result2, Is.True);
|
||||
Assert.That(result3, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Given_NotExistUserSessions_When_Exist_Then_ReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var session = _fixture.Create<UserSession>();
|
||||
|
||||
await using var context = new GovorDbContext(_options);
|
||||
var repository = new UserSessionsRepository(context);
|
||||
|
||||
// Act
|
||||
var result1 = repository.Exist(session);
|
||||
var result2 = repository.Exist(session.Id);
|
||||
var result3 = repository.Exist(session.RefreshToken);
|
||||
// Assert
|
||||
Assert.That(result1, Is.False);
|
||||
Assert.That(result2, Is.False);
|
||||
Assert.That(result3, Is.False);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Models.Users;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Govor.Data.Configurations;
|
||||
|
||||
public class UserSessionConfiguration : IEntityTypeConfiguration<UserSession>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserSession> builder)
|
||||
{
|
||||
builder.HasKey(us => us.Id);
|
||||
|
||||
builder.Property(us => us.RefreshToken)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(us => us.DeviceInfo)
|
||||
.HasMaxLength(200);
|
||||
|
||||
builder.HasOne<User>()
|
||||
.WithMany()
|
||||
.HasForeignKey(us => us.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Models.Messages;
|
||||
using Govor.Core.Models.Users;
|
||||
@@ -10,6 +9,7 @@ namespace Govor.Data;
|
||||
public class GovorDbContext(DbContextOptions<GovorDbContext> options) : DbContext(options)
|
||||
{
|
||||
public virtual DbSet<User> Users { get; set; }
|
||||
public virtual DbSet<UserSession> UserSessions { get; set; }
|
||||
public virtual DbSet<Friendship> Friendships { get; set; }
|
||||
public virtual DbSet<PrivateChat> PrivateChats { get; set; }
|
||||
public virtual DbSet<Admin> Admins { get; set; }
|
||||
@@ -29,6 +29,7 @@ public class GovorDbContext(DbContextOptions<GovorDbContext> options) : DbContex
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new UserSessionConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new FriendshipConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new UserConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new InvitationConfiguration());
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Govor.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Govor.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(GovorDbContext))]
|
||||
[Migration("20250718130008_usersessions")]
|
||||
partial class usersessions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.ChatGroup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<Guid>("ImageId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<bool>("IsChannel")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsPrivate")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ChatGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Friendship", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("AddresseeId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("RequesterId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AddresseeId");
|
||||
|
||||
b.HasIndex("RequesterId");
|
||||
|
||||
b.ToTable("Friendships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("GroupId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.ToTable("GroupAdmins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("GroupId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("InvitationCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<int>("MaxParticipants")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("UserMakerId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("UserMakerId");
|
||||
|
||||
b.ToTable("GroupInvitations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.GroupMembership", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("GroupId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("InvitationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<bool>("IsBanned")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("InvitationId");
|
||||
|
||||
b.ToTable("GroupMemberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Invitation", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<int>("MaxParticipants")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Invitations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.MediaFile", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("MediaType")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("MineType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<Guid>("UploaderId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MediaFiles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("MediaFileId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaFileId");
|
||||
|
||||
b.HasIndex("MessageId");
|
||||
|
||||
b.ToTable("MediaAttachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Messages.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("ChatGroupId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("EditedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("EncryptedContent")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<Guid?>("PrivateChatId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("RecipientId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int>("RecipientType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("ReplyToMessageId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("SentAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChatGroupId");
|
||||
|
||||
b.HasIndex("PrivateChatId");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("ReactedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("ReactionCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("MessageId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MessageReactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("ViewedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MessageId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MessageViews");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.PrivateChat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("UserAId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("UserBId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PrivateChats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.UserSession", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("DeviceInfo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsRevoked")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserSessions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Users.Admin", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("UserId");
|
||||
|
||||
b.ToTable("Admins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Users.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateOnly>("CreatedOn")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<Guid>("IconId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("InviteId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("WasOnline")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InviteId");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
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)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
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.UserSession", b =>
|
||||
{
|
||||
b.HasOne("Govor.Core.Models.Users.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.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.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.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.User", b =>
|
||||
{
|
||||
b.Navigation("ReceivedFriendRequests");
|
||||
|
||||
b.Navigation("SentFriendRequests");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Govor.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class usersessions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserSessions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
RefreshToken = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DeviceInfo = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
ExpiresAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
IsRevoked = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserSessions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserSessions_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserSessions_UserId",
|
||||
table: "UserSessions",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserSessions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,6 +364,40 @@ namespace Govor.Data.Migrations
|
||||
b.ToTable("PrivateChats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.UserSession", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("DeviceInfo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsRevoked")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserSessions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Users.Admin", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
@@ -529,6 +563,15 @@ namespace Govor.Data.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.UserSession", b =>
|
||||
{
|
||||
b.HasOne("Govor.Core.Models.Users.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Govor.Core.Models.Users.Admin", b =>
|
||||
{
|
||||
b.HasOne("Govor.Core.Models.Users.User", "User")
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
using Govor.Core.Infrastructure.Extensions;
|
||||
using Govor.Core.Models;
|
||||
using Govor.Core.Repositories.UserSessionsRepository;
|
||||
using Govor.Data.Repositories.Exceptions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Govor.Data.Repositories;
|
||||
|
||||
public class UserSessionsRepository : IUserSessionsRepository
|
||||
{
|
||||
private GovorDbContext _context;
|
||||
|
||||
public UserSessionsRepository(GovorDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<UserSession>> GetAllAsync()
|
||||
{
|
||||
return await _context.UserSessions
|
||||
.AsNoTracking()
|
||||
.ToListOrThrowIfEmpty(new NotFoundException("Database is empty"));
|
||||
}
|
||||
|
||||
public async Task<UserSession> GetByIdAsync(Guid sessionId)
|
||||
{
|
||||
return await _context.UserSessions
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(session => session.Id == sessionId)
|
||||
?? throw new NotFoundByKeyException<Guid>(sessionId, "Session with given id does not exist");
|
||||
}
|
||||
|
||||
public async Task<List<UserSession>> GetByUserIdAsync(Guid userId)
|
||||
{
|
||||
return await _context.UserSessions
|
||||
.AsNoTracking()
|
||||
.Where(session => session.UserId == userId)
|
||||
.ToListOrThrowIfEmpty(new NotFoundByKeyException<Guid>(userId, "Sessions with given user id does not exist"));
|
||||
}
|
||||
|
||||
public async Task<List<UserSession>> GetByCreatedAtAsync(DateTime createdAt)
|
||||
{
|
||||
return await _context.UserSessions
|
||||
.AsNoTracking()
|
||||
.Where(session => session.CreatedAt == createdAt)
|
||||
.ToListOrThrowIfEmpty(new NotFoundByKeyException<DateTime>(createdAt, "Sessions with given created at does not exist"));
|
||||
}
|
||||
|
||||
public async Task<List<UserSession>> GetByExpiresAtAsync(DateTime expiresAt)
|
||||
{
|
||||
return await _context.UserSessions
|
||||
.AsNoTracking()
|
||||
.Where(session => session.ExpiresAt == expiresAt)
|
||||
.ToListOrThrowIfEmpty(new NotFoundByKeyException<DateTime>(expiresAt, "Sessions with given expires at does not exist"));
|
||||
}
|
||||
|
||||
public async Task<List<UserSession>> GetByRevokedAsync(bool isRevoked)
|
||||
{
|
||||
return await _context.UserSessions
|
||||
.AsNoTracking()
|
||||
.Where(session => session.IsRevoked == isRevoked)
|
||||
.ToListOrThrowIfEmpty(new NotFoundByKeyException<bool>(isRevoked, "Sessions is revoked does not exist"));
|
||||
}
|
||||
|
||||
public async Task AddAsync(UserSession userSession)
|
||||
{
|
||||
try
|
||||
{
|
||||
_context.UserSessions.Add(userSession);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new AdditionException("Failed to add user session", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(UserSession userSession)
|
||||
{
|
||||
try
|
||||
{
|
||||
//_validator.Validate(user);
|
||||
|
||||
var rowsAffected = await _context.UserSessions
|
||||
.Where(u => u.Id == userSession.Id)
|
||||
.ExecuteUpdateAsync(u => u
|
||||
.SetProperty(a => a.UserId, userSession.UserId)
|
||||
.SetProperty(u => u.RefreshToken, userSession.RefreshToken)
|
||||
.SetProperty(u => u.DeviceInfo, userSession.DeviceInfo)
|
||||
.SetProperty(u => u.CreatedAt, userSession.CreatedAt)
|
||||
.SetProperty(u => u.ExpiresAt, userSession.ExpiresAt)
|
||||
.SetProperty(u => u.IsRevoked, userSession.IsRevoked)
|
||||
);
|
||||
|
||||
if (rowsAffected == 0)
|
||||
throw new UpdateException($"Not found user session by given id {userSession.Id}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new UpdateException($"Error when updating the user session {userSession.Id}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(Guid sessionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await GetByIdAsync(sessionId);
|
||||
|
||||
_context.UserSessions.Remove(result);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (NotFoundByKeyException<Guid> ex)
|
||||
{
|
||||
throw new RemoveException($"Not found session by given id {sessionId}", ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RemoveException("Error when removing the session", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveByUserIdAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await GetByUserIdAsync(userId);
|
||||
|
||||
_context.UserSessions.RemoveRange(result);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (NotFoundByKeyException<Guid> ex)
|
||||
{
|
||||
throw new RemoveException($"Not found user sessions by given user id {userId}", ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RemoveException("Error when removing the user sessions", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Exist(Guid sessionId)
|
||||
{
|
||||
return _context.UserSessions.Any(session => session.Id == sessionId);
|
||||
}
|
||||
|
||||
public bool Exist(string refresh)
|
||||
{
|
||||
return _context.UserSessions.Any(session => session.RefreshToken == refresh);
|
||||
}
|
||||
|
||||
public bool Exist(UserSession userSession)
|
||||
{
|
||||
return _context.UserSessions.Any(session => session.Id == userSession.Id &&
|
||||
session.RefreshToken == userSession.RefreshToken &&
|
||||
session.IsRevoked == userSession.IsRevoked &&
|
||||
session.CreatedAt == userSession.CreatedAt &&
|
||||
session.ExpiresAt == userSession.ExpiresAt &&
|
||||
session.DeviceInfo == userSession.DeviceInfo &&
|
||||
session.UserId == userSession.UserId);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify IDE code to run analysis without container (Applied in CI/CD pipeline)
|
||||
ide: QDNET
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
Reference in New Issue
Block a user