Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,5 @@ FodyWeavers.xsd
*.sln.iml

.containers/

.vscode/settings.json
6 changes: 5 additions & 1 deletion src/Application/Common/Behaviors/DomainEventBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
var response = await next();

// Only dispatch events for commands, not queries
if (request is ICommand)
// Check if request implements ICommand<> (generic command interface)
var isCommand = request.GetType().GetInterfaces()
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommand<>));

if (isCommand)
{
logger.LogDebug("Command completed, checking for domain events to dispatch");

Expand Down
17 changes: 9 additions & 8 deletions src/Application/Features/Auth/Login/LoginCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Application.Features.Auth.Login;
/// </summary>
public sealed class LoginCommandHandler(
IUserRepository userRepository,
IPasswordHasher passwordHasher,
IAuthService authService,
ITokenService tokenService) : ICommandHandler<LoginCommand, Result<LoginResponse>>
{
public async Task<Result<LoginResponse>> Handle(LoginCommand request, CancellationToken cancellationToken)
Expand All @@ -19,18 +19,19 @@ public async Task<Result<LoginResponse>> Handle(LoginCommand request, Cancellati
var user = await userRepository.GetByEmailAsync(request.Email, cancellationToken)
?? throw new UnauthorizedException("Invalid email or password.");

// Verify password
if (!passwordHasher.VerifyPassword(request.Password, user.PasswordHash))
{
throw new UnauthorizedException("Invalid email or password.");
}

// Check if user is active
if (user.IsDeleted)
{
throw new ForbiddenException("User account has been deactivated.");
}

// Verify password using Identity
var isValidPassword = await authService.CheckPasswordAsync(request.Email, request.Password);
if (!isValidPassword)
{
throw new UnauthorizedException("Invalid email or password.");
}

// Generate tokens
var userInfo = new UserInfo
{
Expand All @@ -47,7 +48,7 @@ public async Task<Result<LoginResponse>> Handle(LoginCommand request, Cancellati
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresAt = DateTime.UtcNow.AddHours(1), // TODO: Get from configuration
ExpiresAt = DateTime.UtcNow.AddHours(1),
User = userInfo
};

Expand Down
30 changes: 30 additions & 0 deletions src/Application/Features/Auth/Register/RegisterCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Application.Common;
using Domain.Common;

namespace Application.Features.Auth.Register;

/// <summary>
/// Command to register a new user
/// </summary>
public sealed record RegisterCommand : ICommand<Result<RegisterResponse>>
{
/// <summary>
/// User's email address
/// </summary>
public required string Email { get; init; }

/// <summary>
/// User's password
/// </summary>
public required string Password { get; init; }

/// <summary>
/// Confirm password
/// </summary>
public required string ConfirmPassword { get; init; }

/// <summary>
/// User's full name
/// </summary>
public required string FullName { get; init; }
}
79 changes: 79 additions & 0 deletions src/Application/Features/Auth/Register/RegisterCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Application.Common;
using Application.Common.Exceptions;
using Application.Features.Auth.Login;
using Application.Interfaces;
using Domain.Common;
using Domain.Constants;
using DomainUser = Domain.Entities.User;

namespace Application.Features.Auth.Register;

/// <summary>
/// Handler for user registration
/// </summary>
public sealed class RegisterCommandHandler(
IUserRepository userRepository,
IAuthService authService,
ITokenService tokenService) : ICommandHandler<RegisterCommand, Result<RegisterResponse>>
{
public async Task<Result<RegisterResponse>> Handle(RegisterCommand request, CancellationToken cancellationToken)
{
// Check if email already exists
var existingUser = await userRepository.GetByEmailAsync(request.Email, cancellationToken);
if (existingUser != null)
{
return Result.Failure<RegisterResponse>(
Error.Conflict("User.EmailExists", "Email address is already registered"));
}

// Create new Domain user
var user = new DomainUser
{
Id = Guid.NewGuid(),
Email = request.Email,
FullName = request.FullName,
Roles = [UserRoles.User],
IsDeleted = false,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};

// Save to domain users table
var createdUser = await userRepository.CreateAsync(user, cancellationToken);

// Create Identity user with same ID and password
var identityCreated = await authService.CreateIdentityUserAsync(
createdUser.Id,
createdUser.Email,
createdUser.FullName,
request.Password,
createdUser.Roles);

if (!identityCreated)
{
return Result.Failure<RegisterResponse>(
Error.Failure("User.IdentityFailed", "Failed to create Identity user"));
}

var userInfo = new UserInfo
{
Id = createdUser.Id,
Email = createdUser.Email,
FullName = createdUser.FullName,
Roles = createdUser.Roles
};

// Generate tokens for auto-login
var accessToken = tokenService.GenerateAccessToken(userInfo);
var refreshToken = tokenService.GenerateRefreshToken();

return Result.Success(new RegisterResponse
{
UserId = createdUser.Id,
Email = createdUser.Email,
FullName = createdUser.FullName,
AccessToken = accessToken,
RefreshToken = refreshToken
});
}
}
33 changes: 33 additions & 0 deletions src/Application/Features/Auth/Register/RegisterCommandValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using FluentValidation;

namespace Application.Features.Auth.Register;

/// <summary>
/// Validator for RegisterCommand
/// </summary>
public sealed class RegisterCommandValidator : AbstractValidator<RegisterCommand>
{
public RegisterCommandValidator()
{
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format")
.MaximumLength(200).WithMessage("Email must not exceed 200 characters");

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 digit");

RuleFor(x => x.ConfirmPassword)
.NotEmpty().WithMessage("Confirm password is required")
.Equal(x => x.Password).WithMessage("Passwords do not match");

RuleFor(x => x.FullName)
.NotEmpty().WithMessage("Full name is required")
.MinimumLength(2).WithMessage("Full name must be at least 2 characters")
.MaximumLength(200).WithMessage("Full name must not exceed 200 characters");
}
}
32 changes: 32 additions & 0 deletions src/Application/Features/Auth/Register/RegisterResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Application.Features.Auth.Register;

/// <summary>
/// Response after successful registration
/// </summary>
public sealed record RegisterResponse
{
/// <summary>
/// User ID
/// </summary>
public required Guid UserId { get; init; }

/// <summary>
/// User's email
/// </summary>
public required string Email { get; init; }

/// <summary>
/// User's full name
/// </summary>
public required string FullName { get; init; }

/// <summary>
/// Access token (optional - auto login after register)
/// </summary>
public string? AccessToken { get; init; }

/// <summary>
/// Refresh token (optional - auto login after register)
/// </summary>
public string? RefreshToken { get; init; }
}
66 changes: 0 additions & 66 deletions src/Application/Features/User/CreateUser/CreateUserCommand.cs

This file was deleted.

This file was deleted.

Loading