From 109ade474c52302c86587495fb93b3686b25a86b Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Wed, 11 Feb 2026 17:36:27 -0500 Subject: [PATCH] Add validation and dtos --- src/Core/API/API.Core/API.Core.csproj | 1 + .../API/API.Core/Contracts/Auth/AuthDTO.cs | 4 ++ src/Core/API/API.Core/Contracts/Auth/Login.cs | 23 ++++++++ .../API/API.Core/Contracts/Auth/Register.cs | 54 +++++++++++++++++++ .../API.Core/Contracts/Common/ResponseBody.cs | 12 +++++ .../API.Core/Controllers/AuthController.cs | 53 ++++++++++-------- src/Core/API/API.Core/Program.cs | 4 ++ 7 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 src/Core/API/API.Core/Contracts/Auth/AuthDTO.cs create mode 100644 src/Core/API/API.Core/Contracts/Auth/Login.cs create mode 100644 src/Core/API/API.Core/Contracts/Auth/Register.cs create mode 100644 src/Core/API/API.Core/Contracts/Common/ResponseBody.cs diff --git a/src/Core/API/API.Core/API.Core.csproj b/src/Core/API/API.Core/API.Core.csproj index badce45..0d83a68 100644 --- a/src/Core/API/API.Core/API.Core.csproj +++ b/src/Core/API/API.Core/API.Core.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Core/API/API.Core/Contracts/Auth/AuthDTO.cs b/src/Core/API/API.Core/Contracts/Auth/AuthDTO.cs new file mode 100644 index 0000000..0d08dee --- /dev/null +++ b/src/Core/API/API.Core/Contracts/Auth/AuthDTO.cs @@ -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); diff --git a/src/Core/API/API.Core/Contracts/Auth/Login.cs b/src/Core/API/API.Core/Contracts/Auth/Login.cs new file mode 100644 index 0000000..96a8126 --- /dev/null +++ b/src/Core/API/API.Core/Contracts/Auth/Login.cs @@ -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 +{ + public LoginRequestValidator() + { + RuleFor(x => x.Username) + .NotEmpty().WithMessage("Username is required"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("Password is required"); + } +} + diff --git a/src/Core/API/API.Core/Contracts/Auth/Register.cs b/src/Core/API/API.Core/Contracts/Auth/Register.cs new file mode 100644 index 0000000..012575d --- /dev/null +++ b/src/Core/API/API.Core/Contracts/Auth/Register.cs @@ -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 +{ + 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"); + } +} + + + diff --git a/src/Core/API/API.Core/Contracts/Common/ResponseBody.cs b/src/Core/API/API.Core/Contracts/Common/ResponseBody.cs new file mode 100644 index 0000000..e5a6e71 --- /dev/null +++ b/src/Core/API/API.Core/Contracts/Common/ResponseBody.cs @@ -0,0 +1,12 @@ +namespace API.Core.Contracts.Common; + +public record ResponseBody +{ + public required string Message { get; init; } + public required T Payload { get; init; } +} + +public record ResponseBody +{ + public required string Message { get; init; } +} diff --git a/src/Core/API/API.Core/Controllers/AuthController.cs b/src/Core/API/API.Core/Controllers/AuthController.cs index a0caf65..201c491 100644 --- a/src/Core/API/API.Core/Controllers/AuthController.cs +++ b/src/Core/API/API.Core/Controllers/AuthController.cs @@ -1,3 +1,5 @@ +using API.Core.Contracts.Auth; +using API.Core.Contracts.Common; using Domain.Core.Entities; using Microsoft.AspNetCore.Mvc; using Service.Core.Auth; @@ -9,27 +11,12 @@ namespace API.Core.Controllers [Route("api/[controller]")] 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")] public async Task> Register([FromBody] RegisterRequest req) { - var user = new UserAccount + + var created = await auth.RegisterAsync(new UserAccount { UserAccountId = Guid.Empty, Username = req.Username, @@ -37,10 +24,23 @@ namespace API.Core.Controllers LastName = req.LastName, Email = req.Email, DateOfBirth = req.DateOfBirth - }; + }, req.Password); - var created = await auth.RegisterAsync(user, req.Password); - return CreatedAtAction(nameof(Register), new { id = created.UserAccountId }, created); + var jwtExpiresAt = DateTime.UtcNow.AddHours(1); + var jwt = jwtService.GenerateJwt(created.UserAccountId, created.Username, jwtExpiresAt + + ); + + var response = new ResponseBody + { + Message = "Registration successful.", + Payload = new AuthPayload( + new UserDTO(created.UserAccountId, created.Username), + jwt, + DateTime.UtcNow, + jwtExpiresAt) + }; + return Created("/", response); } [HttpPost("login")] @@ -49,12 +49,19 @@ namespace API.Core.Controllers var userAccount = await auth.LoginAsync(req.Username, req.Password); 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 + { + Message = "Login successful.", + Payload = new AuthPayload(dto, jwt, DateTime.UtcNow, jwtExpiresAt) + }); } } } diff --git a/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs index 4dfb8cd..93b762f 100644 --- a/src/Core/API/API.Core/Program.cs +++ b/src/Core/API/API.Core/Program.cs @@ -1,3 +1,4 @@ +using FluentValidation; using Repository.Core.Repositories.Auth; using Repository.Core.Repositories.UserAccount; using Repository.Core.Sql; @@ -13,6 +14,9 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddOpenApi(); +// Add FluentValidation +builder.Services.AddValidatorsFromAssemblyContaining(); + // Add health checks builder.Services.AddHealthChecks();