feat: Sistema de Usuarios - Backend CRUD + JWT Auth (Issue #1)

Implementación fundacional del proyecto PruebaGentle:
- Arquitectura Clean/Hexagonal: Core, Infrastructure, API
- 6 Stored Procedures para CRUD + autenticación
- JWT authentication con BCrypt password hashing
- Docker Compose (SQL Server + Backend)
- Solución .NET 10 con Dapper + SqlClient

Closes #1
This commit is contained in:
2026-03-31 17:36:04 -03:00
commit 21e7e7b044
34 changed files with 795 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\PruebaGentle.Core\PruebaGentle.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
<PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.5" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,89 @@
using System.Data;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using PruebaGentle.Core.Entities;
using PruebaGentle.Core.Interfaces;
namespace PruebaGentle.Infrastructure.Repositories;
public class UserRepository : IUserRepository
{
private readonly string _connectionString;
public UserRepository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
}
private IDbConnection CreateConnection() => new SqlConnection(_connectionString);
public async Task<User> CreateAsync(User user)
{
using var connection = CreateConnection();
var result = await connection.QuerySingleAsync<User>(
"sp_User_Create",
new
{
user.Username,
user.PasswordHash,
user.Email,
user.NombreCompleto
},
commandType: CommandType.StoredProcedure);
return result;
}
public async Task<User?> GetByIdAsync(int id)
{
using var connection = CreateConnection();
return await connection.QuerySingleOrDefaultAsync<User>(
"sp_User_GetById",
new { Id = id },
commandType: CommandType.StoredProcedure);
}
public async Task<IEnumerable<User>> GetAllAsync()
{
using var connection = CreateConnection();
return await connection.QueryAsync<User>(
"sp_User_GetAll",
commandType: CommandType.StoredProcedure);
}
public async Task<User?> UpdateAsync(User user)
{
using var connection = CreateConnection();
return await connection.QuerySingleOrDefaultAsync<User>(
"sp_User_Update",
new
{
user.Id,
user.Username,
user.Email,
user.NombreCompleto
},
commandType: CommandType.StoredProcedure);
}
public async Task<bool> DeleteAsync(int id)
{
using var connection = CreateConnection();
var affected = await connection.ExecuteAsync(
"sp_User_Delete",
new { Id = id },
commandType: CommandType.StoredProcedure);
return affected > 0;
}
public async Task<User?> GetByUsernameAsync(string username)
{
using var connection = CreateConnection();
return await connection.QuerySingleOrDefaultAsync<User>(
"sp_User_GetByUsername",
new { Username = username },
commandType: CommandType.StoredProcedure);
}
}

View File

@@ -0,0 +1,16 @@
using PruebaGentle.Core.Interfaces;
namespace PruebaGentle.Infrastructure.Services;
public class PasswordHasher : IPasswordHasher
{
public string Hash(string password)
{
return BCrypt.Net.BCrypt.HashPassword(password);
}
public bool Verify(string password, string hash)
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
}