Backend login and register

This commit is contained in:
2026-02-19 13:49:01 +01:00
parent 0b6bb019b6
commit 93a78e4614
62 changed files with 11588 additions and 13 deletions

View File

@@ -6,4 +6,20 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MailKit" Version="4.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.3.9" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageReference Include="MimeKit" Version="4.14.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace Application.Common.Results;
public sealed record Error(string Code, string Message)
{
internal static Error None => new(ErrorTypeConstant.None, string.Empty);
}

View File

@@ -0,0 +1,11 @@
namespace Application.Common.Results;
public class ErrorTypeConstant
{
public const string None = "None";
public const string ValidationError = "ValidationError";
public const string InternalServerError = "InternalServerError";
public const string NotFound = "NotFoundError";
public const string Unauthorized = "UnauthorizedError";
public const string Forbidden = "ForbiddenError";
}

View File

@@ -0,0 +1,37 @@
namespace Application.Common.Results;
public class Result
{
protected Result(bool isSuccess, Error error)
{
if ((isSuccess && error != Error.None) || (!isSuccess && error == Error.None))
throw new InvalidOperationException("Invalid operation");
IsSuccess = isSuccess;
Error = error;
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
public static Result Success()
{
return new Result(true, Error.None);
}
public static Result<TValue> Success<TValue>(TValue value)
{
return new Result<TValue>(value, true, Error.None);
}
public static Result Failure(Error error)
{
return new Result(false, error);
}
public static Result<TValue> Failure<TValue>(Error error)
{
return new Result<TValue>(default!, false, error);
}
}

View File

@@ -0,0 +1,13 @@
namespace Application.Common.Results;
public class Result<TValue> : Result
{
private readonly TValue _value;
protected internal Result(TValue value, bool isSuccess, Error error) : base(isSuccess, error)
{
_value = value;
}
public TValue Value => IsSuccess ? _value : throw new InvalidOperationException("No value for failure result");
}

View File

@@ -0,0 +1,12 @@
namespace Application.DTOs;
public class PagedResult<T>
{
public List<T> Items { get; set; } = [];
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
public bool HasPreviousPage => PageNumber > 1;
public bool HasNext => PageNumber < TotalPages;
}

View File

@@ -0,0 +1,8 @@
namespace Application.DTOs;
public record ResetPasswordDto
{
public required string Email { get; init; }
public required string EmailToken { get; init; }
public required string NewPassword { get; init; }
}

View File

@@ -0,0 +1,10 @@
namespace Application.DTOs;
public record UserDto
{
public int Id { get; init; }
public required string Email { get; init; }
public required string Username { get; init; }
public DateTime LastLogin { get; init; }
public List<string> Roles { get; init; } = [];
}

View File

@@ -0,0 +1,29 @@
using Application.Common.Results;
namespace Application.Errors;
public static class AuthError
{
public static Error InvalidRegisterRequest => new(ErrorTypeConstant.ValidationError, "Invalid register request");
public static Error EmailAlreadyExists => new(ErrorTypeConstant.ValidationError, "E-Mail already exists");
public static Error UsernameAlreadyExists => new(ErrorTypeConstant.ValidationError, "Username already exists");
public static Error InvalidLoginRequest => new(ErrorTypeConstant.ValidationError, "Invalid login request");
public static Error UserNotFound => new(ErrorTypeConstant.NotFound, "User not found");
public static Error InvalidPassword => new(ErrorTypeConstant.ValidationError, "Invalid Password");
public static Error InvalidResetLink => new(ErrorTypeConstant.ValidationError, "Invalid reset link");
public static Error CreateInvalidLoginRequestError(IEnumerable<string> errors)
{
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
}
public static Error CreateInvalidRegisterRequestError(IEnumerable<string> errors)
{
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
}
}

View File

@@ -0,0 +1,54 @@
using Application.Common.Results;
namespace Application.Errors;
public static class UserError
{
public static Error InternalServerError =>
new(ErrorTypeConstant.InternalServerError, "something went wrong");
public static Error UserNotFound =>
new(ErrorTypeConstant.NotFound, "User not found");
public static Error UserCookieConsentNotFound =>
new(ErrorTypeConstant.NotFound, "Cookie consent not found for user");
public static Error FailedToAssignRole =>
new(ErrorTypeConstant.InternalServerError, "failed to assign role");
public static Error FailedToRevokeRole =>
new(ErrorTypeConstant.InternalServerError, "failed to revoke role");
public static Error UserAlreadyHasRole =>
new(ErrorTypeConstant.ValidationError, "User already has role");
public static Error UserHasNoRole =>
new(ErrorTypeConstant.ValidationError, "User already has no role");
public static Error FailedToRevokeCookieConsent =>
new(ErrorTypeConstant.InternalServerError, "failed to revoke cookie consent");
public static Error UserAlreadyHasCookieConsent =>
new(ErrorTypeConstant.ValidationError, "User already has cookie consent");
public static Error UserHasNoCookieConsent =>
new(ErrorTypeConstant.ValidationError, "User already has no cookie consent");
public static Error CannotDeleteYourself =>
new(ErrorTypeConstant.ValidationError, "You cannot delete your own account");
public static Error CreateInvalidUserUpdateRequestError(IEnumerable<string> errors)
{
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
}
public static Error CreateInvalidCookieConsentError(IEnumerable<string> errors)
{
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
}
public static Error CreateInvalidLoginRequestError(IEnumerable<string> errors)
{
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
}
}

View File

@@ -0,0 +1,20 @@
using Application.Interfaces;
using Application.Services;
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
namespace Application.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddValidatorsFromAssembly(typeof(ServiceCollectionExtensions).Assembly);
services.AddScoped<IAuthenticationService, AuthenticationService>();
services.AddScoped<IJwtService, JwtService>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IUserService, UserService>();
return services;
}
}

View File

@@ -0,0 +1,14 @@
using Application.Common.Results;
using Application.DTOs;
using Application.Models;
namespace Application.Interfaces;
public interface IAuthenticationService
{
Task<Result> RegisterAsync(RegisterRequest request);
Task<Result> LoginAsync(LoginRequest request);
Task<Result> RefreshTokensAsync(RefreshTokenRequest request);
Task<Result> SendResetEmailAsync(string email);
Task<Result> ResetPasswordAsync(ResetPasswordDto resetPasswordDto);
}

View File

@@ -0,0 +1,8 @@
using Application.Models;
namespace Application.Interfaces;
public interface IEmailService
{
void SendEmailAsync(EmailRequest emailRequest);
}

View File

@@ -0,0 +1,10 @@
using Domain.Entities;
namespace Application.Interfaces;
public interface IJwtService
{
Task<string> GenerateTokenAsync(User user);
Task<string> GenerateAndSaveRefreshTokenAsync(User user);
Task<User?> ValidateRefreshTokenAsync(int userId, string refreshToken);
}

View File

@@ -0,0 +1,15 @@
using Application.Common.Results;
using Application.DTOs;
using Application.Models;
namespace Application.Interfaces;
public interface IUserService
{
Task<Result<PagedResult<UserDto>>> GetAsync(int pageNumber = 1, int pageSize = 10);
Task<Result<string>> UpdateAsync(UserUpdateRequest user);
Task<Result<string>> DeleteAsync(int id, int currentUserId);
Task<Result<UserDto>> GetUserByIdAsync(int id);
Task<Result<string>> AssignRoleAsync(AssingRoleRequest roleRequest);
Task<Result<string>> RevokeRoleAsync(AssingRoleRequest roleRequest);
}

View File

@@ -0,0 +1,3 @@
namespace Application.Models;
public record AssingRoleRequest(int UserId, int RoleId);

View File

@@ -0,0 +1,15 @@
namespace Application.Models;
public class EmailRequest
{
public EmailRequest(string to, string subject, string content)
{
To = to;
Subject = subject;
Content = content;
}
public string To { get; set; }
public string Subject { get; set; }
public string Content { get; set; }
}

View File

@@ -0,0 +1,3 @@
namespace Application.Models;
public record LoginRequest(string Email, string Password);

View File

@@ -0,0 +1,7 @@
namespace Application.Models;
public class RefreshTokenRequest
{
public int UserId { get; set; }
public required string RefreshToken { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace Application.Models;
public record RegisterRequest(
string Username,
string Email,
string Password
);

View File

@@ -0,0 +1,7 @@
namespace Application.Models;
public class TokenResponse
{
public required string AccessToken { get; set; }
public required string RefreshToken { get; set; }
}

View File

@@ -0,0 +1,3 @@
namespace Application.Models;
public record UserUpdateRequest(int Id, string Username, string Email);

View File

@@ -0,0 +1,147 @@
using System.Security.Cryptography;
using Application.Common.Results;
using Application.DTOs;
using Application.Errors;
using Application.Interfaces;
using Application.Models;
using Application.Validators;
using Domain.Entities;
using Domain.Interface;
using Infrastructure.Utilities;
using Microsoft.AspNetCore.Identity;
namespace Application.Services;
public class AuthenticationService(
IUnitOfWork unitOfWork,
IUserRepository iUserRepository,
LoginRequestValidator loginRequestValidator,
RegisterRequestValidator registerRequestValidator,
IJwtService jwtService,
IEmailService emailService) : IAuthenticationService
{
public async Task<Result> RegisterAsync(RegisterRequest registerRequest)
{
var validationResult = await registerRequestValidator.ValidateAsync(registerRequest);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(x => x.ErrorMessage);
return Result.Failure(AuthError.CreateInvalidRegisterRequestError(errors));
}
var emailExists = await iUserRepository.GetUserByEmailAsync(registerRequest.Email);
if (emailExists is not null) return Result.Failure(AuthError.EmailAlreadyExists);
var usernameExists = await iUserRepository.GetUserByUsernameAsync(registerRequest.Username);
if (usernameExists is not null) return Result.Failure(AuthError.UsernameAlreadyExists);
var user = new User
{
Username = registerRequest.Username,
Email = registerRequest.Email,
Password = registerRequest.Password,
UserRoles = [new UserRole { RoleId = 3 }]
};
var passwordHasher = new PasswordHasher<User>();
var hashedPassword = passwordHasher.HashPassword(user, registerRequest.Password);
user.Password = hashedPassword;
await iUserRepository.AddAsync(user);
await unitOfWork.CommitAsync();
return Result.Success("User registered successfully.");
}
public async Task<Result> LoginAsync(LoginRequest loginRequest)
{
var validationResult = await loginRequestValidator.ValidateAsync(loginRequest);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(x => x.ErrorMessage);
return Result.Failure(AuthError.CreateInvalidLoginRequestError(errors));
}
var (email, password) = loginRequest;
var user = await iUserRepository.GetUserByEmailAsync(email);
if (user is null) return Result.Failure(AuthError.UserNotFound);
var passwordHasher = new PasswordHasher<User>();
var verificationResult = passwordHasher.VerifyHashedPassword(user, user.Password, password);
if (verificationResult == PasswordVerificationResult.Failed)
return Result.Failure(AuthError.InvalidPassword);
user.LastLogin = DateTime.UtcNow;
iUserRepository.Update(user);
var token = new TokenResponse
{
AccessToken = await jwtService.GenerateTokenAsync(user),
RefreshToken = await jwtService.GenerateAndSaveRefreshTokenAsync(user)
};
var result = new
{
Token = token, user.Username
};
return Result.Success(result);
}
public async Task<Result> RefreshTokensAsync(RefreshTokenRequest request)
{
var user = await jwtService.ValidateRefreshTokenAsync(request.UserId, request.RefreshToken);
if (user is null) return Result.Failure(AuthError.UserNotFound);
var result = new TokenResponse
{
AccessToken = await jwtService.GenerateTokenAsync(user),
RefreshToken = await jwtService.GenerateAndSaveRefreshTokenAsync(user)
};
return Result.Success(result);
}
public async Task<Result> SendResetEmailAsync(string email)
{
var user = await iUserRepository.GetUserByEmailAsync(email);
if (user is null) return Result.Failure(AuthError.UserNotFound);
var tokenBytes = RandomNumberGenerator.GetBytes(64);
var emailToken = Convert.ToBase64String(tokenBytes);
user.ResetPasswordToken = emailToken;
user.ResetPasswordTokenExpiryTime = DateTime.UtcNow.AddMinutes(15);
var emailModel = new EmailRequest(email, "Reset Password!!", EmailBody.EmailStringBody(email, emailToken));
emailService.SendEmailAsync(emailModel);
iUserRepository.Update(user);
await unitOfWork.CommitAsync();
return Result.Success("Reset email sent successfully");
}
public async Task<Result> ResetPasswordAsync(ResetPasswordDto resetPasswordDto)
{
// Normalize the incoming token if '+' was converted to space by transport layers.
var normalizedToken = (resetPasswordDto.EmailToken ?? string.Empty).Replace(" ", "+");
var user = await iUserRepository.GetUserByEmailAsync(resetPasswordDto.Email);
if (user is null) return Result.Failure(AuthError.UserNotFound);
var tokenCode = user.ResetPasswordToken;
var emailTokenExpiryTime = user.ResetPasswordTokenExpiryTime;
// Validate token and expiration using UTC to match stored times
if (string.IsNullOrWhiteSpace(normalizedToken) || tokenCode != normalizedToken ||
emailTokenExpiryTime < DateTime.UtcNow) return Result.Failure(AuthError.InvalidResetLink);
var passwordHasher = new PasswordHasher<User>();
var hashedPassword = passwordHasher.HashPassword(user, resetPasswordDto.NewPassword);
user.Password = hashedPassword;
// Invalidate the reset token after successful use
user.ResetPasswordToken = null;
user.ResetPasswordTokenExpiryTime = default;
iUserRepository.Update(user);
await unitOfWork.CommitAsync();
return Result.Success("Password reset successfully");
}
}

View File

@@ -0,0 +1,49 @@
using Application.Interfaces;
using Application.Models;
using MailKit.Net.Smtp;
using Microsoft.Extensions.Configuration;
using MimeKit;
using MimeKit.Text;
namespace Application.Services;
public class EmailService(IConfiguration config, Func<ISmtpClient>? smtpClientFactory)
: IEmailService
{
private readonly Func<ISmtpClient> _smtpClientFactory = smtpClientFactory ?? (() => new SmtpClient());
public EmailService(IConfiguration config) : this(config, null)
{
}
public void SendEmailAsync(EmailRequest emailRequest)
{
var emailMessage = new MimeMessage();
var from = config["EmailSettings:From"];
emailMessage.From.Add(new MailboxAddress("RssReader", from));
emailMessage.To.Add(new MailboxAddress(emailRequest.To, emailRequest.To));
emailMessage.Subject = emailRequest.Subject;
emailMessage.Body = new TextPart(TextFormat.Html)
{
Text = string.Format(emailRequest.Content)
};
var client = _smtpClientFactory();
try
{
client.Connect(config["EmailSettings:SmtpServer"], 465, true);
client.Authenticate(config["EmailSettings:From"], config["EmailSettings:Password"]);
client.Send(emailMessage);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
finally
{
client.Disconnect(true);
client.Dispose();
}
}
}

View File

@@ -0,0 +1,65 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Application.Interfaces;
using Domain.Entities;
using Domain.Interface;
using Infrastructure.Utilities;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
namespace Application.Services;
public class JwtService(
IConfiguration configuration,
IUserRepository userRepository,
IUnitOfWork unitOfWork) : IJwtService
{
public async Task<string> GenerateTokenAsync(User user)
{
var secretKey = configuration["Jwt:Key"];
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey!));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var roles = await userRepository.GetUserRolesByEmailAsync(user.Email);
var claims = new List<Claim>
{
new(ClaimTypes.Email, user.Email),
new("UserId", user.Id.ToString()),
new("username", user.Username)
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddDays(1), // set a token expiration time
SigningCredentials = credentials,
Issuer = configuration["Jwt:Issuer"],
Audience = configuration["Jwt:Audience"]
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(securityToken);
}
public async Task<string> GenerateAndSaveRefreshTokenAsync(User user)
{
var refreshToken = GenerateRefreshTokenHelper.GenerateRefreshToken();
user.RefreshToken = refreshToken;
user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(5);
userRepository.Update(user);
await unitOfWork.CommitAsync();
return refreshToken;
}
public async Task<User?> ValidateRefreshTokenAsync(int userId, string refreshToken)
{
var user = await userRepository.GetUserByIdAsync(userId);
if (user is null || user.RefreshToken != refreshToken
|| user.RefreshTokenExpiryTime <= DateTime.UtcNow)
return null;
return user;
}
}

View File

@@ -0,0 +1,163 @@
using Application.Common.Results;
using Application.DTOs;
using Application.Errors;
using Application.Interfaces;
using Application.Models;
using Application.Validators;
using Domain.Interface;
namespace Application.Services;
public class UserService(
IUnitOfWork unitOfWork,
UserUpdateRequestValidator userUpdateRequestValidator,
IUserRepository userRepository,
IUserRoleRepository userRoleRepository) : IUserService
{
public async Task<Result<PagedResult<UserDto>>> GetAsync(int pageNumber = 1, int pageSize = 10)
{
try
{
// fetch all users
var users = await userRepository.GetAllAsync();
var totalCount = users.Count;
var pagedItems = users
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(user => new UserDto
{
Id = user.Id,
Email = user.Email,
Username = user.Username,
LastLogin = user.LastLogin,
Roles = user.UserRoles.Select(x => x.Role?.Name ?? "Unknown").ToList()
})
.ToList();
var pageResult = new PagedResult<UserDto>
{
Items = pagedItems,
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize
};
return Result.Success(pageResult);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public async Task<Result<string>> UpdateAsync(UserUpdateRequest userUpdateRequest)
{
try
{
// validate request
var validationResult = await userUpdateRequestValidator.ValidateAsync(userUpdateRequest);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(a => a.ErrorMessage);
return Result.Failure<string>(UserError.CreateInvalidUserUpdateRequestError(errors));
}
// check if a user exists
var user = await userRepository.GetByIdAsync(userUpdateRequest.Id);
if (user == null) return Result.Failure<string>(UserError.UserNotFound);
// update user
user.Username = userUpdateRequest.Username;
user.Email = userUpdateRequest.Email;
userRepository.Update(user);
await unitOfWork.CommitAsync();
return Result.Success("User updated successfully");
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public async Task<Result<string>> DeleteAsync(int id, int currentUserId)
{
try
{
// Prevent users from deleting themselves
if (id == currentUserId) return Result.Failure<string>(UserError.CannotDeleteYourself);
var user = await userRepository.GetByIdAsync(id);
if (user == null) return Result.Failure<string>(UserError.UserNotFound);
userRepository.Delete(user);
await unitOfWork.CommitAsync();
return Result.Success("User deleted successfully");
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public async Task<Result<UserDto>> GetUserByIdAsync(int id)
{
try
{
var user = await userRepository.GetByIdAsync(id);
if (user is null) return Result.Failure<UserDto>(UserError.UserNotFound);
var userDetails = new UserDto
{
Id = user.Id,
Email = user.Email,
Username = user.Username,
LastLogin = user.LastLogin,
Roles = user.UserRoles.Select(x => x.Role?.Name ?? "Unknown").ToList()
};
return Result.Success(userDetails);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public async Task<Result<string>> AssignRoleAsync(AssingRoleRequest roleRequest)
{
try
{
var isUserHasRole = userRoleRepository.HasRoleAsync(roleRequest.UserId, roleRequest.RoleId);
if (isUserHasRole.Result) return Result.Failure<string>(UserError.UserAlreadyHasRole);
var result = await userRoleRepository.AddRoleAsync(roleRequest.UserId, roleRequest.RoleId);
return result
? Result.Success("Role assigned successfully")
: Result.Failure<string>(UserError.FailedToAssignRole);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public async Task<Result<string>> RevokeRoleAsync(AssingRoleRequest roleRequest)
{
try
{
var isUserHasRole = userRoleRepository.HasRoleAsync(roleRequest.UserId, roleRequest.RoleId);
if (!isUserHasRole.Result) return Result.Failure<string>(UserError.UserHasNoRole);
var result = await userRoleRepository.RemoveRoleAsync(roleRequest.UserId, roleRequest.RoleId);
return result
? Result.Success("Role revoked successfully")
: Result.Failure<string>(UserError.FailedToRevokeRole);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}

View File

@@ -0,0 +1,17 @@
using Application.Models;
using FluentValidation;
namespace Application.Validators;
public class LoginRequestValidator : AbstractValidator<LoginRequest>
{
public LoginRequestValidator()
{
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required.")
.EmailAddress().WithMessage("Email is not valid.");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required.");
}
}

View File

@@ -0,0 +1,29 @@
using Application.Models;
using FluentValidation;
namespace Application.Validators;
public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
{
public RegisterRequestValidator()
{
RuleFor(x => x.Email)
.NotEmpty().WithMessage("{PropertyName} is required.")
.EmailAddress().WithMessage("{PropertyName} is not valid.");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("{PropertyName} is required.")
.MinimumLength(10).WithMessage("{PropertyName} must be at least 10 characters long.")
.Matches("[A-Z]").WithMessage("{PropertyName} must contain at least one uppercase letter.")
.Matches("[a-z]").WithMessage("{PropertyName} must contain at least one lowercase letter.")
.Matches("[0-9]").WithMessage("{PropertyName} must contain at least one digit.")
.Matches("[^a-zA-Z0-9]").WithMessage("{PropertyName} must contain at least one special character.");
RuleFor(x => x.Username)
.NotEmpty().WithMessage("{PropertyName} is required.")
.MinimumLength(3).WithMessage("{PropertyName} must be at least 3 characters long.")
.MaximumLength(20).WithMessage("{PropertyName} must not exceed 20 characters.")
.Matches("^[a-zA-Z0-9_]*$")
.WithMessage("{PropertyName} can only contain letters, numbers, and underscores.");
}
}

View File

@@ -0,0 +1,21 @@
using Application.Models;
using FluentValidation;
namespace Application.Validators;
public class UserUpdateRequestValidator : AbstractValidator<UserUpdateRequest>
{
public UserUpdateRequestValidator()
{
RuleFor(x => x.Username)
.NotEmpty().WithMessage("Username is required.")
.MaximumLength(50).WithMessage(user => "Username must not exceed 50 characters.");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required.")
.EmailAddress().WithMessage("Email is invalid.");
RuleFor(x => x.Id)
.NotEmpty().GreaterThan(0).WithMessage("Id is required.");
}
}