# Day 7 Technical Architecture
# Email Service & User Management

**Version**: 1.0
**Date**: 2025-11-03
**Sprint**: M1 Sprint 2 - Day 7
**Author**: Architecture Team
**Status**: Ready for Implementation
**Related PRD**: [DAY7-PRD.md](./DAY7-PRD.md)

---

## Table of Contents

1. [Overview](#1-overview)
2. [Technology Stack Decisions](#2-technology-stack-decisions)
3. [Core Architecture Components](#3-core-architecture-components)
4. [Database Schema Design](#4-database-schema-design)
5. [Security Architecture (ADRs)](#5-security-architecture-adrs)
6. [Integration Architecture](#6-integration-architecture)
7. [Implementation Phases](#7-implementation-phases)
8. [Deployment Considerations](#8-deployment-considerations)

---

## 1. Overview

### 1.1 Scope

Day 7 implements the foundation for secure user communication and team collaboration:

| Component | Purpose | Priority |
|-----------|---------|----------|
| **Email Service** | Send transactional emails (verification, reset, invitations) | P0 |
| **Email Verification** | Validate user email ownership | P0 |
| **Password Reset** | Self-service password recovery | P0 |
| **User Invitations** | Team member onboarding | P0 |

### 1.2 Architecture Principles

1. **Abstraction First**: Email provider is pluggable (SendGrid, SMTP, Mock)
2. **Security by Design**: All tokens hashed (SHA-256), short expiration windows
3. **Fail-Safe Operations**: Email delivery failures don't block user actions
4. **Domain-Driven Design**: Token entities are first-class domain objects
5. **Modular Boundaries**: Email service is infrastructure concern, separated from domain logic

### 1.3 System Context

```
┌──────────────────────────────────────────────────────────┐
│                    Application Layer                     │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────┐  │
│  │ Register    │  │ ForgotPassword│  │ InviteUser     │  │
│  │ Tenant      │  │ Command      │  │ Command        │  │
│  └──────┬──────┘  └──────┬───────┘  └────────┬───────┘  │
└─────────┼─────────────────┼──────────────────┼──────────┘
          │                 │                  │
          └─────────────────┼──────────────────┘
                            ↓
┌──────────────────────────────────────────────────────────┐
│                  Infrastructure Layer                    │
│  ┌───────────────────────────────────────────────────┐   │
│  │              IEmailService (Abstraction)          │   │
│  │  + SendEmailAsync(EmailMessage message)           │   │
│  └───────────────────┬───────────────────────────────┘   │
│                      │                                    │
│       ┌──────────────┼──────────────┐                     │
│       ↓              ↓              ↓                     │
│  ┌─────────┐  ┌──────────┐  ┌──────────┐                │
│  │SendGrid │  │  SMTP    │  │  Mock    │                │
│  │ Service │  │ Service  │  │ Service  │                │
│  └─────────┘  └──────────┘  └──────────┘                │
│                                                           │
│  ┌───────────────────────────────────────────────────┐   │
│  │         IEmailTemplateRenderer                    │   │
│  │  + RenderAsync(templateName, data)                │   │
│  └───────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────┘
                            ↓
┌──────────────────────────────────────────────────────────┐
│                      Domain Layer                        │
│  ┌──────────────────┐  ┌──────────────────┐             │
│  │ EmailVerification│  │ PasswordReset    │             │
│  │ Token (Entity)   │  │ Token (Entity)   │             │
│  └──────────────────┘  └──────────────────┘             │
│  ┌──────────────────┐                                    │
│  │ Invitation       │                                    │
│  │ (Entity)         │                                    │
│  └──────────────────┘                                    │
└──────────────────────────────────────────────────────────┘
```

---

## 2. Technology Stack Decisions

### 2.1 Email Service Provider

**Decision**: Hybrid approach with **SendGrid as primary, SMTP as fallback**

| Provider | Use Case | Pros | Cons |
|----------|----------|------|------|
| **SendGrid** | Production | • 99.9% delivery rate<br>• Built-in analytics<br>• Bounce handling<br>• 100 emails/day free tier | • External dependency<br>• Requires API key |
| **SMTP** | Development & Self-hosted | • No external dependencies<br>• Air-gapped support<br>• Works with MailHog/Papercut | • Lower delivery rate<br>• Manual spam management |
| **Mock** | Testing | • No actual sends<br>• Fast tests<br>• File logging | • Not for production |

**Implementation Strategy**:
```csharp
// Configuration-driven selection
services.AddScoped<IEmailService>(provider =>
{
    var config = provider.GetRequiredService<EmailSettings>();
    return config.Provider switch
    {
        "SendGrid" => new SendGridEmailService(config.SendGrid),
        "Smtp" => new SmtpEmailService(config.Smtp),
        "Mock" => new MockEmailService(config.MockSettings),
        _ => throw new InvalidOperationException($"Unknown email provider: {config.Provider}")
    };
});
```

**Rationale**:
- **Production**: SendGrid ensures high deliverability for critical emails (password resets, verifications)
- **Development**: SMTP with MailHog allows offline development without external dependencies
- **Testing**: Mock service enables fast, deterministic unit/integration tests

### 2.2 Email Template Engine

**Decision**: Use **C# String Interpolation + Razor Templates** (Hybrid)

| Option | When to Use | Pros | Cons |
|--------|-------------|------|------|
| **C# String Interpolation** | Simple templates (text-only) | • No dependencies<br>• Fast rendering<br>• Type-safe | • Limited HTML support<br>• No layouts |
| **Razor Templates** | Complex HTML templates | • Full HTML support<br>• Layout inheritance<br>• IntelliSense | • RazorLight dependency<br>• Slower rendering |

**Implementation**:
```csharp
// Simple text emails: String interpolation
public string RenderSimple(string userName, string link) =>
    $"Hi {userName},\n\nClick here to verify: {link}";

// Complex HTML emails: Razor
public async Task<string> RenderHtmlAsync(string templateName, object model)
{
    var engine = new RazorLightEngineBuilder()
        .UseFileSystemProject(Path.Combine(AppContext.BaseDirectory, "EmailTemplates"))
        .UseMemoryCachingProvider()
        .Build();

    return await engine.CompileRenderAsync(templateName, model);
}
```

**Day 7 Recommendation**: Start with **String Interpolation** for simplicity. Upgrade to Razor if HTML complexity grows.

### 2.3 Token Storage & Hashing

**Decision**: Database storage with **SHA-256 hashing**

| Aspect | Decision | Rationale |
|--------|----------|-----------|
| **Storage** | PostgreSQL tables (3 new tables) | • ACID guarantees<br>• Existing infrastructure<br>• Easy expiration queries |
| **Hashing Algorithm** | SHA-256 | • Fast (token lookup performance)<br>• Sufficient for time-limited tokens<br>• Industry standard |
| **Token Format** | Base64URL-encoded random 256-bit | • URL-safe<br>• Cryptographically secure<br>• Collision-resistant |

**Alternative Rejected**: Redis for token storage
- **Reason**: Adds operational complexity for marginal performance gain. PostgreSQL with indexing is sufficient for Day 7 scale.
- **Future**: Revisit for 10M+ tokens/day scenarios.

---

## 3. Core Architecture Components

### 3.1 Email Service Abstraction

#### Interface Design

```csharp
namespace ColaFlow.Modules.Identity.Application.Services;

/// <summary>
/// Abstraction for sending transactional emails.
/// Implementations: SendGrid, SMTP, Mock (for testing).
/// </summary>
public interface IEmailService
{
    /// <summary>
    /// Send an email asynchronously.
    /// </summary>
    /// <param name="message">Email message details</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>EmailSendResult with success status and metadata</returns>
    Task<EmailSendResult> SendEmailAsync(
        EmailMessage message,
        CancellationToken cancellationToken = default);
}

/// <summary>
/// Email message data transfer object.
/// </summary>
public record EmailMessage(
    string ToAddress,
    string ToName,
    string Subject,
    string TextBody,
    string? HtmlBody = null,
    string? FromAddress = null,
    string? FromName = null);

/// <summary>
/// Result of email send operation.
/// </summary>
public record EmailSendResult(
    bool Success,
    string? MessageId = null,
    string? ErrorMessage = null);
```

#### Implementation: SendGrid

```csharp
namespace ColaFlow.Modules.Identity.Infrastructure.Services;

public class SendGridEmailService : IEmailService
{
    private readonly SendGridClient _client;
    private readonly string _defaultFromAddress;
    private readonly string _defaultFromName;
    private readonly ILogger<SendGridEmailService> _logger;

    public SendGridEmailService(
        SendGridSettings settings,
        ILogger<SendGridEmailService> logger)
    {
        _client = new SendGridClient(settings.ApiKey);
        _defaultFromAddress = settings.FromAddress;
        _defaultFromName = settings.FromName;
        _logger = logger;
    }

    public async Task<EmailSendResult> SendEmailAsync(
        EmailMessage message,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var from = new EmailAddress(
                message.FromAddress ?? _defaultFromAddress,
                message.FromName ?? _defaultFromName);

            var to = new EmailAddress(message.ToAddress, message.ToName);

            var msg = MailHelper.CreateSingleEmail(
                from,
                to,
                message.Subject,
                message.TextBody,
                message.HtmlBody);

            var response = await _client.SendEmailAsync(msg, cancellationToken);

            if (response.IsSuccessStatusCode)
            {
                _logger.LogInformation(
                    "Email sent successfully to {Email}. Subject: {Subject}",
                    message.ToAddress,
                    message.Subject);

                return new EmailSendResult(
                    Success: true,
                    MessageId: response.Headers.GetValues("X-Message-Id").FirstOrDefault());
            }

            var body = await response.Body.ReadAsStringAsync(cancellationToken);
            _logger.LogWarning(
                "Email send failed. StatusCode: {StatusCode}, Body: {Body}",
                response.StatusCode,
                body);

            return new EmailSendResult(
                Success: false,
                ErrorMessage: $"SendGrid returned {response.StatusCode}");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Exception sending email to {Email}", message.ToAddress);
            return new EmailSendResult(
                Success: false,
                ErrorMessage: ex.Message);
        }
    }
}
```

**Key Design Decisions**:
1. **Non-blocking**: Email failures return error result but don't throw exceptions
2. **Logging**: All sends logged for audit trail
3. **Graceful Degradation**: Commands continue even if email fails (see Integration section)

#### Implementation: SMTP (Simplified)

```csharp
public class SmtpEmailService : IEmailService
{
    private readonly SmtpSettings _settings;
    private readonly ILogger<SmtpEmailService> _logger;

    public async Task<EmailSendResult> SendEmailAsync(
        EmailMessage message,
        CancellationToken cancellationToken = default)
    {
        try
        {
            using var smtpClient = new SmtpClient(_settings.Host, _settings.Port)
            {
                Credentials = new NetworkCredential(_settings.Username, _settings.Password),
                EnableSsl = _settings.EnableSsl
            };

            var mailMessage = new MailMessage(
                from: new MailAddress(_settings.FromAddress, _settings.FromName),
                to: new MailAddress(message.ToAddress, message.ToName))
            {
                Subject = message.Subject,
                Body = message.HtmlBody ?? message.TextBody,
                IsBodyHtml = message.HtmlBody != null
            };

            await smtpClient.SendMailAsync(mailMessage, cancellationToken);

            _logger.LogInformation("Email sent via SMTP to {Email}", message.ToAddress);
            return new EmailSendResult(Success: true);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "SMTP send failed to {Email}", message.ToAddress);
            return new EmailSendResult(Success: false, ErrorMessage: ex.Message);
        }
    }
}
```

### 3.2 Security Token Base Class

All security tokens (verification, reset, invitation) share common characteristics. Use an **abstract base class** for consistency.

```csharp
namespace ColaFlow.Modules.Identity.Domain.Aggregates.SecurityTokens;

/// <summary>
/// Abstract base class for all time-limited security tokens.
/// Enforces consistent token hashing and expiration logic.
/// </summary>
public abstract class SecurityToken : Entity
{
    /// <summary>
    /// SHA-256 hash of the actual token (never store plaintext token).
    /// </summary>
    public string TokenHash { get; protected set; } = string.Empty;

    /// <summary>
    /// Token expiration timestamp (UTC).
    /// </summary>
    public DateTime ExpiresAt { get; protected set; }

    /// <summary>
    /// Timestamp when token was created (UTC).
    /// </summary>
    public DateTime CreatedAt { get; protected set; }

    /// <summary>
    /// Timestamp when token was used/consumed (UTC). Null if not used.
    /// </summary>
    public DateTime? UsedAt { get; protected set; }

    /// <summary>
    /// User ID associated with this token.
    /// </summary>
    public UserId UserId { get; protected set; } = null!;

    /// <summary>
    /// Check if token is expired.
    /// </summary>
    public bool IsExpired => DateTime.UtcNow > ExpiresAt;

    /// <summary>
    /// Check if token has been used.
    /// </summary>
    public bool IsUsed => UsedAt.HasValue;

    /// <summary>
    /// Check if token is valid (not expired and not used).
    /// </summary>
    public bool IsValid => !IsExpired && !IsUsed;

    /// <summary>
    /// Mark token as used (prevents reuse).
    /// </summary>
    public void MarkAsUsed()
    {
        if (IsUsed)
            throw new InvalidOperationException("Token has already been used");

        if (IsExpired)
            throw new InvalidOperationException("Cannot use expired token");

        UsedAt = DateTime.UtcNow;
    }

    /// <summary>
    /// Validate provided token against stored hash.
    /// </summary>
    /// <param name="providedToken">The plaintext token to validate</param>
    /// <returns>True if token matches hash</returns>
    public bool ValidateToken(string providedToken)
    {
        if (string.IsNullOrWhiteSpace(providedToken))
            return false;

        var providedHash = HashToken(providedToken);
        return TokenHash == providedHash;
    }

    /// <summary>
    /// Hash a token using SHA-256.
    /// </summary>
    protected static string HashToken(string token)
    {
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(token);
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToBase64String(hash);
    }

    /// <summary>
    /// Generate a cryptographically secure random token (256-bit, base64url-encoded).
    /// </summary>
    protected static string GenerateToken()
    {
        var randomBytes = new byte[32]; // 256 bits
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomBytes);

        // Base64URL encoding (URL-safe, no padding)
        return Convert.ToBase64String(randomBytes)
            .Replace("+", "-")
            .Replace("/", "_")
            .TrimEnd('=');
    }
}
```

**Design Rationale**:
- **Inheritance over Duplication**: 3 token types share 80% of logic
- **Immutability**: Protected setters prevent external modification
- **Validation**: Centralized expiration/usage checks
- **Security**: Token generation and hashing encapsulated

### 3.3 Domain Entities

#### EmailVerificationToken

```csharp
namespace ColaFlow.Modules.Identity.Domain.Aggregates.SecurityTokens;

/// <summary>
/// Email verification token entity.
/// Lifetime: 24 hours.
/// </summary>
public sealed class EmailVerificationToken : SecurityToken
{
    /// <summary>
    /// Email address being verified.
    /// </summary>
    public Email Email { get; private set; } = null!;

    /// <summary>
    /// Tenant ID for multi-tenant isolation.
    /// </summary>
    public TenantId TenantId { get; private set; } = null!;

    // EF Core constructor
    private EmailVerificationToken() : base() { }

    /// <summary>
    /// Factory method to create new verification token.
    /// </summary>
    /// <returns>Tuple of (entity, plaintext token for email)</returns>
    public static (EmailVerificationToken entity, string plaintextToken) Create(
        UserId userId,
        Email email,
        TenantId tenantId)
    {
        var plaintextToken = GenerateToken();
        var tokenHash = HashToken(plaintextToken);

        var entity = new EmailVerificationToken
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            Email = email,
            TenantId = tenantId,
            TokenHash = tokenHash,
            ExpiresAt = DateTime.UtcNow.AddHours(24),
            CreatedAt = DateTime.UtcNow
        };

        return (entity, plaintextToken);
    }
}
```

#### PasswordResetToken

```csharp
/// <summary>
/// Password reset token entity.
/// Lifetime: 1 hour.
/// </summary>
public sealed class PasswordResetToken : SecurityToken
{
    public Email Email { get; private set; } = null!;
    public TenantId TenantId { get; private set; } = null!;

    // EF Core constructor
    private PasswordResetToken() : base() { }

    public static (PasswordResetToken entity, string plaintextToken) Create(
        UserId userId,
        Email email,
        TenantId tenantId)
    {
        var plaintextToken = GenerateToken();
        var tokenHash = HashToken(plaintextToken);

        var entity = new PasswordResetToken
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            Email = email,
            TenantId = tenantId,
            TokenHash = tokenHash,
            ExpiresAt = DateTime.UtcNow.AddHours(1), // Shorter for security
            CreatedAt = DateTime.UtcNow
        };

        return (entity, plaintextToken);
    }
}
```

#### Invitation

```csharp
/// <summary>
/// User invitation entity.
/// Lifetime: 7 days.
/// </summary>
public sealed class Invitation : SecurityToken
{
    public Email InviteeEmail { get; private set; } = null!;
    public TenantId TenantId { get; private set; } = null!;
    public TenantRole AssignedRole { get; private set; }
    public UserId InvitedBy { get; private set; } = null!;
    public InvitationStatus Status { get; private set; }

    // EF Core constructor
    private Invitation() : base() { }

    public static (Invitation entity, string plaintextToken) Create(
        Email inviteeEmail,
        TenantId tenantId,
        TenantRole assignedRole,
        UserId invitedBy)
    {
        // Validate role (cannot invite as TenantOwner or AIAgent)
        if (assignedRole == TenantRole.TenantOwner || assignedRole == TenantRole.AIAgent)
            throw new ArgumentException($"Cannot invite user with role {assignedRole}");

        var plaintextToken = GenerateToken();
        var tokenHash = HashToken(plaintextToken);

        var entity = new Invitation
        {
            Id = Guid.NewGuid(),
            UserId = UserId.Empty, // Will be set when accepted
            InviteeEmail = inviteeEmail,
            TenantId = tenantId,
            AssignedRole = assignedRole,
            InvitedBy = invitedBy,
            TokenHash = tokenHash,
            Status = InvitationStatus.Pending,
            ExpiresAt = DateTime.UtcNow.AddDays(7),
            CreatedAt = DateTime.UtcNow
        };

        return (entity, plaintextToken);
    }

    public void Accept(UserId userId)
    {
        if (Status != InvitationStatus.Pending)
            throw new InvalidOperationException($"Invitation is {Status}, cannot accept");

        MarkAsUsed();
        UserId = userId;
        Status = InvitationStatus.Accepted;
    }

    public void Cancel()
    {
        if (Status != InvitationStatus.Pending)
            throw new InvalidOperationException($"Invitation is {Status}, cannot cancel");

        Status = InvitationStatus.Canceled;
    }
}

public enum InvitationStatus
{
    Pending = 0,
    Accepted = 1,
    Canceled = 2,
    Expired = 3
}
```

### 3.4 Repository Interfaces

```csharp
namespace ColaFlow.Modules.Identity.Domain.Repositories;

public interface IEmailVerificationTokenRepository
{
    Task<EmailVerificationToken?> GetByTokenHashAsync(
        string tokenHash,
        CancellationToken cancellationToken = default);

    Task<EmailVerificationToken?> GetActiveByUserIdAsync(
        UserId userId,
        CancellationToken cancellationToken = default);

    Task AddAsync(
        EmailVerificationToken token,
        CancellationToken cancellationToken = default);

    Task UpdateAsync(
        EmailVerificationToken token,
        CancellationToken cancellationToken = default);
}

public interface IPasswordResetTokenRepository
{
    Task<PasswordResetToken?> GetByTokenHashAsync(
        string tokenHash,
        CancellationToken cancellationToken = default);

    Task InvalidateAllByUserIdAsync(
        UserId userId,
        CancellationToken cancellationToken = default);

    Task AddAsync(
        PasswordResetToken token,
        CancellationToken cancellationToken = default);

    Task UpdateAsync(
        PasswordResetToken token,
        CancellationToken cancellationToken = default);
}

public interface IInvitationRepository
{
    Task<Invitation?> GetByTokenHashAsync(
        string tokenHash,
        CancellationToken cancellationToken = default);

    Task<Invitation?> GetByIdAsync(
        Guid invitationId,
        CancellationToken cancellationToken = default);

    Task<IReadOnlyList<Invitation>> GetAllByTenantAsync(
        TenantId tenantId,
        int pageNumber,
        int pageSize,
        InvitationStatus? status = null,
        CancellationToken cancellationToken = default);

    Task<int> CountByTenantAsync(
        TenantId tenantId,
        InvitationStatus? status = null,
        CancellationToken cancellationToken = default);

    Task AddAsync(
        Invitation invitation,
        CancellationToken cancellationToken = default);

    Task UpdateAsync(
        Invitation invitation,
        CancellationToken cancellationToken = default);
}
```

### 3.5 Command/Query Structure

#### Commands

```csharp
// Email Verification
public record VerifyEmailCommand(string Token) : IRequest<Result>;
public record ResendVerificationEmailCommand(string TenantSlug, string Email) : IRequest<Result>;

// Password Reset
public record ForgotPasswordCommand(string TenantSlug, string Email) : IRequest<Result>;
public record ResetPasswordCommand(string Token, string NewPassword) : IRequest<Result>;

// User Invitation
public record InviteUserCommand(
    Guid TenantId,
    string Email,
    TenantRole Role) : IRequest<Result<InvitationDto>>;

public record AcceptInvitationCommand(
    string Token,
    string FullName,
    string Password) : IRequest<Result<LoginResponseDto>>;

public record CancelInvitationCommand(
    Guid TenantId,
    Guid InvitationId) : IRequest<Result>;
```

#### Queries

```csharp
public record GetTenantInvitationsQuery(
    Guid TenantId,
    int PageNumber = 1,
    int PageSize = 20,
    InvitationStatus? Status = null) : IRequest<Result<PagedList<InvitationDto>>>;

public record GetInvitationByTokenQuery(
    string Token) : IRequest<Result<InvitationDetailsDto>>;
```

---

## 4. Database Schema Design

### 4.1 New Tables

#### email_verification_tokens

```sql
CREATE TABLE email_verification_tokens (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id             UUID NOT NULL,
    tenant_id           UUID NOT NULL,
    email               VARCHAR(255) NOT NULL,
    token_hash          VARCHAR(64) NOT NULL, -- SHA-256 base64
    expires_at          TIMESTAMP NOT NULL,
    created_at          TIMESTAMP NOT NULL DEFAULT NOW(),
    used_at             TIMESTAMP NULL,

    CONSTRAINT fk_email_verification_tokens_user
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    CONSTRAINT fk_email_verification_tokens_tenant
        FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);

-- Indexes for performance
CREATE INDEX idx_email_verification_tokens_token_hash
    ON email_verification_tokens(token_hash);

CREATE INDEX idx_email_verification_tokens_user_id
    ON email_verification_tokens(user_id)
    WHERE used_at IS NULL AND expires_at > NOW();

CREATE INDEX idx_email_verification_tokens_expires_at
    ON email_verification_tokens(expires_at)
    WHERE used_at IS NULL;
```

**Design Notes**:
- **token_hash**: Indexed for O(1) lookup during verification
- **Partial index on user_id**: Only active tokens (performance optimization)
- **Cascade delete**: Remove tokens when user/tenant deleted
- **Expiration index**: Efficient cleanup of expired tokens

#### password_reset_tokens

```sql
CREATE TABLE password_reset_tokens (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id             UUID NOT NULL,
    tenant_id           UUID NOT NULL,
    email               VARCHAR(255) NOT NULL,
    token_hash          VARCHAR(64) NOT NULL,
    expires_at          TIMESTAMP NOT NULL,
    created_at          TIMESTAMP NOT NULL DEFAULT NOW(),
    used_at             TIMESTAMP NULL,

    CONSTRAINT fk_password_reset_tokens_user
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    CONSTRAINT fk_password_reset_tokens_tenant
        FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);

-- Indexes
CREATE INDEX idx_password_reset_tokens_token_hash
    ON password_reset_tokens(token_hash);

CREATE INDEX idx_password_reset_tokens_user_id
    ON password_reset_tokens(user_id)
    WHERE used_at IS NULL AND expires_at > NOW();

CREATE INDEX idx_password_reset_tokens_expires_at
    ON password_reset_tokens(expires_at)
    WHERE used_at IS NULL;
```

#### invitations

```sql
CREATE TABLE invitations (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL,
    invitee_email       VARCHAR(255) NOT NULL,
    assigned_role       VARCHAR(50) NOT NULL, -- TenantAdmin, Developer, Guest
    invited_by_user_id  UUID NOT NULL,
    token_hash          VARCHAR(64) NOT NULL,
    status              INT NOT NULL DEFAULT 0, -- 0=Pending, 1=Accepted, 2=Canceled, 3=Expired
    user_id             UUID NULL, -- Set when accepted
    expires_at          TIMESTAMP NOT NULL,
    created_at          TIMESTAMP NOT NULL DEFAULT NOW(),
    used_at             TIMESTAMP NULL,

    CONSTRAINT fk_invitations_tenant
        FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
    CONSTRAINT fk_invitations_invited_by
        FOREIGN KEY (invited_by_user_id) REFERENCES users(id) ON DELETE RESTRICT,
    CONSTRAINT fk_invitations_user
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
    CONSTRAINT ck_invitations_role
        CHECK (assigned_role IN ('TenantAdmin', 'Developer', 'Guest'))
);

-- Indexes
CREATE INDEX idx_invitations_token_hash
    ON invitations(token_hash);

CREATE INDEX idx_invitations_tenant_id_status
    ON invitations(tenant_id, status);

CREATE UNIQUE INDEX idx_invitations_unique_pending
    ON invitations(tenant_id, invitee_email)
    WHERE status = 0 AND expires_at > NOW();

CREATE INDEX idx_invitations_expires_at
    ON invitations(expires_at)
    WHERE status = 0;
```

**Design Notes**:
- **Unique constraint**: Prevent duplicate pending invitations for same email
- **RESTRICT on invited_by**: Prevent deletion of user who sent invitations
- **CHECK constraint**: Enforce valid roles at database level

### 4.2 Schema Changes to Existing Tables

#### users table modifications

```sql
-- Add email verification timestamp (if not already exists)
ALTER TABLE users
    ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMP NULL;

-- Remove deprecated columns (if they exist on User entity)
ALTER TABLE users
    DROP COLUMN IF EXISTS email_verification_token,
    DROP COLUMN IF EXISTS password_reset_token,
    DROP COLUMN IF EXISTS password_reset_token_expires_at;
```

**Migration Strategy**:
1. Check existing User entity for deprecated columns
2. Create migration to drop them (data loss acceptable for Day 7, as these were never used)
3. Add `email_verified_at` if missing

### 4.3 EF Core Entity Configurations

#### EmailVerificationTokenConfiguration

```csharp
namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Configurations;

public class EmailVerificationTokenConfiguration : IEntityTypeConfiguration<EmailVerificationToken>
{
    public void Configure(EntityTypeBuilder<EmailVerificationToken> builder)
    {
        builder.ToTable("email_verification_tokens");

        builder.HasKey(t => t.Id);
        builder.Property(t => t.Id).HasColumnName("id");

        builder.Property(t => t.TokenHash)
            .HasColumnName("token_hash")
            .HasMaxLength(64)
            .IsRequired();

        builder.Property(t => t.ExpiresAt)
            .HasColumnName("expires_at")
            .IsRequired();

        builder.Property(t => t.CreatedAt)
            .HasColumnName("created_at")
            .IsRequired();

        builder.Property(t => t.UsedAt)
            .HasColumnName("used_at");

        // Value Objects
        builder.Property(t => t.UserId)
            .HasColumnName("user_id")
            .HasConversion(
                id => id.Value,
                value => UserId.Create(value))
            .IsRequired();

        builder.Property(t => t.TenantId)
            .HasColumnName("tenant_id")
            .HasConversion(
                id => id.Value,
                value => TenantId.Create(value))
            .IsRequired();

        builder.Property(t => t.Email)
            .HasColumnName("email")
            .HasMaxLength(255)
            .HasConversion(
                email => email.Value,
                value => Email.Create(value).Value)
            .IsRequired();

        // Relationships
        builder.HasOne<User>()
            .WithMany()
            .HasForeignKey(t => t.UserId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.HasOne<Tenant>()
            .WithMany()
            .HasForeignKey(t => t.TenantId)
            .OnDelete(DeleteBehavior.Cascade);

        // Indexes
        builder.HasIndex(t => t.TokenHash)
            .HasDatabaseName("idx_email_verification_tokens_token_hash");

        builder.HasIndex(t => t.UserId)
            .HasDatabaseName("idx_email_verification_tokens_user_id")
            .HasFilter("used_at IS NULL AND expires_at > NOW()");
    }
}
```

**Note**: Repeat similar configurations for `PasswordResetTokenConfiguration` and `InvitationConfiguration`.

### 4.4 Migration Approach

```bash
# Create migration
cd src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure
dotnet ef migrations add Day7_EmailAndInvitations --context IdentityDbContext --output-dir Persistence/Migrations

# Review generated migration
# Ensure it includes:
# - CREATE TABLE for 3 new tables
# - CREATE INDEX for all specified indexes
# - ALTER TABLE users DROP COLUMN (deprecated token fields)
# - ALTER TABLE users ADD COLUMN email_verified_at

# Apply migration
dotnet ef database update --context IdentityDbContext
```

---

## 5. Security Architecture (ADRs)

### ADR-013: Token Hashing with SHA-256

**Status**: Accepted
**Date**: 2025-11-03
**Context**: We need to securely store security tokens (email verification, password reset, invitations) in the database.

**Decision**: Use SHA-256 for hashing tokens before database storage.

**Rationale**:

| Aspect | SHA-256 | BCrypt | PBKDF2 | Decision |
|--------|---------|--------|--------|----------|
| **Purpose** | Token hashing | Password hashing | Password hashing | ✅ SHA-256 |
| **Speed** | Very fast (~500K ops/sec) | Slow (12 rounds = ~50 ops/sec) | Medium | Fast lookup needed |
| **Collision Resistance** | Excellent (256-bit) | N/A | N/A | Tokens are random |
| **Rainbow Table Resistance** | Not applicable (tokens are random, not user-chosen) | Excellent | Excellent | Not a concern |
| **Use Case Fit** | ✅ Time-limited random tokens | ❌ User passwords | ❌ User passwords | ✅ |

**Why NOT BCrypt?**
- BCrypt is designed for **slow** hashing (defense against brute force)
- Tokens are **already cryptographically random** (256-bit entropy)
- Token expiration provides time-bound security
- Performance: Token lookups happen on every verification/reset request

**Implementation**:
```csharp
protected static string HashToken(string token)
{
    using var sha256 = SHA256.Create();
    var bytes = Encoding.UTF8.GetBytes(token);
    var hash = sha256.ComputeHash(bytes);
    return Convert.ToBase64String(hash); // 44 characters
}
```

**Security Properties**:
- **One-way**: Cannot reverse hash to plaintext token
- **Deterministic**: Same token always produces same hash (lookup possible)
- **Fast**: O(1) database lookup with indexed `token_hash` column
- **Collision-resistant**: 2^256 possible hashes

**Consequences**:
- ✅ High-performance token validation
- ✅ Database breach doesn't expose plaintext tokens
- ✅ Simple implementation (no salt management)
- ⚠️ Tokens must have high entropy (256-bit random)
- ⚠️ Must enforce short expiration windows

---

### ADR-014: Email Enumeration Prevention

**Status**: Accepted
**Date**: 2025-11-03
**Context**: Password reset and email verification endpoints could reveal if an email exists in the system.

**Decision**: Always return success (200 OK) regardless of whether the email exists.

**Vulnerability Scenario**:
```
❌ Bad Implementation:
POST /api/auth/forgot-password { "email": "admin@victim.com" }
→ 404 Not Found: "Email not found"

Attacker learns: admin@victim.com is NOT a registered user

✅ Good Implementation:
POST /api/auth/forgot-password { "email": "admin@victim.com" }
→ 200 OK: "If an account exists, a reset link has been sent"

Attacker learns: Nothing
```

**Implementation Pattern**:
```csharp
public async Task<Result> Handle(ForgotPasswordCommand command, CancellationToken ct)
{
    // 1. Look up user (internal logic)
    var user = await _userRepository.GetByEmailAsync(tenantId, email, ct);

    // 2. If user exists, send email (internal)
    if (user is not null)
    {
        var (token, plaintextToken) = PasswordResetToken.Create(user.Id, email, tenantId);
        await _tokenRepository.AddAsync(token, ct);
        await _emailService.SendPasswordResetEmailAsync(email, plaintextToken, ct);
    }

    // 3. ALWAYS return success (same response for exists/not-exists)
    return Result.Success("If an account exists, a reset link has been sent.");
}
```

**Mitigation Checklist**:
- ✅ Same HTTP status code (200) for exists/not-exists
- ✅ Same response message (generic)
- ✅ Same response time (no timing attacks)
- ✅ Rate limiting (prevent mass enumeration)
- ✅ Audit logging (detect enumeration attempts)

**Trade-offs**:
- ✅ Prevents user enumeration attacks
- ✅ Improves privacy (attackers can't harvest email lists)
- ⚠️ Slightly worse UX (user doesn't know if email was wrong)
- ⚠️ Support burden (users may not realize they used wrong email)

**Related Security Measures**:
- Rate limiting: Max 3 requests per email per hour
- Honeypot emails: Log suspicious patterns (e.g., 100 different emails from same IP)

---

### ADR-015: Rate Limiting Strategy

**Status**: Accepted
**Date**: 2025-11-03
**Context**: Email endpoints are vulnerable to abuse (spam, enumeration, DoS).

**Decision**: Implement multi-layer rate limiting with different strategies per endpoint.

#### Rate Limiting Tiers

| Endpoint | Limit | Window | Scope | Rationale |
|----------|-------|--------|-------|-----------|
| **Verification Email** | 3 requests | 1 hour | Per email | Prevent spam, normal user needs 1-2 max |
| **Password Reset** | 3 requests | 1 hour | Per email | Prevent enumeration, balance security vs UX |
| **User Invitation** | 20 invitations | 1 hour | Per tenant | Prevent bulk spam, allow team onboarding |
| **Accept Invitation** | 5 attempts | 15 minutes | Per token | Prevent brute force (token is 256-bit, near impossible) |

#### Implementation: In-Memory + Redis Hybrid

**Phase 1 (Day 7)**: In-memory rate limiting with `MemoryCache`
```csharp
public class RateLimitingService
{
    private readonly IMemoryCache _cache;

    public async Task<bool> IsAllowedAsync(string key, int maxRequests, TimeSpan window)
    {
        var cacheKey = $"ratelimit:{key}";

        if (_cache.TryGetValue(cacheKey, out int count))
        {
            if (count >= maxRequests)
                return false;

            _cache.Set(cacheKey, count + 1, window);
            return true;
        }

        _cache.Set(cacheKey, 1, window);
        return true;
    }
}
```

**Phase 2 (Post-Day 7)**: Redis-based distributed rate limiting
```csharp
// Use Redis INCR with expiration
public async Task<bool> IsAllowedAsync(string key, int maxRequests, TimeSpan window)
{
    var redisKey = $"ratelimit:{key}";
    var count = await _redis.StringIncrementAsync(redisKey);

    if (count == 1)
        await _redis.KeyExpireAsync(redisKey, window);

    return count <= maxRequests;
}
```

#### Integration with Commands

```csharp
public class ForgotPasswordCommandHandler : IRequestHandler<ForgotPasswordCommand, Result>
{
    private readonly IRateLimitingService _rateLimiter;

    public async Task<Result> Handle(ForgotPasswordCommand command, CancellationToken ct)
    {
        // Rate limit check
        var rateLimitKey = $"forgot-password:{command.Email}";
        if (!await _rateLimiter.IsAllowedAsync(rateLimitKey, maxRequests: 3, TimeSpan.FromHours(1)))
        {
            return Result.Failure("Too many password reset requests. Please try again later.");
        }

        // ... rest of handler logic
    }
}
```

**Consequences**:
- ✅ Prevents abuse (spam, DoS, enumeration)
- ✅ Low latency (in-memory for Day 7)
- ✅ Scalable (Redis for distributed systems)
- ⚠️ In-memory limits don't work across multiple API instances (acceptable for Day 7)
- ⚠️ Requires Redis for production multi-instance deployments

---

## 6. Integration Architecture

### 6.1 Email Service Integration Points

#### RegisterTenantCommandHandler (Modified)

```csharp
public class RegisterTenantCommandHandler : IRequestHandler<RegisterTenantCommand, Result<TenantDto>>
{
    private readonly IEmailService _emailService;
    private readonly IEmailVerificationTokenRepository _tokenRepository;
    // ... other dependencies

    public async Task<Result<TenantDto>> Handle(
        RegisterTenantCommand command,
        CancellationToken ct)
    {
        // 1. Create tenant and admin user (existing logic)
        var tenant = Tenant.Create(...);
        var adminUser = User.CreateLocal(...);
        await _tenantRepository.AddAsync(tenant, ct);
        await _userRepository.AddAsync(adminUser, ct);

        // 2. Generate verification token (NEW)
        var (verificationToken, plaintextToken) = EmailVerificationToken.Create(
            adminUser.Id,
            adminUser.Email,
            tenant.Id);

        await _tokenRepository.AddAsync(verificationToken, ct);

        // 3. Send verification email (NEW - non-blocking)
        var emailResult = await _emailService.SendEmailAsync(new EmailMessage(
            ToAddress: adminUser.Email.Value,
            ToName: adminUser.FullName.Value,
            Subject: "Verify your email - ColaFlow",
            TextBody: $"Click here to verify: https://app.colaflow.io/verify-email?token={plaintextToken}",
            HtmlBody: RenderVerificationEmailHtml(adminUser.FullName.Value, plaintextToken)
        ), ct);

        // 4. Log email failure but don't block registration
        if (!emailResult.Success)
        {
            _logger.LogWarning(
                "Failed to send verification email to {Email}: {Error}",
                adminUser.Email.Value,
                emailResult.ErrorMessage);
        }

        // 5. Return success (even if email failed)
        return Result.Success(MapToDto(tenant, adminUser, jwtToken));
    }
}
```

**Key Design Decision**: **Email failure is non-blocking**
- ✅ User registration succeeds even if SendGrid is down
- ✅ User can still login (verification is optional for Day 7)
- ✅ User can request resend later

#### ForgotPasswordCommandHandler (New)

```csharp
public class ForgotPasswordCommandHandler : IRequestHandler<ForgotPasswordCommand, Result>
{
    public async Task<Result> Handle(ForgotPasswordCommand command, CancellationToken ct)
    {
        // 1. Rate limiting
        if (!await _rateLimiter.IsAllowedAsync($"forgot-password:{command.Email}", 3, TimeSpan.FromHours(1)))
            return Result.Failure("Too many requests. Try again in 1 hour.");

        // 2. Lookup tenant and user
        var tenant = await _tenantRepository.GetBySlugAsync(command.TenantSlug, ct);
        if (tenant is null)
            return Result.Success("If an account exists, a reset link has been sent."); // Enumerate protection

        var email = Email.Create(command.Email).Value;
        var user = await _userRepository.GetByEmailAsync(tenant.Id, email, ct);
        if (user is null)
            return Result.Success("If an account exists, a reset link has been sent."); // Enumerate protection

        // 3. Invalidate old reset tokens
        await _resetTokenRepository.InvalidateAllByUserIdAsync(user.Id, ct);

        // 4. Create new reset token
        var (resetToken, plaintextToken) = PasswordResetToken.Create(user.Id, email, tenant.Id);
        await _resetTokenRepository.AddAsync(resetToken, ct);

        // 5. Send reset email
        await _emailService.SendEmailAsync(new EmailMessage(
            ToAddress: email.Value,
            ToName: user.FullName.Value,
            Subject: "Reset your password - ColaFlow",
            TextBody: $"Reset link: https://app.colaflow.io/reset-password?token={plaintextToken}",
            HtmlBody: RenderPasswordResetEmailHtml(user.FullName.Value, plaintextToken)
        ), ct);

        // 6. Always return success (enumerate protection)
        return Result.Success("If an account exists, a reset link has been sent.");
    }
}
```

#### InviteUserCommandHandler (New)

```csharp
public class InviteUserCommandHandler : IRequestHandler<InviteUserCommand, Result<InvitationDto>>
{
    public async Task<Result<InvitationDto>> Handle(InviteUserCommand command, CancellationToken ct)
    {
        // 1. Authorization check (must be TenantOwner or TenantAdmin)
        var currentUser = await _currentUserService.GetCurrentUserAsync(ct);
        var role = await _roleRepository.GetUserRoleAsync(currentUser.Id, command.TenantId, ct);
        if (role != TenantRole.TenantOwner && role != TenantRole.TenantAdmin)
            return Result.Failure("Insufficient permissions");

        // 2. Validate tenant ownership
        if (currentUser.TenantId != command.TenantId)
            return Result.Failure("Cross-tenant invitation not allowed");

        // 3. Validate email not already member
        var email = Email.Create(command.Email).Value;
        if (await _userRepository.ExistsByEmailAsync(command.TenantId, email, ct))
            return Result.Failure("User already exists in this tenant");

        // 4. Check for existing pending invitation
        var existingInvitations = await _invitationRepository.GetAllByTenantAsync(
            command.TenantId, 1, 100, InvitationStatus.Pending, ct);

        if (existingInvitations.Any(i => i.InviteeEmail == email && i.IsValid))
            return Result.Failure("A pending invitation already exists for this email");

        // 5. Create invitation
        var (invitation, plaintextToken) = Invitation.Create(
            email,
            command.TenantId,
            command.Role,
            currentUser.Id);

        await _invitationRepository.AddAsync(invitation, ct);

        // 6. Send invitation email
        var tenant = await _tenantRepository.GetByIdAsync(command.TenantId, ct);
        await _emailService.SendEmailAsync(new EmailMessage(
            ToAddress: email.Value,
            ToName: email.Value, // We don't know their name yet
            Subject: $"You're invited to join {tenant.Name} on ColaFlow",
            TextBody: $"Accept invitation: https://app.colaflow.io/accept-invitation?token={plaintextToken}",
            HtmlBody: RenderInvitationEmailHtml(tenant.Name, currentUser.FullName.Value, command.Role, plaintextToken)
        ), ct);

        // 7. Return invitation details
        return Result.Success(MapToDto(invitation));
    }
}
```

### 6.2 Domain Events Integration

Day 7 introduces new domain events for audit logging and future integrations:

```csharp
// New Events
public record EmailVerifiedEvent(UserId UserId, Email Email, TenantId TenantId, DateTime VerifiedAt);
public record PasswordResetRequestedEvent(UserId UserId, Email Email, TenantId TenantId, string IpAddress);
public record PasswordResetCompletedEvent(UserId UserId, TenantId TenantId);
public record UserInvitedEvent(Guid InvitationId, Email InviteeEmail, TenantId TenantId, UserId InvitedBy, TenantRole Role);
public record InvitationAcceptedEvent(Guid InvitationId, UserId NewUserId, TenantId TenantId);
```

**Event Handlers** (for audit logging):
```csharp
public class EmailVerifiedEventHandler : INotificationHandler<EmailVerifiedEvent>
{
    private readonly IAuditLogService _auditLog;

    public async Task Handle(EmailVerifiedEvent @event, CancellationToken ct)
    {
        await _auditLog.LogAsync(new AuditLogEntry(
            EntityType: "User",
            EntityId: @event.UserId.Value,
            Action: "EmailVerified",
            Details: $"Email {@event.Email.Value} verified",
            TenantId: @event.TenantId.Value,
            Timestamp: @event.VerifiedAt
        ), ct);
    }
}
```

### 6.3 API Endpoint Integration

New endpoints to add to `AuthController`:

```csharp
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    // Email Verification
    [HttpPost("verify-email")]
    [AllowAnonymous]
    public async Task<IActionResult> VerifyEmail(
        [FromBody] VerifyEmailRequest request,
        CancellationToken ct)
    {
        var command = new VerifyEmailCommand(request.Token);
        var result = await _mediator.Send(command, ct);
        return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Error);
    }

    [HttpPost("resend-verification")]
    [AllowAnonymous]
    public async Task<IActionResult> ResendVerification(
        [FromBody] ResendVerificationRequest request,
        CancellationToken ct)
    {
        var command = new ResendVerificationEmailCommand(request.TenantSlug, request.Email);
        var result = await _mediator.Send(command, ct);
        return Ok(new { message = "If an account exists, a verification email has been sent." });
    }

    // Password Reset
    [HttpPost("forgot-password")]
    [AllowAnonymous]
    public async Task<IActionResult> ForgotPassword(
        [FromBody] ForgotPasswordRequest request,
        CancellationToken ct)
    {
        var command = new ForgotPasswordCommand(request.TenantSlug, request.Email);
        var result = await _mediator.Send(command, ct);
        return Ok(new { message = "If an account exists, a reset link has been sent." });
    }

    [HttpPost("reset-password")]
    [AllowAnonymous]
    public async Task<IActionResult> ResetPassword(
        [FromBody] ResetPasswordRequest request,
        CancellationToken ct)
    {
        var command = new ResetPasswordCommand(request.Token, request.NewPassword);
        var result = await _mediator.Send(command, ct);
        return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Error);
    }
}
```

New controller for invitations:

```csharp
[ApiController]
[Route("api/tenants/{tenantId}/invitations")]
[Authorize]
public class InvitationsController : ControllerBase
{
    [HttpPost]
    [RequireTenantOwner] // Custom authorization policy
    public async Task<IActionResult> InviteUser(
        [FromRoute] Guid tenantId,
        [FromBody] InviteUserRequest request,
        CancellationToken ct)
    {
        var command = new InviteUserCommand(tenantId, request.Email, request.Role);
        var result = await _mediator.Send(command, ct);
        return result.IsSuccess ? CreatedAtAction(nameof(GetInvitation), new { id = result.Value.Id }, result.Value) : BadRequest(result.Error);
    }

    [HttpGet]
    public async Task<IActionResult> GetInvitations(
        [FromRoute] Guid tenantId,
        [FromQuery] int pageNumber = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] InvitationStatus? status = null,
        CancellationToken ct)
    {
        var query = new GetTenantInvitationsQuery(tenantId, pageNumber, pageSize, status);
        var result = await _mediator.Send(query, ct);
        return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Error);
    }

    [HttpDelete("{invitationId}")]
    public async Task<IActionResult> CancelInvitation(
        [FromRoute] Guid tenantId,
        [FromRoute] Guid invitationId,
        CancellationToken ct)
    {
        var command = new CancelInvitationCommand(tenantId, invitationId);
        var result = await _mediator.Send(command, ct);
        return result.IsSuccess ? NoContent() : BadRequest(result.Error);
    }
}

[ApiController]
[Route("api/invitations")]
public class PublicInvitationsController : ControllerBase
{
    [HttpPost("accept")]
    [AllowAnonymous]
    public async Task<IActionResult> AcceptInvitation(
        [FromBody] AcceptInvitationRequest request,
        CancellationToken ct)
    {
        var command = new AcceptInvitationCommand(request.Token, request.FullName, request.Password);
        var result = await _mediator.Send(command, ct);
        return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Error);
    }
}
```

---

## 7. Implementation Phases

### Phase 1: Email Infrastructure (Priority P0)
**Duration**: 4-6 hours
**Goal**: Get email sending working end-to-end

**Tasks**:
1. ✅ Create `IEmailService` interface
2. ✅ Implement `SendGridEmailService`
3. ✅ Implement `SmtpEmailService`
4. ✅ Implement `MockEmailService` (for tests)
5. ✅ Add email configuration to `appsettings.json`
6. ✅ Create email templates (simple string interpolation for Day 7)
7. ✅ Register services in DI container
8. ✅ Write unit tests for email services

**Acceptance Criteria**:
- [ ] Email service can send test email via SendGrid
- [ ] Email service can send test email via SMTP (MailHog)
- [ ] Mock service logs emails to file in development
- [ ] Configuration is environment-specific

**Testing**:
```bash
# Start MailHog (Docker)
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog

# Test SMTP send
curl -X POST http://localhost:5167/api/test/send-email \
  -H "Content-Type: application/json" \
  -d '{"to": "test@example.com", "subject": "Test", "body": "Hello"}'

# Check MailHog UI: http://localhost:8025
```

---

### Phase 2: Email Verification Flow (Priority P0)
**Duration**: 6-8 hours
**Goal**: Users can verify their email addresses

**Tasks**:
1. ✅ Create `SecurityToken` base class
2. ✅ Create `EmailVerificationToken` entity
3. ✅ Create `IEmailVerificationTokenRepository` interface and implementation
4. ✅ Create EF Core entity configuration
5. ✅ Generate and apply database migration
6. ✅ Create `VerifyEmailCommand` and handler
7. ✅ Create `ResendVerificationEmailCommand` and handler
8. ✅ Modify `RegisterTenantCommandHandler` to send verification email
9. ✅ Add `/api/auth/verify-email` endpoint
10. ✅ Add `/api/auth/resend-verification` endpoint
11. ✅ Write integration tests

**Acceptance Criteria**:
- [ ] Registration sends verification email
- [ ] User can verify email with token
- [ ] Invalid/expired tokens return 400
- [ ] Resend works and invalidates old token
- [ ] Email enumeration is prevented

**Testing**:
```powershell
# 1. Register tenant
$regResponse = Invoke-RestMethod -Uri "http://localhost:5167/api/tenants/register" `
    -Method Post -ContentType "application/json" `
    -Body '{"tenantName":"Test","tenantSlug":"test","adminEmail":"admin@test.com","adminPassword":"Admin@123","adminFullName":"Admin"}'

# 2. Extract token from email logs (in development mode)
$token = "...token-from-log..."

# 3. Verify email
Invoke-RestMethod -Uri "http://localhost:5167/api/auth/verify-email" `
    -Method Post -ContentType "application/json" `
    -Body "{`"token`":`"$token`"}"

# 4. Test resend
Invoke-RestMethod -Uri "http://localhost:5167/api/auth/resend-verification" `
    -Method Post -ContentType "application/json" `
    -Body '{"tenantSlug":"test","email":"admin@test.com"}'
```

---

### Phase 3: Password Reset Flow (Priority P0)
**Duration**: 6-8 hours
**Goal**: Users can reset forgotten passwords

**Tasks**:
1. ✅ Create `PasswordResetToken` entity
2. ✅ Create `IPasswordResetTokenRepository` interface and implementation
3. ✅ Create EF Core entity configuration
4. ✅ Generate and apply database migration
5. ✅ Create `ForgotPasswordCommand` and handler
6. ✅ Create `ResetPasswordCommand` and handler
7. ✅ Implement password complexity validation
8. ✅ Implement refresh token revocation on password reset
9. ✅ Add `/api/auth/forgot-password` endpoint
10. ✅ Add `/api/auth/reset-password` endpoint
11. ✅ Write integration tests

**Acceptance Criteria**:
- [ ] User can request password reset
- [ ] Reset email is sent with valid token
- [ ] User can reset password with token
- [ ] Password complexity is enforced
- [ ] All refresh tokens are invalidated on reset
- [ ] Used tokens cannot be reused
- [ ] Email enumeration is prevented

**Testing**:
```powershell
# 1. Request password reset
Invoke-RestMethod -Uri "http://localhost:5167/api/auth/forgot-password" `
    -Method Post -ContentType "application/json" `
    -Body '{"tenantSlug":"test","email":"admin@test.com"}'

# 2. Extract token from email
$resetToken = "...token-from-email..."

# 3. Reset password
Invoke-RestMethod -Uri "http://localhost:5167/api/auth/reset-password" `
    -Method Post -ContentType "application/json" `
    -Body "{`"token`":`"$resetToken`",`"newPassword`":`"NewPassword@123`"}"

# 4. Login with new password
Invoke-RestMethod -Uri "http://localhost:5167/api/auth/login" `
    -Method Post -ContentType "application/json" `
    -Body '{"tenantSlug":"test","email":"admin@test.com","password":"NewPassword@123"}'
```

---

### Phase 4: User Invitation System (Priority P0)
**Duration**: 8-10 hours
**Goal**: Tenant owners can invite team members

**Tasks**:
1. ✅ Create `Invitation` entity with `InvitationStatus` enum
2. ✅ Create `IInvitationRepository` interface and implementation
3. ✅ Create EF Core entity configuration
4. ✅ Generate and apply database migration
5. ✅ Create `InviteUserCommand` and handler
6. ✅ Create `AcceptInvitationCommand` and handler
7. ✅ Create `CancelInvitationCommand` and handler
8. ✅ Create `GetTenantInvitationsQuery` and handler
9. ✅ Create authorization policies (`RequireTenantOwner`, `RequireTenantAdmin`)
10. ✅ Add `/api/tenants/{id}/invitations` endpoints (POST, GET, DELETE)
11. ✅ Add `/api/invitations/accept` endpoint
12. ✅ Write integration tests (unblock 3 skipped tests)

**Acceptance Criteria**:
- [ ] Tenant owner can invite user with role
- [ ] Invitation email is sent
- [ ] User can accept invitation and create account
- [ ] Accepting invitation auto-logs user in
- [ ] Cannot invite with TenantOwner or AIAgent role
- [ ] Cannot invite existing members
- [ ] Tenant owner can view pending invitations
- [ ] Tenant owner can cancel pending invitations
- [ ] 3 previously skipped integration tests now pass

**Testing**:
```powershell
# 1. Login as tenant owner
$loginResponse = Invoke-RestMethod -Uri "http://localhost:5167/api/auth/login" `
    -Method Post -ContentType "application/json" `
    -Body '{"tenantSlug":"test","email":"admin@test.com","password":"Admin@123"}'
$token = $loginResponse.accessToken

# 2. Invite user
$inviteResponse = Invoke-RestMethod -Uri "http://localhost:5167/api/tenants/{tenantId}/invitations" `
    -Method Post -ContentType "application/json" `
    -Headers @{ Authorization = "Bearer $token" } `
    -Body '{"email":"developer@test.com","role":"Developer"}'

# 3. List invitations
$invitations = Invoke-RestMethod -Uri "http://localhost:5167/api/tenants/{tenantId}/invitations" `
    -Method Get -Headers @{ Authorization = "Bearer $token" }

# 4. Extract token from email
$inviteToken = "...token-from-email..."

# 5. Accept invitation
$acceptResponse = Invoke-RestMethod -Uri "http://localhost:5167/api/invitations/accept" `
    -Method Post -ContentType "application/json" `
    -Body "{`"token`":`"$inviteToken`",`"fullName`":`"John Developer`",`"password`":`"Dev@123`"}"

# 6. Verify new user can login
Invoke-RestMethod -Uri "http://localhost:5167/api/auth/login" `
    -Method Post -ContentType "application/json" `
    -Body '{"tenantSlug":"test","email":"developer@test.com","password":"Dev@123"}'
```

---

### Phase 5: Polish & Testing (Priority P1)
**Duration**: 4-6 hours
**Goal**: Production readiness

**Tasks**:
1. ✅ Add rate limiting service
2. ✅ Add comprehensive logging
3. ✅ Write email template HTML (upgrade from string interpolation)
4. ✅ Add password complexity validation
5. ✅ Write unit tests for all commands/queries
6. ✅ Write integration tests for all endpoints
7. ✅ Update API documentation (Swagger)
8. ✅ Test edge cases (expired tokens, invalid tokens, etc.)
9. ✅ Load test email sending (ensure SendGrid quota)
10. ✅ Update Day 7 implementation summary

**Acceptance Criteria**:
- [ ] All tests passing (unit + integration)
- [ ] Code coverage >80%
- [ ] Rate limiting working
- [ ] Swagger docs updated
- [ ] Email templates production-ready
- [ ] No compiler warnings

---

## 8. Deployment Considerations

### 8.1 Configuration Management

**appsettings.Development.json**:
```json
{
  "EmailSettings": {
    "Provider": "Smtp",
    "FromAddress": "noreply@colaflow.local",
    "FromName": "ColaFlow Development",
    "SaveEmailsToFile": true,
    "EmailOutputPath": "temp/emails",
    "Smtp": {
      "Host": "localhost",
      "Port": 1025,
      "EnableSsl": false
    }
  }
}
```

**appsettings.Production.json**:
```json
{
  "EmailSettings": {
    "Provider": "SendGrid",
    "FromAddress": "noreply@colaflow.io",
    "FromName": "ColaFlow",
    "SendGrid": {
      "ApiKey": "${SENDGRID_API_KEY}" // Injected from environment variable
    }
  }
}
```

**Environment Variables** (Production):
```bash
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxxx
COLAFLOW_EMAIL_FROM=noreply@colaflow.io
ASPNETCORE_ENVIRONMENT=Production
```

### 8.2 Database Migration Strategy

**Deployment Steps**:
```bash
# 1. Backup production database
pg_dump colaflow_prod > backup_before_day7.sql

# 2. Apply migration (zero-downtime)
dotnet ef database update --context IdentityDbContext --connection "Host=prod-db;Database=colaflow_prod"

# 3. Verify migration
psql colaflow_prod -c "\d email_verification_tokens"
psql colaflow_prod -c "\d password_reset_tokens"
psql colaflow_prod -c "\d invitations"

# 4. Run smoke tests
curl -X POST https://api.colaflow.io/api/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{"tenantSlug":"test","email":"test@example.com"}'
```

**Rollback Plan** (if issues occur):
```bash
# 1. Revert database migration
dotnet ef migrations remove --context IdentityDbContext

# 2. Restore from backup
psql colaflow_prod < backup_before_day7.sql

# 3. Redeploy previous API version
```

### 8.3 Monitoring & Alerting

**Metrics to Monitor**:
| Metric | Threshold | Alert |
|--------|-----------|-------|
| Email send failure rate | >5% | Warning |
| Email send failure rate | >20% | Critical |
| Token validation failure rate | >10% | Warning |
| Password reset requests | >1000/hour | Suspicious activity |
| Invitation acceptance rate | <30% | Poor UX indicator |
| Token expiration before use | >50% | Expiration too short |

**Logging Strategy**:
```csharp
// INFO level
_logger.LogInformation("Email sent to {Email} for {Purpose}", email, "EmailVerification");

// WARNING level
_logger.LogWarning("Email send failed to {Email}: {Error}", email, error);

// ERROR level (only for unexpected exceptions)
_logger.LogError(ex, "Unexpected error in email service");

// AUDIT level (security events)
_auditLogger.Log("PASSWORD_RESET_REQUESTED", userId, tenantId, ipAddress);
```

### 8.4 Security Checklist

**Pre-Production**:
- [ ] SendGrid API key stored in Azure Key Vault (not appsettings)
- [ ] HTTPS enforced for all email links
- [ ] Rate limiting enabled (Redis-based for multi-instance)
- [ ] Email enumeration protection verified
- [ ] Token expiration windows validated
- [ ] SQL injection prevention verified (parameterized queries)
- [ ] XSS prevention in email templates (HTML encoding)
- [ ] CORS configured correctly
- [ ] Audit logging enabled for all security events

**Post-Deployment**:
- [ ] Monitor for unusual password reset patterns
- [ ] Monitor for email bounce rates
- [ ] Monitor for failed token validations
- [ ] Review security logs weekly

---

## Summary

### Key Deliverables

| Component | Status | Priority |
|-----------|--------|----------|
| Email Service Abstraction | ✅ Designed | P0 |
| SendGrid Integration | ✅ Designed | P0 |
| Email Verification Flow | ✅ Designed | P0 |
| Password Reset Flow | ✅ Designed | P0 |
| User Invitation System | ✅ Designed | P0 |
| Database Schema (3 tables) | ✅ Designed | P0 |
| Security ADRs (3 ADRs) | ✅ Written | P0 |
| Integration Architecture | ✅ Defined | P0 |
| Implementation Phases | ✅ Planned | P0 |

### Architecture Highlights

1. **Abstraction Layer**: Email provider is pluggable (SendGrid/SMTP/Mock)
2. **Domain-Driven Design**: Security tokens are first-class domain entities with business logic
3. **Security First**: SHA-256 token hashing, email enumeration prevention, rate limiting
4. **Fail-Safe**: Email failures don't block user actions
5. **Scalability**: In-memory rate limiting for Day 7, Redis-ready for production

### Next Steps

1. **Backend Team**: Implement Phase 1-4 in order
2. **Frontend Team**: Build email verification/reset UI (Day 8)
3. **QA Team**: Prepare test scenarios for all 4 features
4. **DevOps Team**: Provision SendGrid account and configure secrets

### Estimated Timeline

- **Phase 1** (Email Infrastructure): 4-6 hours
- **Phase 2** (Email Verification): 6-8 hours
- **Phase 3** (Password Reset): 6-8 hours
- **Phase 4** (Invitations): 8-10 hours
- **Phase 5** (Polish): 4-6 hours

**Total**: 28-38 hours (~4-5 working days for 1 backend engineer)

---

**Document Status**: ✅ Ready for Implementation
**Last Updated**: 2025-11-03
**Review Required**: Backend Lead, Security Team, Product Manager
