141 lines
4.8 KiB
C#
141 lines
4.8 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|