Files
SIG-CM2.0/src/api/SIGCM2.Infrastructure/Persistence/UsuarioRepository.cs

297 lines
10 KiB
C#
Raw Normal View History

using System.Text;
using Dapper;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Common;
using SIGCM2.Domain.Entities;
namespace SIGCM2.Infrastructure.Persistence;
public sealed class UsuarioRepository : IUsuarioRepository
{
private readonly SqlConnectionFactory _connectionFactory;
public UsuarioRepository(SqlConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<Usuario?> GetByUsernameAsync(string username)
{
const string sql = """
SELECT
Id, Username, PasswordHash,
Nombre, Apellido, Email,
Rol, PermisosJson, Activo,
FechaModificacion, UltimoLogin, MustChangePassword
FROM dbo.Usuario
WHERE Username = @Username
AND Activo = 1
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
var row = await connection.QuerySingleOrDefaultAsync<UsuarioRow>(sql, new { Username = username });
if (row is null) return null;
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,
FechaModificacion, UltimoLogin, MustChangePassword
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);
}
public async Task<bool> ExistsByUsernameAsync(string username, CancellationToken ct = default)
{
const string sql = """
SELECT COUNT(1) FROM dbo.Usuario WHERE Username = @Username
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var count = await connection.ExecuteScalarAsync<int>(sql, new { Username = username });
return count > 0;
}
public async Task<int> AddAsync(Usuario usuario, CancellationToken ct = default)
{
// DF handles: Activo (1), PermisosJson ('[]'), FechaCreacion (GETDATE())
const string sql = """
INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Email, Rol)
OUTPUT INSERTED.Id
VALUES (@Username, @PasswordHash, @Nombre, @Apellido, @Email, @Rol)
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var id = await connection.ExecuteScalarAsync<int>(sql, new
{
usuario.Username,
usuario.PasswordHash,
usuario.Nombre,
usuario.Apellido,
usuario.Email,
usuario.Rol
});
return id;
}
// UDT-008 ─────────────────────────────────────────────────────────────────
public async Task UpdateUltimoLoginAsync(int id, DateTime utcNow, CancellationToken ct = default)
{
const string sql = """
UPDATE dbo.Usuario SET UltimoLogin = @Utc WHERE Id = @Id
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
await connection.ExecuteAsync(sql, new { Utc = utcNow, Id = id });
}
public async Task<PagedResult<UsuarioListItem>> GetPagedAsync(UsuariosQuery query, CancellationToken ct = default)
{
// Clamp paging params
var page = Math.Max(1, query.Page);
var pageSize = Math.Clamp(query.PageSize, 1, 100);
var offset = (page - 1) * pageSize;
var where = new StringBuilder("WHERE 1=1");
var parameters = new DynamicParameters();
parameters.Add("PageSize", pageSize);
parameters.Add("Offset", offset);
if (!string.IsNullOrWhiteSpace(query.Rol))
{
where.Append(" AND Rol = @Rol");
parameters.Add("Rol", query.Rol);
}
if (query.Activo.HasValue)
{
where.Append(" AND Activo = @Activo");
parameters.Add("Activo", query.Activo.Value ? 1 : 0);
}
if (!string.IsNullOrWhiteSpace(query.Search))
{
where.Append(" AND (Username LIKE @Search OR Nombre LIKE @Search OR Apellido LIKE @Search OR Email LIKE @Search)");
parameters.Add("Search", $"%{query.Search}%");
}
var sql = $"""
SELECT
Id, Username, Nombre, Apellido, Email, Rol, Activo, UltimoLogin, FechaModificacion,
COUNT(*) OVER() AS TotalCount
FROM dbo.Usuario
{where}
ORDER BY Username
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var rows = await connection.QueryAsync<UsuarioPagedRow>(sql, parameters);
var list = rows.ToList();
var total = list.Count > 0 ? list[0].TotalCount : 0;
var items = list.Select(r => new UsuarioListItem(
r.Id, r.Username, r.Nombre, r.Apellido, r.Email, r.Rol, r.Activo, r.UltimoLogin, r.FechaModificacion
)).ToList();
return new PagedResult<UsuarioListItem>(items, page, pageSize, total);
}
public async Task<Usuario?> GetDetailAsync(int id, CancellationToken ct = default)
=> await GetByIdAsync(id, ct);
public async Task UpdateAsync(int id, UpdateUsuarioFields fields, DateTime fechaModificacion, CancellationToken ct = default)
{
const string sql = """
UPDATE dbo.Usuario
SET Nombre = @Nombre,
Apellido = @Apellido,
Email = @Email,
Rol = @Rol,
Activo = @Activo,
FechaModificacion = @FechaModificacion
WHERE Id = @Id
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
await connection.ExecuteAsync(sql, new
{
fields.Nombre,
fields.Apellido,
fields.Email,
fields.Rol,
fields.Activo,
FechaModificacion = fechaModificacion,
Id = id
});
}
public async Task UpdatePasswordAsync(int id, string passwordHash, bool mustChangePassword, CancellationToken ct = default)
{
const string sql = """
UPDATE dbo.Usuario
SET PasswordHash = @PasswordHash,
MustChangePassword = @MustChangePassword,
FechaModificacion = SYSUTCDATETIME()
WHERE Id = @Id
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
await connection.ExecuteAsync(sql, new
{
PasswordHash = passwordHash,
MustChangePassword = mustChangePassword ? 1 : 0,
Id = id
});
}
public async Task<int> CountActiveAdminsAsync(CancellationToken ct = default)
{
const string sql = """
SELECT COUNT(1) FROM dbo.Usuario WITH (NOLOCK) WHERE Activo = 1 AND Rol = 'admin'
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
return await connection.ExecuteScalarAsync<int>(sql);
}
// UDT-009 ─────────────────────────────────────────────────────────────────
public async Task UpdatePermisosJsonAsync(int id, string permisosJson, DateTime fechaModificacion, CancellationToken ct = default)
{
const string sql = """
UPDATE dbo.Usuario
SET PermisosJson = @PermisosJson,
FechaModificacion = @FechaModificacion
WHERE Id = @Id
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
await connection.ExecuteAsync(sql, new
{
PermisosJson = permisosJson,
FechaModificacion = fechaModificacion,
Id = id
});
}
// ── mapping ───────────────────────────────────────────────────────────────
private static Usuario MapRow(UsuarioRow row)
=> new(
id: row.Id,
username: row.Username,
passwordHash: row.PasswordHash,
nombre: row.Nombre,
apellido: row.Apellido,
email: row.Email,
rol: row.Rol,
permisosJson: row.PermisosJson,
activo: row.Activo,
fechaModificacion: row.FechaModificacion,
ultimoLogin: row.UltimoLogin,
mustChangePassword: row.MustChangePassword
);
// Flat DTO for Dapper mapping (avoids polluting domain entity with Dapper attributes)
private sealed record UsuarioRow(
int Id,
string Username,
string PasswordHash,
string Nombre,
string Apellido,
string? Email,
string Rol,
string PermisosJson,
bool Activo,
DateTime? FechaModificacion,
DateTime? UltimoLogin,
bool MustChangePassword
);
private sealed record UsuarioPagedRow(
int Id,
string Username,
string Nombre,
string Apellido,
string? Email,
string Rol,
bool Activo,
DateTime? UltimoLogin,
DateTime? FechaModificacion,
int TotalCount
);
}