feat: add Participant and MagicToken models with automatic migrations
Implements Phase 2 infrastructure for participant registration and authentication: Database Models: - Add Participant model with exchange scoping and soft deletes - Add MagicToken model for passwordless authentication - Add participants relationship to Exchange model - Include proper indexes and foreign key constraints Migration Infrastructure: - Generate Alembic migration for new models - Create entrypoint.sh script for automatic migrations on container startup - Update Containerfile to use entrypoint script and include uv binary - Remove db.create_all() in favor of migration-based schema management This establishes the foundation for implementing stories 4.1-4.3, 5.1-5.3, and 10.1. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
353
docs/decisions/0004-dev-mode-email-logging.md
Normal file
353
docs/decisions/0004-dev-mode-email-logging.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# 0004. Development Mode Email Logging
|
||||
|
||||
Date: 2025-12-22
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
When testing Sneaky Klaus in development and QA environments without email access, participants cannot receive magic links via email. This blocks QA teams from testing the authentication flow and participant workflows.
|
||||
|
||||
### Problem Statement
|
||||
|
||||
1. **No Email Service in Dev/QA**: Development environments often don't have access to Resend or external email services
|
||||
2. **Testing Blocker**: Without magic links, QA cannot test participant registration, authentication, and session flows
|
||||
3. **Local Development**: Developers need a way to obtain magic links when working locally without email infrastructure
|
||||
4. **Security Requirement**: This feature MUST be completely disabled in production
|
||||
|
||||
### Requirements
|
||||
|
||||
1. **Development Only**: Feature must only activate when `FLASK_ENV=development`
|
||||
2. **Logging**: Full magic link URLs logged to application logs for retrieval
|
||||
3. **Accessibility**: QA can retrieve links from container logs using standard tools
|
||||
4. **No Production Exposure**: Absolutely cannot be enabled in production
|
||||
5. **Clear Documentation**: Must warn developers about security implications
|
||||
|
||||
## Decision
|
||||
|
||||
We will implement **development mode email logging** where:
|
||||
|
||||
1. When `FLASK_ENV=development`, magic link generation logs the complete URL to application logs
|
||||
2. The full magic link URL (including token) is logged at INFO level with a clear DEV MODE prefix
|
||||
3. QA retrieves links from container logs using `podman logs` or equivalent tools
|
||||
4. A runtime check prevents this feature from ever being enabled in production, even if the code exists
|
||||
5. Email is still sent (when available), but we also provide the logging fallback
|
||||
|
||||
### Rationale
|
||||
|
||||
**Why Log Instead of Display in UI**:
|
||||
- Magic links contain 32 bytes of cryptographic randomness - not easily displayed in a UX-friendly way
|
||||
- Logging to application logs is a standard development practice
|
||||
- Container logs are easily accessible in QA environments
|
||||
- No code changes needed for QA to access links
|
||||
|
||||
**Why Log to Application Logs**:
|
||||
- Standard location for operational information
|
||||
- Automatically captured by container logging systems
|
||||
- Can be retrieved with `podman logs` without code changes
|
||||
- Works across all deployment scenarios (local, Docker, Podman, etc.)
|
||||
|
||||
**Why Require FLASK_ENV=development**:
|
||||
- Environment-based gates are best practice for dev-only features
|
||||
- Clear, explicit control
|
||||
- Cannot be accidentally enabled in production
|
||||
- Aligns with Flask conventions
|
||||
|
||||
**Why Still Send Email**:
|
||||
- When email IS available, participants get the real experience
|
||||
- Logging is a fallback for testing without email
|
||||
- Both paths work; QA can test with or without actual email service
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Unblocks QA**: Testing can proceed without email infrastructure
|
||||
- **Developer Friendly**: Developers can work locally without email setup
|
||||
- **Simple Implementation**: Minimal code changes required
|
||||
- **Standard Practice**: Logging is a standard development pattern
|
||||
- **Production Safe**: Environment-based control prevents any production exposure
|
||||
- **No Breaking Changes**: Existing email workflow unchanged
|
||||
|
||||
### Negative
|
||||
|
||||
- **Requires Awareness**: QA/developers must know to check logs for links
|
||||
- **Log Noise**: Application logs include URLs (only in development)
|
||||
- **Copy-Paste**: Manual link copying from logs is less convenient than email
|
||||
|
||||
### Neutral
|
||||
|
||||
- **No UI Changes**: No changes to participant-facing experience
|
||||
- **Email Dependency**: If email IS working, participants still get email
|
||||
- **Additional State**: No database or session changes needed
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Magic Link Logging
|
||||
|
||||
When a magic link is generated in development mode, the full URL is logged:
|
||||
|
||||
```
|
||||
[INFO] DEV MODE: Magic link generated for participant user@example.com
|
||||
[INFO] DEV MODE: Full magic link URL: https://sneaky-klaus.local/auth/participant/magic/k7Jx9mP2qR4sT6vN8bC1dE3fG5hI0jK7lM9nO2pQ4rS6tU8vW1xY3zA5
|
||||
```
|
||||
|
||||
### Environment Check
|
||||
|
||||
The logging is guarded by environment check:
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
def should_log_dev_mode_links() -> bool:
|
||||
"""
|
||||
Check if development mode email logging is enabled.
|
||||
|
||||
CRITICAL: This must NEVER be True in production.
|
||||
Only enable when explicitly running with FLASK_ENV=development.
|
||||
"""
|
||||
env = os.environ.get('FLASK_ENV', '').lower()
|
||||
return env == 'development'
|
||||
```
|
||||
|
||||
### Where Links Are Logged
|
||||
|
||||
Magic link URLs are logged during generation in the notification service:
|
||||
|
||||
```python
|
||||
def send_registration_confirmation(participant_id: int, token: str):
|
||||
"""Send registration confirmation email with magic link."""
|
||||
participant = Participant.query.get(participant_id)
|
||||
exchange = Exchange.query.get(participant.exchange_id)
|
||||
|
||||
# Build magic link URL
|
||||
magic_link_url = url_for(
|
||||
'auth.participant_magic_link',
|
||||
token=token,
|
||||
_external=True
|
||||
)
|
||||
|
||||
# Development mode: log the link for QA access
|
||||
if should_log_dev_mode_links():
|
||||
logger.info(f"DEV MODE: Magic link generated for participant {participant.email}")
|
||||
logger.info(f"DEV MODE: Full magic link URL: {magic_link_url}")
|
||||
|
||||
# Send email (if email service available)
|
||||
try:
|
||||
send_email(
|
||||
to=participant.email,
|
||||
template='registration_confirmation',
|
||||
variables={
|
||||
'participant_name': participant.name,
|
||||
'exchange_name': exchange.name,
|
||||
'magic_link_url': magic_link_url,
|
||||
'exchange_date': exchange.date,
|
||||
'budget': exchange.budget
|
||||
}
|
||||
)
|
||||
except EmailServiceUnavailable:
|
||||
# In development, logging fallback ensures we can still test
|
||||
logger.info(f"Email service unavailable, but link logged for DEV MODE testing")
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### CRITICAL: Production Safety
|
||||
|
||||
**This feature MUST NEVER be enabled in production.**
|
||||
|
||||
1. **Environment Check**: Code checks `FLASK_ENV` at runtime
|
||||
2. **No Configuration Flag**: Not configurable; only environment-based
|
||||
3. **Clear Warning**: Code comments explicitly state the security implication
|
||||
4. **Immutable in Production**: Production deployments set `FLASK_ENV=production` or unset
|
||||
5. **Audit Trail**: If logging were somehow enabled in production, it would be obvious in logs
|
||||
|
||||
### Attack Vector: Link Interception
|
||||
|
||||
**Threat**: Magic links logged in development contain full tokens. If logs are exposed, attacker can use tokens.
|
||||
|
||||
**Mitigations**:
|
||||
- Development environment is not production
|
||||
- Container logs are only accessible in development/QA networks
|
||||
- Tokens have 1-hour expiration
|
||||
- Single-use enforcement prevents replay
|
||||
- In production, this feature is completely disabled
|
||||
|
||||
### Attack Vector: Log Exposure
|
||||
|
||||
**Threat**: Production logs somehow exposed and contain dev-mode links.
|
||||
|
||||
**Mitigations**:
|
||||
- Feature only enabled when `FLASK_ENV=development`
|
||||
- Production environments never set this variable
|
||||
- If accidentally enabled, logs clearly show "DEV MODE" prefix
|
||||
- Log aggregation systems should filter dev logs from production
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```python
|
||||
def test_dev_mode_logging_disabled_in_production():
|
||||
"""Ensure development mode logging is never enabled in production."""
|
||||
# Simulate production environment
|
||||
os.environ['FLASK_ENV'] = 'production'
|
||||
|
||||
# Development mode feature should be disabled
|
||||
assert should_log_dev_mode_links() is False
|
||||
|
||||
def test_dev_mode_logging_enabled_in_development():
|
||||
"""Ensure development mode logging is enabled in development."""
|
||||
# Simulate development environment
|
||||
os.environ['FLASK_ENV'] = 'development'
|
||||
|
||||
# Development mode feature should be enabled
|
||||
assert should_log_dev_mode_links() is True
|
||||
|
||||
def test_magic_link_logged_in_dev_mode(caplog):
|
||||
"""Test that magic links are logged in development mode."""
|
||||
os.environ['FLASK_ENV'] = 'development'
|
||||
|
||||
participant = create_test_participant()
|
||||
token = generate_magic_token(participant.id, participant.exchange_id)
|
||||
|
||||
# Send confirmation (would log the link)
|
||||
send_registration_confirmation(participant.id, token)
|
||||
|
||||
# Check log contains DEV MODE indicator and URL
|
||||
assert 'DEV MODE' in caplog.text
|
||||
assert 'magic_link_url' in caplog.text or 'Full magic link URL' in caplog.text
|
||||
|
||||
def test_magic_link_not_logged_in_production(caplog):
|
||||
"""Test that magic links are NOT logged in production."""
|
||||
os.environ['FLASK_ENV'] = 'production'
|
||||
|
||||
participant = create_test_participant()
|
||||
token = generate_magic_token(participant.id, participant.exchange_id)
|
||||
|
||||
# Send confirmation (should NOT log the link)
|
||||
send_registration_confirmation(participant.id, token)
|
||||
|
||||
# Check log does NOT contain DEV MODE indicator
|
||||
assert 'DEV MODE' not in caplog.text
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```python
|
||||
def test_qa_workflow_with_dev_mode_logging():
|
||||
"""
|
||||
Simulate QA workflow:
|
||||
1. Register participant
|
||||
2. Retrieve magic link from logs
|
||||
3. Use link to authenticate
|
||||
"""
|
||||
os.environ['FLASK_ENV'] = 'development'
|
||||
|
||||
with app.test_client() as client:
|
||||
# Step 1: Register participant
|
||||
response = client.post('/exchange/test-slug/register', data={
|
||||
'name': 'QA Tester',
|
||||
'email': 'qa@example.com',
|
||||
'gift_ideas': 'Testing gifts',
|
||||
'csrf_token': get_csrf_token()
|
||||
})
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
# Step 2: Extract magic link from logs
|
||||
# In real test, would use caplog fixture or retrieve from log file
|
||||
# For this example, extract from app logs
|
||||
logs = get_application_logs()
|
||||
magic_link_match = re.search(r'DEV MODE: Full magic link URL: ([^ ]+)', logs)
|
||||
assert magic_link_match is not None
|
||||
|
||||
magic_link_url = magic_link_match.group(1)
|
||||
# Extract token from URL
|
||||
token = magic_link_url.split('/magic/')[-1]
|
||||
|
||||
# Step 3: Use magic link to authenticate
|
||||
response = client.get(f'/auth/participant/magic/{token}')
|
||||
|
||||
# Should redirect to dashboard
|
||||
assert response.status_code == 302
|
||||
assert 'dashboard' in response.location
|
||||
|
||||
# Session should be created
|
||||
with client.session_transaction() as sess:
|
||||
assert sess['user_type'] == 'participant'
|
||||
assert sess['user_id'] is not None
|
||||
```
|
||||
|
||||
## How QA Uses Dev Mode
|
||||
|
||||
### For Podman Containers
|
||||
|
||||
```bash
|
||||
# Register a participant via web UI
|
||||
# Then retrieve the magic link from container logs:
|
||||
podman logs sneaky-klaus-qa | grep "DEV MODE"
|
||||
|
||||
# Output:
|
||||
# [INFO] DEV MODE: Magic link generated for participant alice@example.com
|
||||
# [INFO] DEV MODE: Full magic link URL: https://localhost:5000/auth/participant/magic/k7Jx9mP2qR4sT6vN8bC1dE3fG5hI0jK7lM9nO2pQ4rS6tU8vW1xY3zA5
|
||||
|
||||
# Copy the URL and paste into browser or use curl:
|
||||
curl https://localhost:5000/auth/participant/magic/k7Jx9mP2qR4sT6vN8bC1dE3fG5hI0jK7lM9nO2pQ4rS6tU8vW1xY3zA5
|
||||
```
|
||||
|
||||
### For Local Development
|
||||
|
||||
```bash
|
||||
# Run Flask development server with FLASK_ENV=development
|
||||
FLASK_ENV=development uv run flask run
|
||||
|
||||
# Register a participant via web UI
|
||||
# Magic link appears in terminal output:
|
||||
# [INFO] DEV MODE: Magic link generated for participant alice@example.com
|
||||
# [INFO] DEV MODE: Full magic link URL: http://localhost:5000/auth/participant/magic/...
|
||||
|
||||
# Copy and use the link
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
No configuration needed. Feature is automatically enabled when:
|
||||
|
||||
```
|
||||
FLASK_ENV=development
|
||||
```
|
||||
|
||||
Feature is automatically disabled when:
|
||||
|
||||
```
|
||||
FLASK_ENV=production
|
||||
# or unset (defaults to production behavior)
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Enhanced Dev Tools
|
||||
|
||||
If QA requests additional features:
|
||||
|
||||
1. **Dev-Only Endpoint**: Create `/dev/magic-links` endpoint that lists recent links (only in development)
|
||||
2. **Email Override**: Allow configuration to always log links even if email sends successfully
|
||||
3. **Log Export**: Endpoint to export recent logs as JSON for automated testing
|
||||
|
||||
### Production Monitoring
|
||||
|
||||
Consider monitoring/alerting if dev mode logging somehow gets enabled in production:
|
||||
|
||||
```python
|
||||
if should_log_dev_mode_links() and in_production():
|
||||
send_alert("CRITICAL: Dev mode logging enabled in production!")
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-0002: Authentication Strategy](./0002-authentication-strategy.md)
|
||||
- [Participant Auth Component Design](../designs/v0.2.0/components/participant-auth.md)
|
||||
- [Flask Environment and Configuration](https://flask.palletsprojects.com/en/latest/config/)
|
||||
Reference in New Issue
Block a user