Skip to content

Commit

Permalink
forgot password
Browse files Browse the repository at this point in the history
  • Loading branch information
matinayo committed Aug 8, 2024
1 parent 66a292c commit dbfa603
Show file tree
Hide file tree
Showing 18 changed files with 335 additions and 206 deletions.
11 changes: 11 additions & 0 deletions HalceraAPI.Common/AppsettingsOptions/EmailSenderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace HalceraAPI.Common.AppsettingsOptions
{
public class EmailSenderOptions
{
public string? SendGridKey { get; set; }

public string? SendGridUser { get; set; }

public string? SendGridEmail { get; set; }
}
}
2 changes: 1 addition & 1 deletion HalceraAPI.Common/HalceraAPI.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.21.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.36.0" />
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions HalceraAPI.Common/Utilities/EmailConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace HalceraAPI.Common.Utilities
{
public static class EmailConstants
{
public const string ForgotPasswordSubject = "Request to change password";

public static string ForgotPasswordPlainTextMessage(string userPasswordResetToken)
{
return $"Request to change password {userPasswordResetToken}";
}
public const string ForgotPasswordHtmlMessage = "Request to change password";
}
}
3 changes: 1 addition & 2 deletions HalceraAPI.DataAccess/DbInitializer/DbInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public void Initialize()

private List<Roles> LoadApplicationRoles()
{
// Create all roles required
List<Roles> applicationRoles = new()
{
new() {
Expand All @@ -63,7 +62,7 @@ private void LoadAdminUser(Roles? roleId)
{
Name = "Admin",
Email = "[email protected]",
PasswordHash = BCrypt.Net.BCrypt.HashPassword("MASTER123*"),
PasswordHash = BCrypt.Net.BCrypt.HashPassword("string"),
Roles = roleId != null ? new List<Roles>() { roleId } : null
};

Expand Down
49 changes: 26 additions & 23 deletions HalceraAPI.Model/ApplicationUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@

namespace HalceraAPI.Models
{
/// <summary>
/// Application User Models
/// </summary>
public class ApplicationUser
{
[Key]
Expand All @@ -24,21 +21,34 @@ public class ApplicationUser
public string PasswordHash { get; set; } = string.Empty;

public bool Active { get; set; } = true;
/// <summary>
/// End date if account is locked
/// </summary>

public DateTime? LockoutEnd { get; set; }

public DateTime? UserCreatedDate { get; set; } = DateTime.UtcNow;

public DateTime? DateLastModified { get; set; }

public DateTime? LastLoginDate { get; set; }

public ICollection<Roles>? Roles { get; set; }

public int? RefreshTokenId { get; set; }

[ForeignKey(nameof(RefreshTokenId))]
public RefreshToken? RefreshToken { get; set; }

public bool AccountDeleted { get; set; }

public DateTime? DateAccountDeleted { get; set; }

public string? PasswordResetToken { get; set; }

public DateTime? ResetTokenExpires { get; set; }

public DateTime? PasswordResetDate { get; set; }

public int? AddressId { get; set; }

[ForeignKey(nameof(AddressId))]
public BaseAddress? Address { get; set; }

Expand Down Expand Up @@ -75,11 +85,16 @@ public void Register(string? password)
LastLoginDate = DateTime.UtcNow;
}

/// <summary>
/// Set created password hash
/// </summary>
/// <param name="password">Password string request</param>
/// <returns>Password Hash</returns>
public void ResetPassword(string password)
{
ValidateAccountStatus();
SetPasswordHash(password);
PasswordResetDate = DateTime.UtcNow;
DateLastModified = DateTime.UtcNow;
PasswordResetToken = null;
ResetTokenExpires = null;
}

private void SetPasswordHash(string? password)
{
if (string.IsNullOrWhiteSpace(password))
Expand All @@ -90,10 +105,6 @@ private void SetPasswordHash(string? password)
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password);
}

/// <summary>
/// Verify user password
/// </summary>
/// <param name="requestedPassword">Input password from user</param>
private void VerifyPassword(string? requestedPassword)
{
if (string.IsNullOrWhiteSpace(requestedPassword)
Expand All @@ -103,10 +114,6 @@ private void VerifyPassword(string? requestedPassword)
}
}

/// <summary>
/// Validating Account status and preferences
/// </summary>
/// <param name="applicationUser">Application User from db</param>
private void ValidateAccountStatus()
{
bool accountIsInactive = !Active;
Expand All @@ -126,10 +133,6 @@ private void ValidateAccountStatus()
}
}

/// <summary>
/// Generate Refresh Token
/// </summary>
/// <returns>New refresh token</returns>
public void GenerateRefreshToken()
{
RefreshToken = new RefreshToken
Expand Down
13 changes: 13 additions & 0 deletions HalceraAPI.Services/Contract/IEmailSenderOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace HalceraAPI.Services.Contract
{
public interface IEmailSenderOperation
{
Task SendEmailAsync(string receiverEmail, string subject, string plainTextMessage, string htmlMessage);
}
}
5 changes: 5 additions & 0 deletions HalceraAPI.Services/Contract/IIdentityOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using HalceraAPI.Services.Dtos.ApplicationUser;
using HalceraAPI.Services.Dtos.RefreshToken;
using HalceraAPI.Services.Dtos.Role;
using HalceraAPI.Services.Dtos.Identity;

namespace HalceraAPI.Services.Contract
{
Expand All @@ -14,5 +15,9 @@ public interface IIdentityOperation
Task<ApplicationUser> GetLoggedInUserAsync();
Task<IEnumerable<RoleResponse>> GetApplicationRoles();
Task<ApplicationUser?> GetUserWithEmail(string? email);
Task ForgotPassword(string email);
Task ResetUserPassword(ResetUserPasswordRequest resetUserPasswordRequest);

void Logout();
}
}
2 changes: 1 addition & 1 deletion HalceraAPI.Services/Contract/IUserOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using HalceraAPI.Services.Dtos.APIResponse;
using HalceraAPI.Services.Dtos.ApplicationUser;
using HalceraAPI.Services.Dtos.BaseAddress;
using HalceraAPI.Services.Dtos.Role;

namespace HalceraAPI.Services.Contract
{
Expand All @@ -20,5 +19,6 @@ Task<APIResponse<IEnumerable<UserResponse>>> GetUsersAsync(
Task LockUnlockUserAsync(string userId, AccountAction accountAction);
Task <APIResponse<UserAuthResponse>> DeleteRoleFromUserAsync(string userId, int roleId);
Task<APIResponse<UserAuthResponse>> UpdateUserRoleUserAsync(string userId, int roleId);
void ResetPassword(string email);
}
}
8 changes: 1 addition & 7 deletions HalceraAPI.Services/Dtos/ApplicationUser/LoginRequest.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;

namespace HalceraAPI.Services.Dtos.ApplicationUser
{
Expand All @@ -14,7 +9,6 @@ public class LoginRequest
public string? Email { get; set; }

[Required]
[StringLength(255, ErrorMessage = "Must be between 5 and 255 characters", MinimumLength = 5)]
[DataType(DataType.Password)]
public string? Password { get; set; }
}
Expand Down
25 changes: 25 additions & 0 deletions HalceraAPI.Services/Dtos/Identity/ResetUserPasswordRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;

namespace HalceraAPI.Services.Dtos.Identity
{
public class ResetUserPasswordRequest
{
[Required]
[EmailAddress]
public required string Email { get; set; }

[Required]
public required string OTP { get; set; }

[Required]
[StringLength(255, ErrorMessage = "Must be between 5 and 255 characters", MinimumLength = 5)]
[DataType(DataType.Password)]
public required string Password { get; set; }

[Required]
[StringLength(255, ErrorMessage = "Must be between 5 and 255 characters", MinimumLength = 5)]
[DataType(DataType.Password)]
[Compare(nameof(Password))]
public required string ConfirmPassword { get; set; }
}
}
3 changes: 2 additions & 1 deletion HalceraAPI.Services/HalceraAPI.Services.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
Expand All @@ -11,6 +11,7 @@
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Paystack.Net" Version="1.1.0" />
<PackageReference Include="SendGrid" Version="9.29.3" />
</ItemGroup>

<ItemGroup>
Expand Down
34 changes: 34 additions & 0 deletions HalceraAPI.Services/Operations/EmailSenderOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

using HalceraAPI.Common.AppsettingsOptions;
using HalceraAPI.Services.Contract;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;

namespace HalceraAPI.Services.Operations
{
public class EmailSenderOperation : IEmailSenderOperation
{
private readonly EmailSenderOptions _emailOptions;

public EmailSenderOperation(IOptions<EmailSenderOptions> options)
{
_emailOptions = options.Value;
}

public async Task SendEmailAsync(string receiverEmail, string subject, string plainTextMessage, string htmlMessage)
{
await Execute(receiverEmail, subject, plainTextMessage, htmlMessage);
}

private async Task Execute(string receiverEmail, string subject, string plainTextMessage, string htmlMessage)
{
var client = new SendGridClient(_emailOptions.SendGridKey);
var from = new EmailAddress(_emailOptions.SendGridEmail, _emailOptions.SendGridUser);
var to = new EmailAddress(receiverEmail);

var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextMessage, htmlMessage);
await client.SendEmailAsync(msg);
}
}
}
54 changes: 53 additions & 1 deletion HalceraAPI.Services/Operations/IdentityOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using HalceraAPI.Models;
using HalceraAPI.Services.Contract;
using HalceraAPI.Services.Dtos.ApplicationUser;
using HalceraAPI.Services.Dtos.Identity;
using HalceraAPI.Services.Dtos.RefreshToken;
using HalceraAPI.Services.Dtos.Role;
using HalceraAPI.Services.Token;
Expand All @@ -20,17 +21,20 @@ public class IdentityOperation : IIdentityOperation
private readonly IMapper _mapper;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly JWTOptions jwtOptions;
private readonly IEmailSenderOperation _emailSenderOperation;

public IdentityOperation(
IUnitOfWork unitOfWork,
IMapper mapper,
IHttpContextAccessor httpContextAccessor,
IOptions<JWTOptions> options)
IOptions<JWTOptions> options,
IEmailSenderOperation emailSenderOperation)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_httpContextAccessor = httpContextAccessor;
jwtOptions = options.Value;
_emailSenderOperation = emailSenderOperation;
}

public async Task<UserAuthResponse> Register(RegisterRequest registerRequest)
Expand Down Expand Up @@ -153,5 +157,53 @@ private async Task SetUserRole(IEnumerable<RoleRequest>? rolesId, ApplicationUse
}
}
}

public async Task ForgotPassword(string email)
{
var user = await _unitOfWork.ApplicationUser
.GetFirstOrDefault(user => user.Email.Trim().ToLower().Equals(email.Trim().ToLower()));

if (user != null)
{
user.PasswordResetToken = CreateRandomToken();
user.ResetTokenExpires = DateTime.UtcNow.AddHours(1);
await _unitOfWork.SaveAsync();

await _emailSenderOperation.SendEmailAsync(
"[email protected]", //user.Email,
EmailConstants.ForgotPasswordSubject,
EmailConstants.ForgotPasswordPlainTextMessage(user.PasswordResetToken),
EmailConstants.ForgotPasswordHtmlMessage);
}
}

public async Task ResetUserPassword(ResetUserPasswordRequest resetUserPasswordRequest)
{
var user = await _unitOfWork.ApplicationUser
.GetFirstOrDefault(user => user.Email.Trim().ToLower().Equals(resetUserPasswordRequest.Email.Trim().ToLower()));

if (user != null && user.PasswordResetToken != null && DateTime.UtcNow < user.ResetTokenExpires)
{
if (resetUserPasswordRequest.OTP.Trim().Equals(user.PasswordResetToken.Trim()))
{
user.ResetPassword(resetUserPasswordRequest.Password);
await _unitOfWork.SaveAsync();

return;
}
}

throw new Exception("Invalid Token");
}

public void Logout()
{
throw new NotImplementedException();
}

private static string CreateRandomToken()
{
return Guid.NewGuid().ToString()[..5];
}
}
}
1 change: 0 additions & 1 deletion HalceraAPI.Services/Operations/ShoppingCartOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,6 @@ private PaymentDetails ProcessPaymentOrderDetails(PaymentDetailsRequest paymentD

private static decimal GetTotalAmountToBePaidDuringCheckout(IEnumerable<ShoppingCart> cartItemsFromDb, Currency currencyToBePaidIn)
{
// TODO: only product that are active
decimal totalAmount = 0.0M;
List<OrderDetails> orderDetails = new();

Expand Down
Loading

0 comments on commit dbfa603

Please sign in to comment.