Backend login and register
This commit is contained in:
147
src/Application/Services/AuthenticationService.cs
Normal file
147
src/Application/Services/AuthenticationService.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
49
src/Application/Services/EmailService.cs
Normal file
49
src/Application/Services/EmailService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Application/Services/JwtService.cs
Normal file
65
src/Application/Services/JwtService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
163
src/Application/Services/UserService.cs
Normal file
163
src/Application/Services/UserService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user