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>
This commit is contained in:
179
docs/decisions/0001-core-technology-stack.md
Normal file
179
docs/decisions/0001-core-technology-stack.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 0001. Core Technology Stack
|
||||
|
||||
Date: 2025-12-22
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (Updated 2025-12-22)
|
||||
|
||||
## Context
|
||||
|
||||
Sneaky Klaus is a self-hosted Secret Santa organization application designed for individuals, families, and small organizations who want full control over their data. The application must be:
|
||||
|
||||
- Easy to self-host via containerization
|
||||
- Simple to deploy and maintain
|
||||
- Minimal in external dependencies
|
||||
- Suitable for small-scale usage (dozens of participants, not thousands)
|
||||
- Functional without complex infrastructure
|
||||
|
||||
Key requirements that inform technology choices:
|
||||
|
||||
1. **Self-hosting first**: Users should be able to deploy with a single container
|
||||
2. **Simplicity**: The tech stack should be straightforward and well-documented
|
||||
3. **No participant accounts**: Authentication must support passwordless magic links
|
||||
4. **Email delivery**: Must send transactional emails reliably
|
||||
5. **Background jobs**: Must handle scheduled tasks (reminders, data purging)
|
||||
6. **Data persistence**: Must store user data reliably but doesn't need high-scale database features
|
||||
|
||||
## Decision
|
||||
|
||||
We will use the following core technology stack:
|
||||
|
||||
| Component | Technology | Version Constraint |
|
||||
|-----------|------------|-------------------|
|
||||
| **Backend Framework** | Flask | ^3.0 |
|
||||
| **Language** | Python | ^3.11 |
|
||||
| **Database** | SQLite | ^3.40 (via Python stdlib) |
|
||||
| **Email Service** | Resend | Latest SDK |
|
||||
| **Deployment** | Docker | Latest |
|
||||
| **Package Manager** | uv | Latest |
|
||||
| **Background Jobs** | APScheduler | ^3.10 |
|
||||
| **Template Engine** | Jinja2 | ^3.1 (Flask default) |
|
||||
| **WSGI Server** | Gunicorn | ^21.0 (production) |
|
||||
| **Form Handling** | Flask-WTF (includes WTForms) | ^1.2 |
|
||||
| **Session Management** | Flask-Session | ^0.8 |
|
||||
| **Timezone Validation** | pytz | Latest |
|
||||
| **CSS Framework** | Pico CSS | Latest (via CDN) |
|
||||
|
||||
### Key Technology Rationale
|
||||
|
||||
**Flask**: Lightweight, well-documented, excellent for small-to-medium applications. Large ecosystem, straightforward patterns, and no unnecessary complexity.
|
||||
|
||||
**Python 3.11+**: Modern Python with performance improvements, excellent type hinting support, and active security support.
|
||||
|
||||
**SQLite**: Perfect for self-hosted applications. Zero-configuration, single-file database, excellent for read-heavy workloads with occasional writes. Eliminates need for separate database server.
|
||||
|
||||
**Resend**: Modern transactional email API with excellent deliverability, simple API, and reasonable pricing for small-scale usage.
|
||||
|
||||
**Docker**: Industry-standard containerization. Single container deployment simplifies self-hosting significantly.
|
||||
|
||||
**uv**: Fast, modern Python package manager and project manager. Significantly faster than pip, with better dependency resolution and lockfile support.
|
||||
|
||||
**APScheduler**: In-process job scheduling. Eliminates need for separate job queue infrastructure (Redis, Celery) while still supporting background tasks like reminder emails and data purging.
|
||||
|
||||
**Jinja2**: Flask's default templating engine. Server-side rendering eliminates need for frontend JavaScript framework, simplifying deployment and maintenance.
|
||||
|
||||
**Gunicorn**: Production-ready WSGI server for Flask applications. Well-tested, stable, and appropriate for the scale of this application.
|
||||
|
||||
**Flask-WTF**: Integrates WTForms with Flask, providing form validation, CSRF protection, and secure form handling. Industry-standard for Flask applications.
|
||||
|
||||
**Flask-Session**: Server-side session management for Flask. Stores session data in SQLite, providing secure session handling without client-side storage concerns.
|
||||
|
||||
**pytz**: Standard Python library for timezone validation and handling. Required for validating IANA timezone names in exchange configurations.
|
||||
|
||||
**Pico CSS**: Minimal, classless CSS framework delivered via CDN. Provides clean, semantic styling without requiring a build step or complex class names. Fully responsive and accessible out of the box.
|
||||
|
||||
### Frontend Approach
|
||||
|
||||
**Pure server-side rendering** with Jinja2 templates. No JavaScript framework (React, Vue, etc.). This decision:
|
||||
|
||||
- Eliminates build tooling complexity
|
||||
- Reduces deployment artifacts (no separate frontend bundle)
|
||||
- Simplifies security (no client-side state management)
|
||||
- Ensures full functionality without JavaScript enabled
|
||||
- Maintains mobile-friendliness through responsive CSS
|
||||
|
||||
Progressive enhancement with minimal JavaScript for interactivity (copy-to-clipboard, form validation) is acceptable but not required for core functionality.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Simple deployment**: Single container with no external service dependencies (except Resend for email)
|
||||
- **Low resource requirements**: SQLite and in-process job scheduling minimize memory and CPU usage
|
||||
- **Fast development**: Flask's simplicity and Jinja2's straightforward templating accelerate development
|
||||
- **Easy debugging**: All code runs in single process, simplifying troubleshooting
|
||||
- **Predictable performance**: Server-side rendering is fast and consistent
|
||||
- **No build step**: Templates render directly; no frontend compilation required
|
||||
- **Security by default**: Server-side rendering reduces attack surface compared to client-side SPAs
|
||||
- **Excellent for scale target**: Perfect for dozens to hundreds of participants per deployment
|
||||
|
||||
### Negative
|
||||
|
||||
- **SQLite limitations**: Not suitable if application needs to scale to thousands of concurrent users (not a concern for target use case)
|
||||
- **No horizontal scaling**: Single SQLite file prevents multi-instance deployment (acceptable trade-off for simplicity)
|
||||
- **Email vendor lock-in**: Resend is the only supported email provider (could be abstracted later if needed)
|
||||
- **APScheduler constraints**: Job scheduling tied to application process lifetime; jobs don't survive application restarts (acceptable for reminder scheduling)
|
||||
- **Less interactive UI**: Server-side rendering means no SPA-style instant interactivity (acceptable trade-off for simplicity)
|
||||
|
||||
### Neutral
|
||||
|
||||
- **Python expertise required**: Development requires Python knowledge (expected for Flask application)
|
||||
- **Database portability**: SQLite schema could be migrated to PostgreSQL if scaling needs change, but would require development effort
|
||||
- **Email testing**: Requires Resend account for development (free tier available) or mocking in tests
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Database Considerations
|
||||
|
||||
SQLite will be configured with:
|
||||
- WAL (Write-Ahead Logging) mode for better concurrency
|
||||
- Foreign keys enabled
|
||||
- Appropriate timeout for locked database scenarios
|
||||
- Regular backups recommended via volume mounts
|
||||
|
||||
### Job Scheduling Considerations
|
||||
|
||||
APScheduler will run in-process with:
|
||||
- JobStore backed by SQLite for job persistence across restarts (for scheduled jobs)
|
||||
- Executor using thread pool for background tasks
|
||||
- Misfire grace time configured appropriately for reminders
|
||||
|
||||
### Email Configuration
|
||||
|
||||
Resend integration will:
|
||||
- Store API key in environment variable (not in code)
|
||||
- Support template-based emails
|
||||
- Handle failures gracefully with logging
|
||||
- Rate limit appropriately
|
||||
|
||||
### Development vs Production
|
||||
|
||||
- **Development**: Flask development server, SQLite in local file
|
||||
- **Production**: Gunicorn with multiple workers, SQLite in mounted volume, proper logging
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Database Alternatives
|
||||
|
||||
**PostgreSQL**: More scalable but requires separate database container/service, significantly complicating self-hosting. Overkill for target scale.
|
||||
|
||||
**MySQL/MariaDB**: Same drawbacks as PostgreSQL for this use case.
|
||||
|
||||
### Job Queue Alternatives
|
||||
|
||||
**Celery + Redis**: More robust job processing but requires Redis container, significantly complicating deployment. Overkill for reminder emails and daily data purging tasks.
|
||||
|
||||
**Cron + separate script**: Could work but fragments application logic and complicates deployment.
|
||||
|
||||
### Email Service Alternatives
|
||||
|
||||
**SendGrid**: Viable alternative but more complex API and pricing structure.
|
||||
|
||||
**Amazon SES**: Requires AWS account and more complex setup. Higher barrier for self-hosters.
|
||||
|
||||
**SMTP**: Requires users to configure their own SMTP server, significantly increasing setup complexity and deliverability issues.
|
||||
|
||||
### Frontend Alternatives
|
||||
|
||||
**React/Vue SPA**: Considered but rejected. Would require build tooling, increase deployment complexity, and provide minimal benefit for the application's relatively simple UI needs.
|
||||
|
||||
**HTMX**: Considered for progressive enhancement. May be added later but not required for MVP.
|
||||
|
||||
## References
|
||||
|
||||
- Flask documentation: https://flask.palletsprojects.com/
|
||||
- SQLite documentation: https://www.sqlite.org/docs.html
|
||||
- Resend documentation: https://resend.com/docs
|
||||
- APScheduler documentation: https://apscheduler.readthedocs.io/
|
||||
- uv documentation: https://docs.astral.sh/uv/
|
||||
298
docs/decisions/0002-authentication-strategy.md
Normal file
298
docs/decisions/0002-authentication-strategy.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# 0002. Authentication Strategy
|
||||
|
||||
Date: 2025-12-22
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Sneaky Klaus has two distinct user types with different authentication needs:
|
||||
|
||||
1. **Administrator**: Single admin account for entire installation. Needs persistent access to manage exchanges. Must be able to recover access if password is forgotten.
|
||||
|
||||
2. **Participants**: Multiple participants across multiple exchanges. Should have frictionless authentication without password management burden. Same participant may join multiple exchanges using same email.
|
||||
|
||||
Key requirements:
|
||||
|
||||
- **Security**: Authentication must be secure and follow best practices
|
||||
- **Simplicity for participants**: No password required; minimal friction to access information
|
||||
- **Admin control**: Admin needs traditional authenticated session for management tasks
|
||||
- **Password recovery**: Admin must be able to recover access via email
|
||||
- **Session management**: Sessions should persist appropriately but expire for security
|
||||
- **Email verification**: Participant email addresses must be verified (implicit via magic link)
|
||||
|
||||
## Decision
|
||||
|
||||
We will implement a **dual authentication strategy**:
|
||||
|
||||
### Admin Authentication: Password-Based
|
||||
|
||||
**Login Flow**:
|
||||
1. Admin enters email and password
|
||||
2. Password hashed with bcrypt, compared to stored hash
|
||||
3. On success, session created with admin role
|
||||
4. Session cookie set with appropriate security flags
|
||||
|
||||
**Password Requirements**:
|
||||
- Minimum 12 characters
|
||||
- No complexity requirements (no mandatory special chars, numbers, etc.)
|
||||
- This follows modern NIST guidance: length matters more than complexity
|
||||
|
||||
**Password Recovery Flow**:
|
||||
1. Admin requests password reset from login page
|
||||
2. System sends time-limited reset token (1 hour expiration) to admin email
|
||||
3. Reset link directs to password reset form
|
||||
4. Token validated, new password set
|
||||
5. Token invalidated after single use
|
||||
|
||||
**Session Management**:
|
||||
- Server-side sessions stored in database or cache
|
||||
- 7-day sliding expiration window (extends on activity)
|
||||
- Secure, HTTP-only session cookies
|
||||
- SameSite=Lax for CSRF protection
|
||||
- Logout explicitly destroys session
|
||||
|
||||
### Participant Authentication: Magic Links
|
||||
|
||||
**Magic Link Flow**:
|
||||
1. Participant requests access (from registration page or email)
|
||||
2. System generates cryptographically random token (256-bit)
|
||||
3. Token stored in database with 1-hour expiration
|
||||
4. Email sent with magic link: `/participant/auth/{token}`
|
||||
5. Clicking link validates token and creates session
|
||||
6. Token invalidated after single use
|
||||
|
||||
**Session Management**:
|
||||
- Server-side sessions stored in database
|
||||
- 7-day sliding expiration window (extends on activity)
|
||||
- Secure, HTTP-only session cookies
|
||||
- SameSite=Lax for CSRF protection
|
||||
- Sessions scoped to participant's exchanges only
|
||||
- No explicit logout needed (session expires naturally)
|
||||
|
||||
**Token Generation**:
|
||||
- Use Python's `secrets` module for cryptographic randomness
|
||||
- Tokens are 32-byte random values, URL-safe base64 encoded
|
||||
- Tokens stored as hashed values in database (using SHA-256)
|
||||
- Original token never stored in plain text
|
||||
|
||||
### Security Measures
|
||||
|
||||
**Password Storage**:
|
||||
- bcrypt with cost factor 12 (adjustable)
|
||||
- Passwords never logged or exposed in error messages
|
||||
- Password reset tokens hashed before storage
|
||||
|
||||
**Session Security**:
|
||||
- Session IDs are cryptographically random
|
||||
- Sessions stored server-side (not client-side JWTs)
|
||||
- Session data includes: user ID, role (admin/participant), creation time, last activity
|
||||
- Cookie flags: `Secure=True` (HTTPS only), `HttpOnly=True`, `SameSite=Lax`
|
||||
|
||||
**Rate Limiting**:
|
||||
- Login attempts: 5 per email per 15 minutes
|
||||
- Magic link requests: 3 per email per hour
|
||||
- Password reset requests: 3 per email per hour
|
||||
- Implemented at application level, tracked in database or cache
|
||||
|
||||
**Token Expiration**:
|
||||
- Magic link tokens: 1 hour
|
||||
- Password reset tokens: 1 hour
|
||||
- Admin sessions: 7 days (sliding window)
|
||||
- Participant sessions: 7 days (sliding window)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Participant convenience**: No password to remember; access via email
|
||||
- **Email verification**: Magic links implicitly verify participant email addresses
|
||||
- **Admin security**: Traditional password-based auth provides familiar security model
|
||||
- **Password recovery**: Admin can self-serve password reset without external support
|
||||
- **Sliding sessions**: Activity extends session, reducing re-authentication friction
|
||||
- **Security best practices**: Modern password requirements (length over complexity)
|
||||
- **CSRF protection**: SameSite cookies prevent cross-site request forgery
|
||||
- **Token security**: One-time-use tokens prevent replay attacks
|
||||
|
||||
### Negative
|
||||
|
||||
- **Email dependency**: Magic links require working email delivery (mitigated by Resend reliability)
|
||||
- **Token expiration UX**: 1-hour expiration may frustrate slow email checkers (acceptable trade-off for security)
|
||||
- **Session storage**: Server-side sessions require database/cache storage (minimal overhead)
|
||||
- **No remember-me for admin**: 7-day max session requires re-login (acceptable for security)
|
||||
|
||||
### Neutral
|
||||
|
||||
- **Dual auth complexity**: Maintaining two auth flows adds implementation complexity (necessary for different user needs)
|
||||
- **Rate limiting overhead**: Requires tracking attempts per user (minimal performance impact)
|
||||
- **Session cleanup**: Expired sessions must be periodically purged (handled via background job)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Database Schema
|
||||
|
||||
**Admin User**:
|
||||
```python
|
||||
class Admin(Model):
|
||||
id: int
|
||||
email: str (unique, indexed)
|
||||
password_hash: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
```
|
||||
|
||||
**Participant** (simplified for auth):
|
||||
```python
|
||||
class Participant(Model):
|
||||
id: int
|
||||
email: str (indexed)
|
||||
exchange_id: int (foreign key)
|
||||
# ... other fields
|
||||
```
|
||||
|
||||
**Session**:
|
||||
```python
|
||||
class Session(Model):
|
||||
id: str (session ID, primary key)
|
||||
user_id: int
|
||||
user_type: str ('admin' | 'participant')
|
||||
created_at: datetime
|
||||
last_activity: datetime
|
||||
expires_at: datetime
|
||||
data: JSON (optional additional session data)
|
||||
```
|
||||
|
||||
**Auth Token** (magic links and password reset):
|
||||
```python
|
||||
class AuthToken(Model):
|
||||
id: int
|
||||
token_hash: str (indexed)
|
||||
token_type: str ('magic_link' | 'password_reset')
|
||||
email: str
|
||||
participant_id: int (nullable, for magic links)
|
||||
exchange_id: int (nullable, for magic links)
|
||||
created_at: datetime
|
||||
expires_at: datetime
|
||||
used_at: datetime (nullable)
|
||||
```
|
||||
|
||||
**Rate Limit**:
|
||||
```python
|
||||
class RateLimit(Model):
|
||||
id: int
|
||||
key: str (e.g., "login:admin@example.com", indexed)
|
||||
attempts: int
|
||||
window_start: datetime
|
||||
expires_at: datetime
|
||||
```
|
||||
|
||||
### Flask Session Configuration
|
||||
|
||||
```python
|
||||
app.config['SESSION_TYPE'] = 'sqlalchemy' # Server-side sessions
|
||||
app.config['SESSION_PERMANENT'] = True
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
||||
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = True # Sliding window
|
||||
```
|
||||
|
||||
### Authentication Decorators
|
||||
|
||||
```python
|
||||
@login_required # Requires any authenticated user
|
||||
@admin_required # Requires admin role
|
||||
@participant_required # Requires participant role
|
||||
```
|
||||
|
||||
### URL Structure
|
||||
|
||||
**Admin**:
|
||||
- `/admin/login` - Login form
|
||||
- `/admin/logout` - Logout
|
||||
- `/admin/forgot-password` - Request password reset
|
||||
- `/admin/reset-password/{token}` - Reset password form
|
||||
|
||||
**Participant**:
|
||||
- `/participant/auth/{token}` - Magic link endpoint
|
||||
- `/participant/logout` - Optional logout
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### OAuth/Social Login
|
||||
|
||||
**Rejected**: Adds external dependencies, complicates self-hosting, and provides minimal benefit for a self-hosted application where users control the deployment.
|
||||
|
||||
### JWT Tokens
|
||||
|
||||
**Rejected for sessions**: JWTs are stateless, making them difficult to invalidate (e.g., on logout or security incident). Server-side sessions provide better control.
|
||||
|
||||
**Considered for magic links**: Could use JWTs for magic links, but custom tokens are simpler and equally secure.
|
||||
|
||||
### Passkeys/WebAuthn
|
||||
|
||||
**Deferred**: Modern and secure but adds implementation complexity. Could be added in future version for admin auth.
|
||||
|
||||
### Email Verification Codes
|
||||
|
||||
**Rejected**: 6-digit codes are less secure than magic links and require users to manually copy/paste, reducing convenience.
|
||||
|
||||
### Participant Passwords
|
||||
|
||||
**Rejected**: Violates core principle of frictionless participant experience. Participants joining Secret Santa events shouldn't need to manage yet another password.
|
||||
|
||||
### Longer Magic Link Expiration
|
||||
|
||||
**Rejected**: 1 hour balances security with usability. Longer expiration increases risk if email account is compromised.
|
||||
|
||||
### Shorter Session Duration
|
||||
|
||||
**Considered**: 24-hour sessions would be more secure but require frequent re-authentication. 7-day sliding window balances security with convenience.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Password Reset Token Timing Attack
|
||||
|
||||
To prevent email enumeration via timing attacks:
|
||||
- Always show "If an account exists, you'll receive an email" message
|
||||
- Perform same-time operations regardless of email existence
|
||||
- Don't reveal whether email is registered
|
||||
|
||||
### Magic Link Security
|
||||
|
||||
- Tokens are single-use and time-limited
|
||||
- Token hashing prevents database compromise from exposing valid tokens
|
||||
- Rate limiting prevents brute force token guessing
|
||||
- Tokens scoped to specific participant and exchange
|
||||
|
||||
### Session Fixation Prevention
|
||||
|
||||
- New session ID generated on login
|
||||
- Old session destroyed on logout
|
||||
- Session ID rotated on privilege elevation
|
||||
|
||||
### Brute Force Protection
|
||||
|
||||
- Rate limiting on all auth endpoints
|
||||
- Progressive delays on repeated failures (optional enhancement)
|
||||
- Account lockout not implemented (single admin, participant magic links)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for future versions:
|
||||
|
||||
1. **Admin 2FA**: Time-based OTP for additional admin security
|
||||
2. **Passkeys**: WebAuthn support for passwordless admin auth
|
||||
3. **Session device tracking**: Show admin active sessions and allow revocation
|
||||
4. **Remember-me for admin**: Optional extended session with re-authentication for sensitive actions
|
||||
5. **Magic link preview protection**: Use confirmation step before activating magic link
|
||||
|
||||
## References
|
||||
|
||||
- NIST Password Guidelines: https://pages.nist.gov/800-63-3/sp800-63b.html
|
||||
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
|
||||
- Flask Session Management: https://flask.palletsprojects.com/en/latest/quickstart/#sessions
|
||||
- Python secrets module: https://docs.python.org/3/library/secrets.html
|
||||
Reference in New Issue
Block a user