Update project files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ANDREW HOSFORD
2026-04-07 11:59:48 -05:00
parent dd227be9b0
commit 0e8978f48c
50 changed files with 5878 additions and 241 deletions

302
.planning/research/STACK.md Normal file
View File

@@ -0,0 +1,302 @@
# Stack Research: Multi-Tenancy & User Accounts
**Domain:** Requirements management tool with multi-user support
**Researched:** 2026-03-18
**Confidence:** HIGH
## Executive Summary
Adding user accounts, invite-only registration, RBAC, and tenant isolation to your existing FastAPI + SQLAlchemy + SQLite stack requires minimal new dependencies. The core stack remains unchanged - only authentication primitives need upgrading from shared password to individual accounts.
**Key additions:**
- **pwdlib** with Argon2 for password hashing (replaces bcrypt/passlib)
- **email-validator** for email validation in Pydantic models
- **secrets** module (stdlib) for invite token generation
- **Tenant isolation** via SQLAlchemy query filters (no library needed)
**No changes needed:** Your existing FastAPI, SQLAlchemy 2.0, SQLite, PyJWT, and Angular 17 stack remain unchanged.
## Recommended Stack Additions
### Authentication & Security
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| pwdlib | 0.4.0+ | Password hashing with Argon2 | Modern replacement for passlib (deprecated in Python 3.13). FastAPI official docs now recommend pwdlib. Argon2 is GPU-resistant and OWASP-recommended for 2026. |
| email-validator | 2.2.0+ | Email validation for user registration | Required by Pydantic's EmailStr type. Standard for email validation with deliverability checks. |
| secrets | stdlib | Secure invite token generation | Python standard library (3.6+). Cryptographically secure token generation with 256-bit randomness. Use `secrets.token_urlsafe(32)` for invite links. |
### NO Library Needed (Use Patterns)
| Capability | Implementation | Why No Library |
|------------|----------------|----------------|
| Multi-tenancy | SQLAlchemy query filters with `user_id` FK | Simple pattern - add `user_id` column to all tables, filter queries by current user. PostgreSQL Row-Level Security (RLS) not available in SQLite. |
| RBAC | Enum + dependency injection | Three roles (viewer/editor/owner) fit in simple Enum. Use FastAPI Depends() to check role on routes. Libraries like fastapi-user-auth add unnecessary complexity for 3 roles. |
| Edit attribution | Add `modified_by_user_id` column | You already track edit history - just add user reference to existing `requirement_history` table. |
| Email sending | Python `smtplib` (stdlib) | Built-in library sufficient for invite emails. Use FastAPI BackgroundTasks for async sending. No need for Celery yet. |
## Unchanged Core Stack
Your existing stack is already optimal for this use case:
| Technology | Version | Status | Notes |
|------------|---------|--------|-------|
| FastAPI | Current | ✓ Keep | Dependency injection perfect for RBAC |
| SQLAlchemy | 2.0+ | ✓ Keep | Event listeners support tenant filtering |
| SQLite | 3.x | ✓ Keep | WAL mode supports concurrent readers + 1 writer (sufficient for small team) |
| PyJWT | 2.x | ✓ Keep | Already used for JWT - extend payload to include user_id and role |
| Angular | 17 | ✓ Keep | Frontend unchanged - JWT payload has new fields |
| Pydantic | 2.x | ✓ Keep | Add email-validator extra for EmailStr |
## Installation
```bash
# Backend additions only
pip install "pwdlib[argon2]>=0.4.0"
pip install "pydantic[email]>=2.0"
# No changes to frontend
```
## Integration Patterns
### 1. Password Hashing (pwdlib + Argon2)
```python
from pwdlib import PasswordHash
# Initialize once at startup
password_hash = PasswordHash.recommended()
# Hash password on registration
hashed = password_hash.hash("user_password")
# Verify on login
is_valid = password_hash.verify("user_password", hashed)
```
**Why this pattern:** FastAPI docs migrated from passlib to pwdlib in 2026. Argon2 is more secure than bcrypt and future-proof.
### 2. Email Validation (Pydantic EmailStr)
```python
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr # Automatically validated
password: str
```
**Why this pattern:** Pydantic's EmailStr uses email-validator under the hood. No manual validation needed.
### 3. Invite Token Generation (secrets)
```python
import secrets
from datetime import datetime, timedelta
# Generate invite token
invite_token = secrets.token_urlsafe(32) # 256 bits
expires_at = datetime.utcnow() + timedelta(hours=24)
# Store in database
invite = Invite(token=invite_token, email=email, expires_at=expires_at)
```
**Why this pattern:** Standard library, cryptographically secure, URL-safe for email links.
### 4. Multi-Tenancy (SQLAlchemy Filters)
```python
# Add to all models
class RequirementNode(Base):
__tablename__ = "requirement_nodes"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# ... other columns
# Filter queries by current user
def get_requirements(db: Session, current_user: User):
return db.query(RequirementNode).filter(
RequirementNode.user_id == current_user.id
).all()
```
**Why this pattern:** SQLite doesn't support PostgreSQL's Row-Level Security. Manual filtering is explicit and debuggable. Add SQLAlchemy event listener for automatic filtering if needed later.
### 5. RBAC (Enum + Dependency Injection)
```python
from enum import Enum
from fastapi import Depends, HTTPException
class Role(str, Enum):
VIEWER = "viewer"
EDITOR = "editor"
OWNER = "owner"
def require_role(required: Role):
def check_role(current_user: User = Depends(get_current_user)):
if current_user.role not in [required, Role.OWNER]: # Owner has all permissions
raise HTTPException(status_code=403, detail="Insufficient permissions")
return current_user
return check_role
# Use in routes
@app.delete("/requirements/{id}")
async def delete_requirement(
id: int,
current_user: User = Depends(require_role(Role.EDITOR))
):
...
```
**Why this pattern:** FastAPI's Depends() makes role checks declarative. No library needed for 3 roles.
### 6. JWT Payload Extension
```python
# Extend existing JWT to include user info
def create_access_token(user: User) -> str:
payload = {
"sub": str(user.id),
"email": user.email,
"role": user.role.value,
"exp": datetime.utcnow() + timedelta(hours=24)
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
```
**Why this pattern:** Minimal change to existing JWT implementation. Frontend receives role for UI decisions.
## What NOT to Add
| Avoid | Why | Use Instead |
|-------|-----|-------------|
| FastAPI-Users | Over-engineered for your use case. Brings OAuth, 2FA, cookie auth you don't need. | Custom auth with pwdlib + PyJWT (you already have JWT logic) |
| Alembic | SQLite migrations are low-risk. You're self-hosted single-user currently. | Direct SQL migration scripts for v2 upgrade |
| Celery / Redis | Overkill for invite emails. Adds deployment complexity. | FastAPI BackgroundTasks + smtplib |
| SQLAlchemy-History | You already have `requirement_history` table with snapshots. | Add `modified_by_user_id` FK to existing table |
| PostgreSQL RLS libraries | SQLite doesn't support Row-Level Security. | Manual SQLAlchemy query filters |
| Casbin (RBAC library) | Policy engine for 100+ permission combinations. You have 3 roles. | Python Enum + FastAPI Depends() |
## Database Schema Changes
### New Tables
```sql
-- Users table
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
hashed_password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'viewer',
is_active BOOLEAN NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Invite tokens table
CREATE TABLE invites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT UNIQUE NOT NULL,
email TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'viewer',
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP,
created_by INTEGER REFERENCES users(id)
);
```
### Modified Tables
Add `user_id` FK to existing tables:
- `requirement_nodes` - owner of requirement
- `cross_pillar_links` - who created link
- `projects` - project owner
- `requirement_history` - who made the change (`modified_by_user_id`)
- `graph_layouts` - who saved layout
**Migration strategy:** Existing data gets assigned to "system" user (id=1) created during migration.
## SQLite Concurrency Considerations
### WAL Mode (Already Recommended)
```python
# Ensure WAL mode is enabled
engine = create_engine(
"sqlite:///data/planner.db",
connect_args={"check_same_thread": False},
echo=False
)
with engine.connect() as conn:
conn.execute(text("PRAGMA journal_mode=WAL"))
```
**Capabilities:**
- Multiple concurrent readers (unlimited)
- One writer at a time (sufficient for small team collaboration)
- Writers don't block readers
**Limitations:**
- No true concurrent writes (BEGIN CONCURRENT is experimental, not production-ready)
- Single writer serialization means write-heavy workloads may see SQLITE_BUSY errors
**When to migrate to PostgreSQL:** If you exceed ~10 concurrent users with frequent writes (requirements updates). For now, SQLite + WAL handles your use case.
## Version Compatibility Matrix
| Package | Version | Compatible With | Notes |
|---------|---------|-----------------|-------|
| pwdlib | 0.4.0+ | Python 3.9+ | Requires `pwdlib[argon2]` extra for Argon2 support |
| email-validator | 2.2.0+ | Pydantic 2.x | Required by `pydantic[email]` extra |
| PyJWT | 2.8.0+ | FastAPI 0.100+ | Already in your stack - no changes needed |
| SQLAlchemy | 2.0.0+ | Python 3.12+ | Already in your stack - no changes needed |
| Pydantic | 2.0+ | FastAPI 0.100+ | Requires email-validator for EmailStr |
## Migration Checklist
- [ ] Install pwdlib and email-validator
- [ ] Create `users` and `invites` tables
- [ ] Add `user_id` FK to all existing tables
- [ ] Migrate existing data to "system" user
- [ ] Replace shared password logic with user registration/login
- [ ] Add role-based route decorators
- [ ] Update JWT payload to include user_id and role
- [ ] Add query filters for tenant isolation
- [ ] Update frontend to handle user-specific data
- [ ] Test multi-user scenarios with WAL mode
## Security Checklist
- [ ] Use `pwdlib.PasswordHash.recommended()` for Argon2 defaults
- [ ] Validate all user inputs with Pydantic (EmailStr, password length)
- [ ] Use `secrets.token_urlsafe(32)` for invite tokens (256-bit)
- [ ] Set invite token expiration (24 hours recommended)
- [ ] Filter ALL queries by `user_id` (tenant isolation)
- [ ] Use FastAPI Depends() for role checks on protected routes
- [ ] Set JWT expiration (24 hours recommended)
- [ ] Enable SQLite WAL mode for concurrent access
- [ ] Rate limit registration and login endpoints (future: middleware)
- [ ] Use HTTPS in production (Caddy/nginx reverse proxy)
## Sources
**HIGH Confidence (Official Documentation):**
- [FastAPI OAuth2 with JWT](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/) - Official security tutorial
- [pwdlib introduction](https://github.com/frankie567/pwdlib/discussions/1) - Modern password hashing
- [Python secrets module](https://docs.python.org/3/library/secrets.html) - Secure token generation
- [Pydantic email validation](https://docs.pydantic.dev/latest/install/) - EmailStr documentation
- [SQLite WAL mode](https://www.sqlite.org/wal.html) - Official SQLite concurrency documentation
**MEDIUM Confidence (Industry Best Practices 2026):**
- [TestDriven.io FastAPI JWT Auth](https://testdriven.io/blog/fastapi-jwt-auth/) - Production patterns
- [FastAPI RBAC Tutorial](https://www.permit.io/blog/fastapi-rbac-full-implementation-tutorial) - Role-based access control
- [Tenant Isolation with SQLAlchemy](https://personal-web-9c834.web.app/blog/pg-tenant-isolation/) - Multi-tenancy patterns
- [The New Way To Generate Secure Tokens](https://blog.miguelgrinberg.com/post/the-new-way-to-generate-secure-tokens-in-python) - Token generation security
- [FastAPI Dependency Injection](https://fastapi.tiangolo.com/tutorial/security/get-current-user/) - Current user pattern
---
*Stack research for: req-planner v2.0 Multi-Tenancy & User Accounts*
*Researched: 2026-03-18*
*Confidence: HIGH - All recommendations verified against official docs and 2026 best practices*