Add complete email verification system with token-based verification. Changes: - Created EmailVerificationToken domain entity with expiration and verification tracking - Created EmailVerifiedEvent domain event for audit trail - Updated User entity with IsEmailVerified property and VerifyEmail method - Created IEmailVerificationTokenRepository interface and implementation - Created SecurityTokenService for secure token generation and SHA-256 hashing - Created EmailVerificationTokenConfiguration for EF Core mapping - Updated IdentityDbContext to include EmailVerificationTokens DbSet - Created SendVerificationEmailCommand and handler for sending verification emails - Created VerifyEmailCommand and handler for email verification - Added POST /api/auth/verify-email endpoint to AuthController - Integrated email verification into RegisterTenantCommandHandler - Registered all new services in DependencyInjection - Created and applied AddEmailVerification database migration - Build successful with no compilation errors Database Schema: - email_verification_tokens table with indexes on token_hash and user_id - 24-hour token expiration - One-time use tokens with verification tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
110 lines
4.1 KiB
C#
110 lines
4.1 KiB
C#
using ColaFlow.Modules.Identity.Application.Commands.SendVerificationEmail;
|
|
using ColaFlow.Modules.Identity.Application.Services;
|
|
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
|
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
|
|
using ColaFlow.Modules.Identity.Domain.Repositories;
|
|
using MediatR;
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace ColaFlow.Modules.Identity.Application.Commands.RegisterTenant;
|
|
|
|
public class RegisterTenantCommandHandler(
|
|
ITenantRepository tenantRepository,
|
|
IUserRepository userRepository,
|
|
IJwtService jwtService,
|
|
IPasswordHasher passwordHasher,
|
|
IRefreshTokenService refreshTokenService,
|
|
IUserTenantRoleRepository userTenantRoleRepository,
|
|
IMediator mediator,
|
|
IConfiguration configuration)
|
|
: IRequestHandler<RegisterTenantCommand, RegisterTenantResult>
|
|
{
|
|
public async Task<RegisterTenantResult> Handle(
|
|
RegisterTenantCommand request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// 1. Validate slug uniqueness
|
|
var slug = TenantSlug.Create(request.TenantSlug);
|
|
var slugExists = await tenantRepository.ExistsBySlugAsync(slug, cancellationToken);
|
|
if (slugExists)
|
|
{
|
|
throw new InvalidOperationException($"Tenant slug '{request.TenantSlug}' is already taken");
|
|
}
|
|
|
|
// 2. Create tenant
|
|
var plan = Enum.Parse<SubscriptionPlan>(request.SubscriptionPlan);
|
|
var tenant = Tenant.Create(
|
|
TenantName.Create(request.TenantName),
|
|
slug,
|
|
plan);
|
|
|
|
await tenantRepository.AddAsync(tenant, cancellationToken);
|
|
|
|
// 3. Create admin user with hashed password
|
|
var hashedPassword = passwordHasher.HashPassword(request.AdminPassword);
|
|
var adminUser = User.CreateLocal(
|
|
TenantId.Create(tenant.Id),
|
|
Email.Create(request.AdminEmail),
|
|
hashedPassword,
|
|
FullName.Create(request.AdminFullName));
|
|
|
|
await userRepository.AddAsync(adminUser, cancellationToken);
|
|
|
|
// 4. Assign TenantOwner role to admin user
|
|
var tenantOwnerRole = UserTenantRole.Create(
|
|
UserId.Create(adminUser.Id),
|
|
TenantId.Create(tenant.Id),
|
|
TenantRole.TenantOwner);
|
|
|
|
await userTenantRoleRepository.AddAsync(tenantOwnerRole, cancellationToken);
|
|
|
|
// 5. Generate JWT token with role
|
|
var accessToken = jwtService.GenerateToken(adminUser, tenant, TenantRole.TenantOwner);
|
|
|
|
// 6. Generate refresh token
|
|
var refreshToken = await refreshTokenService.GenerateRefreshTokenAsync(
|
|
adminUser,
|
|
ipAddress: null,
|
|
userAgent: null,
|
|
cancellationToken);
|
|
|
|
// 7. Send verification email (non-blocking)
|
|
var baseUrl = configuration["App:BaseUrl"] ?? "http://localhost:3000";
|
|
var sendEmailCommand = new SendVerificationEmailCommand(
|
|
adminUser.Id,
|
|
request.AdminEmail,
|
|
baseUrl);
|
|
|
|
// Fire and forget - don't wait for email to send
|
|
_ = mediator.Send(sendEmailCommand, cancellationToken);
|
|
|
|
// 8. Return result
|
|
return new RegisterTenantResult(
|
|
new Dtos.TenantDto
|
|
{
|
|
Id = tenant.Id,
|
|
Name = tenant.Name.Value,
|
|
Slug = tenant.Slug.Value,
|
|
Status = tenant.Status.ToString(),
|
|
Plan = tenant.Plan.ToString(),
|
|
SsoEnabled = tenant.SsoConfig != null,
|
|
SsoProvider = tenant.SsoConfig?.Provider.ToString(),
|
|
CreatedAt = tenant.CreatedAt,
|
|
UpdatedAt = tenant.UpdatedAt ?? tenant.CreatedAt
|
|
},
|
|
new Dtos.UserDto
|
|
{
|
|
Id = adminUser.Id,
|
|
TenantId = tenant.Id,
|
|
Email = adminUser.Email.Value,
|
|
FullName = adminUser.FullName.Value,
|
|
Status = adminUser.Status.ToString(),
|
|
AuthProvider = adminUser.AuthProvider.ToString(),
|
|
IsEmailVerified = adminUser.EmailVerifiedAt.HasValue,
|
|
CreatedAt = adminUser.CreatedAt
|
|
},
|
|
accessToken,
|
|
refreshToken);
|
|
}
|
|
}
|