# Day 6 Architecture Design: Role Management API + Email Verification

**Date**: 2025-11-03
**Author**: System Architect
**Status**: Ready for Implementation

---

## Executive Summary

This document provides comprehensive technical architecture for **Day 6 development**, building upon the successful Day 5 implementation (Refresh Token + RBAC + Integration Tests). Day 6 focuses on two key feature areas:

1. **Role Management API** (Priority 1) - Enable tenant owners to manage user roles
2. **Email Verification** (Priority 2) - Complete email verification flow with anti-abuse mechanisms

Both features are designed with **MCP integration** in mind, following Clean Architecture principles and maintaining backward compatibility with existing Day 5 implementation.

---

## Table of Contents

- [1. Day 5 Recap: What's Already Built](#1-day-5-recap-whats-already-built)
- [2. Scenario A: Role Management API](#2-scenario-a-role-management-api)
- [3. Scenario B: Email Verification](#3-scenario-b-email-verification)
- [4. Scenario C: Combined Implementation](#4-scenario-c-combined-implementation)
- [5. Implementation Roadmap](#5-implementation-roadmap)
- [6. Risk Assessment](#6-risk-assessment)
- [7. Testing Strategy](#7-testing-strategy)
- [8. MCP Integration Considerations](#8-mcp-integration-considerations)

---

## 1. Day 5 Recap: What's Already Built

### 1.1 Existing Infrastructure

Day 5 successfully implemented:

✅ **Refresh Token Mechanism**
- `RefreshToken` entity with token family tracking
- `RefreshTokenService` with rotation and revocation
- `/api/auth/refresh`, `/api/auth/logout`, `/api/auth/logout-all` endpoints

✅ **RBAC System**
- 5 tenant-level roles: `TenantOwner`, `TenantAdmin`, `TenantMember`, `TenantGuest`, `AIAgent`
- `UserTenantRole` entity with role assignment tracking
- JWT claims include `tenant_role` for authorization
- Authorization policies configured

✅ **Integration Testing**
- 31 tests, 100% pass rate
- Test infrastructure for auth flows

### 1.2 Existing Database Schema

**Already in database**:
```sql
-- identity.users (with email verification fields)
CREATE TABLE identity.users (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    email VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255),
    full_name VARCHAR(255) NOT NULL,
    status VARCHAR(50) NOT NULL,
    auth_provider VARCHAR(50) NOT NULL,
    email_verified_at TIMESTAMP NULL,
    email_verification_token VARCHAR(500) NULL,
    password_reset_token VARCHAR(500) NULL,
    password_reset_token_expires_at TIMESTAMP NULL,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NULL,
    last_login_at TIMESTAMP NULL
);

-- identity.user_tenant_roles
CREATE TABLE identity.user_tenant_roles (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    tenant_id UUID NOT NULL,
    role VARCHAR(50) NOT NULL,
    assigned_at TIMESTAMP NOT NULL,
    assigned_by_user_id UUID NULL,
    CONSTRAINT uq_user_tenant_role UNIQUE (user_id, tenant_id)
);

-- identity.refresh_tokens
CREATE TABLE identity.refresh_tokens (
    id UUID PRIMARY KEY,
    token_hash VARCHAR(128) NOT NULL UNIQUE,
    user_id UUID NOT NULL,
    tenant_id UUID NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP NOT NULL,
    revoked_at TIMESTAMP NULL,
    token_family UUID NOT NULL
);
```

### 1.3 What's Missing (Day 6 Goals)

❌ **Role Management API**: No endpoints to assign/update/remove roles
❌ **Email Verification Flow**: Tokens not generated, emails not sent
❌ **Email Service**: No email provider integration (SendGrid/SMTP)
❌ **Anti-abuse Mechanisms**: No rate limiting on email operations
❌ **User Management API**: No endpoints to list/view users

---

## 2. Scenario A: Role Management API

### 2.1 Overview

Enable **TenantOwner** to manage user roles within their tenant. This is critical for:
- Delegating administrative responsibilities
- Controlling access to sensitive operations
- Preparing for multi-project role assignments

### 2.2 Database Design

**No new tables needed** - Day 5 already created `user_tenant_roles` table.

**Add index for performance**:
```sql
-- Optimize role lookups by tenant
CREATE INDEX IF NOT EXISTS idx_user_tenant_roles_tenant_role
ON identity.user_tenant_roles(tenant_id, role);
```

### 2.3 API Design

#### 2.3.1 Endpoints

| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
| GET | `/api/tenants/{tenantId}/users` | List all users in tenant | TenantAdmin+ |
| GET | `/api/tenants/{tenantId}/users/{userId}` | Get user details | TenantAdmin+ |
| POST | `/api/tenants/{tenantId}/users/{userId}/role` | Assign role to user | TenantOwner |
| PUT | `/api/tenants/{tenantId}/users/{userId}/role` | Update user's role | TenantOwner |
| DELETE | `/api/tenants/{tenantId}/users/{userId}/role` | Remove user from tenant | TenantOwner |

#### 2.3.2 DTOs

**Request DTOs**:

```csharp
// POST/PUT /api/tenants/{tenantId}/users/{userId}/role
public record AssignRoleRequest
{
    [Required]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public TenantRole Role { get; init; }
}

// Query parameters for user listing
public record ListUsersQuery
{
    public TenantRole? Role { get; init; }
    public UserStatus? Status { get; init; }
    public int Page { get; init; } = 1;
    public int PageSize { get; init; } = 20;
    public string? SearchTerm { get; init; }
}
```

**Response DTOs**:

```csharp
public record UserWithRoleDto
{
    public Guid UserId { get; init; }
    public string Email { get; init; } = string.Empty;
    public string FullName { get; init; } = string.Empty;
    public TenantRole Role { get; init; }
    public UserStatus Status { get; init; }
    public DateTime? LastLoginAt { get; init; }
    public DateTime? EmailVerifiedAt { get; init; }
    public DateTime AssignedAt { get; init; }
    public Guid? AssignedByUserId { get; init; }
    public string? AssignedByUserName { get; init; }
}

public record PagedResult<T>
{
    public IReadOnlyList<T> Items { get; init; } = Array.Empty<T>();
    public int TotalCount { get; init; }
    public int Page { get; init; }
    public int PageSize { get; init; }
    public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
}
```

### 2.4 Domain Layer Design

**No new entities needed** - Day 5 already has `UserTenantRole`.

**Add business validation methods to `UserTenantRole`**:

```csharp
// Add to UserTenantRole.cs
public static class UserTenantRoleValidator
{
    public static void ValidateRoleChange(UserTenantRole existingRole, TenantRole newRole, Guid operatorUserId)
    {
        // Rule 1: Cannot remove the last TenantOwner
        if (existingRole.Role == TenantRole.TenantOwner && newRole != TenantRole.TenantOwner)
        {
            throw new InvalidOperationException(
                "Cannot remove the last TenantOwner. Assign another TenantOwner first.");
        }

        // Rule 2: Cannot self-demote from TenantOwner
        if (existingRole.Role == TenantRole.TenantOwner &&
            existingRole.UserId.Value == operatorUserId &&
            newRole != TenantRole.TenantOwner)
        {
            throw new InvalidOperationException(
                "Cannot demote yourself from TenantOwner. Have another owner perform this action.");
        }

        // Rule 3: AIAgent role requires special permission (future)
        if (newRole == TenantRole.AIAgent)
        {
            throw new InvalidOperationException(
                "AIAgent role cannot be assigned manually. Use MCP integration.");
        }
    }
}
```

### 2.5 Application Layer Design

#### 2.5.1 Commands

**File**: `Application/Commands/AssignUserRole/AssignUserRoleCommand.cs`

```csharp
public record AssignUserRoleCommand(
    Guid TenantId,
    Guid UserId,
    TenantRole Role
) : IRequest<UserWithRoleDto>;

public class AssignUserRoleCommandHandler : IRequestHandler<AssignUserRoleCommand, UserWithRoleDto>
{
    private readonly IUserRepository _userRepository;
    private readonly IUserTenantRoleRepository _roleRepository;
    private readonly ITenantRepository _tenantRepository;
    private readonly ILogger<AssignUserRoleCommandHandler> _logger;

    public async Task<UserWithRoleDto> Handle(
        AssignUserRoleCommand request,
        CancellationToken cancellationToken)
    {
        // 1. Validate tenant exists
        var tenant = await _tenantRepository.GetByIdAsync(request.TenantId, cancellationToken);
        if (tenant == null || tenant.Status != TenantStatus.Active)
            throw new NotFoundException($"Tenant {request.TenantId} not found or inactive");

        // 2. Validate user exists in tenant
        var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
        if (user == null || user.TenantId.Value != request.TenantId)
            throw new NotFoundException($"User {request.UserId} not found in tenant");

        if (user.Status != UserStatus.Active)
            throw new InvalidOperationException("Cannot assign role to inactive user");

        // 3. Check if role already assigned
        var existingRole = await _roleRepository.GetByUserAndTenantAsync(
            request.UserId,
            request.TenantId,
            cancellationToken);

        if (existingRole != null)
            throw new InvalidOperationException(
                $"User already has role {existingRole.Role}. Use update endpoint instead.");

        // 4. Validate AIAgent role restriction
        if (request.Role == TenantRole.AIAgent)
            throw new InvalidOperationException("AIAgent role cannot be assigned manually");

        // 5. Create role assignment
        var role = UserTenantRole.Create(
            UserId.From(request.UserId),
            TenantId.From(request.TenantId),
            request.Role,
            assignedByUserId: null // Set from HTTP context in controller
        );

        await _roleRepository.AddAsync(role, cancellationToken);

        _logger.LogInformation(
            "Assigned role {Role} to user {UserId} in tenant {TenantId}",
            request.Role, request.UserId, request.TenantId);

        // 6. Return DTO
        return new UserWithRoleDto
        {
            UserId = user.Id,
            Email = user.Email.Value,
            FullName = user.FullName.Value,
            Role = role.Role,
            Status = user.Status,
            LastLoginAt = user.LastLoginAt,
            EmailVerifiedAt = user.EmailVerifiedAt,
            AssignedAt = role.AssignedAt
        };
    }
}
```

**File**: `Application/Commands/UpdateUserRole/UpdateUserRoleCommand.cs`

```csharp
public record UpdateUserRoleCommand(
    Guid TenantId,
    Guid UserId,
    TenantRole NewRole,
    Guid OperatorUserId
) : IRequest<UserWithRoleDto>;

public class UpdateUserRoleCommandHandler : IRequestHandler<UpdateUserRoleCommand, UserWithRoleDto>
{
    private readonly IUserRepository _userRepository;
    private readonly IUserTenantRoleRepository _roleRepository;
    private readonly ILogger<UpdateUserRoleCommandHandler> _logger;

    public async Task<UserWithRoleDto> Handle(
        UpdateUserRoleCommand request,
        CancellationToken cancellationToken)
    {
        // 1. Get existing role
        var existingRole = await _roleRepository.GetByUserAndTenantAsync(
            request.UserId,
            request.TenantId,
            cancellationToken);

        if (existingRole == null)
            throw new NotFoundException("User role not found. Use assign endpoint to create.");

        // 2. Validate role change
        await ValidateRoleChangeAsync(
            existingRole,
            request.NewRole,
            request.OperatorUserId,
            request.TenantId,
            cancellationToken);

        // 3. Update role
        existingRole.UpdateRole(request.NewRole, request.OperatorUserId);
        await _roleRepository.UpdateAsync(existingRole, cancellationToken);

        _logger.LogInformation(
            "Updated role for user {UserId} in tenant {TenantId} from {OldRole} to {NewRole}",
            request.UserId, request.TenantId, existingRole.Role, request.NewRole);

        // 4. Load user for DTO
        var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);

        return new UserWithRoleDto
        {
            UserId = user!.Id,
            Email = user.Email.Value,
            FullName = user.FullName.Value,
            Role = existingRole.Role,
            Status = user.Status,
            LastLoginAt = user.LastLoginAt,
            EmailVerifiedAt = user.EmailVerifiedAt,
            AssignedAt = existingRole.AssignedAt,
            AssignedByUserId = existingRole.AssignedByUserId
        };
    }

    private async Task ValidateRoleChangeAsync(
        UserTenantRole existingRole,
        TenantRole newRole,
        Guid operatorUserId,
        Guid tenantId,
        CancellationToken cancellationToken)
    {
        // Rule 1: Cannot self-demote from TenantOwner
        if (existingRole.Role == TenantRole.TenantOwner &&
            existingRole.UserId.Value == operatorUserId &&
            newRole != TenantRole.TenantOwner)
        {
            throw new InvalidOperationException(
                "Cannot demote yourself from TenantOwner");
        }

        // Rule 2: Cannot remove last TenantOwner
        if (existingRole.Role == TenantRole.TenantOwner && newRole != TenantRole.TenantOwner)
        {
            var ownerCount = await _roleRepository.CountByTenantAndRoleAsync(
                tenantId,
                TenantRole.TenantOwner,
                cancellationToken);

            if (ownerCount <= 1)
            {
                throw new InvalidOperationException(
                    "Cannot remove the last TenantOwner. Assign another owner first.");
            }
        }

        // Rule 3: AIAgent role restriction
        if (newRole == TenantRole.AIAgent)
        {
            throw new InvalidOperationException("AIAgent role cannot be assigned manually");
        }
    }
}
```

**File**: `Application/Commands/RemoveUserFromTenant/RemoveUserFromTenantCommand.cs`

```csharp
public record RemoveUserFromTenantCommand(
    Guid TenantId,
    Guid UserId,
    Guid OperatorUserId
) : IRequest<bool>;

public class RemoveUserFromTenantCommandHandler : IRequestHandler<RemoveUserFromTenantCommand, bool>
{
    private readonly IUserRepository _userRepository;
    private readonly IUserTenantRoleRepository _roleRepository;
    private readonly IRefreshTokenRepository _refreshTokenRepository;
    private readonly ILogger<RemoveUserFromTenantCommandHandler> _logger;

    public async Task<bool> Handle(
        RemoveUserFromTenantCommand request,
        CancellationToken cancellationToken)
    {
        // 1. Get existing role
        var existingRole = await _roleRepository.GetByUserAndTenantAsync(
            request.UserId,
            request.TenantId,
            cancellationToken);

        if (existingRole == null)
            throw new NotFoundException("User not found in tenant");

        // 2. Validate not removing last owner
        if (existingRole.Role == TenantRole.TenantOwner)
        {
            var ownerCount = await _roleRepository.CountByTenantAndRoleAsync(
                request.TenantId,
                TenantRole.TenantOwner,
                cancellationToken);

            if (ownerCount <= 1)
            {
                throw new InvalidOperationException(
                    "Cannot remove the last TenantOwner");
            }
        }

        // 3. Validate not removing self (optional - can be allowed)
        if (request.UserId == request.OperatorUserId)
        {
            throw new InvalidOperationException("Cannot remove yourself from tenant");
        }

        // 4. Delete role (cascade will handle cleanup)
        await _roleRepository.DeleteAsync(existingRole, cancellationToken);

        // 5. Revoke all refresh tokens for this user in this tenant
        var tokens = await _refreshTokenRepository.GetByUserAndTenantAsync(
            request.UserId,
            request.TenantId,
            cancellationToken);

        foreach (var token in tokens.Where(t => !t.RevokedAt.HasValue))
        {
            token.Revoke("User removed from tenant");
        }

        await _refreshTokenRepository.UpdateRangeAsync(tokens, cancellationToken);

        // 6. Optionally deactivate user (if they're not in other tenants)
        // For now, just remove role

        _logger.LogInformation(
            "Removed user {UserId} from tenant {TenantId}",
            request.UserId, request.TenantId);

        return true;
    }
}
```

#### 2.5.2 Queries

**File**: `Application/Queries/ListTenantUsers/ListTenantUsersQuery.cs`

```csharp
public record ListTenantUsersQuery(
    Guid TenantId,
    TenantRole? Role = null,
    UserStatus? Status = null,
    string? SearchTerm = null,
    int Page = 1,
    int PageSize = 20
) : IRequest<PagedResult<UserWithRoleDto>>;

public class ListTenantUsersQueryHandler : IRequestHandler<ListTenantUsersQuery, PagedResult<UserWithRoleDto>>
{
    private readonly IUserRepository _userRepository;
    private readonly IUserTenantRoleRepository _roleRepository;

    public async Task<PagedResult<UserWithRoleDto>> Handle(
        ListTenantUsersQuery request,
        CancellationToken cancellationToken)
    {
        // 1. Get all roles for tenant
        var roles = await _roleRepository.GetByTenantAsync(
            request.TenantId,
            cancellationToken);

        // 2. Filter by role if specified
        if (request.Role.HasValue)
        {
            roles = roles.Where(r => r.Role == request.Role.Value).ToList();
        }

        // 3. Load users for these roles
        var userIds = roles.Select(r => r.UserId.Value).ToList();
        var users = await _userRepository.GetByIdsAsync(userIds, cancellationToken);

        // 4. Filter by status
        if (request.Status.HasValue)
        {
            users = users.Where(u => u.Status == request.Status.Value).ToList();
        }

        // 5. Filter by search term
        if (!string.IsNullOrWhiteSpace(request.SearchTerm))
        {
            var searchLower = request.SearchTerm.ToLower();
            users = users.Where(u =>
                u.Email.Value.ToLower().Contains(searchLower) ||
                u.FullName.Value.ToLower().Contains(searchLower)
            ).ToList();
        }

        // 6. Pagination
        var totalCount = users.Count;
        var pagedUsers = users
            .Skip((request.Page - 1) * request.PageSize)
            .Take(request.PageSize)
            .ToList();

        // 7. Build DTOs
        var userDtos = pagedUsers.Select(user =>
        {
            var role = roles.First(r => r.UserId.Value == user.Id);
            return new UserWithRoleDto
            {
                UserId = user.Id,
                Email = user.Email.Value,
                FullName = user.FullName.Value,
                Role = role.Role,
                Status = user.Status,
                LastLoginAt = user.LastLoginAt,
                EmailVerifiedAt = user.EmailVerifiedAt,
                AssignedAt = role.AssignedAt,
                AssignedByUserId = role.AssignedByUserId
            };
        }).ToList();

        return new PagedResult<UserWithRoleDto>
        {
            Items = userDtos,
            TotalCount = totalCount,
            Page = request.Page,
            PageSize = request.PageSize
        };
    }
}
```

### 2.6 Infrastructure Layer

**Add repository method**: `IUserTenantRoleRepository.cs`

```csharp
// Add to existing interface
Task<int> CountByTenantAndRoleAsync(
    Guid tenantId,
    TenantRole role,
    CancellationToken cancellationToken = default);
```

**Implementation**: `UserTenantRoleRepository.cs`

```csharp
// Add to existing repository
public async Task<int> CountByTenantAndRoleAsync(
    Guid tenantId,
    TenantRole role,
    CancellationToken cancellationToken)
{
    return await _context.UserTenantRoles
        .CountAsync(r => r.TenantId.Value == tenantId && r.Role == role, cancellationToken);
}
```

**Add repository method**: `IUserRepository.cs`

```csharp
// Add to existing interface
Task<IReadOnlyList<User>> GetByIdsAsync(
    IEnumerable<Guid> userIds,
    CancellationToken cancellationToken = default);
```

### 2.7 API Layer

**New Controller**: `API/Controllers/TenantUsersController.cs`

```csharp
[ApiController]
[Route("api/tenants/{tenantId:guid}/users")]
[Authorize]
public class TenantUsersController : ControllerBase
{
    private readonly IMediator _mediator;
    private readonly ILogger<TenantUsersController> _logger;

    public TenantUsersController(IMediator mediator, ILogger<TenantUsersController> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }

    /// <summary>
    /// List all users in tenant
    /// </summary>
    [HttpGet]
    [Authorize(Roles = "TenantOwner,TenantAdmin")]
    [ProducesResponseType(typeof(PagedResult<UserWithRoleDto>), 200)]
    public async Task<ActionResult<PagedResult<UserWithRoleDto>>> ListUsers(
        Guid tenantId,
        [FromQuery] ListUsersQuery query)
    {
        // Validate tenant access
        var userTenantId = Guid.Parse(User.FindFirstValue("tenant_id")!);
        if (userTenantId != tenantId)
            return Forbid();

        var fullQuery = query with { TenantId = tenantId };
        var result = await _mediator.Send(fullQuery);

        return Ok(result);
    }

    /// <summary>
    /// Assign role to user (creates new role assignment)
    /// </summary>
    [HttpPost("{userId:guid}/role")]
    [Authorize(Roles = "TenantOwner")]
    [ProducesResponseType(typeof(UserWithRoleDto), 200)]
    [ProducesResponseType(400)]
    [ProducesResponseType(403)]
    [ProducesResponseType(409)]
    public async Task<ActionResult<UserWithRoleDto>> AssignRole(
        Guid tenantId,
        Guid userId,
        [FromBody] AssignRoleRequest request)
    {
        try
        {
            // Validate tenant access
            var userTenantId = Guid.Parse(User.FindFirstValue("tenant_id")!);
            if (userTenantId != tenantId)
                return Forbid();

            var command = new AssignUserRoleCommand(tenantId, userId, request.Role);
            var result = await _mediator.Send(command);

            return Ok(result);
        }
        catch (InvalidOperationException ex)
        {
            _logger.LogWarning(ex, "Failed to assign role");
            return Conflict(new { message = ex.Message });
        }
        catch (NotFoundException ex)
        {
            return NotFound(new { message = ex.Message });
        }
    }

    /// <summary>
    /// Update user's role
    /// </summary>
    [HttpPut("{userId:guid}/role")]
    [Authorize(Roles = "TenantOwner")]
    [ProducesResponseType(typeof(UserWithRoleDto), 200)]
    public async Task<ActionResult<UserWithRoleDto>> UpdateRole(
        Guid tenantId,
        Guid userId,
        [FromBody] AssignRoleRequest request)
    {
        try
        {
            var userTenantId = Guid.Parse(User.FindFirstValue("tenant_id")!);
            if (userTenantId != tenantId)
                return Forbid();

            var operatorUserId = Guid.Parse(User.FindFirstValue("user_id")!);
            var command = new UpdateUserRoleCommand(tenantId, userId, request.Role, operatorUserId);
            var result = await _mediator.Send(command);

            return Ok(result);
        }
        catch (InvalidOperationException ex)
        {
            _logger.LogWarning(ex, "Failed to update role");
            return Conflict(new { message = ex.Message });
        }
        catch (NotFoundException ex)
        {
            return NotFound(new { message = ex.Message });
        }
    }

    /// <summary>
    /// Remove user from tenant (deletes role assignment)
    /// </summary>
    [HttpDelete("{userId:guid}/role")]
    [Authorize(Roles = "TenantOwner")]
    [ProducesResponseType(204)]
    public async Task<IActionResult> RemoveUser(Guid tenantId, Guid userId)
    {
        try
        {
            var userTenantId = Guid.Parse(User.FindFirstValue("tenant_id")!);
            if (userTenantId != tenantId)
                return Forbid();

            var operatorUserId = Guid.Parse(User.FindFirstValue("user_id")!);
            var command = new RemoveUserFromTenantCommand(tenantId, userId, operatorUserId);
            await _mediator.Send(command);

            return NoContent();
        }
        catch (InvalidOperationException ex)
        {
            _logger.LogWarning(ex, "Failed to remove user");
            return Conflict(new { message = ex.Message });
        }
        catch (NotFoundException ex)
        {
            return NotFound(new { message = ex.Message });
        }
    }
}
```

### 2.8 Security Considerations

**Authorization Rules**:
1. Only `TenantOwner` can assign/update/remove roles
2. `TenantAdmin` can view user list
3. Users must be in the same tenant as the target user
4. Cannot self-demote from `TenantOwner`
5. Cannot remove last `TenantOwner`
6. `AIAgent` role cannot be assigned manually (reserved for MCP)

**Audit Logging** (future enhancement):
```csharp
// Log all role changes to audit table
public record RoleChangeAuditLog
{
    public Guid Id { get; init; }
    public Guid TenantId { get; init; }
    public Guid UserId { get; init; }
    public TenantRole OldRole { get; init; }
    public TenantRole NewRole { get; init; }
    public Guid ChangedByUserId { get; init; }
    public DateTime ChangedAt { get; init; }
    public string Reason { get; init; } = string.Empty;
}
```

### 2.9 Complexity & Time Estimate

| Task | Complexity | Time |
|------|-----------|------|
| Commands (Assign/Update/Remove) | Medium | 3 hours |
| Queries (List users) | Low | 1 hour |
| Repository methods | Low | 1 hour |
| Controller & DTOs | Low | 1.5 hours |
| Validation logic | Medium | 1.5 hours |
| Integration tests | Medium | 2 hours |
| **Total** | - | **10 hours** |

---

## 3. Scenario B: Email Verification

### 3.1 Overview

Complete the email verification flow with:
- Email verification token generation
- SendGrid/SMTP integration
- Verification endpoint
- Resend verification email
- Anti-abuse mechanisms (rate limiting)

### 3.2 Database Design

**Update existing `users` table**:

```sql
-- Add missing column
ALTER TABLE identity.users
ADD COLUMN IF NOT EXISTS email_verification_token_expires_at TIMESTAMP NULL;

-- Add index for verification token lookup
CREATE INDEX IF NOT EXISTS idx_users_email_verification_token
ON identity.users(email_verification_token)
WHERE email_verification_token IS NOT NULL;
```

**New table for rate limiting** (optional, can use in-memory cache):

```sql
CREATE TABLE IF NOT EXISTS identity.email_rate_limits (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL,
    tenant_id UUID NOT NULL,
    operation_type VARCHAR(50) NOT NULL, -- 'verification', 'password_reset'
    last_sent_at TIMESTAMP NOT NULL,
    attempts_count INT NOT NULL DEFAULT 1,

    CONSTRAINT uq_email_rate_limit UNIQUE (email, tenant_id, operation_type)
);

CREATE INDEX idx_email_rate_limits_email ON identity.email_rate_limits(email, tenant_id);
CREATE INDEX idx_email_rate_limits_cleanup ON identity.email_rate_limits(last_sent_at);
```

### 3.3 Email Service Design

#### 3.3.1 Technology Selection

| Provider | Pros | Cons | Recommendation |
|----------|------|------|----------------|
| **SendGrid** | Easy setup, 100 emails/day free, good deliverability | Rate limits on free tier | ✅ **Recommended for MVP** |
| **AWS SES** | Very cheap ($0.10/1000), highly scalable | Complex setup, requires AWS account | Production upgrade |
| **MailKit (SMTP)** | No external dependency, self-hosted | Requires SMTP server, lower deliverability | Development fallback |
| **Mailgun** | Developer-friendly | Limited free tier | Alternative |

**Decision**: Use **SendGrid** for MVP with **MailKit fallback** for local development.

#### 3.3.2 Interface Design

**File**: `Application/Services/IEmailService.cs`

```csharp
public interface IEmailService
{
    /// <summary>
    /// Send email verification email
    /// </summary>
    Task SendEmailVerificationAsync(
        string recipientEmail,
        string recipientName,
        string verificationToken,
        string tenantSlug,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Send password reset email
    /// </summary>
    Task SendPasswordResetAsync(
        string recipientEmail,
        string recipientName,
        string resetToken,
        string tenantSlug,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Send welcome email after verification
    /// </summary>
    Task SendWelcomeEmailAsync(
        string recipientEmail,
        string recipientName,
        string tenantName,
        CancellationToken cancellationToken = default);
}
```

#### 3.3.3 SendGrid Implementation

**File**: `Infrastructure/Services/SendGridEmailService.cs`

```csharp
public class SendGridEmailService : IEmailService
{
    private readonly IConfiguration _configuration;
    private readonly ILogger<SendGridEmailService> _logger;
    private readonly SendGridClient _client;

    public SendGridEmailService(
        IConfiguration configuration,
        ILogger<SendGridEmailService> logger)
    {
        _configuration = configuration;
        _logger = logger;

        var apiKey = _configuration["SendGrid:ApiKey"];
        if (string.IsNullOrEmpty(apiKey))
        {
            _logger.LogWarning("SendGrid API key not configured");
            throw new InvalidOperationException("SendGrid API key not configured");
        }

        _client = new SendGridClient(apiKey);
    }

    public async Task SendEmailVerificationAsync(
        string recipientEmail,
        string recipientName,
        string verificationToken,
        string tenantSlug,
        CancellationToken cancellationToken)
    {
        var from = new EmailAddress(
            _configuration["SendGrid:FromEmail"] ?? "noreply@colaflow.com",
            "ColaFlow");

        var to = new EmailAddress(recipientEmail, recipientName);

        var verificationUrl = BuildVerificationUrl(verificationToken, tenantSlug);

        var subject = "Verify your ColaFlow email address";
        var plainTextContent = $@"
Hello {recipientName},

Please verify your email address by clicking the link below:

{verificationUrl}

This link expires in 24 hours.

If you didn't create this account, please ignore this email.

Best regards,
ColaFlow Team
";

        var htmlContent = $@"
<!DOCTYPE html>
<html>
<head>
    <style>
        body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
        .container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
        .header {{ background-color: #4CAF50; color: white; padding: 20px; text-align: center; }}
        .content {{ padding: 20px; background-color: #f9f9f9; }}
        .button {{ display: inline-block; background-color: #4CAF50; color: white;
                   padding: 12px 24px; text-decoration: none; border-radius: 4px;
                   font-weight: bold; }}
        .footer {{ text-align: center; padding: 20px; color: #666; font-size: 12px; }}
    </style>
</head>
<body>
    <div class=""container"">
        <div class=""header"">
            <h1>Welcome to ColaFlow!</h1>
        </div>
        <div class=""content"">
            <p>Hello {recipientName},</p>
            <p>Thank you for registering with ColaFlow. Please verify your email address to complete your registration.</p>
            <p style=""text-align: center; margin: 30px 0;"">
                <a href=""{verificationUrl}"" class=""button"">Verify Email Address</a>
            </p>
            <p>Or copy and paste this link into your browser:</p>
            <p style=""word-break: break-all; color: #666;"">{verificationUrl}</p>
            <p><strong>This link expires in 24 hours.</strong></p>
            <p>If you didn't create this account, please ignore this email.</p>
        </div>
        <div class=""footer"">
            <p>&copy; 2025 ColaFlow. All rights reserved.</p>
        </div>
    </div>
</body>
</html>
";

        var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);

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

        if (response.StatusCode != System.Net.HttpStatusCode.OK &&
            response.StatusCode != System.Net.HttpStatusCode.Accepted)
        {
            _logger.LogError(
                "Failed to send verification email to {Email}, status: {Status}",
                recipientEmail, response.StatusCode);

            throw new InvalidOperationException($"Failed to send verification email: {response.StatusCode}");
        }

        _logger.LogInformation("Sent verification email to {Email}", recipientEmail);
    }

    public async Task SendPasswordResetAsync(
        string recipientEmail,
        string recipientName,
        string resetToken,
        string tenantSlug,
        CancellationToken cancellationToken)
    {
        // Similar implementation
        // URL: https://app.colaflow.com/{tenantSlug}/reset-password?token={resetToken}
        throw new NotImplementedException("Password reset email - Day 7");
    }

    public async Task SendWelcomeEmailAsync(
        string recipientEmail,
        string recipientName,
        string tenantName,
        CancellationToken cancellationToken)
    {
        // Similar implementation
        throw new NotImplementedException("Welcome email - Day 7");
    }

    private string BuildVerificationUrl(string token, string tenantSlug)
    {
        var baseUrl = _configuration["App:FrontendUrl"] ?? "http://localhost:3000";
        return $"{baseUrl}/{tenantSlug}/verify-email?token={token}";
    }
}
```

#### 3.3.4 SMTP Fallback (Development)

**File**: `Infrastructure/Services/SmtpEmailService.cs`

```csharp
public class SmtpEmailService : IEmailService
{
    private readonly IConfiguration _configuration;
    private readonly ILogger<SmtpEmailService> _logger;

    public async Task SendEmailVerificationAsync(
        string recipientEmail,
        string recipientName,
        string verificationToken,
        string tenantSlug,
        CancellationToken cancellationToken)
    {
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("ColaFlow", "noreply@colaflow.local"));
        message.To.Add(new MailboxAddress(recipientName, recipientEmail));
        message.Subject = "Verify your ColaFlow email address";

        var verificationUrl = BuildVerificationUrl(verificationToken, tenantSlug);

        var bodyBuilder = new BodyBuilder
        {
            TextBody = $"Please verify your email: {verificationUrl}",
            HtmlBody = $"<p>Please verify your email:</p><p><a href=\"{verificationUrl}\">Verify Email</a></p>"
        };

        message.Body = bodyBuilder.ToMessageBody();

        using var client = new SmtpClient();
        await client.ConnectAsync(
            _configuration["Smtp:Host"] ?? "localhost",
            _configuration.GetValue<int>("Smtp:Port", 587),
            SecureSocketOptions.StartTls,
            cancellationToken);

        await client.AuthenticateAsync(
            _configuration["Smtp:Username"],
            _configuration["Smtp:Password"],
            cancellationToken);

        await client.SendAsync(message, cancellationToken);
        await client.DisconnectAsync(true, cancellationToken);

        _logger.LogInformation("Sent verification email to {Email} via SMTP", recipientEmail);
    }

    // Other methods similar...
}
```

### 3.4 Domain Layer Updates

**Update `User.cs`** with token validation:

```csharp
// Add to User.cs
public void SetEmailVerificationToken(string plainTextToken, DateTime expiresAt)
{
    // Hash token before storage
    EmailVerificationToken = ComputeSha256Hash(plainTextToken);
    EmailVerificationTokenExpiresAt = expiresAt;
    UpdatedAt = DateTime.UtcNow;
}

public bool IsEmailVerificationTokenValid(string plainTextToken)
{
    if (string.IsNullOrEmpty(EmailVerificationToken) ||
        !EmailVerificationTokenExpiresAt.HasValue)
    {
        return false;
    }

    if (DateTime.UtcNow > EmailVerificationTokenExpiresAt)
    {
        return false;
    }

    var tokenHash = ComputeSha256Hash(plainTextToken);
    return EmailVerificationToken == tokenHash;
}

public void VerifyEmailWithToken(string plainTextToken)
{
    if (!IsEmailVerificationTokenValid(plainTextToken))
    {
        throw new InvalidOperationException("Invalid or expired verification token");
    }

    VerifyEmail(); // Call existing method
}

private static string ComputeSha256Hash(string input)
{
    using var sha256 = SHA256.Create();
    var bytes = Encoding.UTF8.GetBytes(input);
    var hash = sha256.ComputeHash(bytes);
    return Convert.ToBase64String(hash);
}
```

### 3.5 Application Layer

#### 3.5.1 Commands

**File**: `Application/Commands/VerifyEmail/VerifyEmailCommand.cs`

```csharp
public record VerifyEmailCommand(string Token, string TenantSlug) : IRequest<bool>;

public class VerifyEmailCommandHandler : IRequestHandler<VerifyEmailCommand, bool>
{
    private readonly IUserRepository _userRepository;
    private readonly ITenantRepository _tenantRepository;
    private readonly IEmailService _emailService;
    private readonly ILogger<VerifyEmailCommandHandler> _logger;

    public async Task<bool> Handle(VerifyEmailCommand request, CancellationToken cancellationToken)
    {
        try
        {
            // 1. Get tenant
            var tenant = await _tenantRepository.GetBySlugAsync(request.TenantSlug, cancellationToken);
            if (tenant == null)
            {
                _logger.LogWarning("Verification failed: tenant {Slug} not found", request.TenantSlug);
                return false;
            }

            // 2. Find user by token hash
            var tokenHash = ComputeSha256Hash(request.Token);
            var user = await _userRepository.GetByEmailVerificationTokenAsync(
                tokenHash,
                tenant.Id,
                cancellationToken);

            if (user == null)
            {
                _logger.LogWarning("Verification failed: token not found");
                return false;
            }

            // 3. Verify token and update user
            user.VerifyEmailWithToken(request.Token);
            await _userRepository.UpdateAsync(user, cancellationToken);

            _logger.LogInformation("Email verified for user {UserId}", user.Id);

            // 4. Send welcome email (optional)
            try
            {
                await _emailService.SendWelcomeEmailAsync(
                    user.Email.Value,
                    user.FullName.Value,
                    tenant.Name.Value,
                    cancellationToken);
            }
            catch (Exception ex)
            {
                // Don't fail verification if welcome email fails
                _logger.LogWarning(ex, "Failed to send welcome email");
            }

            return true;
        }
        catch (InvalidOperationException ex)
        {
            _logger.LogWarning(ex, "Email verification failed");
            return false;
        }
    }

    private static string ComputeSha256Hash(string input)
    {
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(input);
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToBase64String(hash);
    }
}
```

**File**: `Application/Commands/ResendVerificationEmail/ResendVerificationEmailCommand.cs`

```csharp
public record ResendVerificationEmailCommand(
    string Email,
    string TenantSlug
) : IRequest<bool>;

public class ResendVerificationEmailCommandHandler
    : IRequestHandler<ResendVerificationEmailCommand, bool>
{
    private readonly IUserRepository _userRepository;
    private readonly ITenantRepository _tenantRepository;
    private readonly IEmailService _emailService;
    private readonly IEmailRateLimiter _rateLimiter;
    private readonly ILogger<ResendVerificationEmailCommandHandler> _logger;

    public async Task<bool> Handle(
        ResendVerificationEmailCommand request,
        CancellationToken cancellationToken)
    {
        // 1. Find tenant
        var tenant = await _tenantRepository.GetBySlugAsync(request.TenantSlug, cancellationToken);
        if (tenant == null)
        {
            // Always return true to prevent tenant enumeration
            _logger.LogWarning("Resend verification: tenant {Slug} not found", request.TenantSlug);
            return true;
        }

        // 2. Find user
        var email = Email.From(request.Email);
        var user = await _userRepository.GetByEmailAsync(email, tenant.Id, cancellationToken);

        if (user == null)
        {
            // Always return true to prevent email enumeration
            _logger.LogWarning("Resend verification: user {Email} not found", request.Email);
            return true;
        }

        // 3. Check if already verified
        if (user.EmailVerifiedAt.HasValue)
        {
            _logger.LogInformation("User {UserId} already verified", user.Id);
            return true;
        }

        // 4. Check rate limit (1 email per minute per email address)
        if (!await _rateLimiter.AllowEmailOperationAsync(
            request.Email,
            tenant.Id,
            "verification",
            TimeSpan.FromMinutes(1),
            cancellationToken))
        {
            _logger.LogWarning(
                "Rate limit exceeded for email {Email}",
                request.Email);

            // Return true to not reveal rate limiting to potential attackers
            return true;
        }

        // 5. Generate new token
        var token = GenerateUrlSafeToken();
        var expiresAt = DateTime.UtcNow.AddHours(24);
        user.SetEmailVerificationToken(token, expiresAt);

        await _userRepository.UpdateAsync(user, cancellationToken);

        // 6. Send email
        try
        {
            await _emailService.SendEmailVerificationAsync(
                user.Email.Value,
                user.FullName.Value,
                token,
                request.TenantSlug,
                cancellationToken);

            _logger.LogInformation("Resent verification email to user {UserId}", user.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send verification email");
            // Don't throw - token is already saved, user can try again
        }

        return true;
    }

    private static string GenerateUrlSafeToken()
    {
        var tokenBytes = new byte[32];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(tokenBytes);
        return Convert.ToBase64String(tokenBytes)
            .Replace("+", "-")
            .Replace("/", "_")
            .TrimEnd('=');
    }
}
```

### 3.6 Rate Limiting Service

**File**: `Application/Services/IEmailRateLimiter.cs`

```csharp
public interface IEmailRateLimiter
{
    Task<bool> AllowEmailOperationAsync(
        string email,
        Guid tenantId,
        string operationType,
        TimeSpan minInterval,
        CancellationToken cancellationToken = default);
}
```

**Implementation**: `Infrastructure/Services/EmailRateLimiter.cs`

```csharp
public class EmailRateLimiter : IEmailRateLimiter
{
    private readonly IdentityDbContext _context;
    private readonly ILogger<EmailRateLimiter> _logger;

    public async Task<bool> AllowEmailOperationAsync(
        string email,
        Guid tenantId,
        string operationType,
        TimeSpan minInterval,
        CancellationToken cancellationToken)
    {
        var now = DateTime.UtcNow;
        var emailLower = email.ToLower();

        // Try to find existing rate limit record
        var rateLimit = await _context.EmailRateLimits
            .FirstOrDefaultAsync(
                r => r.Email == emailLower &&
                     r.TenantId == tenantId &&
                     r.OperationType == operationType,
                cancellationToken);

        if (rateLimit == null)
        {
            // First time - allow and create record
            _context.EmailRateLimits.Add(new EmailRateLimit
            {
                Id = Guid.NewGuid(),
                Email = emailLower,
                TenantId = tenantId,
                OperationType = operationType,
                LastSentAt = now,
                AttemptsCount = 1
            });

            await _context.SaveChangesAsync(cancellationToken);
            return true;
        }

        // Check if enough time has passed
        var timeSinceLastSend = now - rateLimit.LastSentAt;

        if (timeSinceLastSend < minInterval)
        {
            // Rate limit exceeded
            rateLimit.AttemptsCount++;
            await _context.SaveChangesAsync(cancellationToken);

            _logger.LogWarning(
                "Rate limit exceeded for {Email}, operation: {Operation}, attempts: {Attempts}",
                email, operationType, rateLimit.AttemptsCount);

            return false;
        }

        // Allow operation and update record
        rateLimit.LastSentAt = now;
        rateLimit.AttemptsCount = 1;
        await _context.SaveChangesAsync(cancellationToken);

        return true;
    }
}
```

### 3.7 API Layer

**Update `AuthController.cs`**:

```csharp
// Add to existing AuthController

/// <summary>
/// Verify email address
/// </summary>
[HttpGet("verify-email")]
[AllowAnonymous]
[ProducesResponseType(302)] // Redirect
public async Task<IActionResult> VerifyEmail(
    [FromQuery] string token,
    [FromQuery] string tenant)
{
    if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tenant))
    {
        return Redirect($"{_configuration["App:FrontendUrl"]}/email-verification-failed");
    }

    var command = new VerifyEmailCommand(token, tenant);
    var result = await _mediator.Send(command);

    if (result)
    {
        return Redirect($"{_configuration["App:FrontendUrl"]}/{tenant}/email-verified");
    }
    else
    {
        return Redirect($"{_configuration["App:FrontendUrl"]}/{tenant}/email-verification-failed");
    }
}

/// <summary>
/// Resend verification email
/// </summary>
[HttpPost("resend-verification")]
[AllowAnonymous]
[ProducesResponseType(200)]
public async Task<IActionResult> ResendVerification(
    [FromBody] ResendVerificationRequest request)
{
    var command = new ResendVerificationEmailCommand(request.Email, request.TenantSlug);
    await _mediator.Send(command);

    // Always return success to prevent email enumeration
    return Ok(new
    {
        message = "If the email exists, a verification link has been sent.",
        success = true
    });
}

/// <summary>
/// Check if email is verified
/// </summary>
[HttpGet("email-status")]
[Authorize]
[ProducesResponseType(typeof(EmailStatusDto), 200)]
public async Task<ActionResult<EmailStatusDto>> GetEmailStatus()
{
    var userId = Guid.Parse(User.FindFirstValue("user_id")!);
    var user = await _userRepository.GetByIdAsync(userId);

    if (user == null)
        return NotFound();

    return Ok(new EmailStatusDto
    {
        Email = user.Email.Value,
        IsVerified = user.EmailVerifiedAt.HasValue,
        VerifiedAt = user.EmailVerifiedAt
    });
}

// DTOs
public record ResendVerificationRequest(string Email, string TenantSlug);
public record EmailStatusDto(string Email, bool IsVerified, DateTime? VerifiedAt);
```

### 3.8 Update Registration Flow

**Update `RegisterTenantCommandHandler.cs`**:

```csharp
public async Task<TenantDto> Handle(
    RegisterTenantCommand request,
    CancellationToken cancellationToken)
{
    // ... existing validation and tenant creation ...

    // Create admin user
    var hashedPassword = _passwordHasher.HashPassword(request.AdminPassword);
    var adminUser = User.CreateLocal(tenantId, email, hashedPassword, fullName);

    // Generate email verification token
    var verificationToken = GenerateUrlSafeToken();
    var tokenExpiresAt = DateTime.UtcNow.AddHours(24);
    adminUser.SetEmailVerificationToken(verificationToken, tokenExpiresAt);

    await _userRepository.AddAsync(adminUser, cancellationToken);

    // Assign TenantOwner role
    var tenantRole = UserTenantRole.Create(
        UserId.From(adminUser.Id),
        tenantId,
        TenantRole.TenantOwner);

    await _roleRepository.AddAsync(tenantRole, cancellationToken);

    // Generate JWT (user can login even if email not verified)
    var token = _jwtService.GenerateToken(adminUser, tenant, TenantRole.TenantOwner);

    // Generate refresh token
    var refreshToken = await _refreshTokenService.GenerateRefreshTokenAsync(
        adminUser,
        ipAddress: null,
        userAgent: null,
        cancellationToken);

    // Send verification email (don't fail registration if email fails)
    try
    {
        await _emailService.SendEmailVerificationAsync(
            adminUser.Email.Value,
            adminUser.FullName.Value,
            verificationToken,
            request.TenantSlug,
            cancellationToken);

        _logger.LogInformation(
            "Sent verification email to {Email}",
            adminUser.Email.Value);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to send verification email during registration");
        // Continue - user can resend later
    }

    return new TenantDto
    {
        TenantId = tenant.Id,
        TenantName = tenant.Name.Value,
        TenantSlug = tenant.Slug.Value,
        Plan = tenant.Plan.ToString(),
        AccessToken = token,
        RefreshToken = refreshToken.PlainTextToken,
        ExpiresIn = 3600,
        AdminUser = new UserDto
        {
            UserId = adminUser.Id,
            Email = adminUser.Email.Value,
            FullName = adminUser.FullName.Value,
            EmailVerified = false,
            Role = TenantRole.TenantOwner.ToString()
        }
    };
}

private static string GenerateUrlSafeToken()
{
    var tokenBytes = new byte[32];
    using var rng = RandomNumberGenerator.Create();
    rng.GetBytes(tokenBytes);
    return Convert.ToBase64String(tokenBytes)
        .Replace("+", "-")
        .Replace("/", "_")
        .TrimEnd('=');
}
```

### 3.9 Configuration

**Update `appsettings.Development.json`**:

```json
{
  "SendGrid": {
    "ApiKey": "${SENDGRID_API_KEY}",
    "FromEmail": "noreply@colaflow.local",
    "FromName": "ColaFlow"
  },
  "Smtp": {
    "Host": "localhost",
    "Port": "1025",
    "Username": "",
    "Password": "",
    "UseSsl": false
  },
  "App": {
    "BaseUrl": "http://localhost:5167",
    "FrontendUrl": "http://localhost:3000"
  },
  "EmailVerification": {
    "TokenExpirationHours": "24",
    "RequireVerification": "false",
    "RateLimitMinutes": "1"
  },
  "EmailProvider": "Smtp"
}
```

**Update `appsettings.Production.json`**:

```json
{
  "SendGrid": {
    "ApiKey": "${SENDGRID_API_KEY}",
    "FromEmail": "noreply@colaflow.com",
    "FromName": "ColaFlow"
  },
  "App": {
    "BaseUrl": "https://api.colaflow.com",
    "FrontendUrl": "https://app.colaflow.com"
  },
  "EmailVerification": {
    "TokenExpirationHours": "24",
    "RequireVerification": "true",
    "RateLimitMinutes": "1"
  },
  "EmailProvider": "SendGrid"
}
```

### 3.10 Dependency Injection

**Update `Infrastructure/DependencyInjection.cs`**:

```csharp
public static IServiceCollection AddIdentityInfrastructure(
    this IServiceCollection services,
    IConfiguration configuration)
{
    // ... existing services ...

    // Email service based on configuration
    var emailProvider = configuration["EmailProvider"];

    if (emailProvider == "SendGrid")
    {
        services.AddScoped<IEmailService, SendGridEmailService>();
    }
    else if (emailProvider == "Smtp")
    {
        services.AddScoped<IEmailService, SmtpEmailService>();
    }
    else
    {
        // Default to SMTP for development
        services.AddScoped<IEmailService, SmtpEmailService>();
    }

    // Rate limiter
    services.AddScoped<IEmailRateLimiter, EmailRateLimiter>();

    return services;
}
```

### 3.11 Security Mechanisms

**Anti-Abuse Mechanisms**:

1. **Rate Limiting**:
   - 1 email per minute per email address
   - Tracked in database (persistent across restarts)
   - Configurable via `EmailVerification:RateLimitMinutes`

2. **Email Enumeration Prevention**:
   - Always return success for resend verification (don't reveal if email exists)
   - Generic error messages

3. **Token Security**:
   - 32-byte cryptographically secure random tokens
   - SHA-256 hash stored in database
   - URL-safe base64 encoding
   - 24-hour expiration
   - One-time use only (cleared after verification)

4. **Verification Status Check**:
   - Only authenticated users can check their own email status
   - No endpoint to check other users' email verification status

### 3.12 Complexity & Time Estimate

| Task | Complexity | Time |
|------|-----------|------|
| Email service interface & SendGrid impl | Medium | 2.5 hours |
| SMTP fallback implementation | Low | 1 hour |
| VerifyEmail command & handler | Medium | 1.5 hours |
| ResendVerification command & handler | Medium | 1.5 hours |
| Rate limiter service | Medium | 1.5 hours |
| Update registration flow | Low | 1 hour |
| API endpoints & DTOs | Low | 1 hour |
| Configuration & DI | Low | 0.5 hours |
| Integration tests | Medium | 2 hours |
| **Total** | - | **12.5 hours** |

---

## 4. Scenario C: Combined Implementation

### 4.1 Task Dependencies

```
Day 6 Combined Implementation:

Phase 1: Role Management API (Priority 1)
├── Step 1: Database migration (add index)
├── Step 2: Repository methods
├── Step 3: Commands (Assign/Update/Remove)
├── Step 4: Queries (List users)
├── Step 5: Controller & DTOs
└── Step 6: Integration tests

Phase 2: Email Verification (Priority 2)
├── Step 1: Database migration (add expiration column)
├── Step 2: Email service (SendGrid + SMTP)
├── Step 3: Rate limiter service
├── Step 4: Commands (Verify/Resend)
├── Step 5: Update registration flow
├── Step 6: API endpoints
└── Step 7: Integration tests

No blocking dependencies between phases - can be developed in parallel
```

### 4.2 Database Migration Strategy

**Single migration file** for Day 6:

```csharp
public partial class Day6RoleManagementAndEmailVerification : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Role Management optimizations
        migrationBuilder.Sql(@"
            CREATE INDEX IF NOT EXISTS idx_user_tenant_roles_tenant_role
            ON identity.user_tenant_roles(tenant_id, role);
        ");

        // Email Verification updates
        migrationBuilder.AddColumn<DateTime>(
            name: "email_verification_token_expires_at",
            schema: "identity",
            table: "users",
            type: "timestamp without time zone",
            nullable: true);

        migrationBuilder.Sql(@"
            CREATE INDEX IF NOT EXISTS idx_users_email_verification_token
            ON identity.users(email_verification_token)
            WHERE email_verification_token IS NOT NULL;
        ");

        // Email rate limiting table
        migrationBuilder.CreateTable(
            name: "email_rate_limits",
            schema: "identity",
            columns: table => new
            {
                id = table.Column<Guid>(type: "uuid", nullable: false),
                email = table.Column<string>(type: "character varying(255)", nullable: false),
                tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
                operation_type = table.Column<string>(type: "character varying(50)", nullable: false),
                last_sent_at = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
                attempts_count = table.Column<int>(type: "integer", nullable: false, defaultValue: 1)
            },
            constraints: table =>
            {
                table.PrimaryKey("pk_email_rate_limits", x => x.id);
                table.UniqueConstraint("uq_email_rate_limit", x => new { x.email, x.tenant_id, x.operation_type });
            });

        migrationBuilder.CreateIndex(
            name: "idx_email_rate_limits_email",
            schema: "identity",
            table: "email_rate_limits",
            columns: new[] { "email", "tenant_id" });

        migrationBuilder.CreateIndex(
            name: "idx_email_rate_limits_cleanup",
            schema: "identity",
            table: "email_rate_limits",
            column: "last_sent_at");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "email_rate_limits",
            schema: "identity");

        migrationBuilder.DropIndex(
            name: "idx_users_email_verification_token",
            schema: "identity",
            table: "users");

        migrationBuilder.DropColumn(
            name: "email_verification_token_expires_at",
            schema: "identity",
            table: "users");

        migrationBuilder.DropIndex(
            name: "idx_user_tenant_roles_tenant_role",
            schema: "identity",
            table: "user_tenant_roles");
    }
}
```

### 4.3 Implementation Order

**Recommended order for combined implementation**:

1. **Morning (4 hours)**: Role Management API
   - Database migration
   - Repository methods
   - Commands & queries
   - Controller

2. **Afternoon (4 hours)**: Email Service Core
   - Email service interfaces
   - SendGrid implementation
   - SMTP fallback
   - Rate limiter

3. **Next Day Morning (4 hours)**: Email Verification Flow
   - Commands (Verify/Resend)
   - Update registration flow
   - API endpoints
   - Configuration

4. **Next Day Afternoon (3 hours)**: Testing & Polish
   - Integration tests for role management
   - Integration tests for email verification
   - End-to-end testing
   - Documentation

**Total: 15 hours (2 days)**

### 4.4 Testing Strategy

**Integration Tests Checklist**:

**Role Management**:
- ✅ TenantOwner can assign role to user
- ✅ TenantAdmin cannot assign roles
- ✅ Cannot assign AIAgent role manually
- ✅ Cannot remove last TenantOwner
- ✅ Cannot self-demote from TenantOwner
- ✅ List users returns correct pagination
- ✅ Removing user revokes their refresh tokens

**Email Verification**:
- ✅ Registration sends verification email
- ✅ Verification token works and marks email as verified
- ✅ Expired token is rejected
- ✅ Invalid token is rejected
- ✅ Resend verification works
- ✅ Rate limiting prevents spam
- ✅ Already verified users can login without re-verification

### 4.5 NuGet Packages Required

```xml
<!-- Add to Identity.Infrastructure.csproj -->
<PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="MailKit" Version="4.3.0" />
```

---

## 5. Implementation Roadmap

### 5.1 Day 6 Detailed Schedule

#### Morning Session (8:00 - 12:00) - Role Management API

**8:00 - 9:30**: Database & Domain Layer
- Create migration for Day 6
- Add repository methods (`CountByTenantAndRoleAsync`, `GetByIdsAsync`)
- Add validation logic to `UserTenantRole`

**9:30 - 11:00**: Application Layer
- Implement `AssignUserRoleCommand` & handler
- Implement `UpdateUserRoleCommand` & handler
- Implement `RemoveUserFromTenantCommand` & handler
- Implement `ListTenantUsersQuery` & handler

**11:00 - 12:00**: API Layer
- Create `TenantUsersController`
- Add DTOs (`UserWithRoleDto`, `PagedResult<T>`)
- Test endpoints manually

#### Afternoon Session (13:00 - 17:00) - Email Verification

**13:00 - 14:30**: Email Service
- Implement `IEmailService` interface
- Implement `SendGridEmailService`
- Implement `SmtpEmailService`
- Test email sending locally (SMTP)

**14:30 - 16:00**: Verification Flow
- Implement `VerifyEmailCommand` & handler
- Implement `ResendVerificationEmailCommand` & handler
- Implement `EmailRateLimiter`
- Update `User` entity with token validation

**16:00 - 17:00**: Integration
- Update `RegisterTenantCommandHandler` to send verification email
- Add API endpoints to `AuthController`
- Configure SendGrid/SMTP in appsettings
- Test end-to-end flow

#### Day 7 Morning (8:00 - 11:00) - Testing & Documentation

**8:00 - 10:00**: Integration Tests
- Write tests for role management (8 tests)
- Write tests for email verification (6 tests)
- Run all tests, ensure 100% pass rate

**10:00 - 11:00**: Documentation & Cleanup
- Update API documentation (Swagger)
- Update README with new features
- Create Day 6 implementation summary
- Commit and push changes

### 5.2 Files to Create

**Application Layer** (10 files):
- `Commands/AssignUserRole/AssignUserRoleCommand.cs`
- `Commands/AssignUserRole/AssignUserRoleCommandHandler.cs`
- `Commands/UpdateUserRole/UpdateUserRoleCommand.cs`
- `Commands/UpdateUserRole/UpdateUserRoleCommandHandler.cs`
- `Commands/RemoveUserFromTenant/RemoveUserFromTenantCommand.cs`
- `Commands/RemoveUserFromTenant/RemoveUserFromTenantCommandHandler.cs`
- `Commands/VerifyEmail/VerifyEmailCommand.cs`
- `Commands/VerifyEmail/VerifyEmailCommandHandler.cs`
- `Commands/ResendVerificationEmail/ResendVerificationEmailCommand.cs`
- `Commands/ResendVerificationEmail/ResendVerificationEmailCommandHandler.cs`
- `Queries/ListTenantUsers/ListTenantUsersQuery.cs`
- `Queries/ListTenantUsers/ListTenantUsersQueryHandler.cs`
- `Services/IEmailService.cs`
- `Services/IEmailRateLimiter.cs`
- `Dtos/UserWithRoleDto.cs`
- `Dtos/PagedResult.cs`

**Infrastructure Layer** (5 files):
- `Services/SendGridEmailService.cs`
- `Services/SmtpEmailService.cs`
- `Services/EmailRateLimiter.cs`
- `Persistence/Configurations/EmailRateLimitConfiguration.cs`
- `Persistence/Migrations/XXXXXX_Day6RoleManagementAndEmailVerification.cs`

**API Layer** (1 file):
- `Controllers/TenantUsersController.cs`

**Tests** (2 files):
- `IntegrationTests/RoleManagementTests.cs`
- `IntegrationTests/EmailVerificationTests.cs`

### 5.3 Files to Modify

- `Domain/Aggregates/Users/User.cs` (add token validation)
- `Domain/Repositories/IUserRepository.cs` (add `GetByIdsAsync`, `GetByEmailVerificationTokenAsync`)
- `Domain/Repositories/IUserTenantRoleRepository.cs` (add `CountByTenantAndRoleAsync`)
- `Infrastructure/Persistence/Repositories/UserRepository.cs` (implement new methods)
- `Infrastructure/Persistence/Repositories/UserTenantRoleRepository.cs` (implement new methods)
- `Infrastructure/DependencyInjection.cs` (register email services)
- `Application/Commands/RegisterTenant/RegisterTenantCommandHandler.cs` (add email sending)
- `API/Controllers/AuthController.cs` (add verification endpoints)
- `API/appsettings.Development.json` (add email configuration)
- `API/appsettings.Production.json` (add email configuration)

---

## 6. Risk Assessment

### 6.1 Technical Risks

| Risk | Impact | Probability | Mitigation |
|------|--------|-------------|------------|
| **SendGrid account setup delays** | Medium | Medium | Use SMTP fallback for local development, SendGrid setup can be done later |
| **Rate limiting database contention** | Low | Low | Use in-memory cache for rate limiting if needed (MemoryCache instead of database) |
| **Email deliverability issues** | Medium | Medium | Use reputable provider (SendGrid), configure SPF/DKIM records |
| **Last owner deletion bug** | High | Low | Comprehensive validation logic, integration tests |
| **Token collision** | Low | Very Low | 32-byte cryptographic random tokens have negligible collision probability |
| **Migration conflicts** | Low | Low | Single migration file, test on clean database first |

### 6.2 Security Risks

| Risk | Impact | Mitigation |
|------|--------|------------|
| **Email enumeration** | Medium | Always return success for resend, generic error messages |
| **Token brute force** | Low | 32-byte tokens = 2^256 combinations, 24-hour expiration |
| **Rate limit bypass** | Medium | Persistent database tracking, multiple checks (IP + email) |
| **Privilege escalation** | High | Strict authorization checks, cannot self-demote, cannot remove last owner |
| **CSRF on email verification** | Low | GET endpoint with long random token, no sensitive actions |
| **Email injection** | Low | Use email library (SendGrid SDK, MailKit), no raw SMTP |

### 6.3 Operational Risks

| Risk | Impact | Mitigation |
|------|--------|------------|
| **SendGrid free tier limits** | Medium | Monitor usage, upgrade plan if needed, use batch sending |
| **Email spam folder** | Medium | Configure SPF/DKIM, warm up IP, use reputable sender |
| **Failed email delivery** | Medium | Log failures, allow resend, queue-based retry (future) |
| **Database growth (rate limits)** | Low | Scheduled cleanup job, delete records older than 7 days |

### 6.4 Complexity Assessment

| Component | Complexity | Risk Level | Notes |
|-----------|-----------|------------|-------|
| **Role Management API** | Medium | Low | Well-defined patterns, clear validation rules |
| **Email Service** | Medium | Medium | External dependency (SendGrid), deliverability concerns |
| **Rate Limiting** | Medium | Low | Database-backed, straightforward logic |
| **Email Verification Flow** | Low-Medium | Low | Standard OAuth-like flow |
| **Combined Implementation** | Medium | Medium | No blocking dependencies, but requires careful coordination |

**Total Estimated Time**: 22.5 hours (10 hours role mgmt + 12.5 hours email verification)
**Realistic Time (with buffer)**: 3 working days

---

## 7. Testing Strategy

### 7.1 Unit Tests

**Role Management**:
```csharp
public class UserTenantRoleTests
{
    [Fact]
    public void UpdateRole_ShouldUpdateRole_WhenValid()
    {
        // Arrange
        var role = UserTenantRole.Create(
            UserId.From(Guid.NewGuid()),
            TenantId.From(Guid.NewGuid()),
            TenantRole.TenantMember);

        var updaterId = Guid.NewGuid();

        // Act
        role.UpdateRole(TenantRole.TenantAdmin, updaterId);

        // Assert
        Assert.Equal(TenantRole.TenantAdmin, role.Role);
        Assert.Equal(updaterId, role.AssignedByUserId);
    }
}
```

**Email Verification**:
```csharp
public class UserEmailVerificationTests
{
    [Fact]
    public void IsEmailVerificationTokenValid_ShouldReturnTrue_WhenTokenMatches()
    {
        // Arrange
        var user = CreateTestUser();
        var token = "test-token-123";
        var expiresAt = DateTime.UtcNow.AddHours(24);

        user.SetEmailVerificationToken(token, expiresAt);

        // Act
        var isValid = user.IsEmailVerificationTokenValid(token);

        // Assert
        Assert.True(isValid);
    }

    [Fact]
    public void IsEmailVerificationTokenValid_ShouldReturnFalse_WhenExpired()
    {
        // Arrange
        var user = CreateTestUser();
        var token = "test-token-123";
        var expiresAt = DateTime.UtcNow.AddHours(-1); // Expired

        user.SetEmailVerificationToken(token, expiresAt);

        // Act
        var isValid = user.IsEmailVerificationTokenValid(token);

        // Assert
        Assert.False(isValid);
    }
}
```

### 7.2 Integration Tests

**File**: `tests/IntegrationTests/RoleManagementIntegrationTests.cs`

```csharp
public class RoleManagementIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task AssignRole_ShouldSucceed_WhenTenantOwner()
    {
        // Arrange
        var (tenant, owner) = await CreateTenantWithOwner();
        var member = await CreateUser(tenant.Id, "member@test.com");
        var ownerToken = await LoginUser(owner);

        var request = new AssignRoleRequest { Role = TenantRole.TenantAdmin };

        // Act
        var response = await _client.PostAsJsonAsync(
            $"/api/tenants/{tenant.Id}/users/{member.Id}/role",
            request,
            ownerToken);

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        var result = await response.Content.ReadFromJsonAsync<UserWithRoleDto>();
        Assert.Equal(TenantRole.TenantAdmin, result.Role);
    }

    [Fact]
    public async Task RemoveUser_ShouldFail_WhenLastOwner()
    {
        // Arrange
        var (tenant, owner) = await CreateTenantWithOwner();
        var ownerToken = await LoginUser(owner);

        // Act
        var response = await _client.DeleteAsync(
            $"/api/tenants/{tenant.Id}/users/{owner.Id}/role",
            ownerToken);

        // Assert
        Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
    }

    [Fact]
    public async Task UpdateRole_ShouldFail_WhenSelfDemote()
    {
        // Arrange
        var (tenant, owner) = await CreateTenantWithOwner();
        var ownerToken = await LoginUser(owner);

        var request = new AssignRoleRequest { Role = TenantRole.TenantMember };

        // Act
        var response = await _client.PutAsJsonAsync(
            $"/api/tenants/{tenant.Id}/users/{owner.Id}/role",
            request,
            ownerToken);

        // Assert
        Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
    }
}
```

**File**: `tests/IntegrationTests/EmailVerificationIntegrationTests.cs`

```csharp
public class EmailVerificationIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task RegisterTenant_ShouldSendVerificationEmail()
    {
        // Arrange
        var emailService = _factory.Services.GetRequiredService<IEmailService>();
        var emailSpy = new EmailServiceSpy(emailService);

        var request = new RegisterTenantCommand(
            "Test Corp",
            "test-corp",
            SubscriptionPlan.Professional,
            "admin@test.com",
            "Admin@1234",
            "Test Admin");

        // Act
        var response = await _client.PostAsJsonAsync("/api/tenants/register", request);

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Single(emailSpy.SentEmails);
        Assert.Equal("admin@test.com", emailSpy.SentEmails[0].Recipient);
    }

    [Fact]
    public async Task VerifyEmail_ShouldSucceed_WithValidToken()
    {
        // Arrange
        var (tenant, user, token) = await CreateUserWithVerificationToken();

        // Act
        var response = await _client.GetAsync(
            $"/api/auth/verify-email?token={token}&tenant={tenant.Slug}");

        // Assert
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Contains("email-verified", response.Headers.Location.ToString());

        // Verify in database
        var updatedUser = await GetUser(user.Id);
        Assert.NotNull(updatedUser.EmailVerifiedAt);
    }

    [Fact]
    public async Task ResendVerification_ShouldRespectRateLimit()
    {
        // Arrange
        var (tenant, user) = await CreateUnverifiedUser();

        var request = new ResendVerificationRequest(user.Email, tenant.Slug);

        // Act - First request succeeds
        var response1 = await _client.PostAsJsonAsync("/api/auth/resend-verification", request);
        Assert.Equal(HttpStatusCode.OK, response1.StatusCode);

        // Act - Second request within 1 minute
        var response2 = await _client.PostAsJsonAsync("/api/auth/resend-verification", request);

        // Assert - Still returns 200 (to prevent enumeration), but email not sent
        Assert.Equal(HttpStatusCode.OK, response2.StatusCode);

        // Verify only one email sent
        var emailSpy = _factory.Services.GetRequiredService<EmailServiceSpy>();
        Assert.Single(emailSpy.SentEmails);
    }
}
```

### 7.3 Manual Testing Checklist

**Role Management**:
- [ ] TenantOwner can list all users
- [ ] TenantAdmin can list all users
- [ ] TenantMember cannot list users (403)
- [ ] TenantOwner can assign TenantAdmin role
- [ ] TenantOwner can update user from Member to Admin
- [ ] Cannot assign AIAgent role (400)
- [ ] Cannot remove last TenantOwner (409)
- [ ] Cannot self-demote (409)
- [ ] Pagination works correctly
- [ ] Search by email/name works

**Email Verification**:
- [ ] Registration sends verification email
- [ ] Verification link marks email as verified
- [ ] Expired token shows error page
- [ ] Invalid token shows error page
- [ ] Already verified user shows success
- [ ] Resend verification works
- [ ] Rate limiting prevents spam (test with 2 quick requests)
- [ ] Email status endpoint shows correct status
- [ ] Can login before email verification
- [ ] Welcome email sent after verification (if implemented)

---

## 8. MCP Integration Considerations

### 8.1 Role Management for AI Agents

When implementing MCP Server (future), role management will need to support:

**AI Agent Role Assignment**:
```csharp
// Future MCP endpoint
[HttpPost("api/mcp/register-agent")]
[Authorize(Roles = "TenantOwner")]
public async Task<ActionResult<AgentCredentials>> RegisterAIAgent(
    [FromBody] RegisterAgentRequest request)
{
    // 1. Create AIAgent role for MCP access
    var agentRole = UserTenantRole.Create(
        UserId.From(request.AgentId),
        TenantId.From(request.TenantId),
        TenantRole.AIAgent,
        assignedByUserId: GetCurrentUserId());

    await _roleRepository.AddAsync(agentRole);

    // 2. Generate API key for MCP authentication
    var apiKey = GenerateApiKey();
    await _mcpKeyRepository.AddAsync(new McpApiKey
    {
        KeyHash = ComputeSha256Hash(apiKey),
        UserId = request.AgentId,
        TenantId = request.TenantId,
        Permissions = McpPermissions.Read | McpPermissions.WriteWithApproval
    });

    return Ok(new AgentCredentials
    {
        AgentId = request.AgentId,
        ApiKey = apiKey,
        Permissions = new[] { "read_projects", "write_preview" }
    });
}
```

**Permission Mapping**:
```csharp
public class McpPermissionResolver
{
    public bool HasPermission(TenantRole role, string mcpOperation)
    {
        return role switch
        {
            TenantRole.TenantOwner => true, // All permissions
            TenantRole.TenantAdmin => IsSafeOperation(mcpOperation),
            TenantRole.AIAgent when mcpOperation.StartsWith("read_") => true,
            TenantRole.AIAgent when mcpOperation == "write_preview" => true,
            _ => false
        };
    }

    private bool IsSafeOperation(string operation)
    {
        var safeOps = new[] { "read_projects", "read_issues", "write_preview" };
        return safeOps.Contains(operation);
    }
}
```

### 8.2 Email Verification for Security

**MCP operations requiring verified email**:
```csharp
public class McpAuthorizationHandler : AuthorizationHandler<McpRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        McpRequirement requirement)
    {
        var emailVerified = context.User.HasClaim("email_verified", "true");

        if (!emailVerified && requirement.RequiresVerifiedEmail)
        {
            context.Fail(new AuthorizationFailureReason(
                this,
                "Email verification required for this MCP operation"));
            return Task.CompletedTask;
        }

        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}
```

**Future enhancement**: Add `email_verified` claim to JWT:
```csharp
// Update JwtService.GenerateToken()
claims.Add(new Claim("email_verified", user.EmailVerifiedAt.HasValue.ToString().ToLower()));
```

### 8.3 Audit Logging for MCP

All role changes and email operations should be logged for MCP compliance:

```csharp
public record AuditLog
{
    public Guid Id { get; init; }
    public Guid TenantId { get; init; }
    public Guid ActorUserId { get; init; }
    public string ActorRole { get; init; } = string.Empty;
    public string Action { get; init; } = string.Empty; // "assign_role", "verify_email"
    public string ResourceType { get; init; } = string.Empty; // "user_role", "email"
    public Guid? ResourceId { get; init; }
    public string Details { get; init; } = string.Empty; // JSON
    public DateTime Timestamp { get; init; }
    public string? IpAddress { get; init; }
}
```

---

## 9. Success Criteria

### 9.1 Role Management API

- [ ] **Endpoints Functional**:
  - GET `/api/tenants/{id}/users` returns paginated user list
  - POST `/api/tenants/{id}/users/{userId}/role` assigns role
  - PUT `/api/tenants/{id}/users/{userId}/role` updates role
  - DELETE `/api/tenants/{id}/users/{userId}/role` removes user

- [ ] **Authorization Correct**:
  - Only TenantOwner can assign/update/remove roles
  - TenantAdmin can list users
  - Users in different tenants cannot access each other

- [ ] **Validation Enforced**:
  - Cannot remove last TenantOwner
  - Cannot self-demote from TenantOwner
  - Cannot assign AIAgent role manually
  - User status validation (cannot assign role to inactive user)

- [ ] **Data Integrity**:
  - Role assignments are atomic (database transactions)
  - Removing user revokes their refresh tokens
  - Audit trail maintained (who assigned role, when)

### 9.2 Email Verification

- [ ] **Email Sending Works**:
  - Verification email sent on registration
  - Email contains valid verification link
  - Email deliverability confirmed (check spam folder)

- [ ] **Verification Flow**:
  - Clicking link verifies email
  - Expired tokens rejected with user-friendly message
  - Invalid tokens rejected
  - Already verified users handled gracefully

- [ ] **Resend Verification**:
  - Resend endpoint works
  - Rate limiting prevents spam (1 email/minute)
  - Always returns success (no email enumeration)

- [ ] **Security**:
  - Tokens are cryptographically secure (32 bytes)
  - Tokens stored as SHA-256 hash
  - Token expiration enforced (24 hours)
  - One-time use enforced (token cleared after verification)

### 9.3 Testing & Quality

- [ ] **Integration Tests**:
  - All role management scenarios tested
  - All email verification scenarios tested
  - Rate limiting tested
  - Security edge cases covered

- [ ] **Code Quality**:
  - Clean Architecture principles followed
  - SOLID principles applied
  - No compiler warnings
  - Code reviewed and approved

- [ ] **Documentation**:
  - API documentation updated (Swagger)
  - Architecture document complete
  - Implementation summary created
  - Configuration guide written

---

## 10. Rollback Plan

### 10.1 Database Rollback

```bash
# Rollback Day 6 migration
dotnet ef migrations remove --context IdentityDbContext --project src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure
```

### 10.2 Feature Flags

Add feature flags for gradual rollout:

```json
{
  "Features": {
    "RoleManagementApi": true,
    "EmailVerification": true,
    "EmailProvider": "Smtp" // Can switch to "SendGrid" when ready
  }
}
```

### 10.3 Emergency Procedures

**If email sending fails**:
1. Switch to SMTP fallback in configuration
2. Disable email requirement (`EmailVerification:RequireVerification: false`)
3. Allow manual email verification via database update

**If role management has bugs**:
1. Disable TenantUsersController endpoints
2. Use database scripts for emergency role changes
3. Rollback to Day 5 state

---

## 11. Documentation Deliverables

### 11.1 API Documentation (Swagger)

Update Swagger annotations:
```csharp
/// <summary>
/// List all users in tenant with their assigned roles
/// </summary>
/// <param name="tenantId">Tenant ID</param>
/// <param name="query">Filter and pagination options</param>
/// <response code="200">Returns paginated list of users with roles</response>
/// <response code="403">User does not have permission to list users</response>
[HttpGet]
[Authorize(Roles = "TenantOwner,TenantAdmin")]
[ProducesResponseType(typeof(PagedResult<UserWithRoleDto>), 200)]
[ProducesResponseType(403)]
public async Task<ActionResult<PagedResult<UserWithRoleDto>>> ListUsers(
    Guid tenantId,
    [FromQuery] ListUsersQuery query)
{
    // ...
}
```

### 11.2 Configuration Guide

**Setup SendGrid**:
```bash
# 1. Create SendGrid account (free tier: 100 emails/day)
# https://signup.sendgrid.com/

# 2. Create API key with Mail Send permission
# https://app.sendgrid.com/settings/api_keys

# 3. Set environment variable or appsettings
export SENDGRID_API_KEY="SG.xxxxxxxxxxxxxxxxxxxxxxxx"

# 4. Configure sender email (must be verified in SendGrid)
# Update appsettings.json:
{
  "SendGrid": {
    "ApiKey": "${SENDGRID_API_KEY}",
    "FromEmail": "noreply@colaflow.com"
  }
}
```

**Development SMTP Setup (MailHog)**:
```bash
# Install MailHog for local email testing
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog

# Update appsettings.Development.json
{
  "EmailProvider": "Smtp",
  "Smtp": {
    "Host": "localhost",
    "Port": 1025
  }
}

# View emails at http://localhost:8025
```

### 11.3 Implementation Summary Template

```markdown
# Day 6 Implementation Summary

## Date: 2025-11-XX

## Overview
✅ Role Management API
✅ Email Verification Flow
✅ Integration Tests (XX tests, 100% pass)

## Features Implemented
1. Role Management API
   - List users with roles
   - Assign roles
   - Update roles
   - Remove users from tenant

2. Email Verification
   - SendGrid integration
   - SMTP fallback
   - Verification flow
   - Resend verification
   - Rate limiting

## Files Created
- [List files]

## Files Modified
- [List files]

## Testing Results
- Unit Tests: XX passed
- Integration Tests: XX passed
- Manual Testing: ✅ Passed

## Configuration Changes
- Added SendGrid configuration
- Added SMTP fallback configuration
- Added email rate limiting settings

## Known Issues
- [List any known issues]

## Next Steps (Day 7)
- Password reset flow
- User profile management
- Tenant settings API
```

---

## Conclusion

This Day 6 architecture design provides:

1. **Complete Role Management API** with proper authorization, validation, and audit trails
2. **Production-ready Email Verification** with SendGrid integration, rate limiting, and security
3. **Clear implementation roadmap** with detailed tasks and time estimates
4. **Comprehensive testing strategy** covering unit, integration, and manual testing
5. **MCP integration considerations** for future AI agent role management
6. **Risk assessment and mitigation** for all identified technical and security risks

**Key Design Decisions**:
- Use existing Day 5 infrastructure (no new major tables)
- SendGrid for email with SMTP fallback for development
- Database-backed rate limiting for persistence
- Policy-based authorization for role management
- Generic error messages to prevent enumeration
- Comprehensive validation to prevent privilege escalation

**Estimated Implementation Time**: 2-3 working days (22.5 hours + buffer)

**Ready for Implementation**: ✅ Yes - All technical decisions made, no blocking questions

---

**Document Version**: 1.0
**Last Updated**: 2025-11-03
**Status**: Ready for Product Manager Review & Backend Implementation

---

## Appendix: Quick Reference

### API Endpoints Summary

**Role Management**:
```
GET    /api/tenants/{tenantId}/users              - List users (TenantAdmin+)
POST   /api/tenants/{tenantId}/users/{userId}/role - Assign role (TenantOwner)
PUT    /api/tenants/{tenantId}/users/{userId}/role - Update role (TenantOwner)
DELETE /api/tenants/{tenantId}/users/{userId}/role - Remove user (TenantOwner)
```

**Email Verification**:
```
GET  /api/auth/verify-email?token=xxx&tenant=yyy  - Verify email (Anonymous)
POST /api/auth/resend-verification                - Resend verification (Anonymous)
GET  /api/auth/email-status                       - Check email status (Authenticated)
```

### Role Hierarchy

```
TenantOwner (1)      - Full control
  ├── TenantAdmin (2) - User management
  ├── TenantMember (3) - Default role
  ├── TenantGuest (4)  - Read-only
  └── AIAgent (5)      - MCP integration (not manually assignable)
```

### Configuration Quick Reference

```json
{
  "SendGrid": {
    "ApiKey": "${SENDGRID_API_KEY}",
    "FromEmail": "noreply@colaflow.com"
  },
  "Smtp": {
    "Host": "localhost",
    "Port": 1025
  },
  "EmailVerification": {
    "TokenExpirationHours": 24,
    "RequireVerification": false,
    "RateLimitMinutes": 1
  },
  "EmailProvider": "Smtp"
}
```
