Files
sneakyklaus/docs/designs/v0.1.0/api-spec.md
Phil Skentelbery b077112aba chore: initial project setup
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>
2025-12-22 11:28:15 -07:00

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 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

{
  "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:

{
  "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/<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 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/<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_at timestamp
  • 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 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/<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_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:

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
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 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