Files
ColaFlow/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommandHandler.cs
Yaojia Wang 3dcecc656f feat(backend): Implement email verification flow - Phase 2
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>
2025-11-03 21:30:40 +01:00

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);
}
}