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();