diff --git a/backend/project_plans/PHASE_2_AUTH.json b/backend/project_plans/PHASE_2_AUTH.json new file mode 100644 index 0000000..75fb0fd --- /dev/null +++ b/backend/project_plans/PHASE_2_AUTH.json @@ -0,0 +1,454 @@ +{ + "meta": { + "version": "1.0.0", + "created": "2026-01-27", + "lastUpdated": "2026-01-27", + "planType": "phase", + "phaseId": "PHASE_2", + "phaseName": "Authentication", + "description": "OAuth login (Google, Discord), JWT session management, user management, premium tier tracking", + "totalEstimatedHours": 24, + "totalTasks": 15, + "completedTasks": 0, + "masterPlan": "../PROJECT_PLAN_MASTER.json" + }, + + "goals": [ + "Implement OAuth 2.0 authentication with Google and Discord providers", + "Create JWT-based session management with access/refresh token pattern", + "Build user management service with create, read, update operations", + "Implement FastAPI dependencies for protected endpoints", + "Support account linking (multiple OAuth providers per user)", + "Track premium subscription status with expiration dates" + ], + + "architectureNotes": { + "tokenStrategy": { + "accessToken": "Short-lived JWT (30 min default), contains user_id", + "refreshToken": "Longer-lived JWT (7 days default), stored in Redis for revocation", + "storage": "Refresh tokens tracked in Redis for logout/revocation support" + }, + "oauthFlow": { + "pattern": "Authorization Code Flow with PKCE", + "callback": "Backend receives code, exchanges for tokens, creates/updates user", + "security": "Never store OAuth provider tokens, only OAuth ID" + }, + "accountLinking": { + "strategy": "Email-based matching", + "flow": "If user exists with same email, add OAuth provider to existing account" + }, + "existingInfrastructure": { + "config": "JWT and OAuth settings already in app/config.py Settings class", + "dependencies": "python-jose, passlib, bcrypt already installed", + "userModel": "User model with OAuth fields already in app/db/models/user.py" + } + }, + + "directoryStructure": { + "schemas": "backend/app/schemas/", + "services": "backend/app/services/", + "api": "backend/app/api/", + "tests": "backend/tests/api/, backend/tests/services/" + }, + + "tasks": [ + { + "id": "AUTH-001", + "name": "Create Pydantic schemas for auth", + "description": "Define request/response models for authentication flows", + "category": "critical", + "priority": 1, + "completed": false, + "dependencies": [], + "files": [ + {"path": "app/schemas/__init__.py", "status": "pending"}, + {"path": "app/schemas/auth.py", "status": "pending"}, + {"path": "app/schemas/user.py", "status": "pending"} + ], + "details": [ + "TokenPayload: sub (user_id), exp, iat, type (access/refresh)", + "TokenResponse: access_token, refresh_token, token_type, expires_in", + "UserResponse: id, email, display_name, avatar_url, is_premium, premium_until", + "UserCreate: internal model for user creation from OAuth", + "OAuthUserInfo: normalized structure for OAuth provider data", + "AccountLinkRequest: for linking additional OAuth providers" + ], + "estimatedHours": 1.5 + }, + { + "id": "AUTH-002", + "name": "Create JWT utilities service", + "description": "Functions for creating and verifying JWT tokens", + "category": "critical", + "priority": 2, + "completed": false, + "dependencies": ["AUTH-001"], + "files": [ + {"path": "app/services/jwt_service.py", "status": "pending"} + ], + "details": [ + "create_access_token(user_id: UUID) -> str - Uses settings.jwt_expire_minutes", + "create_refresh_token(user_id: UUID) -> str - Uses settings.jwt_refresh_expire_days", + "decode_token(token: str) -> TokenPayload - Validates and decodes", + "verify_token(token: str) -> UUID | None - Returns user_id or None if invalid", + "Uses python-jose with HS256 algorithm", + "All timing uses datetime.now(UTC) per project standards" + ], + "estimatedHours": 1.5 + }, + { + "id": "AUTH-003", + "name": "Create refresh token Redis storage", + "description": "Redis-based storage for refresh token tracking and revocation", + "category": "high", + "priority": 3, + "completed": false, + "dependencies": ["AUTH-002"], + "files": [ + {"path": "app/services/token_store.py", "status": "pending"} + ], + "details": [ + "Key format: refresh_token:{user_id}:{jti} -> expiration timestamp", + "store_refresh_token(user_id, jti, expires_at) - Store with TTL", + "is_token_valid(user_id, jti) -> bool - Check if not revoked", + "revoke_token(user_id, jti) - Delete specific token", + "revoke_all_user_tokens(user_id) - Logout from all devices", + "Uses existing Redis connection from app/db/redis.py" + ], + "estimatedHours": 1.5 + }, + { + "id": "AUTH-004", + "name": "Create UserService", + "description": "Service layer for user CRUD operations", + "category": "critical", + "priority": 4, + "completed": false, + "dependencies": ["AUTH-001"], + "files": [ + {"path": "app/services/user_service.py", "status": "pending"} + ], + "details": [ + "get_user_by_id(db, user_id: UUID) -> User | None", + "get_user_by_email(db, email: str) -> User | None", + "get_user_by_oauth(db, provider: str, oauth_id: str) -> User | None", + "create_user(db, user_data: UserCreate) -> User", + "update_last_login(db, user_id: UUID) -> None", + "link_oauth_provider(db, user_id: UUID, provider: str, oauth_id: str) -> User", + "update_premium_status(db, user_id: UUID, premium_until: datetime | None) -> User", + "All operations are async using SQLAlchemy async session" + ], + "estimatedHours": 2 + }, + { + "id": "AUTH-005", + "name": "Create OAuthLinkedAccount model", + "description": "Database model for multiple OAuth providers per user (account linking)", + "category": "high", + "priority": 5, + "completed": false, + "dependencies": [], + "files": [ + {"path": "app/db/models/oauth_account.py", "status": "pending"}, + {"path": "app/db/migrations/versions/xxx_add_oauth_accounts.py", "status": "pending"} + ], + "details": [ + "Fields: id, user_id (FK), provider, oauth_id, linked_at", + "Unique constraint on (provider, oauth_id)", + "Migrate existing User.oauth_provider/oauth_id to this table", + "Keep User.oauth_provider/oauth_id as 'primary' for backward compat", + "Relationship: User.linked_accounts -> list[OAuthLinkedAccount]" + ], + "estimatedHours": 2 + }, + { + "id": "AUTH-006", + "name": "Create Google OAuth service", + "description": "Handle Google OAuth authorization code flow", + "category": "critical", + "priority": 6, + "completed": false, + "dependencies": ["AUTH-004"], + "files": [ + {"path": "app/services/oauth/google.py", "status": "pending"}, + {"path": "app/services/oauth/__init__.py", "status": "pending"} + ], + "details": [ + "get_authorization_url(redirect_uri, state) -> str", + "exchange_code_for_tokens(code, redirect_uri) -> GoogleTokens", + "get_user_info(access_token) -> OAuthUserInfo", + "Uses httpx for async HTTP requests", + "Validates state parameter to prevent CSRF", + "Google OAuth endpoints: accounts.google.com/o/oauth2/v2/auth, oauth2.googleapis.com/token", + "User info endpoint: www.googleapis.com/oauth2/v2/userinfo" + ], + "estimatedHours": 2 + }, + { + "id": "AUTH-007", + "name": "Create Discord OAuth service", + "description": "Handle Discord OAuth authorization code flow", + "category": "critical", + "priority": 7, + "completed": false, + "dependencies": ["AUTH-004"], + "files": [ + {"path": "app/services/oauth/discord.py", "status": "pending"} + ], + "details": [ + "get_authorization_url(redirect_uri, state) -> str", + "exchange_code_for_tokens(code, redirect_uri) -> DiscordTokens", + "get_user_info(access_token) -> OAuthUserInfo", + "Uses httpx for async HTTP requests", + "Discord OAuth endpoints: discord.com/api/oauth2/authorize, discord.com/api/oauth2/token", + "User info endpoint: discord.com/api/users/@me", + "Avatar URL construction from user ID and avatar hash" + ], + "estimatedHours": 2 + }, + { + "id": "AUTH-008", + "name": "Create FastAPI auth dependencies", + "description": "Dependency injection for protected endpoints", + "category": "critical", + "priority": 8, + "completed": false, + "dependencies": ["AUTH-002", "AUTH-003", "AUTH-004"], + "files": [ + {"path": "app/api/__init__.py", "status": "pending"}, + {"path": "app/api/deps.py", "status": "pending"} + ], + "details": [ + "OAuth2PasswordBearer scheme for token extraction", + "get_current_user(token) -> User - Validates token, fetches user", + "get_current_active_user() -> User - Ensures user exists and is active", + "get_current_premium_user() -> User - Requires active premium subscription", + "get_optional_user() -> User | None - For endpoints that work with/without auth", + "Proper error responses: 401 Unauthorized, 403 Forbidden" + ], + "estimatedHours": 1.5 + }, + { + "id": "AUTH-009", + "name": "Create auth API router", + "description": "REST endpoints for OAuth login, token refresh, logout", + "category": "critical", + "priority": 9, + "completed": false, + "dependencies": ["AUTH-006", "AUTH-007", "AUTH-008"], + "files": [ + {"path": "app/api/auth.py", "status": "pending"} + ], + "details": [ + "GET /auth/google - Redirects to Google OAuth consent screen", + "GET /auth/google/callback - Handles OAuth callback, returns tokens", + "GET /auth/discord - Redirects to Discord OAuth consent screen", + "GET /auth/discord/callback - Handles OAuth callback, returns tokens", + "POST /auth/refresh - Exchange refresh token for new access token", + "POST /auth/logout - Revoke refresh token", + "State parameter stored in Redis with short TTL for CSRF protection" + ], + "estimatedHours": 3 + }, + { + "id": "AUTH-010", + "name": "Create user API router", + "description": "REST endpoints for user profile and account management", + "category": "high", + "priority": 10, + "completed": false, + "dependencies": ["AUTH-008", "AUTH-004"], + "files": [ + {"path": "app/api/users.py", "status": "pending"} + ], + "details": [ + "GET /users/me - Get current user profile", + "PATCH /users/me - Update display_name, avatar_url", + "GET /users/me/linked-accounts - List linked OAuth providers", + "POST /users/me/link/{provider} - Start account linking flow", + "DELETE /users/me/link/{provider} - Unlink OAuth provider (if not last)", + "All endpoints require authentication via get_current_user dependency" + ], + "estimatedHours": 2 + }, + { + "id": "AUTH-011", + "name": "Integrate routers in main.py", + "description": "Mount auth and user routers, add any required middleware", + "category": "high", + "priority": 11, + "completed": false, + "dependencies": ["AUTH-009", "AUTH-010"], + "files": [ + {"path": "app/main.py", "status": "pending"} + ], + "details": [ + "Include auth router: prefix='/api/auth', tags=['auth']", + "Include users router: prefix='/api/users', tags=['users']", + "Remove TODO comments for router integration", + "Verify CORS allows credentials for token cookies (if used)" + ], + "estimatedHours": 0.5 + }, + { + "id": "AUTH-012", + "name": "Create JWT service tests", + "description": "Unit tests for token creation and verification", + "category": "high", + "priority": 12, + "completed": false, + "dependencies": ["AUTH-002"], + "files": [ + {"path": "tests/services/test_jwt_service.py", "status": "pending"} + ], + "details": [ + "Test create_access_token returns valid JWT", + "Test create_refresh_token returns valid JWT with correct type", + "Test decode_token extracts correct payload", + "Test verify_token returns None for expired tokens", + "Test verify_token returns None for invalid signatures", + "Test token expiration times are correct" + ], + "estimatedHours": 1.5 + }, + { + "id": "AUTH-013", + "name": "Create UserService tests", + "description": "Integration tests for user CRUD operations", + "category": "high", + "priority": 13, + "completed": false, + "dependencies": ["AUTH-004"], + "files": [ + {"path": "tests/services/test_user_service.py", "status": "pending"} + ], + "details": [ + "Test get_user_by_id returns user or None", + "Test get_user_by_oauth finds by provider+oauth_id", + "Test create_user creates with correct fields", + "Test update_last_login updates timestamp", + "Test link_oauth_provider adds linked account", + "Test premium status update", + "Uses real Postgres via testcontainers pattern" + ], + "estimatedHours": 2 + }, + { + "id": "AUTH-014", + "name": "Create OAuth service tests", + "description": "Unit tests for OAuth flows with mocked HTTP", + "category": "high", + "priority": 14, + "completed": false, + "dependencies": ["AUTH-006", "AUTH-007"], + "files": [ + {"path": "tests/services/oauth/test_google.py", "status": "pending"}, + {"path": "tests/services/oauth/test_discord.py", "status": "pending"} + ], + "details": [ + "Mock httpx responses for token exchange", + "Mock httpx responses for user info", + "Test authorization URL construction", + "Test error handling for invalid codes", + "Test OAuthUserInfo normalization", + "Uses respx or pytest-httpx for mocking" + ], + "estimatedHours": 2 + }, + { + "id": "AUTH-015", + "name": "Create auth API endpoint tests", + "description": "Integration tests for auth endpoints", + "category": "high", + "priority": 15, + "completed": false, + "dependencies": ["AUTH-009", "AUTH-010"], + "files": [ + {"path": "tests/api/__init__.py", "status": "pending"}, + {"path": "tests/api/conftest.py", "status": "pending"}, + {"path": "tests/api/test_auth.py", "status": "pending"}, + {"path": "tests/api/test_users.py", "status": "pending"} + ], + "details": [ + "Test OAuth redirect returns correct URL", + "Test callback with mocked OAuth creates user and returns tokens", + "Test refresh endpoint returns new access token", + "Test logout revokes refresh token", + "Test /users/me returns current user", + "Test /users/me update works", + "Uses TestClient with dependency overrides" + ], + "estimatedHours": 3 + } + ], + + "testingStrategy": { + "approach": "Unit tests for services, integration tests for API endpoints", + "mocking": "httpx responses mocked for OAuth providers, fakeredis for token store", + "database": "Real Postgres via testcontainers for UserService tests", + "coverage": "Target 90%+ coverage on new auth code" + }, + + "weeklyRoadmap": { + "week1": { + "theme": "Core Services", + "tasks": ["AUTH-001", "AUTH-002", "AUTH-003", "AUTH-004", "AUTH-005"], + "goals": ["Schemas defined", "JWT working", "UserService complete"] + }, + "week2": { + "theme": "OAuth + API", + "tasks": ["AUTH-006", "AUTH-007", "AUTH-008", "AUTH-009", "AUTH-010", "AUTH-011"], + "goals": ["OAuth flows working", "API endpoints complete", "Integration done"] + }, + "week3": { + "theme": "Testing", + "tasks": ["AUTH-012", "AUTH-013", "AUTH-014", "AUTH-015"], + "goals": ["Full test coverage", "All tests passing"] + } + }, + + "acceptanceCriteria": [ + {"criterion": "User can login with Google OAuth and receive JWT tokens", "met": false}, + {"criterion": "User can login with Discord OAuth and receive JWT tokens", "met": false}, + {"criterion": "Access tokens expire after configured time", "met": false}, + {"criterion": "Refresh tokens can be used to get new access tokens", "met": false}, + {"criterion": "Logout revokes refresh token (cannot be reused)", "met": false}, + {"criterion": "Protected endpoints return 401 without valid token", "met": false}, + {"criterion": "User can link multiple OAuth providers to one account", "met": false}, + {"criterion": "Premium status is tracked with expiration date", "met": false}, + {"criterion": "All tests pass with high coverage", "met": false} + ], + + "securityConsiderations": [ + "OAuth state parameter validated to prevent CSRF attacks", + "OAuth provider tokens never stored (only OAuth ID)", + "JWT secret key loaded from environment, never hardcoded", + "Refresh tokens stored in Redis with TTL for revocation support", + "Access tokens short-lived (30 min) to limit exposure", + "Rate limiting on auth endpoints (future enhancement)", + "HTTPS required in production for all auth endpoints" + ], + + "dependencies": { + "existing": [ + "python-jose>=3.5.0 (already installed)", + "passlib>=1.7.4 (already installed)", + "bcrypt>=5.0.0 (already installed)" + ], + "toAdd": [ + "httpx>=0.25.0 (async HTTP client for OAuth)", + "respx>=0.20.0 (httpx mocking for tests)" + ] + }, + + "phase1Prerequisites": { + "met": [ + "JWT configuration in Settings class", + "OAuth configuration in Settings class", + "User model with OAuth fields", + "python-jose dependency installed", + "Database session management", + "Redis connection utilities" + ] + } +}