# S02: [A] Creator Authentication + Dashboard Shell

**Goal:** Creator registers with invite code, logs in with JWT, sees dashboard shell with nav and profile settings. Existing public routes remain unaffected.
**Demo:** After this: Creator registers with invite code, logs in, sees dashboard shell with nav and profile settings

## Tasks
- [x] **T01: Added User/InviteCode models, Alembic migration 016, auth utilities (bcrypt hashing, JWT, FastAPI dependencies), and Pydantic auth schemas** — Create the User and InviteCode SQLAlchemy models in backend/models.py, generate Alembic migration 016, add PyJWT + passlib[bcrypt] to requirements.txt, and implement the auth utility module (password hashing, JWT encode/decode, FastAPI get_current_user dependency).

## Steps

1. Add `PyJWT>=2.8,<3.0` and `passlib[bcrypt]>=1.7,<2.0` to `backend/requirements.txt` and run `pip install -r backend/requirements.txt`.
2. In `backend/models.py`, add a `UserRole` enum (creator, admin) and the `User` model: id (UUID PK), email (unique, not null), hashed_password, display_name, role (UserRole, default creator), creator_id (FK to creators.id, nullable), is_active (default true), created_at, updated_at. Add the `InviteCode` model: id (UUID PK), code (unique, not null), uses_remaining (default 1), created_by (FK to users.id, nullable), expires_at (nullable), created_at.
3. In `backend/schemas.py`, add Pydantic schemas: `RegisterRequest` (email, password, display_name, invite_code, optional creator_slug), `LoginRequest` (email, password), `TokenResponse` (access_token, token_type), `UserResponse` (id, email, display_name, role, creator_id, is_active, created_at), `UpdateProfileRequest` (optional display_name, optional current_password, optional new_password).
4. Create `backend/auth.py` with: `hash_password(plain) -> str` using passlib bcrypt, `verify_password(plain, hashed) -> bool`, `create_access_token(user_id, role) -> str` using PyJWT HS256 with `settings.app_secret_key` and configurable expiry, `decode_access_token(token) -> dict` with expiry validation, `get_current_user` FastAPI dependency using `OAuth2PasswordBearer(tokenUrl='/api/v1/auth/login')` that decodes token and loads User from DB, `require_role(role)` sub-dependency.
5. Generate Alembic migration: `cd backend && alembic revision --autogenerate -m 'add users and invite_codes'`. Review the generated migration to ensure it creates both tables with correct constraints.

## Must-Haves

- [ ] User model with email, hashed_password, display_name, role enum, creator_id FK, is_active
- [ ] InviteCode model with code, uses_remaining, created_by FK, expires_at
- [ ] Alembic migration 016 creates both tables
- [ ] auth.py exports hash_password, verify_password, create_access_token, decode_access_token, get_current_user, require_role
- [ ] Pydantic request/response schemas for register, login, token, user profile, update profile
- [ ] PyJWT and passlib[bcrypt] in requirements.txt

## Failure Modes

| Dependency | On error | On timeout | On malformed response |
|------------|----------|-----------|----------------------|
| PostgreSQL (migration) | Alembic prints traceback — fix SQL/model and retry | N/A | N/A |
| passlib bcrypt | ImportError at import time — verify pip install | N/A | N/A |

## Negative Tests

- **Malformed inputs**: decode_access_token with expired token raises, with garbage string raises, with missing claims raises
- **Boundary conditions**: InviteCode with uses_remaining=0 should not be usable

## Verification

- `cd backend && python -c "from models import User, InviteCode, UserRole; print('Models OK')"` exits 0
- `cd backend && python -c "from auth import hash_password, verify_password, create_access_token, decode_access_token; print('Auth OK')"` exits 0
- `cd backend && python -c "from schemas import RegisterRequest, LoginRequest, TokenResponse, UserResponse, UpdateProfileRequest; print('Schemas OK')"` exits 0
- Alembic migration file exists in `alembic/versions/` with 'users' and 'invite_codes' in the upgrade function
  - Estimate: 1h
  - Files: backend/models.py, backend/schemas.py, backend/auth.py, backend/config.py, backend/requirements.txt, alembic/versions/016_add_users_and_invite_codes.py
  - Verify: cd backend && python -c "from models import User, InviteCode, UserRole; print('OK')" && python -c "from auth import hash_password, verify_password, create_access_token, decode_access_token; print('OK')" && python -c "from schemas import RegisterRequest, LoginRequest, TokenResponse, UserResponse; print('OK')"
- [x] **T02: Implemented auth API router with register/login/me/update-profile endpoints, seed invite code function, auth test fixtures, and 20 passing integration tests** — Create the auth API router with all endpoints (register, login, me, update profile), register it in main.py, seed initial invite codes, and write comprehensive integration tests.

## Steps

1. Create `backend/routers/auth.py` with APIRouter prefix `/api/v1/auth`:
   - `POST /register`: Validate invite code (exists, uses_remaining > 0, not expired), check email uniqueness, hash password, create User, decrement invite code uses_remaining, optionally link creator_id by slug. Return 201 + UserResponse. Errors: 403 invalid/expired code, 409 duplicate email.
   - `POST /login`: Find user by email, verify password, return TokenResponse with JWT. Errors: 401 invalid credentials.
   - `GET /me`: Requires auth (get_current_user dependency). Return UserResponse.
   - `PUT /me`: Requires auth. Accept UpdateProfileRequest — update display_name if provided, verify current_password and set new_password if provided. Return updated UserResponse.
2. In `backend/main.py`, import and include the auth router.
3. Create a seed invite code mechanism: add a function `seed_invite_codes()` in `backend/auth.py` that creates a default invite code if none exist (code='CHRYSOPEDIA-ALPHA-2026', uses_remaining=100). Call it from a startup event or CLI command. For the test suite, create codes in fixtures.
4. Update `backend/tests/conftest.py`: import User, InviteCode, UserRole into the model imports so they're included in `Base.metadata.create_all`. Add an `invite_code` fixture that creates a test invite code. Add a `registered_user` fixture that creates a user via the register endpoint. Add an `auth_headers` fixture that logs in and returns `{"Authorization": "Bearer <token>"}`.
5. Create `backend/tests/test_auth.py` with integration tests:
   - Register with valid invite code → 201 + user created in DB
   - Register with invalid invite code → 403
   - Register with expired invite code → 403
   - Register duplicate email → 409
   - Login with correct credentials → 200 + JWT token
   - Login with wrong password → 401
   - Login with nonexistent email → 401
   - GET /me with valid token → 200 + user profile
   - GET /me with expired/invalid token → 401
   - GET /me without token → 401
   - PUT /me updates display_name → 200 + updated profile
   - PUT /me changes password → 200, can login with new password
   - Existing public endpoints still work without auth (GET /api/v1/techniques, GET /api/v1/creators)

## Must-Haves

- [ ] POST /register with invite code validation, email uniqueness, password hashing
- [ ] POST /login returning JWT
- [ ] GET /me and PUT /me with auth dependency
- [ ] Auth router registered in main.py
- [ ] All 13 integration tests pass
- [ ] Existing public endpoints verified unaffected

## Failure Modes

| Dependency | On error | On timeout | On malformed response |
|------------|----------|-----------|----------------------|
| PostgreSQL (test DB) | Test fails with connection error — check TEST_DATABASE_URL | N/A | N/A |
| bcrypt hashing | Slow on first call (key derivation) — acceptable for test suite | N/A | N/A |

## Negative Tests

- **Malformed inputs**: Register with missing fields → 422, Register with empty password → 422, Login with empty body → 422
- **Error paths**: Invalid invite code → 403, expired invite code → 403, duplicate email → 409, wrong password → 401, invalid JWT → 401, expired JWT → 401
- **Boundary conditions**: Invite code with uses_remaining=1 decrements to 0 and becomes unusable, invite code with uses_remaining=0 rejected

## Verification

- `cd backend && python -m pytest tests/test_auth.py -v` — all tests pass
- `cd backend && python -m pytest tests/test_public_api.py -v` — existing tests still pass (no auth regression)
  - Estimate: 1.5h
  - Files: backend/routers/auth.py, backend/main.py, backend/auth.py, backend/tests/conftest.py, backend/tests/test_auth.py
  - Verify: cd backend && python -m pytest tests/test_auth.py -v && python -m pytest tests/test_public_api.py -v
- [x] **T03: Added AuthContext provider with JWT persistence, auth API client functions, Login and Register pages with CSS modules, and wired routes into App.tsx** — Create the React auth infrastructure: AuthContext provider with JWT persistence, API client auth functions, login page, register page, and wire AuthContext into App.tsx.

## Steps

1. Create `frontend/src/context/` directory.
2. Create `frontend/src/context/AuthContext.tsx`: React context providing `user` (UserResponse | null), `token` (string | null), `isAuthenticated` (boolean), `login(email, password)` (calls API, stores token in localStorage, fetches /me to populate user), `register(data)` (calls API, returns success/error), `logout()` (clears token + user from state and localStorage). On mount, check localStorage for existing token and call /me to rehydrate session. Export `AuthProvider` component and `useAuth()` hook.
3. Add auth API functions to `frontend/src/api/public-client.ts`:
   - `authRegister(data: RegisterRequest): Promise<UserResponse>` — POST /api/v1/auth/register
   - `authLogin(email: string, password: string): Promise<TokenResponse>` — POST /api/v1/auth/login
   - `authGetMe(token: string): Promise<UserResponse>` — GET /api/v1/auth/me with Authorization header
   - `authUpdateProfile(token: string, data: UpdateProfileRequest): Promise<UserResponse>` — PUT /api/v1/auth/me
   - Add TypeScript interfaces: `RegisterRequest`, `LoginRequest`, `TokenResponse`, `UserResponse`, `UpdateProfileRequest`
   - Modify `request<T>` to automatically inject Authorization header from localStorage when token exists
4. Create `frontend/src/pages/Login.tsx` (CSS module: `Login.module.css`):
   - Email + password form with validation (required fields, email format)
   - Error display for invalid credentials
   - Submit calls `useAuth().login()`, redirects to /creator/dashboard on success
   - Link to register page
   - Uses `useDocumentTitle('Login')` for consistent page titles
5. Create `frontend/src/pages/Register.tsx` (CSS module: `Register.module.css`):
   - Invite code + email + password + confirm password + display name form
   - Client-side validation: required fields, email format, password match, min password length (8 chars)
   - Error display for API errors (invalid code, email taken)
   - Submit calls `useAuth().register()`, redirects to /login with success message on completion
   - Uses `useDocumentTitle('Register')`
6. Wrap `<App />` content with `<AuthProvider>` in App.tsx. Add Route entries for `/login` and `/register`.

## Must-Haves

- [ ] AuthContext with login, register, logout, session rehydration from localStorage
- [ ] API auth functions with TypeScript types in public-client.ts
- [ ] request<T> injects Authorization header when token exists
- [ ] Login page with email/password form, error handling, redirect on success
- [ ] Register page with invite code + full form, validation, error handling
- [ ] CSS modules for both pages (not App.css)
- [ ] Routes /login and /register added to App.tsx
- [ ] `npm run build` passes with zero errors

## Verification

- `cd frontend && npm run build` — zero TypeScript errors
- `grep -r 'AuthProvider' frontend/src/App.tsx` — context is wired
- `test -f frontend/src/context/AuthContext.tsx` — context file exists
- `test -f frontend/src/pages/Login.tsx && test -f frontend/src/pages/Register.tsx` — both pages exist
- `test -f frontend/src/pages/Login.module.css && test -f frontend/src/pages/Register.module.css` — CSS modules exist
  - Estimate: 1.5h
  - Files: frontend/src/context/AuthContext.tsx, frontend/src/api/public-client.ts, frontend/src/pages/Login.tsx, frontend/src/pages/Login.module.css, frontend/src/pages/Register.tsx, frontend/src/pages/Register.module.css, frontend/src/App.tsx
  - Verify: cd frontend && npm run build && test -f src/context/AuthContext.tsx && test -f src/pages/Login.tsx && test -f src/pages/Register.tsx
- [x] **T04: Added ProtectedRoute component, CreatorDashboard with sidebar nav, CreatorSettings with profile/password forms, protected routes in App.tsx, and auth-aware header nav** — Create the creator dashboard shell page with sidebar navigation, profile settings page, ProtectedRoute wrapper component, and update the main nav to show authenticated user state.

## Steps

1. Create `frontend/src/components/ProtectedRoute.tsx`: Wrapper component that checks `useAuth().isAuthenticated`. If not authenticated, redirects to `/login` with a `returnTo` search param preserving the intended destination. If authenticated, renders children.
2. Create `frontend/src/pages/CreatorDashboard.tsx` (CSS module: `CreatorDashboard.module.css`):
   - Two-column layout: left sidebar (240px) + main content area
   - Sidebar nav items: Dashboard (active), Content (placeholder, disabled), Settings (links to /creator/settings). Use NavLink for active state.
   - Main content: Welcome message with user's display_name from `useAuth().user`, placeholder cards for future analytics ("Content stats coming in M020")
   - Uses `useDocumentTitle('Creator Dashboard')`
   - Mobile: sidebar collapses to top horizontal nav or hamburger
3. Create `frontend/src/pages/CreatorSettings.tsx` (CSS module: `CreatorSettings.module.css`):
   - Same dashboard layout (sidebar + content) for navigation consistency
   - Profile section: display_name input (pre-filled), email display (read-only)
   - Password section: current password + new password + confirm new password
   - Submit calls `authUpdateProfile()` via AuthContext, shows success/error feedback
   - Uses `useDocumentTitle('Settings')`
4. In `frontend/src/App.tsx`, add protected routes:
   - `<Route path="/creator/dashboard" element={<ProtectedRoute><CreatorDashboard /></ProtectedRoute>} />`
   - `<Route path="/creator/settings" element={<ProtectedRoute><CreatorSettings /></ProtectedRoute>} />`
5. Update the header/nav in `frontend/src/App.tsx`:
   - When `useAuth().isAuthenticated`: show user display_name, link to /creator/dashboard, and Logout button
   - When not authenticated: show Login link (subtle, in nav area)
   - The existing AdminDropdown remains unchanged — it's a separate concern
6. Ensure all new CSS uses CSS modules. Follow the existing dark theme (use CSS custom properties from App.css where applicable). Dashboard sidebar should use `var(--color-surface-*) / var(--color-text-*)` tokens.

## Must-Haves

- [ ] ProtectedRoute redirects unauthenticated users to /login with returnTo param
- [ ] Dashboard shell with sidebar nav (Dashboard, Content placeholder, Settings)
- [ ] Profile settings page with display_name edit and password change
- [ ] Protected routes wired in App.tsx
- [ ] Nav shows auth state (logged in: name + dashboard + logout; logged out: login link)
- [ ] All CSS in CSS modules, using existing custom properties for dark theme consistency
- [ ] `npm run build` passes with zero errors

## Verification

- `cd frontend && npm run build` — zero TypeScript errors
- `test -f frontend/src/components/ProtectedRoute.tsx` — component exists
- `test -f frontend/src/pages/CreatorDashboard.tsx && test -f frontend/src/pages/CreatorSettings.tsx` — both pages exist
- `grep -q 'ProtectedRoute' frontend/src/App.tsx` — protected routes wired
- `grep -q 'useAuth' frontend/src/App.tsx` — nav auth state wired
  - Estimate: 1.5h
  - Files: frontend/src/components/ProtectedRoute.tsx, frontend/src/pages/CreatorDashboard.tsx, frontend/src/pages/CreatorDashboard.module.css, frontend/src/pages/CreatorSettings.tsx, frontend/src/pages/CreatorSettings.module.css, frontend/src/App.tsx
  - Verify: cd frontend && npm run build && test -f src/components/ProtectedRoute.tsx && test -f src/pages/CreatorDashboard.tsx && test -f src/pages/CreatorSettings.tsx && grep -q 'ProtectedRoute' src/App.tsx
