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:
21
Backend/Dockerfile
Normal file
21
Backend/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY PruebaGentle.Core/PruebaGentle.Core.csproj PruebaGentle.Core/
|
||||
COPY PruebaGentle.Infrastructure/PruebaGentle.Infrastructure.csproj PruebaGentle.Infrastructure/
|
||||
COPY PruebaGentle.API/PruebaGentle.API.csproj PruebaGentle.API/
|
||||
RUN dotnet restore PruebaGentle.API/PruebaGentle.API.csproj
|
||||
COPY . .
|
||||
WORKDIR /src/PruebaGentle.API
|
||||
RUN dotnet build -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "PruebaGentle.API.dll"]
|
||||
73
Backend/PruebaGentle.API/Controllers/AuthController.cs
Normal file
73
Backend/PruebaGentle.API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using PruebaGentle.Core.Config;
|
||||
using PruebaGentle.Core.DTOs;
|
||||
using PruebaGentle.Core.Interfaces;
|
||||
|
||||
namespace PruebaGentle.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPasswordHasher _passwordHasher;
|
||||
private readonly JwtSettings _jwtSettings;
|
||||
|
||||
public AuthController(
|
||||
IUserRepository userRepository,
|
||||
IPasswordHasher passwordHasher,
|
||||
IOptions<JwtSettings> jwtSettings)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
_jwtSettings = jwtSettings.Value;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginDto dto)
|
||||
{
|
||||
var user = await _userRepository.GetByUsernameAsync(dto.Username);
|
||||
if (user == null)
|
||||
return Unauthorized(new { error = "Credenciales inválidas." });
|
||||
|
||||
if (!_passwordHasher.Verify(dto.Password, user.PasswordHash))
|
||||
return Unauthorized(new { error = "Credenciales inválidas." });
|
||||
|
||||
var expiresAt = DateTime.UtcNow.AddHours(_jwtSettings.ExpirationHours);
|
||||
var token = GenerateJwtToken(user.Id, user.Username, user.Email, expiresAt);
|
||||
|
||||
return Ok(new LoginResponseDto
|
||||
{
|
||||
Token = token,
|
||||
ExpiresAt = expiresAt
|
||||
});
|
||||
}
|
||||
|
||||
private string GenerateJwtToken(int userId, string username, string email, DateTime expiresAt)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.UniqueName, username),
|
||||
new Claim(JwtRegisteredClaimNames.Email, email),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: "PruebaGentle",
|
||||
audience: "PruebaGentle",
|
||||
claims: claims,
|
||||
expires: expiresAt,
|
||||
signingCredentials: credentials);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
106
Backend/PruebaGentle.API/Controllers/UsersController.cs
Normal file
106
Backend/PruebaGentle.API/Controllers/UsersController.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PruebaGentle.Core.DTOs;
|
||||
using PruebaGentle.Core.Entities;
|
||||
using PruebaGentle.Core.Interfaces;
|
||||
|
||||
namespace PruebaGentle.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPasswordHasher _passwordHasher;
|
||||
|
||||
public UsersController(IUserRepository userRepository, IPasswordHasher passwordHasher)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateUserDto dto)
|
||||
{
|
||||
var existingUser = await _userRepository.GetByUsernameAsync(dto.Username);
|
||||
if (existingUser != null)
|
||||
return BadRequest(new { error = "El username ya existe." });
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Username = dto.Username,
|
||||
PasswordHash = _passwordHasher.Hash(dto.Password),
|
||||
Email = dto.Email,
|
||||
NombreCompleto = dto.NombreCompleto
|
||||
};
|
||||
|
||||
var created = await _userRepository.CreateAsync(user);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { id = created.Id }, MapToResponse(created));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll()
|
||||
{
|
||||
var users = await _userRepository.GetAllAsync();
|
||||
return Ok(users.Select(MapToResponse));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> GetById(int id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user == null)
|
||||
return NotFound(new { error = "Usuario no encontrado." });
|
||||
|
||||
return Ok(MapToResponse(user));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] UpdateUserDto dto)
|
||||
{
|
||||
var existing = await _userRepository.GetByIdAsync(id);
|
||||
if (existing == null)
|
||||
return NotFound(new { error = "Usuario no encontrado." });
|
||||
|
||||
// Check username/email conflicts with other users
|
||||
var duplicateUser = await _userRepository.GetByUsernameAsync(dto.Username);
|
||||
if (duplicateUser != null && duplicateUser.Id != id)
|
||||
return BadRequest(new { error = "El username ya está en uso por otro usuario." });
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Id = id,
|
||||
Username = dto.Username,
|
||||
Email = dto.Email,
|
||||
NombreCompleto = dto.NombreCompleto
|
||||
};
|
||||
|
||||
var updated = await _userRepository.UpdateAsync(user);
|
||||
return Ok(MapToResponse(updated!));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var existing = await _userRepository.GetByIdAsync(id);
|
||||
if (existing == null)
|
||||
return NotFound(new { error = "Usuario no encontrado." });
|
||||
|
||||
await _userRepository.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static UserResponseDto MapToResponse(User user)
|
||||
{
|
||||
return new UserResponseDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Username = user.Username,
|
||||
Email = user.Email,
|
||||
NombreCompleto = user.NombreCompleto,
|
||||
FechaCreacion = user.FechaCreacion
|
||||
};
|
||||
}
|
||||
}
|
||||
74
Backend/PruebaGentle.API/Program.cs
Normal file
74
Backend/PruebaGentle.API/Program.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using PruebaGentle.Core.Config;
|
||||
using PruebaGentle.Core.Interfaces;
|
||||
using PruebaGentle.Infrastructure.Repositories;
|
||||
using PruebaGentle.Infrastructure.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Bind JwtSettings
|
||||
builder.Services.Configure<JwtSettings>(
|
||||
builder.Configuration.GetSection("JwtSettings"));
|
||||
|
||||
// Dependency Injection
|
||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||
builder.Services.AddSingleton<IPasswordHasher, PasswordHasher>();
|
||||
|
||||
// JWT Authentication
|
||||
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
|
||||
var secretKey = jwtSettings["Secret"] ?? throw new InvalidOperationException("JWT Secret not configured.");
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = "PruebaGentle",
|
||||
ValidAudience = "PruebaGentle",
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Controllers
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// OpenAPI (native .NET 10)
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Middleware pipeline
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
14
Backend/PruebaGentle.API/Properties/launchSettings.json
Normal file
14
Backend/PruebaGentle.API/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5082",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Backend/PruebaGentle.API/PruebaGentle.API.csproj
Normal file
19
Backend/PruebaGentle.API/PruebaGentle.API.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PruebaGentle.Core\PruebaGentle.Core.csproj" />
|
||||
<ProjectReference Include="..\PruebaGentle.Infrastructure\PruebaGentle.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
Backend/PruebaGentle.API/PruebaGentle.API.http
Normal file
6
Backend/PruebaGentle.API/PruebaGentle.API.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@PruebaGentle.API_HostAddress = http://localhost:5082
|
||||
|
||||
GET {{PruebaGentle.API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
8
Backend/PruebaGentle.API/appsettings.Development.json
Normal file
8
Backend/PruebaGentle.API/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Backend/PruebaGentle.API/appsettings.json
Normal file
16
Backend/PruebaGentle.API/appsettings.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Database=PruebaGentle;User Id=sa;Password=YourStrong@Password123;TrustServerCertificate=true;"
|
||||
},
|
||||
"JwtSettings": {
|
||||
"Secret": "ThisIsA32CharacterLongSecretKey!!",
|
||||
"ExpirationHours": 24
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
7
Backend/PruebaGentle.Core/Config/JwtSettings.cs
Normal file
7
Backend/PruebaGentle.Core/Config/JwtSettings.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace PruebaGentle.Core.Config;
|
||||
|
||||
public class JwtSettings
|
||||
{
|
||||
public string Secret { get; set; } = string.Empty;
|
||||
public int ExpirationHours { get; set; } = 24;
|
||||
}
|
||||
9
Backend/PruebaGentle.Core/DTOs/CreateUserDto.cs
Normal file
9
Backend/PruebaGentle.Core/DTOs/CreateUserDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace PruebaGentle.Core.DTOs;
|
||||
|
||||
public class CreateUserDto
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string NombreCompleto { get; set; } = string.Empty;
|
||||
}
|
||||
7
Backend/PruebaGentle.Core/DTOs/LoginDto.cs
Normal file
7
Backend/PruebaGentle.Core/DTOs/LoginDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace PruebaGentle.Core.DTOs;
|
||||
|
||||
public class LoginDto
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
7
Backend/PruebaGentle.Core/DTOs/LoginResponseDto.cs
Normal file
7
Backend/PruebaGentle.Core/DTOs/LoginResponseDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace PruebaGentle.Core.DTOs;
|
||||
|
||||
public class LoginResponseDto
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
}
|
||||
8
Backend/PruebaGentle.Core/DTOs/UpdateUserDto.cs
Normal file
8
Backend/PruebaGentle.Core/DTOs/UpdateUserDto.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace PruebaGentle.Core.DTOs;
|
||||
|
||||
public class UpdateUserDto
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string NombreCompleto { get; set; } = string.Empty;
|
||||
}
|
||||
10
Backend/PruebaGentle.Core/DTOs/UserResponseDto.cs
Normal file
10
Backend/PruebaGentle.Core/DTOs/UserResponseDto.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace PruebaGentle.Core.DTOs;
|
||||
|
||||
public class UserResponseDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string NombreCompleto { get; set; } = string.Empty;
|
||||
public DateTime FechaCreacion { get; set; }
|
||||
}
|
||||
11
Backend/PruebaGentle.Core/Entities/User.cs
Normal file
11
Backend/PruebaGentle.Core/Entities/User.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace PruebaGentle.Core.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string NombreCompleto { get; set; } = string.Empty;
|
||||
public DateTime FechaCreacion { get; set; }
|
||||
}
|
||||
7
Backend/PruebaGentle.Core/Interfaces/IPasswordHasher.cs
Normal file
7
Backend/PruebaGentle.Core/Interfaces/IPasswordHasher.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace PruebaGentle.Core.Interfaces;
|
||||
|
||||
public interface IPasswordHasher
|
||||
{
|
||||
string Hash(string password);
|
||||
bool Verify(string password, string hash);
|
||||
}
|
||||
13
Backend/PruebaGentle.Core/Interfaces/IUserRepository.cs
Normal file
13
Backend/PruebaGentle.Core/Interfaces/IUserRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using PruebaGentle.Core.Entities;
|
||||
|
||||
namespace PruebaGentle.Core.Interfaces;
|
||||
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<User> CreateAsync(User user);
|
||||
Task<User?> GetByIdAsync(int id);
|
||||
Task<IEnumerable<User>> GetAllAsync();
|
||||
Task<User?> UpdateAsync(User user);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
Task<User?> GetByUsernameAsync(string username);
|
||||
}
|
||||
9
Backend/PruebaGentle.Core/PruebaGentle.Core.csproj
Normal file
9
Backend/PruebaGentle.Core/PruebaGentle.Core.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
5
Backend/PruebaGentle.slnx
Normal file
5
Backend/PruebaGentle.slnx
Normal file
@@ -0,0 +1,5 @@
|
||||
<Solution>
|
||||
<Project Path="PruebaGentle.API/PruebaGentle.API.csproj" />
|
||||
<Project Path="PruebaGentle.Core/PruebaGentle.Core.csproj" />
|
||||
<Project Path="PruebaGentle.Infrastructure/PruebaGentle.Infrastructure.csproj" />
|
||||
</Solution>
|
||||
8
Backend/Sql/CreateUsersTable.sql
Normal file
8
Backend/Sql/CreateUsersTable.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE Users (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Username NVARCHAR(50) NOT NULL UNIQUE,
|
||||
PasswordHash NVARCHAR(255) NOT NULL,
|
||||
Email NVARCHAR(100) NOT NULL UNIQUE,
|
||||
NombreCompleto NVARCHAR(100) NOT NULL,
|
||||
FechaCreacion DATETIME NOT NULL DEFAULT GETDATE()
|
||||
);
|
||||
13
Backend/Sql/sp_User_Create.sql
Normal file
13
Backend/Sql/sp_User_Create.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE OR ALTER PROCEDURE sp_User_Create
|
||||
@Username NVARCHAR(50),
|
||||
@PasswordHash NVARCHAR(255),
|
||||
@Email NVARCHAR(100),
|
||||
@NombreCompleto NVARCHAR(100)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT INTO Users (Username, PasswordHash, Email, NombreCompleto)
|
||||
OUTPUT INSERTED.Id, INSERTED.Username, INSERTED.Email, INSERTED.NombreCompleto, INSERTED.FechaCreacion
|
||||
VALUES (@Username, @PasswordHash, @Email, @NombreCompleto);
|
||||
END
|
||||
9
Backend/Sql/sp_User_Delete.sql
Normal file
9
Backend/Sql/sp_User_Delete.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE OR ALTER PROCEDURE sp_User_Delete
|
||||
@Id INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DELETE FROM Users
|
||||
WHERE Id = @Id;
|
||||
END
|
||||
9
Backend/Sql/sp_User_GetAll.sql
Normal file
9
Backend/Sql/sp_User_GetAll.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE OR ALTER PROCEDURE sp_User_GetAll
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT Id, Username, Email, NombreCompleto, FechaCreacion
|
||||
FROM Users
|
||||
ORDER BY FechaCreacion DESC;
|
||||
END
|
||||
10
Backend/Sql/sp_User_GetById.sql
Normal file
10
Backend/Sql/sp_User_GetById.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE OR ALTER PROCEDURE sp_User_GetById
|
||||
@Id INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT Id, Username, Email, NombreCompleto, FechaCreacion
|
||||
FROM Users
|
||||
WHERE Id = @Id;
|
||||
END
|
||||
10
Backend/Sql/sp_User_GetByUsername.sql
Normal file
10
Backend/Sql/sp_User_GetByUsername.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE OR ALTER PROCEDURE sp_User_GetByUsername
|
||||
@Username NVARCHAR(50)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT Id, Username, PasswordHash, Email, NombreCompleto, FechaCreacion
|
||||
FROM Users
|
||||
WHERE Username = @Username;
|
||||
END
|
||||
16
Backend/Sql/sp_User_Update.sql
Normal file
16
Backend/Sql/sp_User_Update.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
CREATE OR ALTER PROCEDURE sp_User_Update
|
||||
@Id INT,
|
||||
@Username NVARCHAR(50),
|
||||
@Email NVARCHAR(100),
|
||||
@NombreCompleto NVARCHAR(100)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE Users
|
||||
SET Username = @Username,
|
||||
Email = @Email,
|
||||
NombreCompleto = @NombreCompleto
|
||||
OUTPUT INSERTED.Id, INSERTED.Username, INSERTED.Email, INSERTED.NombreCompleto, INSERTED.FechaCreacion
|
||||
WHERE Id = @Id;
|
||||
END
|
||||
Reference in New Issue
Block a user