Files
ColaFlow/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Persistence/IdentityDbContext.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

91 lines
3.2 KiB
C#

using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
using ColaFlow.Modules.Identity.Domain.Entities;
using ColaFlow.Modules.Identity.Infrastructure.Services;
using ColaFlow.Shared.Kernel.Common;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace ColaFlow.Modules.Identity.Infrastructure.Persistence;
public class IdentityDbContext(
DbContextOptions<IdentityDbContext> options,
ITenantContext tenantContext,
IMediator mediator)
: DbContext(options)
{
public DbSet<Tenant> Tenants => Set<Tenant>();
public DbSet<User> Users => Set<User>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<UserTenantRole> UserTenantRoles => Set<UserTenantRole>();
public DbSet<EmailVerificationToken> EmailVerificationTokens => Set<EmailVerificationToken>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply all configurations from assembly
modelBuilder.ApplyConfigurationsFromAssembly(typeof(IdentityDbContext).Assembly);
// Configure Global Query Filters (automatic tenant data filtering)
ConfigureGlobalQueryFilters(modelBuilder);
}
private void ConfigureGlobalQueryFilters(ModelBuilder modelBuilder)
{
// User entity global query filter
// Automatically adds: WHERE tenant_id = @current_tenant_id
modelBuilder.Entity<User>().HasQueryFilter(u =>
!tenantContext.IsSet || u.TenantId == tenantContext.TenantId);
// Tenant entity doesn't need filter (need to query all tenants)
}
/// <summary>
/// Disable Query Filter (for admin operations)
/// </summary>
public IQueryable<T> WithoutTenantFilter<T>() where T : class
{
return Set<T>().IgnoreQueryFilters();
}
/// <summary>
/// Override SaveChangesAsync to dispatch domain events before saving
/// </summary>
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// Dispatch domain events BEFORE saving changes
await DispatchDomainEventsAsync(cancellationToken);
// Save changes to database
return await base.SaveChangesAsync(cancellationToken);
}
/// <summary>
/// Dispatch domain events to handlers via MediatR
/// </summary>
private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken)
{
// Get all aggregate roots with pending domain events
var domainEntities = ChangeTracker
.Entries<AggregateRoot>()
.Where(x => x.Entity.DomainEvents.Any())
.Select(x => x.Entity)
.ToList();
// Collect all domain events
var domainEvents = domainEntities
.SelectMany(x => x.DomainEvents)
.ToList();
// Clear events from aggregates (prevent double-publishing)
domainEntities.ForEach(entity => entity.ClearDomainEvents());
// Publish each event via MediatR
foreach (var domainEvent in domainEvents)
{
await mediator.Publish(domainEvent, cancellationToken);
}
}
}