Add validation and dtos

This commit is contained in:
Aaron Po
2026-02-11 17:36:27 -05:00
parent 07a62a0c99
commit 109ade474c
7 changed files with 128 additions and 23 deletions

View File

@@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,4 @@
namespace API.Core.Contracts.Auth;
public record UserDTO(Guid UserAccountId, string Username);
public record AuthPayload(UserDTO User, string AccessToken, DateTime CreatedAt, DateTime ExpiresAt);

View File

@@ -0,0 +1,23 @@
using API.Core.Contracts.Common;
using FluentValidation;
namespace API.Core.Contracts.Auth;
public record LoginRequest
{
public string Username { get; init; } = default!;
public string Password { get; init; } = default!;
}
public class LoginRequestValidator : AbstractValidator<LoginRequest>
{
public LoginRequestValidator()
{
RuleFor(x => x.Username)
.NotEmpty().WithMessage("Username is required");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required");
}
}

View File

@@ -0,0 +1,54 @@
using API.Core.Contracts.Common;
using FluentValidation;
namespace API.Core.Contracts.Auth;
public record RegisterRequest(
string Username,
string FirstName,
string LastName,
string Email,
DateTime DateOfBirth,
string Password
);
public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
{
public RegisterRequestValidator()
{
RuleFor(x => x.Username)
.NotEmpty().WithMessage("Username is required")
.Length(3, 64).WithMessage("Username must be between 3 and 64 characters")
.Matches("^[a-zA-Z0-9._-]+$")
.WithMessage("Username can only contain letters, numbers, dots, underscores, and hyphens");
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required")
.MaximumLength(128).WithMessage("First name cannot exceed 128 characters");
RuleFor(x => x.LastName)
.NotEmpty().WithMessage("Last name is required")
.MaximumLength(128).WithMessage("Last name cannot exceed 128 characters");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format")
.MaximumLength(128).WithMessage("Email cannot exceed 128 characters");
RuleFor(x => x.DateOfBirth)
.NotEmpty().WithMessage("Date of birth is required")
.LessThan(DateTime.Today.AddYears(-19))
.WithMessage("You must be at least 19 years old to register");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required")
.MinimumLength(8).WithMessage("Password must be at least 8 characters")
.Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter")
.Matches("[a-z]").WithMessage("Password must contain at least one lowercase letter")
.Matches("[0-9]").WithMessage("Password must contain at least one number")
.Matches("[^a-zA-Z0-9]").WithMessage("Password must contain at least one special character");
}
}

View File

@@ -0,0 +1,12 @@
namespace API.Core.Contracts.Common;
public record ResponseBody<T>
{
public required string Message { get; init; }
public required T Payload { get; init; }
}
public record ResponseBody
{
public required string Message { get; init; }
}

View File

@@ -1,3 +1,5 @@
using API.Core.Contracts.Auth;
using API.Core.Contracts.Common;
using Domain.Core.Entities; using Domain.Core.Entities;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Service.Core.Auth; using Service.Core.Auth;
@@ -9,27 +11,12 @@ namespace API.Core.Controllers
[Route("api/[controller]")] [Route("api/[controller]")]
public class AuthController(IAuthService auth, IJwtService jwtService) : ControllerBase public class AuthController(IAuthService auth, IJwtService jwtService) : ControllerBase
{ {
public record RegisterRequest(
string Username,
string FirstName,
string LastName,
string Email,
DateTime DateOfBirth,
string Password
);
public record LoginRequest
{
public string Username { get; init; } = default!;
public string Password { get; init; } = default!;
}
private record ResponseBody(string Message, object? Payload);
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult<UserAccount>> Register([FromBody] RegisterRequest req) public async Task<ActionResult<UserAccount>> Register([FromBody] RegisterRequest req)
{ {
var user = new UserAccount
var created = await auth.RegisterAsync(new UserAccount
{ {
UserAccountId = Guid.Empty, UserAccountId = Guid.Empty,
Username = req.Username, Username = req.Username,
@@ -37,10 +24,23 @@ namespace API.Core.Controllers
LastName = req.LastName, LastName = req.LastName,
Email = req.Email, Email = req.Email,
DateOfBirth = req.DateOfBirth DateOfBirth = req.DateOfBirth
}; }, req.Password);
var created = await auth.RegisterAsync(user, req.Password); var jwtExpiresAt = DateTime.UtcNow.AddHours(1);
return CreatedAtAction(nameof(Register), new { id = created.UserAccountId }, created); var jwt = jwtService.GenerateJwt(created.UserAccountId, created.Username, jwtExpiresAt
);
var response = new ResponseBody<AuthPayload>
{
Message = "Registration successful.",
Payload = new AuthPayload(
new UserDTO(created.UserAccountId, created.Username),
jwt,
DateTime.UtcNow,
jwtExpiresAt)
};
return Created("/", response);
} }
[HttpPost("login")] [HttpPost("login")]
@@ -49,12 +49,19 @@ namespace API.Core.Controllers
var userAccount = await auth.LoginAsync(req.Username, req.Password); var userAccount = await auth.LoginAsync(req.Username, req.Password);
if (userAccount is null) if (userAccount is null)
{ {
return Unauthorized(new ResponseBody("Invalid username or password.", null)); return Unauthorized();
} }
var jwt = jwtService.GenerateJwt(userAccount.UserAccountId, userAccount.Username, userAccount.DateOfBirth); UserDTO dto = new(userAccount.UserAccountId, userAccount.Username);
return Ok(new ResponseBody("Logged in successfully.", new { AccessToken = jwt })); var jwtExpiresAt = DateTime.UtcNow.AddHours(1);
var jwt = jwtService.GenerateJwt(userAccount.UserAccountId, userAccount.Username, jwtExpiresAt);
return Ok(new ResponseBody<AuthPayload>
{
Message = "Login successful.",
Payload = new AuthPayload(dto, jwt, DateTime.UtcNow, jwtExpiresAt)
});
} }
} }
} }

View File

@@ -1,3 +1,4 @@
using FluentValidation;
using Repository.Core.Repositories.Auth; using Repository.Core.Repositories.Auth;
using Repository.Core.Repositories.UserAccount; using Repository.Core.Repositories.UserAccount;
using Repository.Core.Sql; using Repository.Core.Sql;
@@ -13,6 +14,9 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
// Add FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// Add health checks // Add health checks
builder.Services.AddHealthChecks(); builder.Services.AddHealthChecks();