feat(infra): implement RefreshTokenRepository with Dapper and add GetByIdAsync to UsuarioRepository
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
using Dapper;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Infrastructure.Persistence;
|
||||
|
||||
public sealed class RefreshTokenRepository : IRefreshTokenRepository
|
||||
{
|
||||
private readonly SqlConnectionFactory _connectionFactory;
|
||||
|
||||
public RefreshTokenRepository(SqlConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<RefreshToken?> GetByHashAsync(string tokenHash, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT Id, UsuarioId, TokenHash, FamilyId,
|
||||
IssuedAt, ExpiresAt, RevokedAt, ReplacedById,
|
||||
CreatedByIp, UserAgent
|
||||
FROM dbo.RefreshToken
|
||||
WHERE TokenHash = @TokenHash
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var row = await connection.QuerySingleOrDefaultAsync<RefreshTokenRow>(sql, new { TokenHash = tokenHash });
|
||||
return row is null ? null : MapRow(row);
|
||||
}
|
||||
|
||||
public async Task<int> AddAsync(RefreshToken token, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO dbo.RefreshToken
|
||||
(UsuarioId, TokenHash, FamilyId, IssuedAt, ExpiresAt, CreatedByIp, UserAgent)
|
||||
VALUES
|
||||
(@UsuarioId, @TokenHash, @FamilyId, @IssuedAt, @ExpiresAt, @CreatedByIp, @UserAgent);
|
||||
SELECT CAST(SCOPE_IDENTITY() AS INT);
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.QuerySingleAsync<int>(sql, new
|
||||
{
|
||||
token.UsuarioId,
|
||||
token.TokenHash,
|
||||
token.FamilyId,
|
||||
token.IssuedAt,
|
||||
token.ExpiresAt,
|
||||
token.CreatedByIp,
|
||||
token.UserAgent,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task RevokeAsync(int id, int? replacedById, DateTime revokedAt, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE dbo.RefreshToken
|
||||
SET RevokedAt = @RevokedAt, ReplacedById = @ReplacedById
|
||||
WHERE Id = @Id AND RevokedAt IS NULL
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
await connection.ExecuteAsync(sql, new { Id = id, ReplacedById = replacedById, RevokedAt = revokedAt });
|
||||
}
|
||||
|
||||
public async Task<int> RevokeFamilyAsync(Guid familyId, DateTime revokedAt, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE dbo.RefreshToken
|
||||
SET RevokedAt = @RevokedAt
|
||||
WHERE FamilyId = @FamilyId AND RevokedAt IS NULL;
|
||||
SELECT @@ROWCOUNT;
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.QuerySingleAsync<int>(sql, new { FamilyId = familyId, RevokedAt = revokedAt });
|
||||
}
|
||||
|
||||
public async Task<int> RevokeAllActiveForUserAsync(int usuarioId, DateTime revokedAt, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE dbo.RefreshToken
|
||||
SET RevokedAt = @RevokedAt
|
||||
WHERE UsuarioId = @UsuarioId AND RevokedAt IS NULL;
|
||||
SELECT @@ROWCOUNT;
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.QuerySingleAsync<int>(sql, new { UsuarioId = usuarioId, RevokedAt = revokedAt });
|
||||
}
|
||||
|
||||
private static RefreshToken MapRow(RefreshTokenRow row) => RefreshTokenRow.Reconstruct(row);
|
||||
|
||||
// Flat Dapper DTO
|
||||
private sealed record RefreshTokenRow(
|
||||
int Id,
|
||||
int UsuarioId,
|
||||
string TokenHash,
|
||||
Guid FamilyId,
|
||||
DateTime IssuedAt,
|
||||
DateTime ExpiresAt,
|
||||
DateTime? RevokedAt,
|
||||
int? ReplacedById,
|
||||
string CreatedByIp,
|
||||
string? UserAgent)
|
||||
{
|
||||
public static RefreshToken Reconstruct(RefreshTokenRow r)
|
||||
{
|
||||
// Build an empty token using the rotation factory from a dummy parent,
|
||||
// then we manually set fields via the available setters.
|
||||
// Since RefreshToken uses init-only properties, we use object initializer.
|
||||
var token = new RefreshToken
|
||||
{
|
||||
Id = r.Id,
|
||||
UsuarioId = r.UsuarioId,
|
||||
TokenHash = r.TokenHash,
|
||||
FamilyId = r.FamilyId,
|
||||
IssuedAt = r.IssuedAt,
|
||||
ExpiresAt = r.ExpiresAt,
|
||||
CreatedByIp = r.CreatedByIp,
|
||||
UserAgent = r.UserAgent,
|
||||
};
|
||||
|
||||
if (r.RevokedAt.HasValue)
|
||||
token.MarkAsPersistedRevocation(r.RevokedAt.Value, r.ReplacedById);
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,32 @@ public sealed class UsuarioRepository : IUsuarioRepository
|
||||
|
||||
if (row is null) return null;
|
||||
|
||||
return new Usuario(
|
||||
return MapRow(row);
|
||||
}
|
||||
|
||||
public async Task<Usuario?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT
|
||||
Id, Username, PasswordHash,
|
||||
Nombre, Apellido, Email,
|
||||
Rol, PermisosJson, Activo
|
||||
FROM dbo.Usuario
|
||||
WHERE Id = @Id
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
|
||||
var row = await connection.QuerySingleOrDefaultAsync<UsuarioRow>(sql, new { Id = id });
|
||||
|
||||
if (row is null) return null;
|
||||
|
||||
return MapRow(row);
|
||||
}
|
||||
|
||||
private static Usuario MapRow(UsuarioRow row)
|
||||
=> new(
|
||||
id: row.Id,
|
||||
username: row.Username,
|
||||
passwordHash: row.PasswordHash,
|
||||
@@ -43,7 +68,6 @@ public sealed class UsuarioRepository : IUsuarioRepository
|
||||
permisosJson: row.PermisosJson,
|
||||
activo: row.Activo
|
||||
);
|
||||
}
|
||||
|
||||
// Flat DTO for Dapper mapping (avoids polluting domain entity with Dapper attributes)
|
||||
private sealed record UsuarioRow(
|
||||
|
||||
Reference in New Issue
Block a user