Merge branch 'main' into feature-worker

# Conflicts:
#	dotnet/AipsWebApi/Controllers/UserController.cs
This commit is contained in:
2026-02-15 16:23:04 +01:00
34 changed files with 940 additions and 30 deletions

View File

@@ -0,0 +1,12 @@
using AipsCore.Application.Common.Authentication.Models;
using AipsCore.Domain.Models.User.ValueObjects;
namespace AipsCore.Application.Abstract.UserContext;
public interface IRefreshTokenManager
{
Task AddAsync(string token, UserId userId, CancellationToken cancellationToken = default);
Task<RefreshToken> GetByValueAsync(string token, CancellationToken cancellationToken = default);
Task RevokeAsync(string token, CancellationToken cancellationToken = default);
Task RevokeAllAsync(UserId userId, CancellationToken cancellationToken = default);
}

View File

@@ -5,5 +5,6 @@ namespace AipsCore.Application.Abstract.UserContext;
public interface ITokenProvider
{
string Generate(User user, IList<UserRole> roles);
string GenerateAccessToken(User user, IList<UserRole> roles);
string GenerateRefreshToken();
}

View File

@@ -0,0 +1,3 @@
namespace AipsCore.Application.Common.Authentication.Dtos;
public record LogInUserResultDto(string AccessToken, string RefreshToken);

View File

@@ -1,3 +1,4 @@
using AipsCore.Application.Common.Authentication.Models;
using AipsCore.Domain.Models.User;
namespace AipsCore.Application.Common.Authentication;
@@ -6,4 +7,5 @@ public interface IAuthService
{
Task SignUpWithPasswordAsync(User user, string password, CancellationToken cancellationToken = default);
Task<LoginResult> LoginWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken = default);
Task<LoginResult> LoginWithRefreshTokenAsync(RefreshToken refreshToken, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,3 @@
namespace AipsCore.Application.Common.Authentication.Models;
public record AccessToken(string Value);

View File

@@ -0,0 +1,5 @@
using AipsCore.Domain.Models.User.ValueObjects;
namespace AipsCore.Application.Common.Authentication.Models;
public record RefreshToken(string Value, string UserId, DateTime ExpiresAt);

View File

@@ -1,3 +0,0 @@
namespace AipsCore.Application.Common.Authentication;
public record Token(string Value);

View File

@@ -1,6 +1,7 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Common.Authentication;
using AipsCore.Application.Common.Authentication.Dtos;
namespace AipsCore.Application.Models.User.Command.LogIn;
public record LogInUserCommand(string Email, string Password) : ICommand<Token>;
public record LogInUserCommand(string Email, string Password) : ICommand<LogInUserResultDto>;

View File

@@ -1,28 +1,40 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Application.Common.Authentication;
using AipsCore.Application.Common.Authentication.Dtos;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Models.User.External;
namespace AipsCore.Application.Models.User.Command.LogIn;
public class LogInUserCommandHandler : ICommandHandler<LogInUserCommand, Token>
public class LogInUserCommandHandler : ICommandHandler<LogInUserCommand, LogInUserResultDto>
{
private readonly IUserRepository _userRepository;
private readonly ITokenProvider _tokenProvider;
private readonly IRefreshTokenManager _refreshTokenManager;
private readonly IAuthService _authService;
private readonly IUnitOfWork _unitOfWork;
public LogInUserCommandHandler(IUserRepository userRepository, ITokenProvider tokenProvider, IAuthService authService)
public LogInUserCommandHandler(
ITokenProvider tokenProvider,
IRefreshTokenManager refreshTokenManager,
IAuthService authService,
IUnitOfWork unitOfWork)
{
_userRepository = userRepository;
_tokenProvider = tokenProvider;
_refreshTokenManager = refreshTokenManager;
_authService = authService;
_unitOfWork = unitOfWork;
}
public async Task<Token> Handle(LogInUserCommand command, CancellationToken cancellationToken = default)
public async Task<LogInUserResultDto> Handle(LogInUserCommand command, CancellationToken cancellationToken = default)
{
var loginResult = await _authService.LoginWithEmailAndPasswordAsync(command.Email, command.Password, cancellationToken);
return new Token(_tokenProvider.Generate(loginResult.User, loginResult.Roles));
var accessToken = _tokenProvider.GenerateAccessToken(loginResult.User, loginResult.Roles);
var refreshToken = _tokenProvider.GenerateRefreshToken();
await _refreshTokenManager.AddAsync(refreshToken, loginResult.User.Id, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return new LogInUserResultDto(accessToken, refreshToken);
}
}

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.User.Command.LogOut;
public record LogOutCommand(string RefreshToken) : ICommand;

View File

@@ -0,0 +1,19 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
namespace AipsCore.Application.Models.User.Command.LogOut;
public class LogOutCommandHandler : ICommandHandler<LogOutCommand>
{
private readonly IRefreshTokenManager _refreshTokenManager;
public LogOutCommandHandler(IRefreshTokenManager refreshTokenManager)
{
_refreshTokenManager = refreshTokenManager;
}
public async Task Handle(LogOutCommand command, CancellationToken cancellationToken = default)
{
await _refreshTokenManager.RevokeAsync(command.RefreshToken, cancellationToken);
}
}

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.User.Command.LogOutAll;
public record LogOutAllCommand : ICommand;

View File

@@ -0,0 +1,23 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
namespace AipsCore.Application.Models.User.Command.LogOutAll;
public class LogOutAllCommandHandler : ICommandHandler<LogOutAllCommand>
{
private readonly IRefreshTokenManager _refreshTokenManager;
private readonly IUserContext _userContext;
public LogOutAllCommandHandler(IRefreshTokenManager refreshTokenManager, IUserContext userContext)
{
_refreshTokenManager = refreshTokenManager;
_userContext = userContext;
}
public Task Handle(LogOutAllCommand command, CancellationToken cancellationToken = default)
{
var userId = _userContext.GetCurrentUserId();
return _refreshTokenManager.RevokeAllAsync(userId, cancellationToken);
}
}

View File

@@ -0,0 +1,6 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Common.Authentication.Dtos;
namespace AipsCore.Application.Models.User.Command.RefreshLogIn;
public record RefreshLogInCommand(string RefreshToken) : ICommand<LogInUserResultDto>;

View File

@@ -0,0 +1,44 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Application.Common.Authentication;
using AipsCore.Application.Common.Authentication.Dtos;
using AipsCore.Domain.Abstract;
namespace AipsCore.Application.Models.User.Command.RefreshLogIn;
public class RefreshLogInCommandHandler : ICommandHandler<RefreshLogInCommand, LogInUserResultDto>
{
private readonly ITokenProvider _tokenProvider;
private readonly IRefreshTokenManager _refreshTokenManager;
private readonly IAuthService _authService;
private readonly IUnitOfWork _unitOfWork;
public RefreshLogInCommandHandler(
ITokenProvider tokenProvider,
IRefreshTokenManager refreshTokenManager,
IAuthService authService,
IUnitOfWork unitOfWork)
{
_tokenProvider = tokenProvider;
_refreshTokenManager = refreshTokenManager;
_authService = authService;
_unitOfWork = unitOfWork;
}
public async Task<LogInUserResultDto> Handle(RefreshLogInCommand command, CancellationToken cancellationToken = default)
{
var refreshToken = await _refreshTokenManager.GetByValueAsync(command.RefreshToken, cancellationToken);
var loginResult = await _authService.LoginWithRefreshTokenAsync(refreshToken, cancellationToken);
var newAccessToken = _tokenProvider.GenerateAccessToken(loginResult.User, loginResult.Roles);
var newRefreshToken = _tokenProvider.GenerateRefreshToken();
await _refreshTokenManager.RevokeAsync(refreshToken.Value, cancellationToken);
await _refreshTokenManager.AddAsync(newRefreshToken, loginResult.User.Id, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return new LogInUserResultDto(newAccessToken, newRefreshToken);
}
}

View File

@@ -1,19 +1,15 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Common.Authentication;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Models.User.External;
using AipsCore.Domain.Models.User.ValueObjects;
namespace AipsCore.Application.Models.User.Command.SignUp;
public class SignUpUserCommandHandler : ICommandHandler<SignUpUserCommand, UserId>
{
private readonly IUserRepository _userRepository;
private readonly IAuthService _authService;
public SignUpUserCommandHandler(IUserRepository userRepository, IAuthService authService)
public SignUpUserCommandHandler(IAuthService authService)
{
_userRepository = userRepository;
_authService = authService;
}