Skip to content

Commit

Permalink
add send register email verification
Browse files Browse the repository at this point in the history
  • Loading branch information
emrecoskun705 committed Sep 11, 2023
1 parent f14566e commit 7d47776
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using LanguageExt;
using LanguageExt.Common;

namespace Unitagram.Application.Contracts.Identity;

public interface IEmailVerificationService
{
Task<Result<Unit>> GenerateAsync(Guid userId, string purpose);
Task<Result<bool>> ValidateAsync(Guid userId, string purpose, string token, int maxRetryCount);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Unitagram.Domain;

namespace Unitagram.Application.Contracts.Persistence;

public interface IOtpConfirmationRepository : IGenericRepository<OtpConfirmation>
{
/// <summary>
/// Retrieves an OTP confirmation by the user's unique identifier and name which are both primary keys.
/// </summary>
/// <param name="userId">The unique identifier of the user.</param>
/// <param name="name">The name or identifier associated with the OTP confirmation.</param>
/// <returns>
/// A task representing the asynchronous operation. The task result is an instance of OtpConfirmation
/// if a matching confirmation is found; otherwise, it returns null.
/// </returns>
Task<OtpConfirmation?> GetByUserIdAndName(Guid userId, string name);
}
5 changes: 4 additions & 1 deletion Unitagram.Identity.UnitTests/AuthServiceUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class AuthServiceUnitTests
private readonly Mock<IDiagnosticContext> _diagnosticContextMock;
private readonly Mock<IUniversityRepository> _universityRepositoryMock;
private readonly Mock<IUniversityUserRepository> _universityUserRepositoryMock;
private readonly Mock<IEmailVerificationService> _verificationServiceMock;
private readonly IFixture _fixture;

private const string ValidJwtTokenWithoutTime =
Expand Down Expand Up @@ -75,6 +76,7 @@ public AuthServiceUnitTests()
_jwtService = new Mock<IJwtService>();
_universityRepositoryMock = new Mock<IUniversityRepository>();
_universityUserRepositoryMock = new Mock<IUniversityUserRepository>();
_verificationServiceMock = new Mock<IEmailVerificationService>();
_dbContext = new Mock<UnitagramIdentityDbContext>();
}

Expand All @@ -87,7 +89,8 @@ private AuthService CreateAuthService()
_diagnosticContextMock.Object,
_universityRepositoryMock.Object,
_universityUserRepositoryMock.Object,
_dbContext.Object);
_dbContext.Object,
_verificationServiceMock.Object);
}

#region Login
Expand Down
1 change: 1 addition & 0 deletions Unitagram.Identity/IdentityServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static IServiceCollection AddIdentityServices(this IServiceCollection ser

services.AddTransient<IJwtService, JwtService>();
services.AddTransient<IAuthService, AuthService>();
services.AddTransient<IEmailVerificationService, EmailVerificationService>();

// Add JWT
services.AddAuthentication(options =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,6 @@ public class SixDigitEmailConfirmationTokenProviderOptions : DataProtectionToken

}

public override Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
{
if (manager == null)
{
throw new ArgumentNullException(nameof(manager));
}

var code = GenerateRandom6DigitCode(); // Generate a 6-digit code as a string
return Task.FromResult(code);
}

private string GenerateRandom6DigitCode()
{
var token = new byte[3];
Expand Down
8 changes: 6 additions & 2 deletions Unitagram.Identity/Services/AuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class AuthService : IAuthService
private readonly IUniversityRepository _universityRepository;
private readonly IUniversityUserRepository _universityUserRepository;
private readonly UnitagramIdentityDbContext _databaseContext;
private readonly IEmailVerificationService _verificationService;

public AuthService(UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
Expand All @@ -32,7 +33,8 @@ public AuthService(UserManager<ApplicationUser> userManager,
IDiagnosticContext diagnosticContext,
IUniversityRepository universityRepository,
IUniversityUserRepository universityUserRepository,
UnitagramIdentityDbContext databaseContext)
UnitagramIdentityDbContext databaseContext,
IEmailVerificationService verificationService)
{
_userManager = userManager;
_signInManager = signInManager;
Expand All @@ -42,6 +44,7 @@ public AuthService(UserManager<ApplicationUser> userManager,
_universityRepository = universityRepository;
_universityUserRepository = universityUserRepository;
_databaseContext = databaseContext;
_verificationService = verificationService;
}

public async Task<Result<AuthResponse>> Login(AuthRequest request)
Expand Down Expand Up @@ -176,7 +179,8 @@ await _universityUserRepository.CreateAsync(new UniversityUser()
user.RefreshTokenExpirationDateTime = jwtResponse.RefreshTokenExpirationDateTime;
await _userManager.UpdateAsync(user);


await _verificationService.GenerateAsync(user.Id, "emailverification");

_diagnosticContext.Set("CreatedUser", user.UserName);
_diagnosticContext.Set("University", getUniversity.Name);

Expand Down
131 changes: 131 additions & 0 deletions Unitagram.Identity/Services/EmailVerificationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Security.Cryptography;
using LanguageExt;
using LanguageExt.Common;
using Microsoft.AspNetCore.Identity;
using Unitagram.Application.Contracts.Email;
using Unitagram.Application.Contracts.Identity;
using Unitagram.Application.Contracts.Persistence;
using Unitagram.Application.Exceptions;
using Unitagram.Application.Models.Email;
using Unitagram.Domain;
using Unitagram.Identity.Models;

namespace Unitagram.Identity.Services;

public class EmailVerificationService : IEmailVerificationService
{
private readonly IOtpConfirmationRepository _otpConfirmationRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IEmailSender _emailSender;

public EmailVerificationService(
IOtpConfirmationRepository otpConfirmationRepository,
UserManager<ApplicationUser> userManager,
IEmailSender emailSender)
{
_otpConfirmationRepository = otpConfirmationRepository;
_userManager = userManager;
_emailSender = emailSender;
}

public async Task<Result<Unit>> GenerateAsync(Guid userId, string purpose)
{
var otpConfirmation = await _otpConfirmationRepository.GetByUserIdAndName(userId, purpose);

if (otpConfirmation is null || IsRetryTimeElapsed(otpConfirmation))
{
var token = GenerateRandom6DigitCode();

await CreateOrUpdateOtpConfirmation(userId, purpose, token);

var user = await _userManager.FindByIdAsync(userId.ToString());

await _emailSender.SendEmail(ConfirmationEmailTemplate.ToEmailMessage(user!.Email!, token), isBodyHtml: true);

return Unit.Default;
}

var minutesDifference = CalculateMinutesDifference(otpConfirmation.RetryDateTimeUtc!.Value);
var exception = new BadRequestException($"Please try again after {minutesDifference} minutes later");
return new Result<Unit>(exception);
}

public async Task<Result<bool>> ValidateAsync(Guid userId, string purpose, string token, int maxRetryCount)
{
// get token using purpose
var otpConfirmation = await _otpConfirmationRepository.GetByUserIdAndName(userId, purpose);

if (otpConfirmation is null || IsRetryTimeElapsed(otpConfirmation))
{
var exception = new BadRequestException("Please create new confirmation code");
return new Result<bool>(exception);
}

// check max retry count is passed
if (otpConfirmation.RetryCount > maxRetryCount)
{
var exception = new BadRequestException("You've exceeded the maximum code usage");
return new Result<bool>(exception);
}

// if invalid token then increment maxRetryCount
if (token != otpConfirmation.Value)
{
otpConfirmation.RetryCount++;
await _otpConfirmationRepository.UpdateAsync(otpConfirmation);
var exception = new BadRequestException("Invalid code");
return new Result<bool>(exception);
}

// if code reaches this part everything is ok and email can be confirmed

// confirm email
var user = await _userManager.FindByIdAsync(userId.ToString());
user!.EmailConfirmed = true;
await _userManager.UpdateAsync(user);

// remove otpConfirmation
await _otpConfirmationRepository.DeleteAsync(otpConfirmation);

return true;
}

private string GenerateRandom6DigitCode()
{
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[4]; // Dört bayt uzunluğunda bir dizi oluşturuyoruz.
rng.GetBytes(bytes); // Rastgele baytları dolduruyoruz.
int code = BitConverter.ToInt32(bytes, 0) % 1000000; // 6 haneli bir sayı üretiyoruz.
if (code < 0) code *= -1; // Negatif bir sayıyı pozitife çeviriyoruz.
return code.ToString("D6"); // Format as a 6-digit string
}
private bool IsRetryTimeElapsed(OtpConfirmation otpConfirmation)
{
var now = DateTimeOffset.UtcNow;
return (otpConfirmation.RetryDateTimeUtc.HasValue && now >= otpConfirmation.RetryDateTimeUtc.Value);
}

private double CalculateMinutesDifference(DateTimeOffset retryDateTime)
{
var now = DateTimeOffset.UtcNow;
var timeDifference = retryDateTime - now;
return timeDifference.TotalMinutes + 1;
}

private async Task CreateOrUpdateOtpConfirmation(Guid userId, string purpose, string token)
{
var retryDateTime = DateTimeOffset.Now.AddMinutes(15);
var otpConfirmation = new OtpConfirmation()
{
UserId = userId,
Name = purpose,
RetryDateTimeUtc = retryDateTime,
RetryCount = 0,
Value = token,
};

await _otpConfirmationRepository.CreateAsync(otpConfirmation);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void Configure(EntityTypeBuilder<OtpConfirmation> builder)
.IsRequired()
.HasMaxLength(10);

builder.Property(o => o.RetryDateTime)
builder.Property(o => o.RetryDateTimeUtc)
.IsRequired(false);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ protected override void Up(MigrationBuilder migrationBuilder)
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Value = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
RetryCount = table.Column<byte>(type: "tinyint", nullable: false, defaultValue: (byte)0),
RetryDateTime = table.Column<DateTime>(type: "datetime2", nullable: true)
RetryDateTimeUtc = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true)
},
constraints: table =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnType("tinyint")
.HasDefaultValue((byte)0);

b.Property<DateTime?>("RetryDateTime")
.HasColumnType("datetime2");
b.Property<DateTimeOffset?>("RetryDateTimeUtc")
.HasColumnType("datetimeoffset");

b.Property<string>("Value")
.IsRequired()
Expand Down
1 change: 1 addition & 0 deletions Unitagram.Persistence/PersistenceServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static IServiceCollection AddPersistenceServices(this IServiceCollection
services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddScoped<IUniversityRepository, UniversityRepository>();
services.AddScoped<IUniversityUserRepository, UniversityUserRepository>();
services.AddScoped<IOtpConfirmationRepository, OtpConfirmationRepository>();

return services;
}
Expand Down
23 changes: 23 additions & 0 deletions Unitagram.Persistence/Repositories/OtpConfirmationRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Unitagram.Application.Contracts.Persistence;
using Unitagram.Domain;
using Unitagram.Persistence.DatabaseContext;

namespace Unitagram.Persistence.Repositories;

public class OtpConfirmationRepository : GenericRepository<OtpConfirmation>, IOtpConfirmationRepository
{
public OtpConfirmationRepository(UnitagramDatabaseContext context) : base(context)
{

}

public async Task<OtpConfirmation?> GetByUserIdAndName(Guid userId, string name)
{
var otpConfirmation = await _context.OtpConfirmation
.Where(o => o.UserId == userId && o.Name == name)
.FirstOrDefaultAsync();

return otpConfirmation;
}
}

0 comments on commit 7d47776

Please sign in to comment.