diff --git a/.vscode/launch.json b/.vscode/launch.json index b66e968..8e0e147 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,7 @@ }, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_URLS": "https://localhost:7001;http://localhost:5001" + "ASPNETCORE_URLS": "http://localhost:10000" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" diff --git a/src/Application/EventHandlers/UserCreatedSendEmailEventHandler.cs b/src/Application/EventHandlers/EmailVerifiedEventHandler.cs similarity index 64% rename from src/Application/EventHandlers/UserCreatedSendEmailEventHandler.cs rename to src/Application/EventHandlers/EmailVerifiedEventHandler.cs index 8463c17..cd916fa 100644 --- a/src/Application/EventHandlers/UserCreatedSendEmailEventHandler.cs +++ b/src/Application/EventHandlers/EmailVerifiedEventHandler.cs @@ -6,17 +6,17 @@ namespace Application.EventHandlers; /// -/// Handler for sending welcome email when user is created +/// Handler for sending welcome email when user verifies their email /// -public sealed class UserCreatedSendEmailEventHandler( +public sealed class EmailVerifiedEventHandler( IEmailService emailService, - ILogger logger) - : IDomainEventHandler + ILogger logger) + : IDomainEventHandler { - public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) + public async Task Handle(EmailVerifiedEvent notification, CancellationToken cancellationToken) { logger.LogInformation( - "Sending welcome email to user {UserId} at {Email}", + "Sending welcome email to verified user {UserId} at {Email}", notification.UserId, notification.Email); @@ -36,7 +36,6 @@ await emailService.SendWelcomeEmailAsync( logger.LogError(ex, "Failed to send welcome email to {Email}", notification.Email); - // Don't throw - we don't want email failures to break the registration flow } } } diff --git a/src/Application/EventHandlers/UserCreatedEventHandler.cs b/src/Application/EventHandlers/UserCreatedEventHandler.cs new file mode 100644 index 0000000..d7ff131 --- /dev/null +++ b/src/Application/EventHandlers/UserCreatedEventHandler.cs @@ -0,0 +1,58 @@ +using Application.Common; +using Application.Interfaces; +using Domain.Events.User; +using Microsoft.Extensions.Logging; + +namespace Application.EventHandlers; + +/// +/// Handler for sending email verification when user is created +/// +public sealed class UserCreatedEventHandler( + IEmailService emailService, + ITokenGenerationService tokenService, + ILogger logger) + : IDomainEventHandler +{ + public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) + { + // If user registered with social provider (Google, etc.), email is already verified + // Skip sending verification email + if (notification.IsEmailVerified) + { + logger.LogInformation( + "User {UserId} registered with verified email (social provider). Skipping verification email.", + notification.UserId); + return; + } + + logger.LogInformation( + "Sending email verification to user {UserId} at {Email}", + notification.UserId, + notification.Email); + + try + { + // Generate email confirmation token using Identity + var token = await tokenService.GenerateEmailConfirmationTokenAsync(notification.UserId); + var verificationLink = $"http://localhost:10000/api/v1/auth/verify-email?userId={notification.UserId}&token={Uri.EscapeDataString(token)}"; + + await emailService.SendEmailVerificationAsync( + notification.Email, + notification.FullName, + verificationLink, + cancellationToken); + + logger.LogInformation( + "Verification email sent successfully to {Email}", + notification.Email); + } + catch (Exception ex) + { + logger.LogError(ex, + "Failed to send verification email to {Email}", + notification.Email); + // Don't throw - we don't want email failures to break the registration flow + } + } +} diff --git a/src/Application/Features/Auth/VerifyEmail/VerifyEmailCommand.cs b/src/Application/Features/Auth/VerifyEmail/VerifyEmailCommand.cs new file mode 100644 index 0000000..2775e87 --- /dev/null +++ b/src/Application/Features/Auth/VerifyEmail/VerifyEmailCommand.cs @@ -0,0 +1,13 @@ +using Application.Common; +using Domain.Common; + +namespace Application.Features.Auth.VerifyEmail; + +/// +/// Command to verify user's email address +/// +public sealed record VerifyEmailCommand : ICommand +{ + public required Guid UserId { get; init; } + public required string Token { get; init; } +} diff --git a/src/Application/Features/Auth/VerifyEmail/VerifyEmailCommandHandler.cs b/src/Application/Features/Auth/VerifyEmail/VerifyEmailCommandHandler.cs new file mode 100644 index 0000000..95ec393 --- /dev/null +++ b/src/Application/Features/Auth/VerifyEmail/VerifyEmailCommandHandler.cs @@ -0,0 +1,58 @@ +using Application.Common; +using Application.Interfaces; +using Domain.Common; +using Domain.Events.User; +using Microsoft.Extensions.Logging; + +namespace Application.Features.Auth.VerifyEmail; + +/// +/// Handler for verifying user's email address +/// +public sealed class VerifyEmailCommandHandler( + IUserRepository userRepository, + ITokenGenerationService tokenService, + ILogger logger) + : ICommandHandler +{ + public async Task Handle(VerifyEmailCommand request, CancellationToken cancellationToken) + { + // Find user by ID + var user = await userRepository.GetByIdAsync(request.UserId, cancellationToken); + if (user == null) + { + logger.LogWarning("User not found: {UserId}", request.UserId); + return Result.Failure(Error.NotFound("User.NotFound", "User not found")); + } + + // Check if email is already verified + var isAlreadyVerified = await userRepository.IsEmailVerifiedAsync(request.UserId, cancellationToken); + if (isAlreadyVerified) + { + logger.LogInformation("Email already confirmed for user {UserId}", request.UserId); + return Result.Success(); + } + + // Validate token using Identity's built-in token provider + // Token is stored in AspNetUserTokens table and validated automatically + var verifySuccess = await tokenService.ConfirmEmailAsync(request.UserId, request.Token); + if (!verifySuccess) + { + logger.LogError("Invalid or expired token for user {UserId}", request.UserId); + return Result.Failure(Error.Failure("User.InvalidToken", "Invalid or expired verification token")); + } + + // Raise EmailVerifiedEvent to send welcome email + user.AddDomainEvent(new EmailVerifiedEvent + { + UserId = user.Id, + Email = user.Email, + FullName = user.FullName + }); + + await userRepository.UpdateAsync(user, cancellationToken); + + logger.LogInformation("Email verified successfully for user {UserId}", request.UserId); + return Result.Success(); + } +} diff --git a/src/Application/Interfaces/IEmailService.cs b/src/Application/Interfaces/IEmailService.cs index 0c9fd2e..b9f5b91 100644 --- a/src/Application/Interfaces/IEmailService.cs +++ b/src/Application/Interfaces/IEmailService.cs @@ -6,7 +6,16 @@ namespace Application.Interfaces; public interface IEmailService { /// - /// Send welcome email to new user + /// Send email verification to new user + /// + /// User's email address + /// User's full name + /// Email verification link + /// Cancellation token + Task SendEmailVerificationAsync(string email, string fullName, string verificationLink, CancellationToken cancellationToken = default); + + /// + /// Send welcome email to verified user /// /// User's email address /// User's full name diff --git a/src/Application/Interfaces/IEmailTemplateService.cs b/src/Application/Interfaces/IEmailTemplateService.cs new file mode 100644 index 0000000..e1caf6c --- /dev/null +++ b/src/Application/Interfaces/IEmailTemplateService.cs @@ -0,0 +1,22 @@ +namespace Application.Interfaces; + +/// +/// Service for generating email templates +/// +public interface IEmailTemplateService +{ + /// + /// Generate welcome email template + /// + string GetWelcomeEmailTemplate(string fullName); + + /// + /// Generate password reset email template + /// + string GetPasswordResetEmailTemplate(string fullName, string resetLink); + + /// + /// Generate email verification template + /// + string GetEmailVerificationTemplate(string fullName, string verificationLink); +} diff --git a/src/Application/Interfaces/ITokenGenerationService.cs b/src/Application/Interfaces/ITokenGenerationService.cs new file mode 100644 index 0000000..6ffee6e --- /dev/null +++ b/src/Application/Interfaces/ITokenGenerationService.cs @@ -0,0 +1,17 @@ +namespace Application.Interfaces; + +/// +/// Service for generating verification tokens +/// +public interface ITokenGenerationService +{ + /// + /// Generate email confirmation token for user + /// + Task GenerateEmailConfirmationTokenAsync(Guid userId); + + /// + /// Confirm email with token + /// + Task ConfirmEmailAsync(Guid userId, string token); +} diff --git a/src/Application/Interfaces/IUserRepository.cs b/src/Application/Interfaces/IUserRepository.cs index c3ab403..665f837 100644 --- a/src/Application/Interfaces/IUserRepository.cs +++ b/src/Application/Interfaces/IUserRepository.cs @@ -26,4 +26,14 @@ public interface IUserRepository /// Create user /// Task CreateAsync(User user, CancellationToken cancellationToken = default); + + /// + /// Verify user's email + /// + Task VerifyEmailAsync(Guid userId, CancellationToken cancellationToken = default); + + /// + /// Check if user's email is verified + /// + Task IsEmailVerifiedAsync(Guid userId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/Events/User/EmailVerifiedEvent.cs b/src/Domain/Events/User/EmailVerifiedEvent.cs new file mode 100644 index 0000000..9a8ff8a --- /dev/null +++ b/src/Domain/Events/User/EmailVerifiedEvent.cs @@ -0,0 +1,14 @@ +using Domain.Common; + +namespace Domain.Events.User; + +/// +/// Event raised when user verifies their email +/// +public sealed record EmailVerifiedEvent : IDomainEvent +{ + public required Guid UserId { get; init; } + public required string Email { get; init; } + public required string FullName { get; init; } + public DateTime OccurredOn { get; init; } = DateTime.UtcNow; +} diff --git a/src/Domain/Events/User/UserCreatedEvent.cs b/src/Domain/Events/User/UserCreatedEvent.cs index 014ed81..79cfb00 100644 --- a/src/Domain/Events/User/UserCreatedEvent.cs +++ b/src/Domain/Events/User/UserCreatedEvent.cs @@ -11,14 +11,16 @@ public sealed record UserCreatedEvent : IDomainEvent public string Email { get; } public string FullName { get; } public List Roles { get; } + public bool IsEmailVerified { get; } public DateTime OccurredOn { get; } - public UserCreatedEvent(Guid userId, string email, string fullName, List roles) + public UserCreatedEvent(Guid userId, string email, string fullName, List roles, bool isEmailVerified = false) { UserId = userId; Email = email; FullName = fullName; Roles = roles; + IsEmailVerified = isEmailVerified; OccurredOn = DateTime.UtcNow; } } diff --git a/src/Domain/Events/User/UserDeactivatedEvent.cs b/src/Domain/Events/User/UserDeactivatedEvent.cs deleted file mode 100644 index c75f62d..0000000 --- a/src/Domain/Events/User/UserDeactivatedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Domain.Common; - -namespace Domain.Events.User; - -/// -/// Domain event raised when a user is deactivated -/// -public sealed record UserDeactivatedEvent : IDomainEvent -{ - public Guid UserId { get; } - public DateTime OccurredOn { get; } - - public UserDeactivatedEvent(Guid userId) - { - UserId = userId; - OccurredOn = DateTime.UtcNow; - } -} diff --git a/src/Domain/Events/User/UserUpdatedEvent.cs b/src/Domain/Events/User/UserUpdatedEvent.cs deleted file mode 100644 index c391460..0000000 --- a/src/Domain/Events/User/UserUpdatedEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Domain.Common; - -namespace Domain.Events.User; - -/// -/// Domain event raised when user information is updated -/// -public sealed record UserUpdatedEvent : IDomainEvent -{ - public Guid UserId { get; } - public string FullName { get; } - public string? PhoneNumber { get; } - public DateTime OccurredOn { get; } - - public UserUpdatedEvent(Guid userId, string fullName, string? phoneNumber) - { - UserId = userId; - FullName = fullName; - PhoneNumber = phoneNumber; - OccurredOn = DateTime.UtcNow; - } -} diff --git a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs index 903312d..c56364b 100644 --- a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Infrastructure.Data.Contexts; using Infrastructure.Repositories; using Infrastructure.Services; +using Infrastructure.Services.Email; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -96,9 +97,11 @@ private static IServiceCollection AddInfrastructureApplicationServices(this ISer // Authentication & Security services.AddScoped(); services.AddScoped(); + services.AddScoped(); - // Email Service + // Email Services services.AddScoped(); + services.AddScoped(); // Domain Events services.AddScoped(); diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 7e9c0d2..ca9462c 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -31,4 +31,13 @@ + + + PreserveNewest + + + PreserveNewest + + + diff --git a/src/Infrastructure/Repositories/UserRepository.cs b/src/Infrastructure/Repositories/UserRepository.cs index a382003..0886270 100644 --- a/src/Infrastructure/Repositories/UserRepository.cs +++ b/src/Infrastructure/Repositories/UserRepository.cs @@ -1,6 +1,8 @@ using Application.Interfaces; using Domain.Entities; using Infrastructure.Data.Contexts; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace Infrastructure.Repositories; @@ -11,10 +13,12 @@ namespace Infrastructure.Repositories; public sealed class UserRepository : IUserRepository { private readonly DataContext _context; + private readonly UserManager _userManager; - public UserRepository(DataContext context) + public UserRepository(DataContext context, UserManager userManager) { _context = context; + _userManager = userManager; } /// @@ -53,4 +57,29 @@ public async Task CreateAsync(User user, CancellationToken cancellationTok await _context.SaveChangesAsync(cancellationToken); return entry.Entity; } + + /// + /// Verify user's email + /// + public async Task VerifyEmailAsync(Guid userId, CancellationToken cancellationToken = default) + { + var identityUser = await _userManager.FindByIdAsync(userId.ToString()); + if (identityUser == null) + { + return false; + } + + identityUser.EmailConfirmed = true; + var result = await _userManager.UpdateAsync(identityUser); + return result.Succeeded; + } + + /// + /// Check if user's email is verified + /// + public async Task IsEmailVerifiedAsync(Guid userId, CancellationToken cancellationToken = default) + { + var identityUser = await _userManager.FindByIdAsync(userId.ToString()); + return identityUser?.EmailConfirmed ?? false; + } } diff --git a/src/Infrastructure/Services/Email/.gitkeep b/src/Infrastructure/Services/Email/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Infrastructure/Services/Email/EmailService.cs b/src/Infrastructure/Services/Email/EmailService.cs new file mode 100644 index 0000000..9080960 --- /dev/null +++ b/src/Infrastructure/Services/Email/EmailService.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Mail; +using Application.Interfaces; +using Infrastructure.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Infrastructure.Services.Email; + +/// +/// Email service implementation using SMTP +/// +public sealed class EmailService : IEmailService +{ + private readonly EmailSettings _emailSettings; + private readonly IEmailTemplateService _templateService; + private readonly ILogger _logger; + + public EmailService( + IOptions emailSettings, + IEmailTemplateService templateService, + ILogger logger) + { + _emailSettings = emailSettings.Value; + _templateService = templateService; + _logger = logger; + } + + public async Task SendEmailVerificationAsync(string email, string fullName, string verificationLink, CancellationToken cancellationToken = default) + { + var subject = "Verify Your Email - Legal Assistant"; + var body = _templateService.GetEmailVerificationTemplate(fullName, verificationLink); + + await SendEmailAsync(email, subject, body, cancellationToken); + } + + public async Task SendWelcomeEmailAsync(string email, string fullName, CancellationToken cancellationToken = default) + { + var subject = "Welcome to Legal Assistant!"; + var body = _templateService.GetWelcomeEmailTemplate(fullName); + + await SendEmailAsync(email, subject, body, cancellationToken); + } + + public async Task SendEmailAsync(string to, string subject, string body, CancellationToken cancellationToken = default) + { + try + { + // Check if email is enabled + if (!_emailSettings.Enabled) + { + _logger.LogWarning("Email service is disabled. Email not sent to {Email}", to); + return; + } + + using var client = new SmtpClient(_emailSettings.SmtpHost, _emailSettings.SmtpPort) + { + Credentials = new NetworkCredential(_emailSettings.SmtpUsername, _emailSettings.SmtpPassword), + EnableSsl = _emailSettings.EnableSsl + }; + + using var mailMessage = new MailMessage + { + From = new MailAddress(_emailSettings.FromEmail, _emailSettings.FromName), + Subject = subject, + Body = body, + IsBodyHtml = true + }; + + mailMessage.To.Add(to); + + await client.SendMailAsync(mailMessage, cancellationToken); + + _logger.LogInformation("Email sent successfully to {Email}", to); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send email to {Email}", to); + // Don't throw - we don't want email failures to break the registration flow + } + } +} diff --git a/src/Infrastructure/Services/Email/EmailTemplateService.cs b/src/Infrastructure/Services/Email/EmailTemplateService.cs new file mode 100644 index 0000000..421950c --- /dev/null +++ b/src/Infrastructure/Services/Email/EmailTemplateService.cs @@ -0,0 +1,58 @@ +using Application.Interfaces; +using Microsoft.Extensions.Configuration; + +namespace Infrastructure.Services.Email; + +/// +/// Service for generating email templates from HTML files +/// +public sealed class EmailTemplateService : IEmailTemplateService +{ + private readonly string _templateBasePath; + private readonly string _baseUrl; + + public EmailTemplateService(IConfiguration configuration) + { + // Get the base path for templates (Infrastructure/Templates/Email) + _templateBasePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Templates", "Email"); + _baseUrl = configuration["AppSettings:BaseUrl"] ?? "http://localhost:10000"; + } + + public string GetWelcomeEmailTemplate(string fullName) + { + var template = LoadTemplate("WelcomeEmail.html"); + return template + .Replace("{{FullName}}", fullName) + .Replace("{{BaseUrl}}", _baseUrl); + } + + public string GetPasswordResetEmailTemplate(string fullName, string resetLink) + { + var template = LoadTemplate("PasswordResetEmail.html"); + return template + .Replace("{{FullName}}", fullName) + .Replace("{{ResetLink}}", resetLink) + .Replace("{{BaseUrl}}", _baseUrl); + } + + public string GetEmailVerificationTemplate(string fullName, string verificationLink) + { + var template = LoadTemplate("EmailVerificationEmail.html"); + return template + .Replace("{{FullName}}", fullName) + .Replace("{{VerificationLink}}", verificationLink) + .Replace("{{BaseUrl}}", _baseUrl); + } + + private string LoadTemplate(string templateName) + { + var templatePath = Path.Combine(_templateBasePath, templateName); + + if (!File.Exists(templatePath)) + { + throw new FileNotFoundException($"Email template not found: {templateName}", templatePath); + } + + return File.ReadAllText(templatePath); + } +} diff --git a/src/Infrastructure/Services/EmailService.cs b/src/Infrastructure/Services/EmailService.cs deleted file mode 100644 index 37a1319..0000000 --- a/src/Infrastructure/Services/EmailService.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Net; -using System.Net.Mail; -using Application.Interfaces; -using Infrastructure.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Infrastructure.Services; - -/// -/// Email service implementation using SMTP -/// -public sealed class EmailService : IEmailService -{ - private readonly EmailSettings _emailSettings; - private readonly ILogger _logger; - - public EmailService(IOptions emailSettings, ILogger logger) - { - _emailSettings = emailSettings.Value; - _logger = logger; - } - - public async Task SendWelcomeEmailAsync(string email, string fullName, CancellationToken cancellationToken = default) - { - var subject = "Welcome to Legal Assistant!"; - var body = GetWelcomeEmailTemplate(fullName); - - await SendEmailAsync(email, subject, body, cancellationToken); - } - - public async Task SendEmailAsync(string to, string subject, string body, CancellationToken cancellationToken = default) - { - try - { - // Check if email is enabled - if (!_emailSettings.Enabled) - { - _logger.LogWarning("Email service is disabled. Email not sent to {Email}", to); - return; - } - - using var client = new SmtpClient(_emailSettings.SmtpHost, _emailSettings.SmtpPort) - { - Credentials = new NetworkCredential(_emailSettings.SmtpUsername, _emailSettings.SmtpPassword), - EnableSsl = _emailSettings.EnableSsl - }; - - using var mailMessage = new MailMessage - { - From = new MailAddress(_emailSettings.FromEmail, _emailSettings.FromName), - Subject = subject, - Body = body, - IsBodyHtml = true - }; - - mailMessage.To.Add(to); - - await client.SendMailAsync(mailMessage, cancellationToken); - - _logger.LogInformation("Email sent successfully to {Email}", to); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to send email to {Email}", to); - // Don't throw - we don't want email failures to break the registration flow - } - } - - private string GetWelcomeEmailTemplate(string fullName) - { - return $@" - - - - - - -
-
-

Welcome to Legal Assistant!

-
-
-

Hello {fullName},

-

Thank you for registering with Legal Assistant!

-

We're excited to have you on board. Your account has been successfully created and you can now start using our services.

-

Here's what you can do:

-
    -
  • Start conversations with our AI legal assistant
  • -
  • Get instant answers to your legal questions
  • -
  • Access your conversation history anytime
  • -
-

If you have any questions or need assistance, feel free to reach out to our support team.

-

Best regards,
The Legal Assistant Team

-
- -
- -"; - } -} diff --git a/src/Infrastructure/Services/TokenGenerationService.cs b/src/Infrastructure/Services/TokenGenerationService.cs new file mode 100644 index 0000000..b14ae7c --- /dev/null +++ b/src/Infrastructure/Services/TokenGenerationService.cs @@ -0,0 +1,48 @@ +using Application.Interfaces; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; + +namespace Infrastructure.Services; + +/// +/// Service for generating verification tokens using ASP.NET Identity +/// +public sealed class TokenGenerationService : ITokenGenerationService +{ + private readonly UserManager _userManager; + + public TokenGenerationService(UserManager userManager) + { + _userManager = userManager; + } + + /// + /// Generate email confirmation token for user + /// Token is automatically stored in AspNetUserTokens table + /// + public async Task GenerateEmailConfirmationTokenAsync(Guid userId) + { + var user = await _userManager.FindByIdAsync(userId.ToString()) + ?? throw new InvalidOperationException($"User not found: {userId}"); + + // Identity automatically stores this token in AspNetUserTokens table + // Token includes: UserId, LoginProvider, Name, Value + return await _userManager.GenerateEmailConfirmationTokenAsync(user); + } + + /// + /// Confirm email with token + /// Identity automatically validates token from AspNetUserTokens table + /// + public async Task ConfirmEmailAsync(Guid userId, string token) + { + var user = await _userManager.FindByIdAsync(userId.ToString()); + if (user == null) + { + return false; + } + + var result = await _userManager.ConfirmEmailAsync(user, token); + return result.Succeeded; + } +} diff --git a/src/Infrastructure/Templates/Email/EmailVerificationEmail.html b/src/Infrastructure/Templates/Email/EmailVerificationEmail.html new file mode 100644 index 0000000..c24a0f3 --- /dev/null +++ b/src/Infrastructure/Templates/Email/EmailVerificationEmail.html @@ -0,0 +1,175 @@ + + + + + + + + + + + diff --git a/src/Infrastructure/Templates/Email/PasswordResetEmail.html b/src/Infrastructure/Templates/Email/PasswordResetEmail.html new file mode 100644 index 0000000..a07454c --- /dev/null +++ b/src/Infrastructure/Templates/Email/PasswordResetEmail.html @@ -0,0 +1,174 @@ + + + + + + + + + + + diff --git a/src/Infrastructure/Templates/Email/WelcomeEmail.html b/src/Infrastructure/Templates/Email/WelcomeEmail.html new file mode 100644 index 0000000..144ba1d --- /dev/null +++ b/src/Infrastructure/Templates/Email/WelcomeEmail.html @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Web.Api/Controllers/V1/AuthController.cs b/src/Web.Api/Controllers/V1/AuthController.cs index 1fc3111..9932f07 100644 --- a/src/Web.Api/Controllers/V1/AuthController.cs +++ b/src/Web.Api/Controllers/V1/AuthController.cs @@ -1,5 +1,6 @@ using Application.Features.Auth.Login; using Application.Features.Auth.Register; +using Application.Features.Auth.VerifyEmail; using Domain.Common; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -58,4 +59,36 @@ public async Task Register([FromBody] RegisterCommand command) _ => BadRequest(ApiResponse.CreateFailure(result.Error.Description)) }; } + + /// + /// Verify user's email address + /// + /// User ID + /// Verification token + /// Verification result + [HttpGet("verify-email")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task VerifyEmail([FromQuery] Guid userId, [FromQuery] string token) + { + var command = new VerifyEmailCommand + { + UserId = userId, + Token = token + }; + + var result = await mediator.Send(command); + + if (result.IsSuccess) + { + return Ok(ApiResponse.CreateSuccess("Email verified successfully! You can now log in.")); + } + + return result.Error!.Type switch + { + ErrorType.NotFound => NotFound(ApiResponse.CreateFailure(result.Error.Description)), + _ => BadRequest(ApiResponse.CreateFailure(result.Error.Description)) + }; + } } diff --git a/src/Web.Api/Program.cs b/src/Web.Api/Program.cs index a38fa16..33b5656 100644 --- a/src/Web.Api/Program.cs +++ b/src/Web.Api/Program.cs @@ -19,6 +19,9 @@ // Add Serilog builder.Host.AddSerilog(); + // Add HttpContextAccessor for accessing HTTP request context in services + builder.Services.AddHttpContextAccessor(); + // Add services to the container. builder.Services.AddControllers();// Add Infrastructure services (Database, Repositories, Services) builder.Services.AddInfrastructureServices(builder.Configuration); @@ -50,6 +53,9 @@ // Add Global Exception Middleware app.UseMiddleware(); + // Serve static files (images, css, js, etc.) + app.UseStaticFiles(); + // Only use HTTPS redirection in production or when HTTPS is properly configured if (app.Environment.IsProduction() || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_HTTPS_PORT"))) { diff --git a/src/Web.Api/appsettings.Development.json b/src/Web.Api/appsettings.Development.json index 0d57799..1374c08 100644 --- a/src/Web.Api/appsettings.Development.json +++ b/src/Web.Api/appsettings.Development.json @@ -5,6 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "AppSettings": { + "BaseUrl": "http://localhost:10000" + }, "Email": { "Enabled": true, "FromEmail": "legalassistant.dut@gmail.com", diff --git a/src/Web.Api/appsettings.json b/src/Web.Api/appsettings.json index 299d248..078b2ad 100644 --- a/src/Web.Api/appsettings.json +++ b/src/Web.Api/appsettings.json @@ -6,6 +6,9 @@ } }, "AllowedHosts": "*", + "AppSettings": { + "BaseUrl": "https://localhost:7001" + }, "ConnectionStrings": { "DefaultConnection": "Host=69.164.244.36;Database=law_chatbot;Username=postgres;Password=Admin@123;Port=5555" }, diff --git a/src/Web.Api/wwwroot/images/email/banner.webp b/src/Web.Api/wwwroot/images/email/banner.webp new file mode 100644 index 0000000..5208e82 Binary files /dev/null and b/src/Web.Api/wwwroot/images/email/banner.webp differ diff --git a/src/Web.Api/wwwroot/images/email/logo.png b/src/Web.Api/wwwroot/images/email/logo.png new file mode 100644 index 0000000..168957c Binary files /dev/null and b/src/Web.Api/wwwroot/images/email/logo.png differ