2026-04-15 17:39:48 -03:00
|
|
|
using System.Text;
|
2026-04-13 21:36:02 -03:00
|
|
|
using Dapper;
|
|
|
|
|
using SIGCM2.Application.Abstractions.Persistence;
|
2026-04-15 17:39:48 -03:00
|
|
|
using SIGCM2.Application.Common;
|
2026-04-13 21:36:02 -03:00
|
|
|
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,
|
2026-04-15 17:39:48 -03:00
|
|
|
Rol, PermisosJson, Activo,
|
|
|
|
|
FechaModificacion, UltimoLogin, MustChangePassword
|
2026-04-13 21:36:02 -03:00
|
|
|
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;
|
|
|
|
|
|
2026-04-14 13:28:29 -03:00
|
|
|
return MapRow(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<Usuario?> GetByIdAsync(int id, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
const string sql = """
|
|
|
|
|
SELECT
|
|
|
|
|
Id, Username, PasswordHash,
|
|
|
|
|
Nombre, Apellido, Email,
|
2026-04-15 17:39:48 -03:00
|
|
|
Rol, PermisosJson, Activo,
|
|
|
|
|
FechaModificacion, UltimoLogin, MustChangePassword
|
2026-04-14 13:28:29 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 10:47:48 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 17:39:48 -03:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 21:33:39 -03:00
|
|
|
// 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
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 17:39:48 -03:00
|
|
|
// ── mapping ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-14 13:28:29 -03:00
|
|
|
private static Usuario MapRow(UsuarioRow row)
|
|
|
|
|
=> new(
|
2026-04-13 21:36:02 -03:00
|
|
|
id: row.Id,
|
|
|
|
|
username: row.Username,
|
|
|
|
|
passwordHash: row.PasswordHash,
|
|
|
|
|
nombre: row.Nombre,
|
|
|
|
|
apellido: row.Apellido,
|
|
|
|
|
email: row.Email,
|
|
|
|
|
rol: row.Rol,
|
|
|
|
|
permisosJson: row.PermisosJson,
|
2026-04-15 17:39:48 -03:00
|
|
|
activo: row.Activo,
|
|
|
|
|
fechaModificacion: row.FechaModificacion,
|
|
|
|
|
ultimoLogin: row.UltimoLogin,
|
|
|
|
|
mustChangePassword: row.MustChangePassword
|
2026-04-13 21:36:02 -03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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,
|
2026-04-15 17:39:48 -03:00
|
|
|
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
|
2026-04-13 21:36:02 -03:00
|
|
|
);
|
|
|
|
|
}
|