3316 lines
98 KiB
Markdown
3316 lines
98 KiB
Markdown
# Day 7 Product Requirements Document
|
|
# Email Service & User Management
|
|
|
|
**Version**: 1.0
|
|
**Date**: 2025-11-03
|
|
**Sprint**: M1 Sprint 2 - Day 7
|
|
**Author**: Product Manager Agent
|
|
**Status**: Ready for Implementation
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
Day 7 completes the core authentication and user management foundation by adding:
|
|
1. **Email Service Integration** - Reliable transactional email infrastructure
|
|
2. **Email Verification Flow** - Ensure valid user email addresses
|
|
3. **Password Reset Flow** - Self-service password recovery
|
|
4. **User Invitation System** - Team member onboarding
|
|
|
|
These features are critical for:
|
|
- Unblocking 3 skipped integration tests (user removal scenarios)
|
|
- Enabling multi-user tenant functionality
|
|
- Completing enterprise-ready authentication flows
|
|
- Meeting security and compliance standards
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Background & Context](#1-background--context)
|
|
2. [Feature 1: Email Service Integration](#2-feature-1-email-service-integration)
|
|
3. [Feature 2: Email Verification Flow](#3-feature-2-email-verification-flow)
|
|
4. [Feature 3: Password Reset Flow](#4-feature-3-password-reset-flow)
|
|
5. [Feature 4: User Invitation System](#5-feature-4-user-invitation-system)
|
|
6. [API Specifications](#6-api-specifications)
|
|
7. [Database Schema Changes](#7-database-schema-changes)
|
|
8. [Security Requirements](#8-security-requirements)
|
|
9. [Email Templates](#9-email-templates)
|
|
10. [Integration Points](#10-integration-points)
|
|
11. [Testing Strategy](#11-testing-strategy)
|
|
12. [Implementation Plan](#12-implementation-plan)
|
|
13. [Risk Assessment](#13-risk-assessment)
|
|
14. [Success Criteria](#14-success-criteria)
|
|
|
|
---
|
|
|
|
## 1. Background & Context
|
|
|
|
### 1.1 Current State (Days 0-6)
|
|
|
|
**Completed Features**:
|
|
- Multi-tenant architecture with tenant isolation
|
|
- JWT authentication with refresh tokens
|
|
- RBAC system with 5 roles (TenantOwner, TenantAdmin, Developer, Guest, AIAgent)
|
|
- Role Management API with cross-tenant security
|
|
- Domain Events infrastructure
|
|
|
|
**Limitations**:
|
|
- No email notifications for registration or login
|
|
- No email verification (security gap)
|
|
- No password reset mechanism (user lockout risk)
|
|
- Single-user tenants only (cannot invite team members)
|
|
- 3 integration tests skipped due to missing invitation feature
|
|
|
|
### 1.2 Business Drivers
|
|
|
|
**User Pain Points**:
|
|
- "I registered but can't invite my team" - Blocks team collaboration
|
|
- "I forgot my password and I'm locked out" - Support burden
|
|
- "Are these emails valid?" - Email bounces, spam issues
|
|
- "How do I know if someone registered with my company email?" - Security concern
|
|
|
|
**Business Impact**:
|
|
- **Without email verification**: ~30% fake/invalid email addresses (industry average)
|
|
- **Without password reset**: 15-20% support tickets for password issues
|
|
- **Without user invitation**: Single-user limitation blocks 80% of enterprise use cases
|
|
- **Without email service**: Cannot send critical security notifications
|
|
|
|
### 1.3 Success Metrics
|
|
|
|
| Metric | Target | Measurement |
|
|
|--------|--------|-------------|
|
|
| Email delivery rate | >99% | SendGrid/SMTP logs |
|
|
| Email verification rate | >85% | Verified users / Total registrations |
|
|
| Password reset success rate | >90% | Successful resets / Attempts |
|
|
| Invitation acceptance rate | >70% | Accepted / Sent invitations |
|
|
| Test coverage | 100% of skipped tests passing | Integration test suite |
|
|
| Support ticket reduction | -50% for password issues | Support ticket tracking |
|
|
|
|
---
|
|
|
|
## 2. Feature 1: Email Service Integration
|
|
|
|
### 2.1 Overview
|
|
|
|
Implement a reliable, configurable email service for sending transactional emails (verification, password reset, invitations, notifications).
|
|
|
|
### 2.2 Technology Decision: SendGrid vs SMTP
|
|
|
|
#### Recommendation: **Hybrid Approach with SendGrid Priority**
|
|
|
|
**Primary**: SendGrid (for production)
|
|
- Industry-standard 99.9% delivery rate
|
|
- Built-in analytics and bounce handling
|
|
- Rate limiting and spam prevention
|
|
- Email validation API
|
|
- Managed infrastructure (no SMTP server maintenance)
|
|
- Free tier: 100 emails/day (sufficient for MVP)
|
|
|
|
**Fallback**: SMTP (for development and self-hosted deployments)
|
|
- No external dependencies
|
|
- Works in air-gapped environments
|
|
- Free for self-hosted email servers
|
|
- Suitable for development/testing with tools like MailHog
|
|
|
|
**Implementation**: Abstraction layer with strategy pattern
|
|
|
|
### 2.3 Requirements
|
|
|
|
#### FR-EMAIL-001: Email Service Abstraction
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Description**: Create abstraction layer supporting multiple email providers
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] `IEmailService` interface defined with `SendEmailAsync(EmailMessage message)` method
|
|
- [ ] SendGrid implementation (`SendGridEmailService`)
|
|
- [ ] SMTP implementation (`SmtpEmailService`)
|
|
- [ ] Provider selection via configuration (`appsettings.json`)
|
|
- [ ] Graceful fallback if primary provider fails
|
|
- [ ] All email sends are logged (INFO level)
|
|
|
|
**User Story**:
|
|
```
|
|
As a system administrator,
|
|
I want to configure email providers without code changes,
|
|
So that I can use SendGrid in production and SMTP in development.
|
|
```
|
|
|
|
#### FR-EMAIL-002: Configuration Management
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Description**: Environment-based email configuration
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Configuration in `appsettings.json` and `appsettings.Development.json`
|
|
- [ ] SendGrid API key stored in User Secrets (development) and Azure Key Vault (production)
|
|
- [ ] SMTP settings: host, port, username, password, enableSSL
|
|
- [ ] Email template base path configurable
|
|
- [ ] From address and display name configurable
|
|
- [ ] Provider selection: `SendGrid`, `Smtp`, `Mock` (for tests)
|
|
|
|
**Configuration Example**:
|
|
```json
|
|
{
|
|
"EmailSettings": {
|
|
"Provider": "SendGrid",
|
|
"FromAddress": "noreply@colaflow.io",
|
|
"FromName": "ColaFlow",
|
|
"SendGrid": {
|
|
"ApiKey": "stored-in-user-secrets-or-keyvault"
|
|
},
|
|
"Smtp": {
|
|
"Host": "smtp.gmail.com",
|
|
"Port": 587,
|
|
"Username": "user@example.com",
|
|
"Password": "stored-in-user-secrets",
|
|
"EnableSsl": true
|
|
},
|
|
"TemplateBasePath": "EmailTemplates"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### FR-EMAIL-003: Email Template System
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Description**: Reusable HTML email templates with placeholders
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Template engine for HTML emails (using C# string interpolation or Razor)
|
|
- [ ] Shared layout template with ColaFlow branding
|
|
- [ ] Template variables: `{{userName}}`, `{{tenantName}}`, `{{verificationUrl}}`, etc.
|
|
- [ ] Plain text fallback for all templates
|
|
- [ ] Templates stored in `EmailTemplates/` folder
|
|
- [ ] Template rendering service: `IEmailTemplateRenderer`
|
|
|
|
**Templates Required** (see section 9 for details):
|
|
1. `EmailVerification.html` - Verification link
|
|
2. `PasswordReset.html` - Password reset link
|
|
3. `UserInvitation.html` - Tenant invitation
|
|
4. `WelcomeEmail.html` - Post-verification welcome (optional)
|
|
|
|
#### FR-EMAIL-004: Development Mode Email Preview
|
|
**Priority**: P1 (Should Have)
|
|
|
|
**Description**: Preview emails in development without sending
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] In development, emails are logged to console with full HTML
|
|
- [ ] Optional: Save emails to `temp/emails/` folder for manual inspection
|
|
- [ ] Mock email service for integration tests (no actual sends)
|
|
- [ ] Configuration flag: `EmailSettings:SaveEmailsToFile` (true in development)
|
|
|
|
**User Story**:
|
|
```
|
|
As a developer,
|
|
I want to preview email templates locally,
|
|
So that I can verify styling and content before deploying.
|
|
```
|
|
|
|
#### FR-EMAIL-005: Rate Limiting & Error Handling
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Description**: Prevent abuse and handle failures gracefully
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Rate limiting: Max 5 emails per user per hour (configurable)
|
|
- [ ] Retry logic for transient failures (3 attempts with exponential backoff)
|
|
- [ ] Circuit breaker pattern for email provider outages
|
|
- [ ] Email send failures logged as WARN (not ERROR to avoid alert fatigue)
|
|
- [ ] Graceful degradation: If email fails, user is informed but operation succeeds
|
|
- [ ] Dead letter queue for failed emails (future: background retry job)
|
|
|
|
**Business Rule**: Email delivery is non-blocking. If email fails, the user action (e.g., registration) still succeeds, but user is notified that email may be delayed.
|
|
|
|
### 2.4 Technical Architecture
|
|
|
|
```
|
|
┌──────────────────────────────────────┐
|
|
│ Application Layer (Commands) │
|
|
│ - RegisterTenant │
|
|
│ - ForgotPassword │
|
|
│ - InviteUser │
|
|
└──────────────┬───────────────────────┘
|
|
│ Calls
|
|
┌──────────────▼───────────────────────┐
|
|
│ IEmailService (Abstraction) │
|
|
│ + SendEmailAsync(EmailMessage) │
|
|
└──────────────┬───────────────────────┘
|
|
│ Implemented by
|
|
┌───────┴────────┐
|
|
│ │
|
|
┌──────▼──────┐ ┌──────▼──────┐
|
|
│ SendGrid │ │ SMTP │
|
|
│ Service │ │ Service │
|
|
└─────────────┘ └─────────────┘
|
|
|
|
┌──────────────────────────────────────┐
|
|
│ IEmailTemplateRenderer │
|
|
│ + RenderTemplateAsync(name, data) │
|
|
└──────────────────────────────────────┘
|
|
```
|
|
|
|
### 2.5 Non-Functional Requirements
|
|
|
|
| Requirement | Target | Priority |
|
|
|-------------|--------|----------|
|
|
| Email send latency | <2 seconds | P0 |
|
|
| Template rendering time | <100ms | P1 |
|
|
| Delivery rate (SendGrid) | >99% | P0 |
|
|
| Rate limiting | 5 emails/user/hour | P0 |
|
|
| Log retention | 30 days | P1 |
|
|
|
|
---
|
|
|
|
## 3. Feature 2: Email Verification Flow
|
|
|
|
### 3.1 Overview
|
|
|
|
Ensure users own the email addresses they register with by requiring email verification.
|
|
|
|
### 3.2 User Journey
|
|
|
|
```
|
|
1. User registers tenant
|
|
↓
|
|
2. System creates user account (status: Active, emailVerified: false)
|
|
↓
|
|
3. System generates verification token (24h expiry)
|
|
↓
|
|
4. System sends verification email with link
|
|
↓
|
|
5. User clicks link → redirected to verification endpoint
|
|
↓
|
|
6. System validates token → marks email as verified
|
|
↓
|
|
7. User redirected to dashboard with success message
|
|
```
|
|
|
|
### 3.3 Requirements
|
|
|
|
#### FR-VERIFY-001: Generate Verification Token
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Token generated on registration (in `RegisterTenantCommandHandler`)
|
|
- [ ] Token is cryptographically random (256-bit, URL-safe)
|
|
- [ ] Token hash stored in database (not plaintext)
|
|
- [ ] Token expires after 24 hours
|
|
- [ ] One active token per user (new token invalidates old)
|
|
- [ ] Token linked to user ID and email address
|
|
|
|
**Technical Implementation**:
|
|
```csharp
|
|
var token = GenerateSecureToken(); // 256-bit random
|
|
var tokenHash = HashToken(token); // SHA-256
|
|
var emailVerificationToken = new EmailVerificationToken
|
|
{
|
|
UserId = user.Id,
|
|
TokenHash = tokenHash,
|
|
Email = user.Email.Value,
|
|
ExpiresAt = DateTime.UtcNow.AddHours(24),
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
```
|
|
|
|
#### FR-VERIFY-002: Send Verification Email
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Email sent immediately after registration
|
|
- [ ] Email contains verification link: `https://app.colaflow.io/verify-email?token={token}`
|
|
- [ ] Link includes tenant slug for context
|
|
- [ ] Email template uses user's full name and tenant name
|
|
- [ ] Email includes "resend" instructions if link expired
|
|
- [ ] Non-blocking: Registration succeeds even if email fails
|
|
|
|
**User Story**:
|
|
```
|
|
As a new user,
|
|
I want to receive a verification email after registration,
|
|
So that I can verify my email address and access all features.
|
|
```
|
|
|
|
#### FR-VERIFY-003: Verify Email Endpoint
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `POST /api/auth/verify-email`
|
|
- [ ] Request body: `{ "token": "..." }`
|
|
- [ ] Validates token existence and expiration
|
|
- [ ] Compares token hash with stored hash
|
|
- [ ] Sets `User.EmailVerifiedAt = DateTime.UtcNow`
|
|
- [ ] Returns success response with redirect URL
|
|
- [ ] Invalid/expired token returns 400 with clear error message
|
|
- [ ] Already verified email returns 200 (idempotent)
|
|
|
|
**Error Messages**:
|
|
- "Verification token is invalid or expired. Please request a new verification email."
|
|
- "Email already verified. You can log in now."
|
|
- "Verification token not found."
|
|
|
|
#### FR-VERIFY-004: Resend Verification Email
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `POST /api/auth/resend-verification`
|
|
- [ ] Request body: `{ "tenantSlug": "...", "email": "..." }`
|
|
- [ ] Rate limited: Max 3 resends per hour per email
|
|
- [ ] Generates new token (invalidates old)
|
|
- [ ] Returns 200 even if email doesn't exist (prevent enumeration)
|
|
- [ ] Logs resend attempts for security monitoring
|
|
|
|
**User Story**:
|
|
```
|
|
As a user who didn't receive the verification email,
|
|
I want to request a new verification email,
|
|
So that I can complete the verification process.
|
|
```
|
|
|
|
#### FR-VERIFY-005: Unverified User Restrictions (Future)
|
|
**Priority**: P2 (Nice to Have, Day 7 Optional)
|
|
|
|
**Business Decision Required**: Should unverified users be able to log in?
|
|
|
|
**Option A (Recommended)**: Allow login, restrict features
|
|
- Unverified users can log in and view dashboard
|
|
- Banner message: "Please verify your email to invite team members"
|
|
- User invitation disabled until email verified
|
|
- Project creation limited to 1 project
|
|
|
|
**Option B**: Block login until verified
|
|
- Login returns 403: "Please verify your email before logging in"
|
|
- Stricter security, but higher support burden
|
|
|
|
**Recommendation**: Option A for Day 7 (better UX, lower support burden)
|
|
|
|
### 3.4 Business Rules
|
|
|
|
| Rule ID | Rule | Priority |
|
|
|---------|------|----------|
|
|
| BR-VERIFY-001 | Token expires after 24 hours | P0 |
|
|
| BR-VERIFY-002 | Only one active token per user | P0 |
|
|
| BR-VERIFY-003 | Verification is idempotent (can verify multiple times) | P0 |
|
|
| BR-VERIFY-004 | Resend limited to 3 times per hour | P0 |
|
|
| BR-VERIFY-005 | Email verification is optional for login (Day 7) | P1 |
|
|
| BR-VERIFY-006 | Future: User invitation requires verified email | P2 |
|
|
|
|
### 3.5 Security Considerations
|
|
|
|
- **Token Hashing**: Store SHA-256 hash, not plaintext token
|
|
- **URL Encoding**: Token must be URL-safe (base64url)
|
|
- **Expiration**: Enforce 24-hour expiration
|
|
- **Rate Limiting**: Prevent spam via resend endpoint
|
|
- **Email Enumeration**: Don't reveal if email exists in resend response
|
|
- **HTTPS Only**: Verification links must use HTTPS
|
|
|
|
---
|
|
|
|
## 4. Feature 3: Password Reset Flow
|
|
|
|
### 4.1 Overview
|
|
|
|
Allow users to securely reset forgotten passwords via email.
|
|
|
|
### 4.2 User Journey
|
|
|
|
```
|
|
1. User clicks "Forgot Password" on login page
|
|
↓
|
|
2. User enters tenant slug + email
|
|
↓
|
|
3. System generates reset token (1h expiry)
|
|
↓
|
|
4. System sends reset email with link
|
|
↓
|
|
5. User clicks link → redirected to reset form
|
|
↓
|
|
6. User enters new password (validated)
|
|
↓
|
|
7. System validates token → updates password
|
|
↓
|
|
8. System invalidates all refresh tokens
|
|
↓
|
|
9. User redirected to login with success message
|
|
```
|
|
|
|
### 4.3 Requirements
|
|
|
|
#### FR-RESET-001: Forgot Password Endpoint
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `POST /api/auth/forgot-password`
|
|
- [ ] Request body: `{ "tenantSlug": "...", "email": "..." }`
|
|
- [ ] Validates tenant and email existence (in background, no revelation)
|
|
- [ ] Generates reset token (256-bit, URL-safe)
|
|
- [ ] Stores token hash with 1-hour expiration
|
|
- [ ] Sends reset email with link
|
|
- [ ] Returns 200 regardless of email existence (prevent enumeration)
|
|
- [ ] Rate limited: Max 3 requests per email per hour
|
|
- [ ] Logs all reset requests for security audit
|
|
|
|
**Response** (always 200, never reveal if email exists):
|
|
```json
|
|
{
|
|
"message": "If an account exists with this email, a password reset link has been sent."
|
|
}
|
|
```
|
|
|
|
**User Story**:
|
|
```
|
|
As a user who forgot my password,
|
|
I want to request a password reset link,
|
|
So that I can regain access to my account.
|
|
```
|
|
|
|
#### FR-RESET-002: Send Password Reset Email
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Email sent only if user exists and is active
|
|
- [ ] Email contains reset link: `https://app.colaflow.io/reset-password?token={token}`
|
|
- [ ] Link expires in 1 hour
|
|
- [ ] Email warns: "If you didn't request this, ignore this email"
|
|
- [ ] Email template uses user's full name
|
|
- [ ] Link includes tenant slug for UX
|
|
|
|
#### FR-RESET-003: Reset Password Endpoint
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `POST /api/auth/reset-password`
|
|
- [ ] Request body: `{ "token": "...", "newPassword": "..." }`
|
|
- [ ] Validates token existence and expiration
|
|
- [ ] Validates new password complexity (see FR-RESET-005)
|
|
- [ ] Compares token hash with stored hash
|
|
- [ ] Updates `User.PasswordHash` with new hashed password
|
|
- [ ] Sets `PasswordResetToken.UsedAt = DateTime.UtcNow`
|
|
- [ ] Invalidates all user's refresh tokens (force re-login)
|
|
- [ ] Marks token as used (cannot reuse)
|
|
- [ ] Returns 200 with success message
|
|
- [ ] Invalid/expired token returns 400
|
|
|
|
**Error Messages**:
|
|
- "Password reset token is invalid or expired. Please request a new one."
|
|
- "Password has already been reset with this token."
|
|
- "New password does not meet complexity requirements."
|
|
|
|
#### FR-RESET-004: Token Invalidation on Use
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Used tokens marked with `UsedAt` timestamp
|
|
- [ ] Used tokens cannot be reused (return 400)
|
|
- [ ] New reset request invalidates previous unused tokens
|
|
- [ ] Expired tokens automatically cleaned up (future: background job)
|
|
|
|
**Business Rule**: Only one active reset token per user. Requesting new reset invalidates old unused tokens.
|
|
|
|
#### FR-RESET-005: Password Complexity Requirements
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Minimum 8 characters
|
|
- [ ] At least 1 uppercase letter
|
|
- [ ] At least 1 lowercase letter
|
|
- [ ] At least 1 number
|
|
- [ ] At least 1 special character (`!@#$%^&*()_+-=[]{}|;:,.<>?`)
|
|
- [ ] Cannot be same as old password (compare hashes)
|
|
- [ ] Clear validation error messages
|
|
|
|
**Validation Error Response**:
|
|
```json
|
|
{
|
|
"errors": {
|
|
"newPassword": [
|
|
"Password must be at least 8 characters long",
|
|
"Password must contain at least one uppercase letter",
|
|
"Password cannot be the same as your current password"
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### FR-RESET-006: Refresh Token Revocation
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] On successful password reset, invalidate all user's refresh tokens
|
|
- [ ] User forced to log in again with new password
|
|
- [ ] Security measure: Ensures attacker with old tokens loses access
|
|
|
|
**Security Rationale**: If a password reset was triggered due to compromise, we must invalidate all existing sessions.
|
|
|
|
### 4.4 Business Rules
|
|
|
|
| Rule ID | Rule | Priority |
|
|
|---------|------|----------|
|
|
| BR-RESET-001 | Reset token expires after 1 hour | P0 |
|
|
| BR-RESET-002 | Max 3 reset requests per email per hour | P0 |
|
|
| BR-RESET-003 | Used tokens cannot be reused | P0 |
|
|
| BR-RESET-004 | New reset invalidates old unused tokens | P0 |
|
|
| BR-RESET-005 | All refresh tokens revoked on password reset | P0 |
|
|
| BR-RESET-006 | Password reset requires valid email verification (future) | P2 |
|
|
|
|
### 4.5 Security Considerations
|
|
|
|
- **Token Hashing**: Store SHA-256 hash, not plaintext
|
|
- **Short Expiration**: 1 hour to minimize attack window
|
|
- **Rate Limiting**: Prevent brute force and abuse
|
|
- **Email Enumeration**: Never reveal if email exists
|
|
- **HTTPS Only**: Reset links must use HTTPS
|
|
- **Token Reuse Prevention**: Mark tokens as used
|
|
- **Session Invalidation**: Revoke all refresh tokens on reset
|
|
- **Audit Logging**: Log all reset attempts with IP and user agent
|
|
|
|
---
|
|
|
|
## 5. Feature 4: User Invitation System
|
|
|
|
### 5.1 Overview
|
|
|
|
Enable tenant owners/admins to invite team members to their tenant.
|
|
|
|
### 5.2 User Journey
|
|
|
|
```
|
|
1. Tenant owner/admin clicks "Invite User"
|
|
↓
|
|
2. Owner enters email + selects role
|
|
↓
|
|
3. System validates email format and role
|
|
↓
|
|
4. System generates invitation token (7 days expiry)
|
|
↓
|
|
5. System sends invitation email with link
|
|
↓
|
|
6. Invited user clicks link → redirected to accept page
|
|
↓
|
|
7. User enters full name + password
|
|
↓
|
|
8. System creates user account + assigns role
|
|
↓
|
|
9. User redirected to dashboard
|
|
```
|
|
|
|
### 5.3 Requirements
|
|
|
|
#### FR-INVITE-001: Create Invitation Endpoint
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `POST /api/tenants/{tenantId}/invitations`
|
|
- [ ] Authorization: `RequireTenantOwner` or `RequireTenantAdmin` policy
|
|
- [ ] Request body: `{ "email": "...", "role": "Developer" }`
|
|
- [ ] Validates tenant ownership (cross-tenant check)
|
|
- [ ] Validates email format
|
|
- [ ] Validates role (cannot invite as TenantOwner or AIAgent)
|
|
- [ ] Prevents duplicate invitations (same email + tenant)
|
|
- [ ] Generates invitation token (256-bit, URL-safe)
|
|
- [ ] Stores invitation with 7-day expiration
|
|
- [ ] Sends invitation email
|
|
- [ ] Returns invitation details
|
|
|
|
**Validation Rules**:
|
|
- Email must be valid format
|
|
- Role must be one of: TenantAdmin, Developer, Guest
|
|
- Cannot invite existing tenant members
|
|
- Cannot invite with invalid role
|
|
|
|
**Response** (201 Created):
|
|
```json
|
|
{
|
|
"id": "uuid",
|
|
"tenantId": "uuid",
|
|
"email": "user@example.com",
|
|
"role": "Developer",
|
|
"status": "Pending",
|
|
"invitedBy": "uuid",
|
|
"invitedAt": "2025-11-03T10:00:00Z",
|
|
"expiresAt": "2025-11-10T10:00:00Z"
|
|
}
|
|
```
|
|
|
|
**User Story**:
|
|
```
|
|
As a tenant owner,
|
|
I want to invite team members to my tenant,
|
|
So that they can collaborate on projects.
|
|
```
|
|
|
|
#### FR-INVITE-002: List Invitations Endpoint
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `GET /api/tenants/{tenantId}/invitations`
|
|
- [ ] Authorization: `RequireTenantOwner` or `RequireTenantAdmin` policy
|
|
- [ ] Validates tenant ownership
|
|
- [ ] Supports pagination: `?pageNumber=1&pageSize=20`
|
|
- [ ] Supports filtering by status: `?status=Pending`
|
|
- [ ] Returns list of invitations with metadata
|
|
- [ ] Includes inviter's name for context
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"items": [
|
|
{
|
|
"id": "uuid",
|
|
"email": "user@example.com",
|
|
"role": "Developer",
|
|
"status": "Pending",
|
|
"invitedBy": {
|
|
"id": "uuid",
|
|
"fullName": "John Doe"
|
|
},
|
|
"invitedAt": "2025-11-03T10:00:00Z",
|
|
"expiresAt": "2025-11-10T10:00:00Z"
|
|
}
|
|
],
|
|
"pageNumber": 1,
|
|
"pageSize": 20,
|
|
"totalCount": 5,
|
|
"totalPages": 1
|
|
}
|
|
```
|
|
|
|
#### FR-INVITE-003: Send Invitation Email
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Email sent immediately after invitation creation
|
|
- [ ] Email contains acceptance link: `https://app.colaflow.io/accept-invitation?token={token}`
|
|
- [ ] Email includes tenant name, inviter name, and assigned role
|
|
- [ ] Email includes expiration date (7 days)
|
|
- [ ] Email has clear call-to-action button
|
|
- [ ] Link includes tenant slug for UX
|
|
|
|
**Email Content Example**:
|
|
```
|
|
Subject: You're invited to join [Tenant Name] on ColaFlow
|
|
|
|
Hi there,
|
|
|
|
[Inviter Name] has invited you to join [Tenant Name] on ColaFlow as a [Role].
|
|
|
|
[Accept Invitation Button]
|
|
|
|
This invitation will expire on [Expiration Date].
|
|
|
|
If you didn't expect this invitation, you can safely ignore this email.
|
|
```
|
|
|
|
#### FR-INVITE-004: Accept Invitation Endpoint
|
|
**Priority**: P0 (Must Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `POST /api/invitations/accept`
|
|
- [ ] Public endpoint (no authentication required)
|
|
- [ ] Request body: `{ "token": "...", "fullName": "...", "password": "..." }`
|
|
- [ ] Validates token existence and expiration
|
|
- [ ] Validates invitation status (must be Pending)
|
|
- [ ] Validates password complexity (same as registration)
|
|
- [ ] Creates new user account in invited tenant
|
|
- [ ] Assigns role from invitation
|
|
- [ ] Marks invitation as Accepted with timestamp
|
|
- [ ] Sends welcome email (optional)
|
|
- [ ] Returns access token + refresh token (auto-login)
|
|
|
|
**Response** (200 OK):
|
|
```json
|
|
{
|
|
"user": {
|
|
"id": "uuid",
|
|
"tenantId": "uuid",
|
|
"email": "user@example.com",
|
|
"fullName": "Jane Doe",
|
|
"role": "Developer"
|
|
},
|
|
"accessToken": "jwt-token",
|
|
"refreshToken": "refresh-token"
|
|
}
|
|
```
|
|
|
|
**Error Cases**:
|
|
- Invitation expired → 400: "This invitation has expired. Please request a new one."
|
|
- Invitation already accepted → 400: "This invitation has already been used."
|
|
- Token invalid → 400: "Invalid invitation token."
|
|
- Email already registered in tenant → 400: "An account with this email already exists in this tenant."
|
|
|
|
#### FR-INVITE-005: Cancel Invitation Endpoint
|
|
**Priority**: P1 (Should Have)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `DELETE /api/tenants/{tenantId}/invitations/{invitationId}`
|
|
- [ ] Authorization: `RequireTenantOwner` or `RequireTenantAdmin` policy
|
|
- [ ] Validates tenant ownership
|
|
- [ ] Validates invitation belongs to tenant
|
|
- [ ] Only pending invitations can be canceled
|
|
- [ ] Marks invitation as Canceled (soft delete)
|
|
- [ ] Returns 204 No Content
|
|
|
|
**User Story**:
|
|
```
|
|
As a tenant owner,
|
|
I want to cancel a pending invitation,
|
|
So that the invitee can no longer accept it if I invited the wrong person.
|
|
```
|
|
|
|
#### FR-INVITE-006: Resend Invitation
|
|
**Priority**: P2 (Nice to Have, Day 7 Optional)
|
|
|
|
**Acceptance Criteria**:
|
|
- [ ] Endpoint: `POST /api/tenants/{tenantId}/invitations/{invitationId}/resend`
|
|
- [ ] Generates new token (invalidates old)
|
|
- [ ] Extends expiration by 7 days from now
|
|
- [ ] Resends invitation email
|
|
- [ ] Rate limited: Max 3 resends per invitation
|
|
|
|
### 5.4 Business Rules
|
|
|
|
| Rule ID | Rule | Priority |
|
|
|---------|------|----------|
|
|
| BR-INVITE-001 | Invitation expires after 7 days | P0 |
|
|
| BR-INVITE-002 | Only TenantOwner and TenantAdmin can invite | P0 |
|
|
| BR-INVITE-003 | Cannot invite as TenantOwner or AIAgent | P0 |
|
|
| BR-INVITE-004 | Cannot invite existing tenant members | P0 |
|
|
| BR-INVITE-005 | One active invitation per email per tenant | P0 |
|
|
| BR-INVITE-006 | Accepting invitation auto-creates user account | P0 |
|
|
| BR-INVITE-007 | Users can belong to multiple tenants (future) | P2 |
|
|
|
|
### 5.5 Multi-Tenant Invitation Handling (Future)
|
|
|
|
**Day 7 Scope**: User can only belong to one tenant (simplification).
|
|
|
|
**Future Enhancement** (M2+):
|
|
- User can accept invitations to multiple tenants
|
|
- On login, user selects which tenant to access
|
|
- `UserTenantRole` table already supports this (user_id + tenant_id + role)
|
|
|
|
**Day 7 Implementation**: Check if user email exists globally. If yes, reject invitation with error: "This email is already registered. Multi-tenant users are coming soon!"
|
|
|
|
### 5.6 Security Considerations
|
|
|
|
- **Token Hashing**: Store SHA-256 hash
|
|
- **Role Validation**: Prevent privilege escalation (cannot invite as TenantOwner)
|
|
- **Cross-Tenant Check**: Ensure inviter belongs to tenant
|
|
- **Email Verification**: Invitation acceptance verifies email ownership
|
|
- **Rate Limiting**: Prevent invitation spam
|
|
- **Expiration**: 7-day expiration balances security and UX
|
|
- **Audit Logging**: Log all invitation actions (create, accept, cancel)
|
|
|
|
---
|
|
|
|
## 6. API Specifications
|
|
|
|
### 6.1 Email Verification Endpoints
|
|
|
|
#### `POST /api/auth/verify-email`
|
|
|
|
**Description**: Verify user's email address with token.
|
|
|
|
**Authorization**: None (public)
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"token": "base64url-encoded-token"
|
|
}
|
|
```
|
|
|
|
**Responses**:
|
|
|
|
**200 OK** - Email verified successfully:
|
|
```json
|
|
{
|
|
"message": "Email verified successfully. You can now log in.",
|
|
"redirectUrl": "/login"
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Invalid or expired token:
|
|
```json
|
|
{
|
|
"error": "Verification token is invalid or expired.",
|
|
"code": "INVALID_TOKEN"
|
|
}
|
|
```
|
|
|
|
**200 OK** - Email already verified (idempotent):
|
|
```json
|
|
{
|
|
"message": "Email already verified.",
|
|
"redirectUrl": "/dashboard"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### `POST /api/auth/resend-verification`
|
|
|
|
**Description**: Resend email verification email.
|
|
|
|
**Authorization**: None (public)
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"tenantSlug": "acme-corp",
|
|
"email": "user@example.com"
|
|
}
|
|
```
|
|
|
|
**Responses**:
|
|
|
|
**200 OK** - Always returns success (prevent email enumeration):
|
|
```json
|
|
{
|
|
"message": "If an account exists, a verification email has been sent."
|
|
}
|
|
```
|
|
|
|
**429 Too Many Requests** - Rate limit exceeded:
|
|
```json
|
|
{
|
|
"error": "Too many verification email requests. Please try again later.",
|
|
"retryAfter": 3600
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6.2 Password Reset Endpoints
|
|
|
|
#### `POST /api/auth/forgot-password`
|
|
|
|
**Description**: Request password reset email.
|
|
|
|
**Authorization**: None (public)
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"tenantSlug": "acme-corp",
|
|
"email": "user@example.com"
|
|
}
|
|
```
|
|
|
|
**Responses**:
|
|
|
|
**200 OK** - Always returns success (prevent email enumeration):
|
|
```json
|
|
{
|
|
"message": "If an account exists, a password reset email has been sent."
|
|
}
|
|
```
|
|
|
|
**429 Too Many Requests** - Rate limit exceeded:
|
|
```json
|
|
{
|
|
"error": "Too many password reset requests. Please try again in 1 hour.",
|
|
"retryAfter": 3600
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### `POST /api/auth/reset-password`
|
|
|
|
**Description**: Reset password with token.
|
|
|
|
**Authorization**: None (public)
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"token": "base64url-encoded-token",
|
|
"newPassword": "SecureP@ssw0rd"
|
|
}
|
|
```
|
|
|
|
**Responses**:
|
|
|
|
**200 OK** - Password reset successfully:
|
|
```json
|
|
{
|
|
"message": "Password reset successfully. You can now log in with your new password.",
|
|
"redirectUrl": "/login"
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Invalid or expired token:
|
|
```json
|
|
{
|
|
"error": "Password reset token is invalid or expired.",
|
|
"code": "INVALID_TOKEN"
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Password complexity requirements not met:
|
|
```json
|
|
{
|
|
"errors": {
|
|
"newPassword": [
|
|
"Password must be at least 8 characters long",
|
|
"Password must contain at least one uppercase letter"
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Token already used:
|
|
```json
|
|
{
|
|
"error": "This password reset link has already been used.",
|
|
"code": "TOKEN_ALREADY_USED"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6.3 User Invitation Endpoints
|
|
|
|
#### `POST /api/tenants/{tenantId}/invitations`
|
|
|
|
**Description**: Invite a user to join tenant.
|
|
|
|
**Authorization**: `RequireTenantOwner` or `RequireTenantAdmin`
|
|
|
|
**Path Parameters**:
|
|
- `tenantId` (Guid) - Target tenant ID
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"email": "newuser@example.com",
|
|
"role": "Developer"
|
|
}
|
|
```
|
|
|
|
**Validation**:
|
|
- Email: Valid email format
|
|
- Role: One of `TenantAdmin`, `Developer`, `Guest` (cannot be `TenantOwner` or `AIAgent`)
|
|
|
|
**Responses**:
|
|
|
|
**201 Created** - Invitation created:
|
|
```json
|
|
{
|
|
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"tenantId": "1fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"email": "newuser@example.com",
|
|
"role": "Developer",
|
|
"status": "Pending",
|
|
"invitedBy": {
|
|
"id": "2fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"fullName": "John Doe"
|
|
},
|
|
"invitedAt": "2025-11-03T10:00:00Z",
|
|
"expiresAt": "2025-11-10T10:00:00Z",
|
|
"acceptedAt": null
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Invalid role:
|
|
```json
|
|
{
|
|
"errors": {
|
|
"role": ["Role must be one of: TenantAdmin, Developer, Guest"]
|
|
}
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - User already invited:
|
|
```json
|
|
{
|
|
"error": "An active invitation for this email already exists.",
|
|
"code": "DUPLICATE_INVITATION"
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - User already member:
|
|
```json
|
|
{
|
|
"error": "A user with this email is already a member of this tenant.",
|
|
"code": "USER_ALREADY_EXISTS"
|
|
}
|
|
```
|
|
|
|
**403 Forbidden** - Cross-tenant access:
|
|
```json
|
|
{
|
|
"error": "Access denied: You can only manage invitations in your own tenant."
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### `GET /api/tenants/{tenantId}/invitations`
|
|
|
|
**Description**: List all invitations for a tenant.
|
|
|
|
**Authorization**: `RequireTenantOwner` or `RequireTenantAdmin`
|
|
|
|
**Path Parameters**:
|
|
- `tenantId` (Guid) - Target tenant ID
|
|
|
|
**Query Parameters**:
|
|
- `pageNumber` (int, optional, default: 1) - Page number
|
|
- `pageSize` (int, optional, default: 20, max: 100) - Items per page
|
|
- `status` (string, optional) - Filter by status: `Pending`, `Accepted`, `Expired`, `Canceled`
|
|
|
|
**Responses**:
|
|
|
|
**200 OK**:
|
|
```json
|
|
{
|
|
"items": [
|
|
{
|
|
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"email": "user1@example.com",
|
|
"role": "Developer",
|
|
"status": "Pending",
|
|
"invitedBy": {
|
|
"id": "2fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"fullName": "John Doe"
|
|
},
|
|
"invitedAt": "2025-11-03T10:00:00Z",
|
|
"expiresAt": "2025-11-10T10:00:00Z",
|
|
"acceptedAt": null
|
|
},
|
|
{
|
|
"id": "4fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"email": "user2@example.com",
|
|
"role": "Guest",
|
|
"status": "Accepted",
|
|
"invitedBy": {
|
|
"id": "2fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"fullName": "John Doe"
|
|
},
|
|
"invitedAt": "2025-11-01T08:00:00Z",
|
|
"expiresAt": "2025-11-08T08:00:00Z",
|
|
"acceptedAt": "2025-11-01T09:30:00Z"
|
|
}
|
|
],
|
|
"pageNumber": 1,
|
|
"pageSize": 20,
|
|
"totalCount": 2,
|
|
"totalPages": 1
|
|
}
|
|
```
|
|
|
|
**403 Forbidden** - Cross-tenant access:
|
|
```json
|
|
{
|
|
"error": "Access denied: You can only view invitations in your own tenant."
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### `POST /api/invitations/accept`
|
|
|
|
**Description**: Accept an invitation and create user account.
|
|
|
|
**Authorization**: None (public)
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"token": "base64url-encoded-token",
|
|
"fullName": "Jane Doe",
|
|
"password": "SecureP@ssw0rd"
|
|
}
|
|
```
|
|
|
|
**Validation**:
|
|
- fullName: 2-100 characters
|
|
- password: Password complexity requirements (8+ chars, uppercase, lowercase, number, special char)
|
|
|
|
**Responses**:
|
|
|
|
**200 OK** - Invitation accepted, user created:
|
|
```json
|
|
{
|
|
"user": {
|
|
"id": "5fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"tenantId": "1fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"email": "newuser@example.com",
|
|
"fullName": "Jane Doe",
|
|
"role": "Developer",
|
|
"status": "Active",
|
|
"isEmailVerified": true,
|
|
"createdAt": "2025-11-03T11:00:00Z"
|
|
},
|
|
"tenant": {
|
|
"id": "1fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
"name": "Acme Corp",
|
|
"slug": "acme-corp"
|
|
},
|
|
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
"refreshToken": "base64-encoded-refresh-token"
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Invalid token:
|
|
```json
|
|
{
|
|
"error": "Invalid or expired invitation token.",
|
|
"code": "INVALID_INVITATION"
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Invitation expired:
|
|
```json
|
|
{
|
|
"error": "This invitation has expired. Please request a new one from your team admin.",
|
|
"code": "INVITATION_EXPIRED"
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Invitation already accepted:
|
|
```json
|
|
{
|
|
"error": "This invitation has already been used.",
|
|
"code": "INVITATION_ALREADY_USED"
|
|
}
|
|
```
|
|
|
|
**400 Bad Request** - Password validation failed:
|
|
```json
|
|
{
|
|
"errors": {
|
|
"password": [
|
|
"Password must be at least 8 characters long",
|
|
"Password must contain at least one special character"
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### `DELETE /api/tenants/{tenantId}/invitations/{invitationId}`
|
|
|
|
**Description**: Cancel a pending invitation.
|
|
|
|
**Authorization**: `RequireTenantOwner` or `RequireTenantAdmin`
|
|
|
|
**Path Parameters**:
|
|
- `tenantId` (Guid) - Target tenant ID
|
|
- `invitationId` (Guid) - Invitation ID
|
|
|
|
**Responses**:
|
|
|
|
**204 No Content** - Invitation canceled successfully
|
|
|
|
**400 Bad Request** - Invitation not pending:
|
|
```json
|
|
{
|
|
"error": "Only pending invitations can be canceled.",
|
|
"code": "INVITATION_NOT_PENDING"
|
|
}
|
|
```
|
|
|
|
**403 Forbidden** - Cross-tenant access:
|
|
```json
|
|
{
|
|
"error": "Access denied: You can only cancel invitations in your own tenant."
|
|
}
|
|
```
|
|
|
|
**404 Not Found** - Invitation not found:
|
|
```json
|
|
{
|
|
"error": "Invitation not found.",
|
|
"code": "INVITATION_NOT_FOUND"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Database Schema Changes
|
|
|
|
### 7.1 New Tables
|
|
|
|
#### `email_verification_tokens`
|
|
|
|
**Purpose**: Store email verification tokens for new users.
|
|
|
|
```sql
|
|
CREATE TABLE email_verification_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
email VARCHAR(255) NOT NULL, -- For validation
|
|
token_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hash
|
|
expires_at TIMESTAMP NOT NULL,
|
|
verified_at TIMESTAMP NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
|
|
INDEX idx_user_id (user_id),
|
|
INDEX idx_token_hash (token_hash),
|
|
INDEX idx_expires_at (expires_at)
|
|
);
|
|
|
|
COMMENT ON TABLE email_verification_tokens IS 'Email verification tokens for user registration';
|
|
COMMENT ON COLUMN email_verification_tokens.token_hash IS 'SHA-256 hash of verification token (not plaintext)';
|
|
COMMENT ON COLUMN email_verification_tokens.verified_at IS 'Timestamp when email was verified (NULL if not verified)';
|
|
```
|
|
|
|
**Indexes**:
|
|
- Primary key on `id`
|
|
- Index on `user_id` (for user lookup)
|
|
- Unique index on `token_hash` (for token validation)
|
|
- Index on `expires_at` (for cleanup queries)
|
|
|
|
**Business Rules**:
|
|
- One active token per user (enforce in application layer)
|
|
- Tokens expire after 24 hours
|
|
- Verified tokens kept for audit (not deleted)
|
|
|
|
---
|
|
|
|
#### `password_reset_tokens`
|
|
|
|
**Purpose**: Store password reset tokens for forgot password flow.
|
|
|
|
```sql
|
|
CREATE TABLE password_reset_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hash
|
|
expires_at TIMESTAMP NOT NULL,
|
|
used_at TIMESTAMP NULL,
|
|
ip_address VARCHAR(45) NULL, -- IPv4 or IPv6
|
|
user_agent VARCHAR(500) NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
|
|
INDEX idx_user_id (user_id),
|
|
INDEX idx_token_hash (token_hash),
|
|
INDEX idx_expires_at (expires_at)
|
|
);
|
|
|
|
COMMENT ON TABLE password_reset_tokens IS 'Password reset tokens for forgot password flow';
|
|
COMMENT ON COLUMN password_reset_tokens.token_hash IS 'SHA-256 hash of reset token (not plaintext)';
|
|
COMMENT ON COLUMN password_reset_tokens.used_at IS 'Timestamp when token was used (NULL if not used)';
|
|
```
|
|
|
|
**Indexes**:
|
|
- Primary key on `id`
|
|
- Index on `user_id` (for user lookup)
|
|
- Unique index on `token_hash` (for token validation)
|
|
- Index on `expires_at` (for cleanup queries)
|
|
|
|
**Business Rules**:
|
|
- Tokens expire after 1 hour
|
|
- Used tokens cannot be reused (`used_at` != NULL)
|
|
- New reset request invalidates old unused tokens
|
|
|
|
---
|
|
|
|
#### `invitations`
|
|
|
|
**Purpose**: Store user invitations to tenants.
|
|
|
|
```sql
|
|
CREATE TABLE invitations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
email VARCHAR(255) NOT NULL,
|
|
role VARCHAR(50) NOT NULL, -- TenantAdmin, Developer, Guest
|
|
token_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hash
|
|
status VARCHAR(20) NOT NULL DEFAULT 'Pending', -- Pending, Accepted, Expired, Canceled
|
|
invited_by_user_id UUID NOT NULL REFERENCES users(id),
|
|
accepted_by_user_id UUID NULL REFERENCES users(id),
|
|
invited_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
accepted_at TIMESTAMP NULL,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
canceled_at TIMESTAMP NULL,
|
|
|
|
INDEX idx_tenant_id (tenant_id),
|
|
INDEX idx_email (email),
|
|
INDEX idx_token_hash (token_hash),
|
|
INDEX idx_status (status),
|
|
INDEX idx_expires_at (expires_at),
|
|
|
|
CONSTRAINT chk_role CHECK (role IN ('TenantAdmin', 'Developer', 'Guest')),
|
|
CONSTRAINT chk_status CHECK (status IN ('Pending', 'Accepted', 'Expired', 'Canceled')),
|
|
CONSTRAINT uq_tenant_email_pending UNIQUE (tenant_id, email, status)
|
|
WHERE status = 'Pending'
|
|
);
|
|
|
|
COMMENT ON TABLE invitations IS 'User invitations to tenants';
|
|
COMMENT ON COLUMN invitations.token_hash IS 'SHA-256 hash of invitation token (not plaintext)';
|
|
COMMENT ON COLUMN invitations.status IS 'Invitation lifecycle status';
|
|
COMMENT ON CONSTRAINT uq_tenant_email_pending ON invitations IS 'Prevent duplicate pending invitations for same email in same tenant';
|
|
```
|
|
|
|
**Indexes**:
|
|
- Primary key on `id`
|
|
- Index on `tenant_id` (for tenant lookup)
|
|
- Index on `email` (for duplicate check)
|
|
- Unique index on `token_hash` (for token validation)
|
|
- Index on `status` (for filtering)
|
|
- Partial unique index on `(tenant_id, email, status)` where status = 'Pending' (prevent duplicates)
|
|
|
|
**Business Rules**:
|
|
- Cannot have multiple pending invitations for same email in same tenant
|
|
- Invitations expire after 7 days
|
|
- Accepted invitations create user account and assign role
|
|
|
|
---
|
|
|
|
### 7.2 Modified Tables
|
|
|
|
#### `users` Table Changes
|
|
|
|
**No schema changes required**. Existing columns support email verification:
|
|
|
|
```sql
|
|
-- Existing columns (no changes needed)
|
|
email_verified_at TIMESTAMP NULL -- NULL = not verified, NOT NULL = verified
|
|
```
|
|
|
|
**Usage**:
|
|
- Set `email_verified_at = NOW()` when email verification succeeds
|
|
- Check `email_verified_at IS NOT NULL` to determine if email is verified
|
|
|
|
---
|
|
|
|
### 7.3 Entity Framework Core Migrations
|
|
|
|
**Migration Name**: `Add_EmailVerification_PasswordReset_Invitations`
|
|
|
|
**Migration Steps**:
|
|
1. Create `email_verification_tokens` table
|
|
2. Create `password_reset_tokens` table
|
|
3. Create `invitations` table
|
|
4. Add indexes and constraints
|
|
5. Seed initial data (none required)
|
|
|
|
**Rollback Strategy**:
|
|
- Drop tables in reverse order
|
|
- No data migration needed (new feature)
|
|
|
|
---
|
|
|
|
### 7.4 Database Cleanup Jobs (Future)
|
|
|
|
**Not in Day 7 scope**, but document for future:
|
|
|
|
```sql
|
|
-- Delete expired email verification tokens (older than 30 days)
|
|
DELETE FROM email_verification_tokens
|
|
WHERE expires_at < NOW() - INTERVAL '30 days';
|
|
|
|
-- Delete used password reset tokens (older than 30 days)
|
|
DELETE FROM password_reset_tokens
|
|
WHERE used_at IS NOT NULL AND used_at < NOW() - INTERVAL '30 days';
|
|
|
|
-- Mark expired invitations as Expired
|
|
UPDATE invitations
|
|
SET status = 'Expired'
|
|
WHERE status = 'Pending' AND expires_at < NOW();
|
|
```
|
|
|
|
**Future**: Implement background job (Hangfire or similar) to run cleanup daily.
|
|
|
|
---
|
|
|
|
## 8. Security Requirements
|
|
|
|
### 8.1 Token Security
|
|
|
|
#### SEC-001: Cryptographically Secure Token Generation
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Requirements**:
|
|
- Use `RandomNumberGenerator.Create()` (not `Random()`)
|
|
- Generate 256-bit (32-byte) tokens
|
|
- Encode as Base64URL for URL safety
|
|
- Never log tokens in plaintext
|
|
|
|
**Implementation**:
|
|
```csharp
|
|
public static string GenerateSecureToken()
|
|
{
|
|
var randomBytes = new byte[32]; // 256 bits
|
|
using (var rng = RandomNumberGenerator.Create())
|
|
{
|
|
rng.GetBytes(randomBytes);
|
|
}
|
|
return Convert.ToBase64String(randomBytes)
|
|
.TrimEnd('=')
|
|
.Replace('+', '-')
|
|
.Replace('/', '_'); // Base64URL encoding
|
|
}
|
|
```
|
|
|
|
#### SEC-002: Token Hashing in Database
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Requirements**:
|
|
- Store SHA-256 hash, never plaintext token
|
|
- Hash before database insert
|
|
- Compare hashes during validation
|
|
|
|
**Implementation**:
|
|
```csharp
|
|
public static string HashToken(string token)
|
|
{
|
|
using (var sha256 = SHA256.Create())
|
|
{
|
|
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token));
|
|
return Convert.ToBase64String(hashBytes);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 8.2 Rate Limiting
|
|
|
|
#### SEC-003: Email Verification Rate Limits
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Limits**:
|
|
- Resend verification: 3 requests per email per hour
|
|
- Verify email: 10 attempts per IP per minute (prevent brute force)
|
|
|
|
**Implementation**: Use ASP.NET Core Rate Limiting middleware or in-memory cache
|
|
|
|
#### SEC-004: Password Reset Rate Limits
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Limits**:
|
|
- Forgot password: 3 requests per email per hour
|
|
- Reset password: 5 attempts per IP per minute (prevent brute force)
|
|
|
|
**Error Response**: 429 Too Many Requests with `Retry-After` header
|
|
|
|
#### SEC-005: Invitation Rate Limits
|
|
**Priority**: P1 (High)
|
|
|
|
**Limits**:
|
|
- Create invitation: 20 invitations per tenant per hour
|
|
- Accept invitation: 5 attempts per token per hour (prevent brute force)
|
|
|
|
### 8.3 Email Enumeration Prevention
|
|
|
|
#### SEC-006: Never Reveal Email Existence
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Requirements**:
|
|
- Forgot password: Always return 200 OK, never reveal if email exists
|
|
- Resend verification: Always return 200 OK, never reveal if email exists
|
|
- Invitation: Return 400 "User already exists" only to authenticated tenant admins
|
|
|
|
**Example**:
|
|
```csharp
|
|
// ✅ CORRECT: Prevent enumeration
|
|
[HttpPost("forgot-password")]
|
|
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordRequest request)
|
|
{
|
|
// Process in background, don't wait
|
|
_ = _emailService.SendPasswordResetEmailIfUserExists(request.Email);
|
|
|
|
// Always return same response
|
|
return Ok(new { message = "If an account exists, a reset email has been sent." });
|
|
}
|
|
|
|
// ❌ WRONG: Reveals if email exists
|
|
[HttpPost("forgot-password")]
|
|
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordRequest request)
|
|
{
|
|
var user = await _userRepository.GetByEmail(request.Email);
|
|
if (user == null)
|
|
return NotFound("Email not found"); // ❌ Enumeration vulnerability!
|
|
|
|
await _emailService.SendPasswordResetEmail(user);
|
|
return Ok();
|
|
}
|
|
```
|
|
|
|
### 8.4 HTTPS Enforcement
|
|
|
|
#### SEC-007: HTTPS-Only Links
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Requirements**:
|
|
- All email links must use `https://` (never `http://`)
|
|
- Redirect HTTP to HTTPS at infrastructure level
|
|
- Set `Strict-Transport-Security` header (HSTS)
|
|
|
|
**Configuration**:
|
|
```csharp
|
|
app.UseHttpsRedirection();
|
|
app.UseHsts(); // Enforce HTTPS for 1 year
|
|
```
|
|
|
|
### 8.5 Input Validation
|
|
|
|
#### SEC-008: Email Validation
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Requirements**:
|
|
- Validate email format using `EmailAddressAttribute` and regex
|
|
- Normalize emails to lowercase
|
|
- Trim whitespace
|
|
- Max length: 255 characters
|
|
- Reject disposable email domains (future enhancement)
|
|
|
|
**Validation**:
|
|
```csharp
|
|
[EmailAddress]
|
|
[MaxLength(255)]
|
|
public string Email { get; set; }
|
|
```
|
|
|
|
#### SEC-009: Password Complexity Validation
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Requirements** (repeated for emphasis):
|
|
- Minimum 8 characters
|
|
- At least 1 uppercase letter
|
|
- At least 1 lowercase letter
|
|
- At least 1 number
|
|
- At least 1 special character
|
|
- Max length: 128 characters (prevent DoS via bcrypt)
|
|
|
|
**Implementation**: Use `DataAnnotations` + custom validator
|
|
|
|
### 8.6 Session Security
|
|
|
|
#### SEC-010: Refresh Token Revocation on Password Reset
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Requirements**:
|
|
- On successful password reset, invalidate all user's refresh tokens
|
|
- Force re-login on all devices
|
|
- Prevent attacker with stolen tokens from maintaining access
|
|
|
|
**Implementation**:
|
|
```csharp
|
|
// In ResetPasswordCommandHandler
|
|
await _refreshTokenService.RevokeAllUserTokensAsync(user.Id, cancellationToken);
|
|
```
|
|
|
|
### 8.7 Audit Logging
|
|
|
|
#### SEC-011: Security Event Logging
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Events to Log** (with IP address, user agent, timestamp):
|
|
1. Email verification sent
|
|
2. Email verification succeeded/failed
|
|
3. Password reset requested
|
|
4. Password reset succeeded/failed
|
|
5. Invitation created
|
|
6. Invitation accepted
|
|
7. Invitation canceled
|
|
8. Rate limit exceeded
|
|
9. Invalid token attempts
|
|
|
|
**Log Format**:
|
|
```json
|
|
{
|
|
"timestamp": "2025-11-03T10:00:00Z",
|
|
"event": "PasswordResetRequested",
|
|
"email": "user@example.com",
|
|
"tenantSlug": "acme-corp",
|
|
"ipAddress": "192.168.1.1",
|
|
"userAgent": "Mozilla/5.0...",
|
|
"success": true
|
|
}
|
|
```
|
|
|
|
### 8.8 Cross-Tenant Security
|
|
|
|
#### SEC-012: Tenant Isolation in Invitations
|
|
**Priority**: P0 (Critical)
|
|
|
|
**Requirements**:
|
|
- Validate `tenantId` from route matches JWT `tenant_id` claim
|
|
- Users can only invite to their own tenant
|
|
- Users can only view invitations for their own tenant
|
|
- Return 403 Forbidden for cross-tenant access attempts
|
|
|
|
**Implementation** (reuse pattern from Day 6):
|
|
```csharp
|
|
var userTenantId = Guid.Parse(User.FindFirst("tenant_id")?.Value);
|
|
if (userTenantId != tenantId)
|
|
return StatusCode(403, new { error = "Access denied: Cross-tenant access not allowed" });
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Email Templates
|
|
|
|
### 9.1 Template Architecture
|
|
|
|
**Template Engine**: C# String Interpolation or Razor Pages (recommend Razor for complex templates)
|
|
|
|
**Template Structure**:
|
|
```
|
|
EmailTemplates/
|
|
├── _Layout.cshtml # Shared layout with branding
|
|
├── EmailVerification.cshtml
|
|
├── PasswordReset.cshtml
|
|
├── UserInvitation.cshtml
|
|
└── WelcomeEmail.cshtml (optional)
|
|
```
|
|
|
|
**Shared Layout** (`_Layout.cshtml`):
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>@ViewBag.Subject - ColaFlow</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white; padding: 30px; text-align: center; }
|
|
.content { background: white; padding: 30px; }
|
|
.button { display: inline-block; padding: 12px 30px; background: #667eea;
|
|
color: white; text-decoration: none; border-radius: 5px; }
|
|
.footer { text-align: center; padding: 20px; color: #777; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🧠 ColaFlow</h1>
|
|
</div>
|
|
<div class="content">
|
|
@RenderBody()
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2025 ColaFlow. All rights reserved.</p>
|
|
<p>This is an automated email. Please do not reply.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### 9.2 Email Verification Template
|
|
|
|
**File**: `EmailTemplates/EmailVerification.cshtml`
|
|
|
|
**Subject**: "Verify your email address - ColaFlow"
|
|
|
|
**Template**:
|
|
```html
|
|
@{
|
|
ViewBag.Subject = "Verify your email address";
|
|
}
|
|
|
|
<h2>Welcome to ColaFlow, @Model.FullName!</h2>
|
|
|
|
<p>Thank you for registering with <strong>@Model.TenantName</strong> on ColaFlow.</p>
|
|
|
|
<p>Please verify your email address by clicking the button below:</p>
|
|
|
|
<p style="text-align: center; margin: 30px 0;">
|
|
<a href="@Model.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: #667eea;">@Model.VerificationUrl</p>
|
|
|
|
<p><strong>This link will expire in 24 hours.</strong></p>
|
|
|
|
<p>If you didn't create an account, you can safely ignore this email.</p>
|
|
|
|
<p>Best regards,<br>The ColaFlow Team</p>
|
|
```
|
|
|
|
**Model**:
|
|
```csharp
|
|
public class EmailVerificationModel
|
|
{
|
|
public string FullName { get; set; }
|
|
public string TenantName { get; set; }
|
|
public string VerificationUrl { get; set; } // https://app.colaflow.io/verify-email?token=...
|
|
}
|
|
```
|
|
|
|
**Plain Text Fallback**:
|
|
```
|
|
Welcome to ColaFlow, [FullName]!
|
|
|
|
Thank you for registering with [TenantName] on ColaFlow.
|
|
|
|
Please verify your email address by clicking this link:
|
|
[VerificationUrl]
|
|
|
|
This link will expire in 24 hours.
|
|
|
|
If you didn't create an account, you can safely ignore this email.
|
|
|
|
Best regards,
|
|
The ColaFlow Team
|
|
|
|
---
|
|
© 2025 ColaFlow. This is an automated email. Please do not reply.
|
|
```
|
|
|
|
### 9.3 Password Reset Template
|
|
|
|
**File**: `EmailTemplates/PasswordReset.cshtml`
|
|
|
|
**Subject**: "Reset your password - ColaFlow"
|
|
|
|
**Template**:
|
|
```html
|
|
@{
|
|
ViewBag.Subject = "Reset your password";
|
|
}
|
|
|
|
<h2>Password Reset Request</h2>
|
|
|
|
<p>Hi @Model.FullName,</p>
|
|
|
|
<p>We received a request to reset the password for your ColaFlow account
|
|
(<strong>@Model.TenantName</strong>).</p>
|
|
|
|
<p>Click the button below to reset your password:</p>
|
|
|
|
<p style="text-align: center; margin: 30px 0;">
|
|
<a href="@Model.ResetUrl" class="button">Reset Password</a>
|
|
</p>
|
|
|
|
<p>Or copy and paste this link into your browser:</p>
|
|
<p style="word-break: break-all; color: #667eea;">@Model.ResetUrl</p>
|
|
|
|
<p><strong>This link will expire in 1 hour.</strong></p>
|
|
|
|
<p style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
|
⚠️ <strong>Security Notice:</strong> If you didn't request a password reset,
|
|
please ignore this email and ensure your account is secure.
|
|
</p>
|
|
|
|
<p>Best regards,<br>The ColaFlow Team</p>
|
|
```
|
|
|
|
**Model**:
|
|
```csharp
|
|
public class PasswordResetModel
|
|
{
|
|
public string FullName { get; set; }
|
|
public string TenantName { get; set; }
|
|
public string ResetUrl { get; set; } // https://app.colaflow.io/reset-password?token=...
|
|
}
|
|
```
|
|
|
|
### 9.4 User Invitation Template
|
|
|
|
**File**: `EmailTemplates/UserInvitation.cshtml`
|
|
|
|
**Subject**: "You're invited to join [TenantName] on ColaFlow"
|
|
|
|
**Template**:
|
|
```html
|
|
@{
|
|
ViewBag.Subject = $"You're invited to join {Model.TenantName} on ColaFlow";
|
|
}
|
|
|
|
<h2>You've been invited! 🎉</h2>
|
|
|
|
<p>Hi there,</p>
|
|
|
|
<p><strong>@Model.InviterName</strong> has invited you to join
|
|
<strong>@Model.TenantName</strong> on ColaFlow as a <strong>@Model.Role</strong>.</p>
|
|
|
|
<p>ColaFlow is an AI-powered project management platform that helps teams
|
|
collaborate more effectively.</p>
|
|
|
|
<p>Click the button below to accept the invitation and create your account:</p>
|
|
|
|
<p style="text-align: center; margin: 30px 0;">
|
|
<a href="@Model.AcceptUrl" class="button">Accept Invitation</a>
|
|
</p>
|
|
|
|
<p>Or copy and paste this link into your browser:</p>
|
|
<p style="word-break: break-all; color: #667eea;">@Model.AcceptUrl</p>
|
|
|
|
<p><strong>This invitation will expire on @Model.ExpiresAt.ToString("MMMM dd, yyyy").</strong></p>
|
|
|
|
<div style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin: 20px 0;">
|
|
<strong>Your Role:</strong> @Model.RoleDescription
|
|
</div>
|
|
|
|
<p>If you didn't expect this invitation, you can safely ignore this email.</p>
|
|
|
|
<p>Best regards,<br>The ColaFlow Team</p>
|
|
```
|
|
|
|
**Model**:
|
|
```csharp
|
|
public class UserInvitationModel
|
|
{
|
|
public string TenantName { get; set; }
|
|
public string InviterName { get; set; }
|
|
public string Role { get; set; } // "Developer"
|
|
public string RoleDescription { get; set; } // "Developers can create and manage projects..."
|
|
public string AcceptUrl { get; set; } // https://app.colaflow.io/accept-invitation?token=...
|
|
public DateTime ExpiresAt { get; set; }
|
|
}
|
|
```
|
|
|
|
**Role Descriptions**:
|
|
- **TenantAdmin**: "Admins can manage team members, view all projects, and configure tenant settings."
|
|
- **Developer**: "Developers can create and manage projects, tasks, and collaborate with the team."
|
|
- **Guest**: "Guests have read-only access to assigned projects and can leave comments."
|
|
|
|
### 9.5 Template Rendering Service
|
|
|
|
**Interface**:
|
|
```csharp
|
|
public interface IEmailTemplateRenderer
|
|
{
|
|
Task<string> RenderHtmlAsync<TModel>(string templateName, TModel model);
|
|
Task<string> RenderPlainTextAsync<TModel>(string templateName, TModel model);
|
|
}
|
|
```
|
|
|
|
**Implementation** (Razor Pages or simple string interpolation for Day 7):
|
|
```csharp
|
|
public class EmailTemplateRenderer : IEmailTemplateRenderer
|
|
{
|
|
private readonly string _templateBasePath;
|
|
|
|
public EmailTemplateRenderer(IConfiguration configuration)
|
|
{
|
|
_templateBasePath = configuration["EmailSettings:TemplateBasePath"] ?? "EmailTemplates";
|
|
}
|
|
|
|
public async Task<string> RenderHtmlAsync<TModel>(string templateName, TModel model)
|
|
{
|
|
var templatePath = Path.Combine(_templateBasePath, $"{templateName}.cshtml");
|
|
// Use RazorEngine or simple file read + string.Replace for Day 7
|
|
// For simplicity, use string interpolation initially
|
|
var template = await File.ReadAllTextAsync(templatePath);
|
|
return RenderTemplate(template, model);
|
|
}
|
|
|
|
private string RenderTemplate<TModel>(string template, TModel model)
|
|
{
|
|
// Simple placeholder replacement for Day 7
|
|
// Future: Use RazorEngine for complex logic
|
|
var properties = typeof(TModel).GetProperties();
|
|
foreach (var prop in properties)
|
|
{
|
|
var value = prop.GetValue(model)?.ToString() ?? "";
|
|
template = template.Replace($"@Model.{prop.Name}", value);
|
|
}
|
|
return template;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 9.6 Email Styling Guidelines
|
|
|
|
**Design Principles**:
|
|
1. **Mobile-First**: 600px max width, responsive design
|
|
2. **Accessibility**: High contrast, readable fonts (16px+ body text)
|
|
3. **Brand Consistency**: ColaFlow purple gradient (#667eea to #764ba2)
|
|
4. **Clear CTAs**: Primary action button prominently displayed
|
|
5. **Plain Text Fallback**: Always provide plain text version
|
|
6. **No External Images**: Embed logos as data URIs or use text-based branding
|
|
|
|
**Testing**:
|
|
- Preview in Gmail, Outlook, Apple Mail
|
|
- Use tools like Litmus or Email on Acid (future)
|
|
- Test dark mode compatibility
|
|
|
|
---
|
|
|
|
## 10. Integration Points
|
|
|
|
### 10.1 Registration Flow Enhancement
|
|
|
|
**Current Flow** (`RegisterTenantCommandHandler`):
|
|
```
|
|
1. Validate slug uniqueness
|
|
2. Create tenant
|
|
3. Create admin user
|
|
4. Assign TenantOwner role
|
|
5. Generate JWT tokens
|
|
6. Return result
|
|
```
|
|
|
|
**Day 7 Enhancement**:
|
|
```
|
|
1. Validate slug uniqueness
|
|
2. Create tenant
|
|
3. Create admin user (with emailVerified = false) ✨ NEW
|
|
4. Assign TenantOwner role
|
|
5. Generate email verification token ✨ NEW
|
|
6. Send verification email (non-blocking) ✨ NEW
|
|
7. Generate JWT tokens
|
|
8. Return result + verification status ✨ NEW
|
|
```
|
|
|
|
**Code Changes**:
|
|
```csharp
|
|
// In RegisterTenantCommandHandler.Handle()
|
|
|
|
// After creating admin user
|
|
var adminUser = User.CreateLocal(
|
|
TenantId.Create(tenant.Id),
|
|
Email.Create(request.AdminEmail),
|
|
hashedPassword,
|
|
FullName.Create(request.AdminFullName));
|
|
|
|
await _userRepository.AddAsync(adminUser, cancellationToken);
|
|
|
|
// ✨ NEW: Generate verification token
|
|
var verificationToken = _tokenGenerator.GenerateSecureToken();
|
|
var verificationTokenHash = _tokenGenerator.HashToken(verificationToken);
|
|
var emailVerificationToken = new EmailVerificationToken
|
|
{
|
|
UserId = adminUser.Id,
|
|
Email = adminUser.Email.Value,
|
|
TokenHash = verificationTokenHash,
|
|
ExpiresAt = DateTime.UtcNow.AddHours(24),
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
await _emailVerificationTokenRepository.AddAsync(emailVerificationToken, cancellationToken);
|
|
|
|
// ✨ NEW: Send verification email (non-blocking)
|
|
_ = _emailService.SendEmailVerificationAsync(new EmailVerificationModel
|
|
{
|
|
FullName = adminUser.FullName.Value,
|
|
TenantName = tenant.Name.Value,
|
|
VerificationUrl = $"{_configuration["AppSettings:WebAppUrl"]}/verify-email?token={verificationToken}"
|
|
});
|
|
|
|
// Continue with existing logic (assign role, generate tokens, etc.)
|
|
```
|
|
|
|
**Response Changes**:
|
|
```json
|
|
{
|
|
"tenant": { ... },
|
|
"user": {
|
|
"id": "...",
|
|
"email": "...",
|
|
"isEmailVerified": false, // ✨ NEW: Always false on registration
|
|
...
|
|
},
|
|
"accessToken": "...",
|
|
"refreshToken": "...",
|
|
"verificationEmailSent": true // ✨ NEW: Indicates email was sent
|
|
}
|
|
```
|
|
|
|
### 10.2 Login Flow Enhancement
|
|
|
|
**Current Flow** (`LoginCommandHandler`):
|
|
```
|
|
1. Find tenant
|
|
2. Find user
|
|
3. Verify password
|
|
4. Get user's role
|
|
5. Generate JWT tokens
|
|
6. Return result
|
|
```
|
|
|
|
**Day 7 Enhancement** (Optional, P2 Priority):
|
|
```
|
|
1. Find tenant
|
|
2. Find user
|
|
3. Verify password
|
|
4. Check email verification status ✨ NEW (optional)
|
|
5. Get user's role
|
|
6. Generate JWT tokens
|
|
7. Return result + verification warning ✨ NEW
|
|
```
|
|
|
|
**Code Changes** (Optional for Day 7):
|
|
```csharp
|
|
// In LoginCommandHandler.Handle()
|
|
|
|
// After verifying password
|
|
if (!user.EmailVerifiedAt.HasValue)
|
|
{
|
|
// Option A: Allow login, return warning (recommended for Day 7)
|
|
// No code change needed, just return verification status in response
|
|
|
|
// Option B: Block login (future enhancement)
|
|
// throw new UnauthorizedAccessException("Please verify your email before logging in.");
|
|
}
|
|
|
|
// Continue with existing logic
|
|
```
|
|
|
|
**Response Changes**:
|
|
```json
|
|
{
|
|
"user": {
|
|
"id": "...",
|
|
"email": "...",
|
|
"isEmailVerified": false, // ✨ Frontend shows banner if false
|
|
...
|
|
},
|
|
"tenant": { ... },
|
|
"accessToken": "...",
|
|
"refreshToken": "...",
|
|
"emailVerificationRequired": false // ✨ NEW: true if blocking login (future)
|
|
}
|
|
```
|
|
|
|
### 10.3 Role Management API Integration
|
|
|
|
**Current**: Role assignment only via RegisterTenant and manual admin actions.
|
|
|
|
**Day 7 Enhancement**: Role assignment via invitation acceptance.
|
|
|
|
**Integration Point**: `AcceptInvitationCommandHandler`
|
|
|
|
```csharp
|
|
// In AcceptInvitationCommandHandler.Handle()
|
|
|
|
// 1. Validate invitation token
|
|
var invitation = await _invitationRepository.GetByTokenHashAsync(tokenHash, cancellationToken);
|
|
if (invitation == null || invitation.ExpiresAt < DateTime.UtcNow)
|
|
throw new InvalidOperationException("Invalid or expired invitation");
|
|
|
|
// 2. Create user account
|
|
var user = User.CreateLocal(
|
|
TenantId.Create(invitation.TenantId),
|
|
Email.Create(invitation.Email),
|
|
hashedPassword,
|
|
FullName.Create(request.FullName));
|
|
user.EmailVerifiedAt = DateTime.UtcNow; // ✨ Email verified via invitation acceptance
|
|
|
|
await _userRepository.AddAsync(user, cancellationToken);
|
|
|
|
// 3. Assign role from invitation
|
|
var userTenantRole = UserTenantRole.Create(
|
|
UserId.Create(user.Id),
|
|
TenantId.Create(invitation.TenantId),
|
|
Enum.Parse<TenantRole>(invitation.Role)); // ✨ Role from invitation
|
|
|
|
await _userTenantRoleRepository.AddAsync(userTenantRole, cancellationToken);
|
|
|
|
// 4. Mark invitation as accepted
|
|
invitation.Status = InvitationStatus.Accepted;
|
|
invitation.AcceptedAt = DateTime.UtcNow;
|
|
invitation.AcceptedByUserId = user.Id;
|
|
await _invitationRepository.UpdateAsync(invitation, cancellationToken);
|
|
|
|
// 5. Generate tokens and return
|
|
```
|
|
|
|
### 10.4 Domain Events Integration
|
|
|
|
**Day 7 Domain Events** (using existing infrastructure from Day 6):
|
|
|
|
#### `EmailVerificationRequestedEvent`
|
|
```csharp
|
|
public class EmailVerificationRequestedEvent : DomainEvent
|
|
{
|
|
public Guid UserId { get; }
|
|
public string Email { get; }
|
|
public string VerificationToken { get; }
|
|
|
|
public EmailVerificationRequestedEvent(Guid userId, string email, string token)
|
|
{
|
|
UserId = userId;
|
|
Email = email;
|
|
VerificationToken = token;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Handler**: Send verification email (async)
|
|
|
|
#### `EmailVerifiedEvent`
|
|
```csharp
|
|
public class EmailVerifiedEvent : DomainEvent
|
|
{
|
|
public Guid UserId { get; }
|
|
public DateTime VerifiedAt { get; }
|
|
|
|
public EmailVerifiedEvent(Guid userId, DateTime verifiedAt)
|
|
{
|
|
UserId = userId;
|
|
VerifiedAt = verifiedAt;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Handler**: Log event, potentially send welcome email
|
|
|
|
#### `PasswordResetRequestedEvent`
|
|
```csharp
|
|
public class PasswordResetRequestedEvent : DomainEvent
|
|
{
|
|
public Guid UserId { get; }
|
|
public string IpAddress { get; }
|
|
public DateTime RequestedAt { get; }
|
|
|
|
public PasswordResetRequestedEvent(Guid userId, string ipAddress)
|
|
{
|
|
UserId = userId;
|
|
IpAddress = ipAddress;
|
|
RequestedAt = DateTime.UtcNow;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Handler**: Send reset email, log security event
|
|
|
|
#### `UserInvitedEvent`
|
|
```csharp
|
|
public class UserInvitedEvent : DomainEvent
|
|
{
|
|
public Guid InvitationId { get; }
|
|
public Guid TenantId { get; }
|
|
public string Email { get; }
|
|
public string Role { get; }
|
|
public Guid InvitedByUserId { get; }
|
|
|
|
public UserInvitedEvent(Guid invitationId, Guid tenantId, string email,
|
|
string role, Guid invitedBy)
|
|
{
|
|
InvitationId = invitationId;
|
|
TenantId = tenantId;
|
|
Email = email;
|
|
Role = role;
|
|
InvitedByUserId = invitedBy;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Handler**: Send invitation email
|
|
|
|
#### `InvitationAcceptedEvent`
|
|
```csharp
|
|
public class InvitationAcceptedEvent : DomainEvent
|
|
{
|
|
public Guid InvitationId { get; }
|
|
public Guid UserId { get; }
|
|
public Guid TenantId { get; }
|
|
public DateTime AcceptedAt { get; }
|
|
|
|
public InvitationAcceptedEvent(Guid invitationId, Guid userId, Guid tenantId)
|
|
{
|
|
InvitationId = invitationId;
|
|
UserId = userId;
|
|
TenantId = tenantId;
|
|
AcceptedAt = DateTime.UtcNow;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Handler**: Log event, notify inviter (future), send welcome email
|
|
|
|
---
|
|
|
|
## 11. Testing Strategy
|
|
|
|
### 11.1 Unit Tests
|
|
|
|
**Coverage Target**: 90%+ for business logic
|
|
|
|
#### Email Service Tests
|
|
```csharp
|
|
// Tests/Modules/Identity/ColaFlow.Modules.Identity.UnitTests/Services/EmailServiceTests.cs
|
|
|
|
public class SendGridEmailServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task SendEmailAsync_WithValidInput_ShouldSucceed()
|
|
{
|
|
// Arrange: Mock SendGrid client
|
|
// Act: Send email
|
|
// Assert: SendGrid API called with correct parameters
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendEmailAsync_WithInvalidApiKey_ShouldThrowException()
|
|
{
|
|
// Arrange: Invalid API key
|
|
// Act & Assert: Should throw UnauthorizedAccessException
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Token Generation Tests
|
|
```csharp
|
|
public class SecureTokenGeneratorTests
|
|
{
|
|
[Fact]
|
|
public void GenerateSecureToken_ShouldReturnUniqueTokens()
|
|
{
|
|
// Generate 1000 tokens, ensure all unique
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateSecureToken_ShouldBeUrlSafe()
|
|
{
|
|
// Assert: No '+', '/', '=' characters
|
|
}
|
|
|
|
[Fact]
|
|
public void HashToken_ShouldBeIdempotent()
|
|
{
|
|
// Same token should produce same hash
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Password Validation Tests
|
|
```csharp
|
|
public class PasswordValidatorTests
|
|
{
|
|
[Theory]
|
|
[InlineData("short", false)] // Too short
|
|
[InlineData("nouppercase1!", false)] // No uppercase
|
|
[InlineData("NOLOWERCASE1!", false)] // No lowercase
|
|
[InlineData("NoNumbers!", false)] // No numbers
|
|
[InlineData("NoSpecialChar1", false)] // No special char
|
|
[InlineData("ValidP@ssw0rd", true)] // Valid
|
|
public void ValidatePassword_ShouldEnforceComplexity(string password, bool expected)
|
|
{
|
|
var result = PasswordValidator.Validate(password);
|
|
Assert.Equal(expected, result.IsValid);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 11.2 Integration Tests
|
|
|
|
**Coverage Target**: All API endpoints + critical flows
|
|
|
|
#### Email Verification Flow Tests
|
|
```csharp
|
|
// Tests/Modules/Identity/ColaFlow.Modules.Identity.IntegrationTests/EmailVerificationTests.cs
|
|
|
|
public class EmailVerificationFlowTests : IClassFixture<DatabaseFixture>
|
|
{
|
|
[Fact]
|
|
public async Task VerifyEmail_WithValidToken_ShouldMarkEmailAsVerified()
|
|
{
|
|
// Arrange: Register tenant (generates verification token)
|
|
var registerResponse = await RegisterTenant();
|
|
var token = GetVerificationTokenFromEmail(); // Mock email capture
|
|
|
|
// Act: Verify email
|
|
var response = await _client.PostAsJsonAsync("/api/auth/verify-email",
|
|
new { token });
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var user = await GetUser(registerResponse.User.Id);
|
|
user.EmailVerifiedAt.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyEmail_WithExpiredToken_ShouldFail()
|
|
{
|
|
// Arrange: Create expired token (manually set ExpiresAt in past)
|
|
// Act: Attempt verification
|
|
// Assert: 400 Bad Request
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResendVerification_ShouldInvalidateOldToken()
|
|
{
|
|
// Arrange: Register + get initial token
|
|
// Act: Resend verification
|
|
// Assert: Old token no longer works, new token works
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResendVerification_ShouldRespectRateLimit()
|
|
{
|
|
// Arrange: Register tenant
|
|
// Act: Resend 4 times quickly
|
|
// Assert: 4th request returns 429 Too Many Requests
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Password Reset Flow Tests
|
|
```csharp
|
|
public class PasswordResetFlowTests : IClassFixture<DatabaseFixture>
|
|
{
|
|
[Fact]
|
|
public async Task ForgotPassword_WithValidEmail_ShouldSendResetEmail()
|
|
{
|
|
// Arrange: Create user
|
|
// Act: Request password reset
|
|
// Assert: 200 OK, email sent (verify with mock email service)
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ForgotPassword_WithNonexistentEmail_ShouldNotRevealExistence()
|
|
{
|
|
// Act: Request reset for nonexistent email
|
|
// Assert: 200 OK (same response as valid email)
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResetPassword_WithValidToken_ShouldUpdatePassword()
|
|
{
|
|
// Arrange: Request password reset, get token
|
|
// Act: Reset password with new password
|
|
// Assert: Password updated, can login with new password
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResetPassword_ShouldRevokeRefreshTokens()
|
|
{
|
|
// Arrange: Login (get refresh token), request password reset
|
|
// Act: Reset password
|
|
// Assert: Old refresh token no longer works
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResetPassword_WithUsedToken_ShouldFail()
|
|
{
|
|
// Arrange: Reset password once
|
|
// Act: Attempt to reuse same token
|
|
// Assert: 400 Bad Request
|
|
}
|
|
}
|
|
```
|
|
|
|
#### User Invitation Flow Tests
|
|
```csharp
|
|
public class UserInvitationFlowTests : IClassFixture<DatabaseFixture>
|
|
{
|
|
[Fact]
|
|
public async Task InviteUser_AsOwner_ShouldCreateInvitation()
|
|
{
|
|
// Arrange: Register tenant as owner
|
|
// Act: Invite user with Developer role
|
|
// Assert: 201 Created, invitation stored, email sent
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InviteUser_WithInvalidRole_ShouldFail()
|
|
{
|
|
// Act: Attempt to invite as TenantOwner or AIAgent
|
|
// Assert: 400 Bad Request
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InviteUser_AsGuest_ShouldFail()
|
|
{
|
|
// Arrange: Create guest user
|
|
// Act: Attempt to invite
|
|
// Assert: 403 Forbidden
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AcceptInvitation_WithValidToken_ShouldCreateUser()
|
|
{
|
|
// Arrange: Owner invites user, get invitation token
|
|
// Act: Accept invitation with name + password
|
|
// Assert: User created, role assigned, email verified, logged in
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AcceptInvitation_WithExpiredToken_ShouldFail()
|
|
{
|
|
// Arrange: Create invitation with past expiration
|
|
// Act: Attempt to accept
|
|
// Assert: 400 Bad Request
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListInvitations_AsOwner_ShouldReturnTenantInvitations()
|
|
{
|
|
// Arrange: Owner creates 3 invitations
|
|
// Act: List invitations
|
|
// Assert: Returns 3 invitations with correct data
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CancelInvitation_AsOwner_ShouldMarkAsCanceled()
|
|
{
|
|
// Arrange: Create invitation
|
|
// Act: Cancel invitation
|
|
// Assert: Status = Canceled, token no longer works
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InviteUser_CrossTenant_ShouldFail()
|
|
{
|
|
// Arrange: Owner of Tenant A
|
|
// Act: Attempt to invite to Tenant B
|
|
// Assert: 403 Forbidden
|
|
}
|
|
}
|
|
```
|
|
|
|
### 11.3 Unblocking Skipped Tests
|
|
|
|
**Day 6 Skipped Tests** (from test report):
|
|
|
|
1. `RemoveUser_AsOwner_ShouldSucceed` - **UNBLOCKED by Invitation System**
|
|
```csharp
|
|
// Now testable:
|
|
// 1. Owner invites user
|
|
// 2. User accepts invitation
|
|
// 3. Owner removes user
|
|
// 4. Assert: User removed, tokens revoked
|
|
```
|
|
|
|
2. `RemoveUser_RevokesTokens_ShouldWork` - **UNBLOCKED by Invitation System**
|
|
```csharp
|
|
// Now testable:
|
|
// 1. Invite user, user accepts and logs in (gets refresh token)
|
|
// 2. Owner removes user
|
|
// 3. Assert: User's refresh tokens revoked
|
|
```
|
|
|
|
3. `RemoveUser_RequiresOwnerPolicy_ShouldBeEnforced` - **UNBLOCKED by Invitation System**
|
|
```csharp
|
|
// Now testable:
|
|
// 1. Invite user as Developer
|
|
// 2. Developer attempts to remove another user
|
|
// 3. Assert: 403 Forbidden
|
|
```
|
|
|
|
**Day 7 Test Deliverable**: Unskip these 3 tests and verify they pass.
|
|
|
|
### 11.4 Test Data Management
|
|
|
|
**Test Email Capture** (for integration tests):
|
|
|
|
```csharp
|
|
// Mock email service that captures emails instead of sending
|
|
public class MockEmailService : IEmailService
|
|
{
|
|
public List<EmailMessage> SentEmails { get; } = new();
|
|
|
|
public Task SendEmailAsync(EmailMessage message)
|
|
{
|
|
SentEmails.Add(message);
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
// Usage in tests
|
|
var emailService = _testServer.Services.GetRequiredService<IEmailService>() as MockEmailService;
|
|
var lastEmail = emailService.SentEmails.Last();
|
|
var verificationToken = ExtractTokenFromEmailBody(lastEmail.Body);
|
|
```
|
|
|
|
**Test Token Extraction**:
|
|
|
|
```csharp
|
|
private string ExtractTokenFromEmailBody(string emailBody)
|
|
{
|
|
// Extract token from URL in email body
|
|
var match = Regex.Match(emailBody, @"token=([a-zA-Z0-9_-]+)");
|
|
return match.Groups[1].Value;
|
|
}
|
|
```
|
|
|
|
### 11.5 Test Coverage Summary
|
|
|
|
| Feature | Unit Tests | Integration Tests | Total |
|
|
|---------|-----------|-------------------|-------|
|
|
| Email Service | 5 | 3 | 8 |
|
|
| Email Verification | 4 | 6 | 10 |
|
|
| Password Reset | 5 | 8 | 13 |
|
|
| User Invitation | 6 | 10 | 16 |
|
|
| Token Generation | 4 | - | 4 |
|
|
| **Total** | **24** | **27** | **51** |
|
|
|
|
**Day 7 Test Goal**: 51 new tests + 3 unskipped tests = **54 total new/updated tests**
|
|
|
|
---
|
|
|
|
## 12. Implementation Plan
|
|
|
|
### 12.1 Implementation Phases
|
|
|
|
#### Phase 1: Email Service Foundation (Day 7.1 - 4 hours)
|
|
|
|
**Priority**: P0 - Foundation for all other features
|
|
|
|
**Tasks**:
|
|
1. Create `IEmailService` interface
|
|
2. Implement `SendGridEmailService`
|
|
3. Implement `SmtpEmailService`
|
|
4. Implement `MockEmailService` (for tests)
|
|
5. Add email configuration to `appsettings.json`
|
|
6. Register services in DI container
|
|
7. Create `IEmailTemplateRenderer` interface
|
|
8. Implement simple template renderer (string interpolation)
|
|
9. Unit tests for email service
|
|
|
|
**Deliverables**:
|
|
- [ ] `IEmailService` interface
|
|
- [ ] 3 email service implementations
|
|
- [ ] Configuration setup
|
|
- [ ] Template rendering infrastructure
|
|
- [ ] 8 unit tests
|
|
|
|
**Dependencies**: None (foundation)
|
|
|
|
**Risk**: Low (well-defined requirements)
|
|
|
|
---
|
|
|
|
#### Phase 2: Email Templates (Day 7.2 - 2 hours)
|
|
|
|
**Priority**: P0 - Required for all email features
|
|
|
|
**Tasks**:
|
|
1. Create shared email layout (`_Layout.cshtml`)
|
|
2. Create `EmailVerification.cshtml` template
|
|
3. Create `PasswordReset.cshtml` template
|
|
4. Create `UserInvitation.cshtml` template
|
|
5. Create plain text fallbacks for all templates
|
|
6. Test template rendering with sample data
|
|
|
|
**Deliverables**:
|
|
- [ ] 4 HTML email templates
|
|
- [ ] 4 plain text templates
|
|
- [ ] Template preview tool (optional)
|
|
|
|
**Dependencies**: Phase 1 (template renderer)
|
|
|
|
**Risk**: Low (static content)
|
|
|
|
---
|
|
|
|
#### Phase 3: Email Verification (Day 7.3 - 6 hours)
|
|
|
|
**Priority**: P0 - Security requirement
|
|
|
|
**Tasks**:
|
|
1. Create `EmailVerificationToken` entity
|
|
2. Create `IEmailVerificationTokenRepository` interface + implementation
|
|
3. Create EF Core migration for `email_verification_tokens` table
|
|
4. Implement token generation and hashing logic
|
|
5. Update `RegisterTenantCommandHandler` to send verification email
|
|
6. Create `VerifyEmailCommand` and handler
|
|
7. Create `ResendVerificationEmailCommand` and handler
|
|
8. Add rate limiting for resend endpoint
|
|
9. Create API endpoints in `AuthController`
|
|
10. Integration tests (6 tests)
|
|
|
|
**Deliverables**:
|
|
- [ ] `EmailVerificationToken` entity and repository
|
|
- [ ] Database migration
|
|
- [ ] 2 commands + handlers
|
|
- [ ] 2 API endpoints
|
|
- [ ] 10 integration tests
|
|
|
|
**Dependencies**: Phase 1, Phase 2
|
|
|
|
**Risk**: Medium (complex flow, security critical)
|
|
|
|
---
|
|
|
|
#### Phase 4: Password Reset (Day 7.4 - 6 hours)
|
|
|
|
**Priority**: P0 - Critical user experience
|
|
|
|
**Tasks**:
|
|
1. Create `PasswordResetToken` entity
|
|
2. Create `IPasswordResetTokenRepository` interface + implementation
|
|
3. Create EF Core migration for `password_reset_tokens` table
|
|
4. Implement token generation and hashing logic
|
|
5. Create `ForgotPasswordCommand` and handler
|
|
6. Create `ResetPasswordCommand` and handler
|
|
7. Implement password complexity validation
|
|
8. Add refresh token revocation on password reset
|
|
9. Add rate limiting for forgot password endpoint
|
|
10. Create API endpoints in `AuthController`
|
|
11. Integration tests (8 tests)
|
|
|
|
**Deliverables**:
|
|
- [ ] `PasswordResetToken` entity and repository
|
|
- [ ] Database migration
|
|
- [ ] 2 commands + handlers
|
|
- [ ] 2 API endpoints
|
|
- [ ] Password validator
|
|
- [ ] 13 integration tests
|
|
|
|
**Dependencies**: Phase 1, Phase 2
|
|
|
|
**Risk**: Medium (security critical, token revocation)
|
|
|
|
---
|
|
|
|
#### Phase 5: User Invitation System (Day 7.5 - 8 hours)
|
|
|
|
**Priority**: P0 - Unblocks multi-user testing
|
|
|
|
**Tasks**:
|
|
1. Create `Invitation` entity with status enum
|
|
2. Create `IInvitationRepository` interface + implementation
|
|
3. Create EF Core migration for `invitations` table
|
|
4. Create `InviteUserCommand` and handler
|
|
5. Create `ListInvitationsQuery` and handler
|
|
6. Create `AcceptInvitationCommand` and handler (creates user + assigns role)
|
|
7. Create `CancelInvitationCommand` and handler
|
|
8. Implement role validation (cannot invite as TenantOwner/AIAgent)
|
|
9. Add duplicate invitation prevention
|
|
10. Create API endpoints in `TenantUsersController` (invite, list, cancel)
|
|
11. Create public endpoint in `AuthController` (accept)
|
|
12. Integration tests (10 tests)
|
|
|
|
**Deliverables**:
|
|
- [ ] `Invitation` entity and repository
|
|
- [ ] Database migration
|
|
- [ ] 4 commands + handlers
|
|
- [ ] 4 API endpoints
|
|
- [ ] 16 integration tests
|
|
|
|
**Dependencies**: Phase 1, Phase 2, Phase 3 (user creation logic)
|
|
|
|
**Risk**: High (complex flow, multi-tenant concerns, role assignment)
|
|
|
|
---
|
|
|
|
#### Phase 6: Unskip Day 6 Tests (Day 7.6 - 2 hours)
|
|
|
|
**Priority**: P0 - Test coverage requirement
|
|
|
|
**Tasks**:
|
|
1. Unskip `RemoveUser_AsOwner_ShouldSucceed`
|
|
2. Unskip `RemoveUser_RevokesTokens_ShouldWork`
|
|
3. Unskip `RemoveUser_RequiresOwnerPolicy_ShouldBeEnforced`
|
|
4. Update tests to use invitation flow for creating second user
|
|
5. Verify all 3 tests pass
|
|
|
|
**Deliverables**:
|
|
- [ ] 3 previously skipped tests now passing
|
|
- [ ] Test report updated
|
|
|
|
**Dependencies**: Phase 5 (invitation system)
|
|
|
|
**Risk**: Low (tests already written, just need invitation infrastructure)
|
|
|
|
---
|
|
|
|
#### Phase 7: Security Hardening & Documentation (Day 7.7 - 2 hours)
|
|
|
|
**Priority**: P1 - Production readiness
|
|
|
|
**Tasks**:
|
|
1. Add comprehensive audit logging for all security events
|
|
2. Verify HTTPS enforcement in configuration
|
|
3. Add rate limiting middleware configuration
|
|
4. Security review of all endpoints
|
|
5. Update API documentation (Swagger/OpenAPI)
|
|
6. Write Day 7 implementation summary document
|
|
7. Update project README with new features
|
|
|
|
**Deliverables**:
|
|
- [ ] Audit logging for all email/auth events
|
|
- [ ] Rate limiting configuration
|
|
- [ ] Security audit checklist
|
|
- [ ] Updated API documentation
|
|
- [ ] Day 7 implementation summary
|
|
|
|
**Dependencies**: All previous phases
|
|
|
|
**Risk**: Low (documentation and configuration)
|
|
|
|
---
|
|
|
|
### 12.2 Implementation Schedule
|
|
|
|
**Total Estimated Time**: 30 hours (3.75 developer days)
|
|
|
|
| Phase | Duration | Start | End | Developer |
|
|
|-------|----------|-------|-----|-----------|
|
|
| Phase 1: Email Service | 4 hours | Day 7.0h | Day 7.4h | Backend |
|
|
| Phase 2: Email Templates | 2 hours | Day 7.4h | Day 7.6h | Backend |
|
|
| Phase 3: Email Verification | 6 hours | Day 7.6h | Day 7.12h | Backend |
|
|
| Phase 4: Password Reset | 6 hours | Day 7.12h | Day 7.18h | Backend |
|
|
| Phase 5: User Invitation | 8 hours | Day 7.18h | Day 7.26h | Backend |
|
|
| Phase 6: Unskip Tests | 2 hours | Day 7.26h | Day 7.28h | QA/Backend |
|
|
| Phase 7: Security & Docs | 2 hours | Day 7.28h | Day 7.30h | Backend/PM |
|
|
|
|
**Recommended Schedule**: 4 working days (7.5 hours/day) with buffer for unexpected issues
|
|
|
|
**Parallel Work Opportunities**:
|
|
- Phases 3 and 4 can be developed in parallel (different developers)
|
|
- Phase 2 (templates) can be done by frontend developer or designer
|
|
|
|
---
|
|
|
|
### 12.3 Implementation Dependencies
|
|
|
|
```
|
|
Phase 1 (Email Service)
|
|
↓
|
|
Phase 2 (Email Templates)
|
|
↓
|
|
┌─────────────┬─────────────┐
|
|
│ │ │
|
|
Phase 3 Phase 4
|
|
(Email (Password
|
|
Verification) Reset)
|
|
│ │ │
|
|
└─────────────┴─────────────┘
|
|
↓
|
|
Phase 5 (User Invitation)
|
|
↓
|
|
Phase 6 (Unskip Tests)
|
|
↓
|
|
Phase 7 (Security & Docs)
|
|
```
|
|
|
|
**Critical Path**: Phase 1 → Phase 2 → Phase 5 → Phase 6 (required for test unblocking)
|
|
|
|
**Optional Path**: Phase 3, Phase 4 (can be deferred to Day 8 if time-constrained, but not recommended)
|
|
|
|
---
|
|
|
|
### 12.4 Definition of Done
|
|
|
|
Each phase is considered complete when:
|
|
|
|
**Phase Completion Criteria**:
|
|
- [ ] All code written and reviewed
|
|
- [ ] Unit tests written and passing (if applicable)
|
|
- [ ] Integration tests written and passing
|
|
- [ ] Code coverage ≥90% for business logic
|
|
- [ ] API documentation updated
|
|
- [ ] Security review completed
|
|
- [ ] No compiler warnings or errors
|
|
- [ ] Database migrations tested (up and down)
|
|
- [ ] Manual testing completed (happy path + error cases)
|
|
|
|
**Day 7 Completion Criteria**:
|
|
- [ ] All 4 features implemented and tested
|
|
- [ ] 51 new tests written and passing
|
|
- [ ] 3 skipped tests from Day 6 now passing
|
|
- [ ] Total test count: 97 tests (46 from Days 4-6 + 51 new)
|
|
- [ ] Email delivery working in development (verified manually)
|
|
- [ ] Security audit checklist 100% complete
|
|
- [ ] Day 7 implementation summary document published
|
|
- [ ] Demo prepared for stakeholder review
|
|
|
|
---
|
|
|
|
## 13. Risk Assessment
|
|
|
|
### 13.1 Technical Risks
|
|
|
|
#### RISK-001: Email Delivery Failures
|
|
**Severity**: HIGH
|
|
**Probability**: MEDIUM
|
|
**Impact**: Users cannot verify emails or reset passwords
|
|
|
|
**Mitigation**:
|
|
1. Use reliable email provider (SendGrid with 99.9% SLA)
|
|
2. Implement retry logic with exponential backoff
|
|
3. Circuit breaker pattern to prevent cascading failures
|
|
4. Fallback to SMTP if SendGrid fails (future enhancement)
|
|
5. Email delivery monitoring and alerting (future: Sentry integration)
|
|
6. Non-blocking email sends (user action succeeds even if email fails)
|
|
|
|
**Rollback Plan**:
|
|
- If email service completely fails, disable email features temporarily
|
|
- Users can still register/login (email verification is optional for Day 7)
|
|
- Manual password reset via admin console (future feature)
|
|
|
|
**Owner**: Backend Team
|
|
|
|
---
|
|
|
|
#### RISK-002: Token Security Vulnerabilities
|
|
**Severity**: CRITICAL
|
|
**Probability**: LOW
|
|
**Impact**: Account takeover, unauthorized access
|
|
|
|
**Mitigation**:
|
|
1. Use cryptographically secure random number generator
|
|
2. Store token hashes (SHA-256), never plaintext
|
|
3. Short token expiration (1h for password reset, 24h for verification)
|
|
4. Token reuse prevention (mark as used)
|
|
5. Rate limiting on token-based endpoints
|
|
6. HTTPS enforcement (no tokens over HTTP)
|
|
7. Security audit of token generation and validation logic
|
|
|
|
**Detection**:
|
|
- Audit logs for suspicious token activity
|
|
- Monitor failed verification attempts
|
|
- Alert on high volume of token generation from single IP
|
|
|
|
**Rollback Plan**:
|
|
- If vulnerability discovered, immediately revoke all outstanding tokens
|
|
- Notify affected users via email (if email service is working)
|
|
- Force password reset for all users (last resort)
|
|
|
|
**Owner**: Security Team / Backend Team
|
|
|
|
---
|
|
|
|
#### RISK-003: Database Migration Failures
|
|
**Severity**: MEDIUM
|
|
**Probability**: LOW
|
|
**Impact**: Deployment blocked, downtime
|
|
|
|
**Mitigation**:
|
|
1. Test migrations in staging environment first
|
|
2. Create rollback migrations for all schema changes
|
|
3. Use EF Core migration idempotency (can run multiple times safely)
|
|
4. Backup database before migration in production
|
|
5. Blue-green deployment strategy (future)
|
|
|
|
**Rollback Plan**:
|
|
```bash
|
|
# If migration fails, rollback:
|
|
dotnet ef database update <PreviousMigrationName>
|
|
```
|
|
|
|
**Owner**: DevOps Team / Backend Team
|
|
|
|
---
|
|
|
|
#### RISK-004: Rate Limiting Bypass
|
|
**Severity**: MEDIUM
|
|
**Probability**: MEDIUM
|
|
**Impact**: Email spam, DoS attacks, abuse
|
|
|
|
**Mitigation**:
|
|
1. Implement rate limiting at multiple layers:
|
|
- Application level (in-memory cache)
|
|
- API Gateway level (future: Azure API Management)
|
|
- Email provider level (SendGrid rate limits)
|
|
2. Use IP-based and email-based rate limiting
|
|
3. CAPTCHA for public endpoints (future enhancement)
|
|
4. Monitor for suspicious activity patterns
|
|
|
|
**Detection**:
|
|
- Logs showing rate limit exceeded
|
|
- Metrics on email send volume per tenant
|
|
- Alert on anomalies (e.g., 100 invitations in 1 minute)
|
|
|
|
**Rollback Plan**:
|
|
- Temporarily increase rate limits if legitimate traffic is blocked
|
|
- Ban abusive IP addresses at firewall level
|
|
- Disable user invitation endpoint if under attack (preserve other features)
|
|
|
|
**Owner**: Backend Team / Security Team
|
|
|
|
---
|
|
|
|
### 13.2 Business Risks
|
|
|
|
#### RISK-005: Email Deliverability Issues
|
|
**Severity**: HIGH
|
|
**Probability**: MEDIUM
|
|
**Impact**: Low email verification rate, user frustration, support burden
|
|
|
|
**Causes**:
|
|
- Emails marked as spam by email providers
|
|
- SPF/DKIM/DMARC not configured properly
|
|
- Email content triggers spam filters
|
|
- Disposable email addresses (not supported)
|
|
|
|
**Mitigation**:
|
|
1. Configure SPF, DKIM, DMARC records for domain
|
|
2. Use reputable email provider (SendGrid with good reputation)
|
|
3. Test emails with major providers (Gmail, Outlook, Yahoo)
|
|
4. Clear, non-promotional email content
|
|
5. "Add to contacts" instructions in emails
|
|
6. Monitor email bounce rates and spam reports
|
|
|
|
**Metrics to Monitor**:
|
|
- Email delivery rate (target: >99%)
|
|
- Email open rate (target: >60%)
|
|
- Spam complaint rate (target: <0.1%)
|
|
- Bounce rate (target: <5%)
|
|
|
|
**Contingency**:
|
|
- If deliverability drops, switch to alternative email provider
|
|
- Provide manual verification option via support ticket
|
|
- Add "didn't receive email?" troubleshooting guide
|
|
|
|
**Owner**: Product Manager / DevOps Team
|
|
|
|
---
|
|
|
|
#### RISK-006: User Adoption of Email Verification
|
|
**Severity**: MEDIUM
|
|
**Probability**: MEDIUM
|
|
**Impact**: Many unverified users, potential data quality issues
|
|
|
|
**Causes**:
|
|
- Users forget to verify
|
|
- Verification email goes to spam
|
|
- Poor UX around verification flow
|
|
|
|
**Mitigation**:
|
|
1. Send verification email immediately on registration (no delay)
|
|
2. Show prominent banner in app: "Please verify your email"
|
|
3. Resend verification option easily accessible
|
|
4. Clear email subject line: "Verify your email - ColaFlow"
|
|
5. Reminder email after 24 hours (future enhancement)
|
|
6. Block critical features until verified (future: user invitation requires verification)
|
|
|
|
**Metrics to Monitor**:
|
|
- Email verification rate (target: >85% within 48h)
|
|
- Time to verification (target: <1 hour median)
|
|
- Resend requests per user (target: <0.5 average)
|
|
|
|
**Contingency**:
|
|
- If verification rate <70%, investigate email deliverability
|
|
- A/B test different email templates
|
|
- Consider SMS verification as alternative (future)
|
|
|
|
**Owner**: Product Manager / UX Team
|
|
|
|
---
|
|
|
|
#### RISK-007: Invitation Spam and Abuse
|
|
**Severity**: MEDIUM
|
|
**Probability**: LOW
|
|
**Impact**: Brand reputation damage, email blacklisting
|
|
|
|
**Scenarios**:
|
|
- Malicious user invites random emails to spam them
|
|
- Competitor invites our customers to confuse them
|
|
- Automated bot creates accounts and sends mass invitations
|
|
|
|
**Mitigation**:
|
|
1. Rate limiting: Max 20 invitations per tenant per hour
|
|
2. Require email verification before user can send invitations (future)
|
|
3. Monitor invitation acceptance rate per tenant (low rate = potential spam)
|
|
4. "Report spam" link in invitation emails
|
|
5. CAPTCHA on invitation endpoint (if abuse detected)
|
|
6. Tenant suspension for repeated abuse
|
|
|
|
**Detection**:
|
|
- Invitation acceptance rate <10% (flag for review)
|
|
- High volume of invitations from new tenant
|
|
- Spam reports from recipients
|
|
|
|
**Response**:
|
|
- Suspend tenant pending investigation
|
|
- Invalidate all pending invitations from tenant
|
|
- Notify sender that abuse was detected
|
|
- Require additional verification to reactivate
|
|
|
|
**Owner**: Product Manager / Security Team
|
|
|
|
---
|
|
|
|
### 13.3 Operational Risks
|
|
|
|
#### RISK-008: Email Service Outage (SendGrid Down)
|
|
**Severity**: MEDIUM
|
|
**Probability**: LOW
|
|
**Impact**: No emails sent, users cannot verify or reset passwords
|
|
|
|
**Mitigation**:
|
|
1. Use SendGrid (99.9% uptime SLA)
|
|
2. Implement fallback to SMTP (future enhancement)
|
|
3. Queue emails for retry if service unavailable
|
|
4. Non-blocking email sends (user actions still succeed)
|
|
5. Status page to inform users of email service issues
|
|
|
|
**Detection**:
|
|
- Monitor SendGrid API health endpoint
|
|
- Alert on consecutive email send failures
|
|
- Track email send success rate metric
|
|
|
|
**Response Plan**:
|
|
1. Confirm SendGrid status page for outage
|
|
2. Enable SMTP fallback (if implemented)
|
|
3. Update status page with incident details
|
|
4. Communicate to users: "Email service temporarily delayed"
|
|
5. Queue failed emails for retry when service recovers
|
|
|
|
**Recovery**:
|
|
- Retry all queued emails when service recovers
|
|
- Verify email delivery success rate returns to normal
|
|
- Post-incident review to prevent recurrence
|
|
|
|
**Owner**: DevOps Team
|
|
|
|
---
|
|
|
|
#### RISK-009: High Email Costs
|
|
**Severity**: LOW
|
|
**Probability**: MEDIUM
|
|
**Impact**: Unexpected infrastructure costs
|
|
|
|
**Scenario**: High user growth leads to email volume exceeding free tier (100 emails/day)
|
|
|
|
**Mitigation**:
|
|
1. Start with SendGrid free tier (100 emails/day)
|
|
2. Monitor daily email volume
|
|
3. Set billing alerts at 80% of free tier
|
|
4. Plan upgrade to paid tier when approaching limit
|
|
5. Optimize email frequency (avoid unnecessary emails)
|
|
|
|
**Cost Projections**:
|
|
- Free tier: 100 emails/day = 3000/month (sufficient for 500 active users)
|
|
- Essentials plan: $19.95/month for 50,000 emails (sufficient for 8,000 users)
|
|
- Pro plan: $89.95/month for 100,000 emails
|
|
|
|
**Contingency**:
|
|
- If costs exceed budget, reduce email frequency (e.g., no reminder emails)
|
|
- Negotiate volume pricing with SendGrid
|
|
- Switch to self-hosted SMTP (trade-off: lower deliverability)
|
|
|
|
**Owner**: Product Manager / Finance Team
|
|
|
|
---
|
|
|
|
### 13.4 Risk Summary Matrix
|
|
|
|
| Risk ID | Risk | Severity | Probability | Mitigation Status | Owner |
|
|
|---------|------|----------|-------------|-------------------|-------|
|
|
| RISK-001 | Email Delivery Failures | HIGH | MEDIUM | ✅ Mitigated | Backend Team |
|
|
| RISK-002 | Token Security Vulnerabilities | CRITICAL | LOW | ✅ Mitigated | Security Team |
|
|
| RISK-003 | Database Migration Failures | MEDIUM | LOW | ✅ Mitigated | DevOps Team |
|
|
| RISK-004 | Rate Limiting Bypass | MEDIUM | MEDIUM | ✅ Mitigated | Backend Team |
|
|
| RISK-005 | Email Deliverability Issues | HIGH | MEDIUM | ⚠️ Monitor | Product Manager |
|
|
| RISK-006 | Low Email Verification Rate | MEDIUM | MEDIUM | ⚠️ Monitor | Product Manager |
|
|
| RISK-007 | Invitation Spam/Abuse | MEDIUM | LOW | ✅ Mitigated | Security Team |
|
|
| RISK-008 | Email Service Outage | MEDIUM | LOW | ⚠️ Partial | DevOps Team |
|
|
| RISK-009 | High Email Costs | LOW | MEDIUM | ✅ Mitigated | Product Manager |
|
|
|
|
**Legend**:
|
|
- ✅ Mitigated: Controls in place, low residual risk
|
|
- ⚠️ Monitor: Requires ongoing monitoring and metrics
|
|
- ❌ Open: Mitigation needed (none for Day 7)
|
|
|
|
---
|
|
|
|
## 14. Success Criteria
|
|
|
|
### 14.1 Functional Success Criteria
|
|
|
|
#### ✅ Email Service Integration
|
|
- [ ] SendGrid email service sends emails successfully in production
|
|
- [ ] SMTP email service sends emails successfully in development
|
|
- [ ] Mock email service captures emails in integration tests
|
|
- [ ] Email templates render correctly with dynamic data
|
|
- [ ] Configuration supports multiple providers without code changes
|
|
|
|
#### ✅ Email Verification Flow
|
|
- [ ] New users receive verification email within 5 seconds of registration
|
|
- [ ] Verification link successfully verifies email and updates database
|
|
- [ ] Expired tokens return appropriate error message
|
|
- [ ] Resend verification email generates new token and invalidates old one
|
|
- [ ] Rate limiting prevents more than 3 resend requests per hour
|
|
- [ ] Verified users show `isEmailVerified: true` in API responses
|
|
|
|
#### ✅ Password Reset Flow
|
|
- [ ] Forgot password request sends reset email (without revealing email existence)
|
|
- [ ] Reset password link successfully updates password
|
|
- [ ] All refresh tokens revoked after password reset
|
|
- [ ] Used reset tokens cannot be reused
|
|
- [ ] Rate limiting prevents more than 3 reset requests per email per hour
|
|
- [ ] Password complexity requirements enforced (8+ chars, uppercase, lowercase, number, special char)
|
|
|
|
#### ✅ User Invitation System
|
|
- [ ] Tenant owners can invite users with valid email and role
|
|
- [ ] Invitations cannot be created for TenantOwner or AIAgent roles
|
|
- [ ] Invitation emails sent immediately with correct tenant and role information
|
|
- [ ] Invited users can accept invitations and create accounts
|
|
- [ ] Accepted invitations automatically assign specified role
|
|
- [ ] Invitation acceptance marks email as verified
|
|
- [ ] Tenant owners can list all invitations with status filtering
|
|
- [ ] Tenant owners can cancel pending invitations
|
|
- [ ] Cross-tenant invitation access returns 403 Forbidden
|
|
|
|
### 14.2 Technical Success Criteria
|
|
|
|
#### ✅ Testing Coverage
|
|
- [ ] 51 new tests written and passing (24 unit + 27 integration)
|
|
- [ ] 3 previously skipped tests from Day 6 now passing
|
|
- [ ] Total test suite: 97 tests with 0 failures
|
|
- [ ] Test coverage ≥90% for new business logic
|
|
- [ ] All tests run in <30 seconds
|
|
|
|
#### ✅ Security Requirements
|
|
- [ ] All tokens stored as SHA-256 hashes (not plaintext)
|
|
- [ ] Tokens are cryptographically secure (256-bit random)
|
|
- [ ] HTTPS enforcement verified in production configuration
|
|
- [ ] Rate limiting active on all public endpoints
|
|
- [ ] Email enumeration prevention verified (forgot password, resend verification)
|
|
- [ ] Cross-tenant security validated on all invitation endpoints
|
|
- [ ] Audit logs capture all security events with IP and timestamp
|
|
|
|
#### ✅ Database Integrity
|
|
- [ ] 3 new tables created with correct schema: `email_verification_tokens`, `password_reset_tokens`, `invitations`
|
|
- [ ] All indexes and constraints created successfully
|
|
- [ ] Database migration runs successfully (up and down)
|
|
- [ ] No data loss in rollback scenario
|
|
- [ ] Foreign key relationships enforce referential integrity
|
|
|
|
#### ✅ Performance Requirements
|
|
- [ ] Email send latency <2 seconds (p95)
|
|
- [ ] Email template rendering <100ms (p95)
|
|
- [ ] API response time <200ms for all endpoints (p95)
|
|
- [ ] Database queries optimized with indexes (no full table scans)
|
|
- [ ] Test suite execution time <30 seconds
|
|
|
|
### 14.3 User Experience Success Criteria
|
|
|
|
#### ✅ Email Quality
|
|
- [ ] All emails render correctly in Gmail, Outlook, Apple Mail
|
|
- [ ] Email subject lines are clear and actionable
|
|
- [ ] Primary CTA (button) is prominently displayed
|
|
- [ ] Emails are mobile-responsive (tested on iPhone and Android)
|
|
- [ ] Plain text fallback provided for all HTML emails
|
|
- [ ] Email branding consistent with ColaFlow design guidelines
|
|
|
|
#### ✅ Error Handling
|
|
- [ ] All error messages are user-friendly and actionable
|
|
- [ ] Invalid tokens show clear expiration and resend instructions
|
|
- [ ] Rate limit errors include retry-after information
|
|
- [ ] Cross-tenant access errors don't leak sensitive information
|
|
- [ ] 500 errors are logged but show generic message to users
|
|
|
|
#### ✅ API Documentation
|
|
- [ ] All new endpoints documented in Swagger/OpenAPI
|
|
- [ ] Request/response examples provided
|
|
- [ ] Error codes and messages documented
|
|
- [ ] Authentication requirements clearly specified
|
|
- [ ] Rate limiting policies documented
|
|
|
|
### 14.4 Business Success Criteria
|
|
|
|
#### ✅ User Adoption Metrics (Track post-deployment)
|
|
- **Email Verification Rate**: ≥85% of users verify email within 48 hours
|
|
- **Invitation Acceptance Rate**: ≥70% of invitations accepted within 7 days
|
|
- **Password Reset Success Rate**: ≥90% of reset attempts succeed
|
|
- **Email Delivery Rate**: ≥99% of emails delivered successfully
|
|
|
|
#### ✅ Support Impact Metrics (Track post-deployment)
|
|
- **Password Reset Support Tickets**: Reduced by ≥50% compared to manual process
|
|
- **Email Verification Support Tickets**: <5% of new users require support
|
|
- **Invitation Issues**: <2% of invitations result in support tickets
|
|
|
|
#### ✅ System Health Metrics (Monitor continuously)
|
|
- **Email Service Uptime**: ≥99.5% (SendGrid SLA: 99.9%)
|
|
- **API Uptime**: ≥99.9% for all email/auth endpoints
|
|
- **Email Send Failure Rate**: <1% of total emails
|
|
- **Rate Limit Hit Rate**: <5% of requests (indicates abuse or legitimate high usage)
|
|
|
|
### 14.5 Documentation Success Criteria
|
|
|
|
#### ✅ Technical Documentation
|
|
- [ ] Day 7 implementation summary document published
|
|
- [ ] API endpoint documentation complete (Swagger)
|
|
- [ ] Database schema changes documented
|
|
- [ ] Security audit report completed
|
|
- [ ] Email template customization guide created
|
|
|
|
#### ✅ User Documentation
|
|
- [ ] Email verification troubleshooting guide
|
|
- [ ] Password reset user guide
|
|
- [ ] User invitation admin guide
|
|
- [ ] FAQ section updated with common issues
|
|
|
|
### 14.6 Deployment Readiness Checklist
|
|
|
|
#### ✅ Pre-Deployment
|
|
- [ ] All tests passing in CI/CD pipeline
|
|
- [ ] Code review completed and approved
|
|
- [ ] Security review completed
|
|
- [ ] Database migration tested in staging
|
|
- [ ] Email deliverability tested with real accounts (Gmail, Outlook)
|
|
- [ ] SendGrid API key configured in production Key Vault
|
|
- [ ] SPF/DKIM/DMARC DNS records configured
|
|
- [ ] Rollback plan documented and tested
|
|
|
|
#### ✅ Deployment
|
|
- [ ] Database migration executed successfully
|
|
- [ ] Application deployed without errors
|
|
- [ ] Health check endpoints return 200 OK
|
|
- [ ] Smoke tests pass (register, verify, reset, invite)
|
|
- [ ] Monitoring dashboards show normal metrics
|
|
|
|
#### ✅ Post-Deployment
|
|
- [ ] Verify email delivery in production (send test emails)
|
|
- [ ] Monitor error logs for first 24 hours
|
|
- [ ] Check email delivery rates in SendGrid dashboard
|
|
- [ ] Verify rate limiting is working (test with high volume)
|
|
- [ ] User acceptance testing with internal team
|
|
- [ ] Stakeholder demo completed
|
|
|
|
---
|
|
|
|
## 15. Appendix
|
|
|
|
### 15.1 Glossary
|
|
|
|
| Term | Definition |
|
|
|------|------------|
|
|
| **Email Verification** | Process of confirming a user owns the email address they registered with |
|
|
| **Password Reset Token** | Time-limited, single-use token sent via email to reset forgotten password |
|
|
| **Invitation Token** | Token embedded in invitation email allowing user to accept and join tenant |
|
|
| **Token Hash** | SHA-256 hash of token stored in database (not plaintext token) |
|
|
| **Rate Limiting** | Restricting number of requests per time period to prevent abuse |
|
|
| **Email Enumeration** | Security vulnerability where attacker can determine if email exists in system |
|
|
| **Transactional Email** | Automated emails triggered by user actions (not marketing emails) |
|
|
| **SendGrid** | Cloud-based email delivery service (SMTP alternative) |
|
|
| **Base64URL** | URL-safe encoding of binary data (no +, /, = characters) |
|
|
| **Idempotent** | Operation that produces same result if executed multiple times |
|
|
|
|
### 15.2 Related Documents
|
|
|
|
- [Day 6 Test Report](DAY6-TEST-REPORT.md) - Details on skipped tests
|
|
- [Product Plan](../product.md) - Overall ColaFlow project vision
|
|
- [M1 Sprint 2 Roadmap](M1-SPRINT-2-ROADMAP.md) - Days 4-10 plan
|
|
- [Security Architecture](SECURITY-ARCHITECTURE.md) - Overall security design
|
|
- [API Documentation](../docs/API.md) - Complete API reference
|
|
|
|
### 15.3 References
|
|
|
|
**Email Best Practices**:
|
|
- [SendGrid Email Best Practices](https://docs.sendgrid.com/ui/sending-email/email-best-practices)
|
|
- [OWASP Email Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Email_Security_Cheat_Sheet.html)
|
|
- [Gmail Sender Guidelines](https://support.google.com/mail/answer/81126)
|
|
|
|
**Security Standards**:
|
|
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
|
|
- [OWASP Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html)
|
|
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
|
|
|
**Technology Documentation**:
|
|
- [SendGrid .NET SDK](https://github.com/sendgrid/sendgrid-csharp)
|
|
- [ASP.NET Core Rate Limiting](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit)
|
|
- [Entity Framework Core Migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/)
|
|
|
|
### 15.4 Change Log
|
|
|
|
| Version | Date | Author | Changes |
|
|
|---------|------|--------|---------|
|
|
| 1.0 | 2025-11-03 | Product Manager Agent | Initial PRD for Day 7 |
|
|
|
|
---
|
|
|
|
**End of Document**
|
|
|
|
**Total Pages**: 45+
|
|
**Word Count**: ~15,000 words
|
|
**Estimated Reading Time**: 60 minutes
|
|
|
|
**Next Steps**:
|
|
1. Review and approve this PRD with stakeholders
|
|
2. Assign implementation to backend team
|
|
3. Schedule kickoff meeting for Day 7 sprint
|
|
4. Begin Phase 1: Email Service Foundation
|
|
|
|
**Questions or Feedback?**
|
|
Contact: Product Manager Agent via ColaFlow coordination channel
|