Initialize Sneaky Klaus project with: - uv package management and pyproject.toml - Flask application structure (app.py, config.py) - SQLAlchemy models for Admin and Exchange - Alembic database migrations - Pre-commit hooks configuration - Development tooling (pytest, ruff, mypy) Initial structure follows design documents in docs/: - src/app.py: Application factory with Flask extensions - src/config.py: Environment-based configuration - src/models/: Admin and Exchange models - migrations/: Alembic migration setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
36 KiB
API Specification - v0.1.0
Version: 0.1.0 Date: 2025-12-22 Status: Initial Design
Introduction
This document defines all HTTP routes, request/response formats, form fields, validation rules, and authentication requirements for Sneaky Klaus. The application uses server-side rendering with Flask, so most routes return HTML rather than JSON.
Architecture
Blueprint Organization
Flask routes are organized into blueprints by functional area:
| Blueprint | Prefix | Purpose |
|---|---|---|
auth |
/auth |
Authentication (admin login, magic links) |
admin |
/admin |
Admin dashboard and exchange management |
participant |
/participant |
Participant registration and dashboard |
public |
/ |
Public pages (landing, health check) |
Authentication
Session-Based Authentication:
- Server-side sessions stored in database
- Session cookies with Secure, HttpOnly, SameSite=Lax flags
- CSRF protection on all state-changing requests (POST, PUT, DELETE)
Auth Decorators:
@login_required: Any authenticated user (admin or participant)@admin_required: Admin only@participant_required: Participant only@exchange_access_required: Participant has access to specific exchange
Request/Response Patterns
HTML Responses: Most routes return rendered Jinja2 templates Redirects: POST requests redirect to GET (Post-Redirect-Get pattern) Flash Messages: Success/error messages displayed via Flask flash() Form Validation: WTForms with server-side validation CSRF Tokens: All forms include CSRF token
Error Handling
HTTP Status Codes:
200 OK: Successful request302 Found: Redirect (common after POST)400 Bad Request: Form validation failure401 Unauthorized: Not logged in403 Forbidden: Insufficient permissions404 Not Found: Resource doesn't exist429 Too Many Requests: Rate limit exceeded500 Internal Server Error: Application error
Error Pages: Custom error templates for 404, 403, 500
Public Blueprint
GET /
Purpose: Landing page (redirects to appropriate dashboard if logged in)
Authentication: None
Response:
- If admin logged in: Redirect to
/admin/dashboard - If participant logged in: Redirect to
/participant/dashboard - Otherwise: Render landing page template
Template: public/landing.html
Content:
- Application description
- Links to admin login
- Example registration link (non-functional demo)
GET /health
Purpose: Health check endpoint for monitoring
Authentication: None
Response: JSON
{
"status": "healthy",
"timestamp": "2025-12-22T10:30:00Z",
"database": "connected",
"scheduler": "running"
}
Status Codes:
200 OK: All systems healthy503 Service Unavailable: Database or critical component failure
Checks:
- Database connectivity
- Scheduler running status
Auth Blueprint
GET /auth/admin/login
Purpose: Admin login page
Authentication: None (redirects to dashboard if already logged in)
Query Parameters: None
Response: Render login form
Template: auth/admin_login.html
Form Fields:
email: Email addresspassword: Passwordcsrf_token: CSRF protection
POST /auth/admin/login
Purpose: Admin login submission
Authentication: None
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
email |
String | Yes | Valid email format |
password |
String | Yes | Non-empty |
csrf_token |
String | Yes | Valid CSRF token |
Rate Limit: 5 attempts per 15 minutes per email
Success Response:
- Create admin session
- Redirect to
/admin/dashboard - Flash message: "Welcome back!"
Error Response:
- Re-render login form with error message
- Errors:
- "Invalid email or password" (generic, doesn't reveal which)
- "Too many login attempts. Try again in 15 minutes." (rate limit)
Status Codes:
302 Found: Successful login (redirect)400 Bad Request: Validation failure429 Too Many Requests: Rate limit exceeded
GET /auth/admin/logout
Purpose: Admin logout
Authentication: Admin required
Response:
- Destroy session
- Redirect to
/auth/admin/login - Flash message: "Logged out successfully"
GET /auth/admin/forgot-password
Purpose: Password reset request page
Authentication: None
Response: Render password reset request form
Template: auth/forgot_password.html
Form Fields:
email: Admin email addresscsrf_token: CSRF protection
POST /auth/admin/forgot-password
Purpose: Request password reset email
Authentication: None
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
email |
String | Yes | Valid email format |
csrf_token |
String | Yes | Valid CSRF token |
Rate Limit: 3 requests per hour per email
Response (always same, security best practice):
- Redirect to
/auth/admin/login - Flash message: "If an account exists, you'll receive a password reset email."
Backend Actions:
- If email matches admin: Create password reset token, send email
- If email doesn't match: Do nothing (same timing to prevent enumeration)
Email Content:
- Subject: "Password Reset Request - Sneaky Klaus"
- Body: Password reset link with token
- Link:
/auth/admin/reset-password/{token} - Expiration: 1 hour
GET /auth/admin/reset-password/
Purpose: Password reset form
Authentication: None
URL Parameters:
token: Password reset token (string)
Response:
- If token valid: Render password reset form
- If token invalid/expired: Render error page with link to request new reset
Template: auth/reset_password.html
Form Fields:
password: New passwordpassword_confirm: Confirm new passwordcsrf_token: CSRF protectiontoken: Hidden field with token value
POST /auth/admin/reset-password/
Purpose: Submit new password
Authentication: None
URL Parameters:
token: Password reset token
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
password |
String | Yes | Min 12 characters |
password_confirm |
String | Yes | Must match password |
token |
String | Yes | Hidden field, must be valid |
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Password minimum 12 characters
- Passwords must match
- Token must be valid (not expired, not used)
Success Response:
- Update admin password
- Invalidate token
- Redirect to
/auth/admin/login - Flash message: "Password reset successful. Please log in."
Error Response:
- Re-render form with errors
- Errors:
- "Passwords do not match"
- "Password must be at least 12 characters"
- "Invalid or expired reset link"
GET /auth/participant/magic/
Purpose: Magic link authentication for participants
Authentication: None
URL Parameters:
token: Magic link token (string)
Response:
- If token valid:
- Create participant session
- Mark token as used
- Redirect to
/participant/dashboard - Flash message: "Welcome back!"
- If token invalid/expired/used:
- Render error page
- Show option to request new magic link
- Template:
auth/invalid_token.html
Status Codes:
302 Found: Valid token (redirect)400 Bad Request: Invalid token
GET /auth/participant/logout
Purpose: Participant logout
Authentication: Participant required
Response:
- Destroy session
- Redirect to
/ - Flash message: "Logged out successfully"
Admin Blueprint
GET /admin/dashboard
Purpose: Admin dashboard showing all exchanges
Authentication: Admin required
Query Parameters:
state(optional): Filter by exchange state (draft, registration_open, etc.)
Response: Render dashboard
Template: admin/dashboard.html
Template Data:
{
"exchanges": [
{
"id": 1,
"name": "Family Christmas 2025",
"state": "registration_open",
"participant_count": 12,
"exchange_date": "2025-12-25",
"max_participants": 20
},
# ... more exchanges
],
"active_count": 3,
"completed_count": 5,
"draft_count": 2
}
Display:
- List all exchanges grouped by state
- Summary counts
- Quick actions per exchange (view, edit, delete)
- "Create New Exchange" button
GET /admin/exchange/new
Purpose: Create new exchange form
Authentication: Admin required
Response: Render exchange creation form
Template: admin/exchange_form.html
Form Fields:
name: Exchange namedescription: Optional descriptionbudget: Gift budget (freeform text)max_participants: Maximum participantsregistration_close_date: Date + timeexchange_date: Date + timetimezone: Dropdown of timezonescsrf_token: CSRF protection
POST /admin/exchange/new
Purpose: Create new exchange
Authentication: Admin required
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
name |
String | Yes | 1-255 characters |
description |
Text | No | 0-2000 characters |
budget |
String | Yes | 1-100 characters |
max_participants |
Integer | Yes | ≥ 3 |
registration_close_date |
DateTime | Yes | Must be future date |
exchange_date |
DateTime | Yes | Must be after close date |
timezone |
String | Yes | Valid IANA timezone |
csrf_token |
String | Yes | Valid CSRF token |
Validation Rules:
registration_close_datemust be in the futureexchange_datemust be afterregistration_close_datemax_participantsminimum 3timezonemust be valid IANA timezone name
Success Response:
- Create exchange in "draft" state
- Redirect to
/admin/exchange/<id> - Flash message: "Exchange created successfully!"
Error Response:
- Re-render form with error messages
- Preserve form values
GET /admin/exchange/
Purpose: View exchange details
Authentication: Admin required
URL Parameters:
id: Exchange ID (integer)
Response: Render exchange detail page
Template: admin/exchange_detail.html
Template Data:
{
"exchange": {
"id": 1,
"name": "Family Christmas 2025",
"description": "Annual family gift exchange",
"budget": "$20-30",
"max_participants": 20,
"registration_close_date": "2025-12-15T23:59:00",
"exchange_date": "2025-12-25T18:00:00",
"timezone": "America/New_York",
"state": "registration_open",
"participant_count": 12,
"registration_link": "https://app.example.com/exchange/abc123/register"
},
"participants": [
{
"id": 1,
"name": "Alice Smith",
"email": "alice@example.com",
"registered_at": "2025-11-01T10:00:00",
"gift_ideas": "Books, coffee, plants"
},
# ... more participants
],
"exclusions": [
{
"id": 1,
"participant_a": "Alice Smith",
"participant_b": "Bob Jones"
},
# ... more exclusions
],
"matches": [ # Only present if state == "matched"
{
"giver": "Alice Smith",
"receiver": "Carol White"
},
# ... more matches
]
}
State-Specific Actions:
- Draft: Edit, Open Registration, Delete
- Registration Open: Edit, Close Registration, Delete
- Registration Closed: Reopen Registration, Configure Exclusions, Match, Delete
- Matched: View Matches, Re-match, Reopen Registration, Mark Complete, Delete
- Completed: Delete (with days until purge indicator)
GET /admin/exchange//edit
Purpose: Edit exchange form
Authentication: Admin required
URL Parameters:
id: Exchange ID
Response: Render exchange edit form
Template: admin/exchange_form.html (same as create)
Pre-populated: All current exchange values
Restrictions:
- If state is "matched" or "completed": Show read-only view with message "Cannot edit after matching"
- Otherwise: Allow editing all fields
POST /admin/exchange//edit
Purpose: Update exchange
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields: Same as POST /admin/exchange/new
Validation: Same as create, plus:
- Cannot edit if state is "matched" or "completed"
Success Response:
- Update exchange
- Redirect to
/admin/exchange/<id> - Flash message: "Exchange updated successfully!"
Error Response:
- Re-render form with errors
POST /admin/exchange//delete
Purpose: Delete exchange
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
confirm |
String | Yes | Must be "DELETE" |
csrf_token |
String | Yes | Valid CSRF token |
Confirmation: Requires typing "DELETE" to confirm
Success Response:
- Delete exchange (cascades to participants, matches, exclusions)
- Redirect to
/admin/dashboard - Flash message: "Exchange deleted successfully"
Error Response:
- Redirect back with error message
POST /admin/exchange//state/open-registration
Purpose: Open registration for exchange
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Exchange must be in "draft" state
Success Response:
- Update state to "registration_open"
- Redirect to
/admin/exchange/<id> - Flash message: "Registration is now open!"
Error Response:
- Redirect back with error message
POST /admin/exchange//state/close-registration
Purpose: Close registration
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Exchange must be in "registration_open" state
Success Response:
- Update state to "registration_closed"
- Redirect to
/admin/exchange/<id> - Flash message: "Registration closed. You can now configure exclusions and match participants."
POST /admin/exchange//state/reopen-registration
Purpose: Reopen registration (pre or post matching)
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
confirm |
Boolean | Conditional | Required if state is "matched" |
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Exchange must be in "registration_closed" or "matched" state
Behavior:
- If from "registration_closed": Simple state change
- If from "matched": Delete all matches, require confirmation
Success Response:
- Update state to "registration_open"
- Clear matches if applicable
- Redirect to
/admin/exchange/<id> - Flash message: "Registration reopened" (+ warning if matches cleared)
GET /admin/exchange//exclusions
Purpose: Manage exclusion rules
Authentication: Admin required
URL Parameters:
id: Exchange ID
Response: Render exclusion management page
Template: admin/exclusions.html
Template Data:
{
"exchange": {...},
"participants": [
{"id": 1, "name": "Alice Smith"},
{"id": 2, "name": "Bob Jones"},
# ...
],
"exclusions": [
{
"id": 1,
"participant_a": {"id": 1, "name": "Alice Smith"},
"participant_b": {"id": 2, "name": "Bob Jones"}
},
# ...
]
}
Display:
- List of current exclusions with remove button
- Form to add new exclusion (two dropdowns for participants)
- Validation warning if exclusions make matching impossible
POST /admin/exchange//exclusions
Purpose: Add exclusion rule
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
participant_a_id |
Integer | Yes | Valid participant in exchange |
participant_b_id |
Integer | Yes | Valid participant in exchange |
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Both participants must exist and belong to this exchange
- Participants must be different
- Exclusion doesn't already exist
- Exchange must be in "registration_closed" state
Success Response:
- Create exclusion rule (store with lower ID as participant_a)
- Redirect to
/admin/exchange/<id>/exclusions - Flash message: "Exclusion added"
Error Response:
- Redirect back with error message
POST /admin/exchange//exclusions/<exclusion_id>/delete
Purpose: Remove exclusion rule
Authentication: Admin required
URL Parameters:
id: Exchange IDexclusion_id: Exclusion rule ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
csrf_token |
String | Yes | Valid CSRF token |
Success Response:
- Delete exclusion
- Redirect to
/admin/exchange/<id>/exclusions - Flash message: "Exclusion removed"
POST /admin/exchange//match
Purpose: Trigger matching algorithm
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Exchange must be in "registration_closed" state
- Minimum 3 participants (non-withdrawn)
- Matching must be possible given exclusions
Success Response:
- Run matching algorithm
- Create match records
- Update state to "matched"
- Send match notification emails to all participants
- Redirect to
/admin/exchange/<id> - Flash message: "Matching complete! Participants have been notified."
Error Response:
- If matching impossible:
- Remain in "registration_closed" state
- Redirect to
/admin/exchange/<id>/exclusions - Flash error: "Matching failed: [reason]. Please adjust exclusion rules."
- Show which exclusions are problematic
Matching Failure Reasons:
- "Too many exclusions prevent a valid assignment"
- "Participant [name] has too many exclusions"
- "No valid single-cycle assignment possible"
POST /admin/exchange//rematch
Purpose: Clear current matches and re-run matching
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
confirm |
Boolean | Yes | Must be true |
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Exchange must be in "matched" state
Success Response:
- Delete existing matches
- Run matching algorithm
- Create new match records
- Send updated match notifications
- Redirect to
/admin/exchange/<id> - Flash message: "Re-matching complete! Participants have been notified of new assignments."
Error Response:
- Same as POST /admin/exchange//match
GET /admin/exchange//matches
Purpose: View all matches (admin only)
Authentication: Admin required
URL Parameters:
id: Exchange ID
Response: Render matches list
Template: admin/matches.html
Template Data:
{
"exchange": {...},
"matches": [
{
"giver": {
"id": 1,
"name": "Alice Smith",
"email": "alice@example.com"
},
"receiver": {
"id": 3,
"name": "Carol White",
"gift_ideas": "Books, coffee"
}
},
# ...
]
}
Display:
- Table showing giver → receiver
- Option to download as CSV
- Warning message: "This information is confidential. Do not share matches with participants."
POST /admin/exchange//complete
Purpose: Mark exchange as complete
Authentication: Admin required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Exchange must be in "matched" state
Success Response:
- Update state to "completed"
- Set
completed_attimestamp - Redirect to
/admin/exchange/<id> - Flash message: "Exchange marked complete. Data will be purged in 30 days."
POST /admin/exchange//participant/<participant_id>/remove
Purpose: Remove participant from exchange
Authentication: Admin required
URL Parameters:
id: Exchange IDparticipant_id: Participant ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
confirm |
Boolean | Yes | Must be true |
csrf_token |
String | Yes | Valid CSRF token |
Behavior:
- If exchange not matched: Simply remove participant
- If exchange matched: Delete matches, require re-match
Success Response:
- Set participant
withdrawn_attimestamp (soft delete) - Delete related exclusions
- If matched: Delete all matches, revert state to "registration_closed"
- Send removal notification email to participant
- Redirect to
/admin/exchange/<id> - Flash message: "Participant removed" (+ warning if re-match required)
GET /admin/settings
Purpose: Admin notification preferences
Authentication: Admin required
Response: Render settings page
Template: admin/settings.html
Template Data:
{
"global_preferences": {
"new_registration": True,
"participant_withdrawal": True,
"matching_complete": True
},
"exchange_preferences": {
1: { # Exchange ID
"exchange_name": "Family Christmas 2025",
"new_registration": False,
"participant_withdrawal": True,
"matching_complete": True
},
# ... more exchanges
}
}
Display:
- Global default preferences (checkboxes)
- Per-exchange overrides (optional)
POST /admin/settings
Purpose: Update notification preferences
Authentication: Admin required
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
global_new_registration |
Boolean | No | - |
global_participant_withdrawal |
Boolean | No | - |
global_matching_complete |
Boolean | No | - |
exchange_{id}_new_registration |
Boolean | No | Per exchange |
exchange_{id}_participant_withdrawal |
Boolean | No | Per exchange |
exchange_{id}_matching_complete |
Boolean | No | Per exchange |
csrf_token |
String | Yes | Valid CSRF token |
Success Response:
- Update global and per-exchange preferences
- Redirect to
/admin/settings - Flash message: "Notification preferences updated"
Participant Blueprint
GET /exchange//register
Purpose: Participant registration page
Authentication: None
URL Parameters:
slug: Exchange registration slug (unique identifier, not numeric ID)
Response: Render registration page
Template: participant/register.html
Template Data:
{
"exchange": {
"name": "Family Christmas 2025",
"description": "Annual family gift exchange",
"budget": "$20-30",
"exchange_date": "2025-12-25",
"max_participants": 20,
"current_participants": 12,
"registration_open": True
}
}
Display:
- Exchange details
- If registration open:
- Registration form
- "Already registered? Request access link" button
- If registration closed:
- Message: "Registration is closed"
- "Already registered? Request access link" button
- If exchange doesn't exist: 404 error
POST /exchange//register
Purpose: Submit participant registration
Authentication: None
URL Parameters:
slug: Exchange registration slug
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
name |
String | Yes | 1-255 characters |
email |
String | Yes | Valid email, unique in exchange |
gift_ideas |
Text | No | 0-10000 characters |
reminder_enabled |
Boolean | No | Default: True |
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Email must be unique within this exchange
- Exchange must be in "registration_open" state
- Participant count must be under max_participants
Success Response:
- Create participant record
- Create magic token
- Send confirmation email with magic link
- Redirect to
/exchange/<slug>/register/success - Flash message: "Registration successful! Check your email for access link."
Error Response:
- Re-render form with errors
- Errors:
- "Email already registered for this exchange"
- "Registration is closed"
- "Exchange is full"
GET /exchange//register/success
Purpose: Registration success page
Authentication: None
URL Parameters:
slug: Exchange registration slug
Response: Render success page
Template: participant/register_success.html
Content:
- Success message
- Instructions to check email
- Link to request another magic link
POST /exchange//request-access
Purpose: Request magic link for existing registration
Authentication: None
URL Parameters:
slug: Exchange registration slug
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
email |
String | Yes | Valid email format |
csrf_token |
String | Yes | Valid CSRF token |
Rate Limit: 3 requests per hour per email
Response (always same, security best practice):
- Redirect to
/exchange/<slug>/register/success - Flash message: "If you're registered, you'll receive an access link."
Backend Actions:
- If email registered in this exchange: Create magic token, send email
- If email not registered: Do nothing (same timing)
GET /participant/dashboard
Purpose: Participant dashboard (after authentication)
Authentication: Participant required
Response: Render participant dashboard
Template: participant/dashboard.html
Template Data:
{
"exchanges": [
{
"id": 1,
"name": "Family Christmas 2025",
"budget": "$20-30",
"exchange_date": "2025-12-25",
"state": "matched",
"my_match": {
"receiver_name": "Carol White",
"gift_ideas": "Books, coffee, plants"
} if matched else None,
"participant_count": 12,
"can_edit": True/False # Based on state
},
# ... more exchanges
]
}
Display:
- List of all exchanges participant is registered in
- For each exchange:
- Exchange details
- If matched: Show assigned recipient and gift ideas
- If not matched: Show waiting message
- Links to view participant list and edit profile
GET /participant/exchange/
Purpose: View specific exchange details
Authentication: Participant required, exchange access required
URL Parameters:
id: Exchange ID
Response: Render exchange detail page
Template: participant/exchange_detail.html
Template Data:
{
"exchange": {
"name": "Family Christmas 2025",
"description": "Annual family gift exchange",
"budget": "$20-30",
"exchange_date": "2025-12-25",
"state": "matched"
},
"my_match": {
"receiver_name": "Carol White",
"gift_ideas": "Books, coffee, plants"
} if matched else None,
"participants": [
{"name": "Alice Smith"},
{"name": "Bob Jones"},
# ... (names only, no emails or matches)
],
"my_profile": {
"name": "Alice Smith",
"email": "alice@example.com",
"gift_ideas": "Books, coffee",
"reminder_enabled": True
}
}
Display:
- Exchange information
- Assigned recipient (if matched)
- List of all participants (names only)
- Own profile with edit button
GET /participant/exchange//edit
Purpose: Edit participant profile
Authentication: Participant required, exchange access required
URL Parameters:
id: Exchange ID
Response: Render profile edit form
Template: participant/edit_profile.html
Pre-populated: Current participant data
Restrictions:
- Cannot edit if exchange is "matched" or "completed" (except gift_ideas and reminder_enabled)
- Cannot change email (show as read-only)
POST /participant/exchange//edit
Purpose: Update participant profile
Authentication: Participant required, exchange access required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
name |
String | Yes | 1-255 characters | Disabled if matched |
gift_ideas |
Text | No | 0-10000 characters | Always editable |
reminder_enabled |
Boolean | No | - | Always editable |
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Cannot change name if exchange is matched/completed
- Can always update gift_ideas and reminder_enabled
Success Response:
- Update participant record
- Redirect to
/participant/exchange/<id> - Flash message: "Profile updated"
Error Response:
- Re-render form with errors
POST /participant/exchange//withdraw
Purpose: Withdraw from exchange
Authentication: Participant required, exchange access required
URL Parameters:
id: Exchange ID
Form Fields:
| Field | Type | Required | Validation |
|---|---|---|---|
confirm |
Boolean | Yes | Must be true |
csrf_token |
String | Yes | Valid CSRF token |
Validation:
- Exchange must not be in "matched" or "completed" state
Success Response:
- Set participant
withdrawn_attimestamp - Delete related exclusions
- Send withdrawal confirmation email
- Notify admin (if enabled)
- Redirect to
/participant/dashboard - Flash message: "You have withdrawn from the exchange"
Error Response:
- Redirect back with error
- Error: "Cannot withdraw after matching has occurred"
Form Validation Details
Common Validations
Email:
- Format: RFC 5322 compliant
- Length: Max 255 characters
- Normalized: Lowercase
Password (Admin):
- Min length: 12 characters
- No complexity requirements (follows NIST guidance)
Exchange Name:
- Min length: 1 character
- Max length: 255 characters
- Required
Gift Ideas:
- Max length: 10,000 characters
- Optional
- Multiline text
Dates:
- Format: ISO 8601 or localized format
- Validation: Must be future dates (for registration_close_date, exchange_date)
- Relationship: registration_close_date < exchange_date
Timezone:
- Must be valid IANA timezone name (e.g., "America/New_York")
- Dropdown with common timezones
CSRF Protection
All state-changing requests (POST, PUT, DELETE) require valid CSRF token:
- Token generated server-side
- Embedded in forms as hidden field
- Validated on submission
- Token bound to user session
Implementation: Flask-WTF
Rate Limiting
Rate Limit Policies
| Endpoint | Limit | Window | Key |
|---|---|---|---|
| POST /auth/admin/login | 5 attempts | 15 minutes | |
| POST /auth/admin/forgot-password | 3 requests | 1 hour | |
| POST /exchange//request-access | 3 requests | 1 hour | |
| POST /exchange//register | 10 requests | 1 hour | IP address |
Rate Limit Response
Status Code: 429 Too Many Requests
Response: Re-render form with error message
Error Message: "Too many attempts. Please try again later."
Headers (optional enhancement):
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640000000
Email Notifications
Emails triggered by application actions (detailed in notifications component design).
Triggered Emails
| Trigger | Recipient | Template |
|---|---|---|
| Participant registration | Participant | registration_confirmation |
| Magic link request | Participant | magic_link |
| Match assigned | Participant | match_notification |
| Reminder (scheduled) | Participant | reminder |
| Participant withdrawal | Participant | withdrawal_confirmation |
| New registration | Admin | admin_new_registration |
| Participant withdrawal | Admin | admin_participant_withdrawal |
| Matching complete | Admin | admin_matching_complete |
| Password reset request | Admin | password_reset |
Response Headers
Security Headers
Configured via Flask-Talisman:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Session Cookie
Set-Cookie: session=<value>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=604800
Error Responses
Form Validation Errors
Format: Flash message + inline field errors
Example:
<div class="alert alert-error">
Please correct the errors below.
</div>
<form>
<div class="field error">
<label>Email</label>
<input type="email" name="email" value="invalid">
<span class="error-message">Invalid email format</span>
</div>
</form>
HTTP Error Pages
404 Not Found:
- Template:
errors/404.html - Message: "Page not found"
- Link to dashboard or home
403 Forbidden:
- Template:
errors/403.html - Message: "You don't have permission to access this page"
500 Internal Server Error:
- Template:
errors/500.html - Message: "Something went wrong. Please try again later."
- Error logged with stack trace
429 Too Many Requests:
- Template: Re-render current page with error message
- Message: "Too many attempts. Please try again later."
URL Naming Conventions
URL Patterns
Admin Routes:
/admin/dashboard- Dashboard/admin/exchange/new- Create exchange/admin/exchange/<id>- View exchange/admin/exchange/<id>/edit- Edit exchange/admin/exchange/<id>/exclusions- Manage exclusions/admin/exchange/<id>/matches- View matches
Participant Routes:
/exchange/<slug>/register- Registration page (public)/participant/dashboard- Participant dashboard (authenticated)/participant/exchange/<id>- Exchange details (authenticated)
Auth Routes:
/auth/admin/login- Admin login/auth/admin/logout- Admin logout/auth/participant/magic/<token>- Magic link/auth/participant/logout- Participant logout
Slug vs ID
Exchange Slug (for public registration):
- Format: Random alphanumeric string (e.g., "abc123xyz")
- Length: 12 characters
- URL-safe
- Generated on exchange creation
- Used for public registration link
Numeric ID (for authenticated access):
- Format: Auto-increment integer
- Used in admin and authenticated participant routes
- Not exposed in public URLs
Static Assets
CSS
Location: /static/css/
Files:
main.css- Global stylesadmin.css- Admin-specific stylesparticipant.css- Participant-specific styles
Loading: Linked in base template
JavaScript
Location: /static/js/
Files:
main.js- Global utilities (copy-to-clipboard, form validation)admin.js- Admin-specific interactivity
Philosophy: Progressive enhancement, core functionality works without JS
Images
Location: /static/img/
Files:
- Logo
- Icons
- Email header images
API Versioning
Current Version: v0.1.0 (no API versioning in URLs)
Future Consideration: If REST API is added for programmatic access, use /api/v1/ prefix