Files
live_recorder/src/LiveRecorder.Application/Services/AuthService.cs
T

143 lines
4.8 KiB
C#

using System.Security.Cryptography;
using LiveRecorder.Application.Abstractions.Auth;
using LiveRecorder.Application.Abstractions.Persistence;
using LiveRecorder.Application.Common;
using LiveRecorder.Application.Models.Auth;
using LiveRecorder.Domain.Entities;
namespace LiveRecorder.Application.Services;
public sealed class AuthService : IAuthService
{
private readonly IUserAccountRepository _userAccountRepository;
private readonly IUserSessionRepository _userSessionRepository;
private readonly IUnitOfWork _unitOfWork;
public AuthService(
IUserAccountRepository userAccountRepository,
IUserSessionRepository userSessionRepository,
IUnitOfWork unitOfWork)
{
_userAccountRepository = userAccountRepository;
_userSessionRepository = userSessionRepository;
_unitOfWork = unitOfWork;
}
public async Task<LoginResponse> LoginAsync(LoginRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var username = request.Username.Trim();
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password))
{
throw new InvalidOperationException("用户名和密码不能为空。");
}
var user = await _userAccountRepository.GetByUsernameAsync(username, cancellationToken)
?? throw new InvalidOperationException("用户名或密码错误。");
if (!user.IsActive || !PasswordHasher.Verify(request.Password, user.PasswordHash))
{
throw new InvalidOperationException("用户名或密码错误。");
}
var now = DateTimeOffset.UtcNow;
var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
var expiresAt = now.AddHours(12);
var session = new UserSession(user.Id, token, expiresAt, now);
await _userSessionRepository.AddAsync(session, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
var authenticatedUser = new AuthenticatedUser
{
UserId = user.Id,
Username = user.Username,
DisplayName = user.DisplayName,
Token = token,
ExpiresAt = expiresAt
};
return new LoginResponse
{
Token = token,
ExpiresAt = expiresAt,
User = authenticatedUser
};
}
public async Task LogoutAsync(string token, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(token))
{
return;
}
var session = await _userSessionRepository.GetByTokenAsync(token, cancellationToken);
if (session is null)
{
return;
}
session.Revoke(DateTimeOffset.UtcNow);
_userSessionRepository.Update(session);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
public async Task<AuthenticatedUser?> ValidateTokenAsync(string token, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(token))
{
return null;
}
var session = await _userSessionRepository.GetByTokenAsync(token, cancellationToken);
if (session is null || !session.IsValid(DateTimeOffset.UtcNow))
{
return null;
}
var user = await _userAccountRepository.GetByIdAsync(session.UserAccountId, cancellationToken);
if (user is null || !user.IsActive)
{
return null;
}
return new AuthenticatedUser
{
UserId = user.Id,
Username = user.Username,
DisplayName = user.DisplayName,
Token = session.Token,
ExpiresAt = session.ExpiresAt
};
}
public async Task ChangePasswordAsync(Guid userId, ChangePasswordRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
if (string.IsNullOrWhiteSpace(request.CurrentPassword) || string.IsNullOrWhiteSpace(request.NewPassword))
{
throw new InvalidOperationException("当前密码和新密码不能为空。");
}
if (request.NewPassword.Length < 6)
{
throw new InvalidOperationException("新密码长度不能少于 6 位。");
}
var user = await _userAccountRepository.GetByIdAsync(userId, cancellationToken)
?? throw new InvalidOperationException("用户不存在。");
if (!PasswordHasher.Verify(request.CurrentPassword, user.PasswordHash))
{
throw new InvalidOperationException("当前密码不正确。");
}
user.UpdatePassword(PasswordHasher.Hash(request.NewPassword));
_userAccountRepository.Update(user);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}