# 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 request - `302 Found`: Redirect (common after POST) - `400 Bad Request`: Form validation failure - `401 Unauthorized`: Not logged in - `403 Forbidden`: Insufficient permissions - `404 Not Found`: Resource doesn't exist - `429 Too Many Requests`: Rate limit exceeded - `500 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 ```json { "status": "healthy", "timestamp": "2025-12-22T10:30:00Z", "database": "connected", "scheduler": "running" } ``` **Status Codes**: - `200 OK`: All systems healthy - `503 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 address - `password`: Password - `csrf_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 failure - `429 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 address - `csrf_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 password - `password_confirm`: Confirm new password - `csrf_token`: CSRF protection - `token`: 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**: ```python { "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 name - `description`: Optional description - `budget`: Gift budget (freeform text) - `max_participants`: Maximum participants - `registration_close_date`: Date + time - `exchange_date`: Date + time - `timezone`: Dropdown of timezones - `csrf_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_date` must be in the future - `exchange_date` must be after `registration_close_date` - `max_participants` minimum 3 - `timezone` must be valid IANA timezone name **Success Response**: - Create exchange in "draft" state - Redirect to `/admin/exchange/` - 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**: ```python { "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/` - 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/` - 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/` - 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/` - 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**: ```python { "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//exclusions` - Flash message: "Exclusion added" **Error Response**: - Redirect back with error message --- ### POST /admin/exchange//exclusions//delete **Purpose**: Remove exclusion rule **Authentication**: Admin required **URL Parameters**: - `id`: Exchange ID - `exclusion_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//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/` - Flash message: "Matching complete! Participants have been notified." **Error Response**: - If matching impossible: - Remain in "registration_closed" state - Redirect to `/admin/exchange//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/` - 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**: ```python { "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_at` timestamp - Redirect to `/admin/exchange/` - Flash message: "Exchange marked complete. Data will be purged in 30 days." --- ### POST /admin/exchange//participant//remove **Purpose**: Remove participant from exchange **Authentication**: Admin required **URL Parameters**: - `id`: Exchange ID - `participant_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_at` timestamp (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/` - 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**: ```python { "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**: ```python { "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//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//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**: ```python { "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**: ```python { "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/` - 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_at` timestamp - 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 | email | | POST /auth/admin/forgot-password | 3 requests | 1 hour | email | | POST /exchange//request-access | 3 requests | 1 hour | email | | 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: ```python 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 ```python Set-Cookie: session=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=604800 ``` --- ## Error Responses ### Form Validation Errors **Format**: Flash message + inline field errors **Example**: ```html
Please correct the errors below.
Invalid email format
``` ### 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/` - View exchange - `/admin/exchange//edit` - Edit exchange - `/admin/exchange//exclusions` - Manage exclusions - `/admin/exchange//matches` - View matches **Participant Routes**: - `/exchange//register` - Registration page (public) - `/participant/dashboard` - Participant dashboard (authenticated) - `/participant/exchange/` - Exchange details (authenticated) **Auth Routes**: - `/auth/admin/login` - Admin login - `/auth/admin/logout` - Admin logout - `/auth/participant/magic/` - 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 styles - `admin.css` - Admin-specific styles - `participant.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 --- ## References - [Flask Routing Documentation](https://flask.palletsprojects.com/en/latest/quickstart/#routing) - [WTForms Documentation](https://wtforms.readthedocs.io/) - [Flask-WTF CSRF Protection](https://flask-wtf.readthedocs.io/en/stable/csrf.html) - [ADR-0002: Authentication Strategy](../../decisions/0002-authentication-strategy.md) - [System Overview](./overview.md) - [Data Model](./data-model.md)