Add Login Module

This commit is contained in:
Thanakarn Klangkasame
2025-10-02 11:18:44 +07:00
parent f505e31cfd
commit 563a341a99
52 changed files with 1127 additions and 2036 deletions

View File

@@ -0,0 +1,102 @@
using System.Data;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using AMREZ.EOP.Abstractions.Applications.Tenancy;
using AMREZ.EOP.Abstractions.Applications.UseCases.Authentications;
using AMREZ.EOP.Abstractions.Infrastructures.Common;
using AMREZ.EOP.Abstractions.Infrastructures.Repositories;
using AMREZ.EOP.Abstractions.Security;
using AMREZ.EOP.Contracts.DTOs.Authentications.Refresh;
using Microsoft.AspNetCore.Http;
namespace AMREZ.EOP.Application.UseCases.Authentications;
public sealed class RefreshUseCase : IRefreshUseCase
{
private readonly ITenantResolver _resolver;
private readonly ITenantRepository _tenants;
private readonly IUserRepository _users;
private readonly IJwtFactory _jwt;
private readonly IHttpContextAccessor _http;
private readonly IUnitOfWork _uow;
private const int RefreshDays = 14;
public RefreshUseCase(
ITenantResolver resolver,
ITenantRepository tenants,
IUserRepository users,
IJwtFactory jwt,
IHttpContextAccessor http,
IUnitOfWork uow)
{ _resolver = resolver; _tenants = tenants; _users = users; _jwt = jwt; _http = http; _uow = uow; }
public async Task<RefreshResponse?> ExecuteAsync(RefreshRequest request, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(request.RefreshToken)) return null;
var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext");
var tenantCtx = _resolver.Resolve(http, request);
if (tenantCtx is null) return null;
await _uow.BeginAsync(tenantCtx, IsolationLevel.ReadCommitted, ct);
try
{
var tn = await _tenants.GetAsync(tenantCtx.TenantKey, ct);
var tenantId = tn.TenantId;
var hash = Sha256(request.RefreshToken);
var session = await _users.FindSessionByRefreshHashAsync(tenantId, hash, ct);
if (session is null || session.RevokedAt.HasValue ||
(session.ExpiresAt.HasValue && session.ExpiresAt.Value <= DateTimeOffset.UtcNow))
{ await _uow.RollbackAsync(ct); return null; }
if (!await _users.IsSessionActiveAsync(session.UserId, session.Id, ct))
{ await _uow.RollbackAsync(ct); return null; }
var tv = await _users.GetTenantTokenVersionAsync(tenantId, ct);
var sstamp = await _users.GetUserSecurityStampAsync(session.UserId, ct) ?? string.Empty;
var claims = new List<Claim>
{
new("sub", session.UserId.ToString()),
new("tenant_id", tenantId.ToString()),
new("sid", session.Id.ToString()),
new("jti", Guid.NewGuid().ToString("N")),
new("tv", tv),
new("sstamp", sstamp)
};
var (access, accessExp) = _jwt.CreateAccessToken(claims);
var newRaw = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
var newHash = Sha256(newRaw);
var now = DateTimeOffset.UtcNow;
var newExp = now.AddDays(RefreshDays);
var ok = await _users.RotateSessionRefreshAsync(tenantId, session.Id, newHash, now, newExp, ct);
if (!ok) { await _uow.RollbackAsync(ct); return null; }
await _uow.CommitAsync(ct);
return new RefreshResponse
{
AccessToken = access,
AccessExpiresAt = accessExp,
RefreshToken = newRaw,
RefreshExpiresAt = newExp
};
}
catch
{
await _uow.RollbackAsync(ct);
throw;
}
}
private static string Sha256(string raw)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
return Convert.ToHexString(bytes);
}
}