diff --git a/backend/main.py b/backend/main.py index 89375ac..2347c46 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, HTTPException, Header, Depends, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse +from starlette.middleware.sessions import SessionMiddleware from pydantic import BaseModel, Field, ConfigDict from typing import List, Optional from datetime import datetime @@ -35,15 +36,56 @@ oauth.register( allowed_origins = ["http://localhost:5173", "https://tasko.dvirlabs.com"] -# Configure CORS +# Environment-aware configuration +ENVIRONMENT = os.getenv('ENVIRONMENT', 'development') +is_production = ENVIRONMENT == 'production' +is_development = ENVIRONMENT == 'development' + +# Configure Session Middleware (required for OAuth state/nonce storage) +# Generate a strong SECRET_KEY: python -c "import secrets; print(secrets.token_hex(32))" +SESSION_SECRET = os.getenv('SESSION_SECRET', 'dev-secret-change-in-production') + +# Session cookie configuration - environment aware +session_config = { + "secret_key": SESSION_SECRET, + "session_cookie": "tasko_session", + "max_age": 3600, # 1 hour + "path": "/", # Available on all routes (required for OAuth callback) + "same_site": "lax", # Allows OAuth redirects while preventing CSRF + "https_only": is_production, # False for HTTP (dev), True for HTTPS (prod) +} + +# Development-specific: Ensure cookies work on localhost +if is_development: + session_config["domain"] = None # Don't set domain for localhost + +app.add_middleware(SessionMiddleware, **session_config) + +# Log session configuration in development +if is_development: + print(f"šŸ” Session Configuration (Development Mode):") + print(f" - Cookie Name: {session_config['session_cookie']}") + print(f" - Path: {session_config['path']}") + print(f" - SameSite: {session_config['same_site']}") + print(f" - HTTPS Only: {session_config['https_only']}") + print(f" - Domain: {session_config.get('domain', 'None (localhost)')}") + +# Configure CORS - MUST use specific origins (not "*") when allow_credentials=True app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, + allow_origins=allowed_origins, # Specific origins required for credentials + allow_credentials=True, # Required for session cookies allow_methods=["*"], allow_headers=["*"], + expose_headers=["*"], # Allow frontend to read all response headers ) +# Log startup info +if is_development: + print(f"šŸš€ Tasko API starting in DEVELOPMENT mode") + print(f" - CORS Origins: {allowed_origins}") + print(f" - Allow Credentials: True (session cookies enabled)") + # Pydantic Models for API class UserResponse(BaseModel): model_config = ConfigDict(from_attributes=True) @@ -228,14 +270,56 @@ def logout(authorization: Optional[str] = Header(None), db: Session = Depends(ge @app.get("/auth/google") async def google_login(request: Request): - """Initiate Google OAuth login""" + """Initiate Google OAuth login - DIRECT BROWSER REDIRECT (not fetch)""" redirect_uri = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8000/auth/google/callback') - return await oauth.google.authorize_redirect(request, redirect_uri) + + # Debug logging for development + if is_development: + print(f"\nšŸ”‘ OAuth Login initiated (/auth/google):") + print(f" - Redirect URI: {redirect_uri}") + print(f" - Request URL: {request.url}") + print(f" - Request method: {request.method}") + print(f" - Request headers Cookie: {request.headers.get('cookie', 'NONE')}") + print(f" - Session available: {hasattr(request, 'session')}") + if hasattr(request, 'session'): + print(f" - Session keys BEFORE: {list(request.session.keys())}") + + # This will set session state and redirect to Google + response = await oauth.google.authorize_redirect(request, redirect_uri) + + if is_development: + print(f" - Session keys AFTER: {list(request.session.keys())}") + print(f" - Response status: {response.status_code}") + print(f" - Response Set-Cookie: {response.headers.get('set-cookie', 'NONE')}") + print(f" - Response Location: {response.headers.get('location', 'NONE')[:100]}...") + + return response @app.get("/auth/google/callback") async def google_callback(request: Request, db: Session = Depends(get_db)): """Handle Google OAuth callback""" try: + # Debug logging for development + if is_development: + print(f"\nšŸ”„ OAuth Callback received (/auth/google/callback):") + print(f" - Request URL: {request.url}") + print(f" - Request method: {request.method}") + print(f" - Request headers Cookie: {request.headers.get('cookie', 'NONE')}") + print(f" - Request query params: {dict(request.query_params)}") + print(f" - Cookies from request.cookies: {list(request.cookies.keys())}") + print(f" - Session available: {hasattr(request, 'session')}") + if hasattr(request, 'session'): + session_keys = list(request.session.keys()) + print(f" - Session keys: {session_keys}") + # Print state for debugging + for key in session_keys: + if 'state' in key.lower(): + value_str = str(request.session[key]) + if len(value_str) > 100: + print(f" - Session[{key}]: {value_str[:100]}...") + else: + print(f" - Session[{key}]: {value_str}") + # Get access token from Google token = await oauth.google.authorize_access_token(request) @@ -297,11 +381,21 @@ async def google_callback(request: Request, db: Session = Depends(get_db)): db.add(new_token) db.commit() + if is_development: + print(f"āœ… OAuth Login SUCCESS!") + print(f" - User: {user.email} (ID: {user.id})") + print(f" - Token generated: {token_str[:20]}...") + print(f" - Redirecting to frontend with token") + # Redirect to frontend with token frontend_url = os.getenv('FRONTEND_URL', 'http://localhost:5173') return RedirectResponse(url=f"{frontend_url}?token={token_str}&user={user.id}") except Exception as e: + if is_development: + print(f"āŒ OAuth Login FAILED: {str(e)}") + import traceback + traceback.print_exc() raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}") @app.get("/auth/google/url") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 027209c..a5d89ae 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react' import Auth from './Auth' import './App.css' -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' +// Use relative URLs to leverage Vite proxy (same-origin = cookies work!) +const API_URL = import.meta.env.VITE_API_URL || '' // Empty string = same-origin const AVAILABLE_ICONS = [ 'šŸ“', 'šŸ’¼', 'šŸ ', 'šŸ›’', 'šŸŽÆ', 'šŸ’Ŗ', 'šŸ“š', 'āœˆļø', diff --git a/frontend/src/Auth.jsx b/frontend/src/Auth.jsx index 4c36f04..79a8de8 100644 --- a/frontend/src/Auth.jsx +++ b/frontend/src/Auth.jsx @@ -1,7 +1,8 @@ import { useState, useEffect } from 'react' import './Auth.css' -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' +// Use relative URLs to leverage Vite proxy (same-origin = cookies work!) +const API_URL = import.meta.env.VITE_API_URL || '' // Empty string = same-origin function Auth({ onLogin }) { const [isLogin, setIsLogin] = useState(true) @@ -94,21 +95,11 @@ function Auth({ onLogin }) { setLoading(true) setError('') - try { - const response = await fetch(`${API_URL}/auth/google/url`) - const data = await response.json() - - if (data.url) { - window.location.href = data.url - } else { - setError('Failed to initiate Google login') - setLoading(false) - } - } catch (error) { - setError('Failed to connect to Google login') - setLoading(false) - console.error('Google login error:', error) - } + // CORRECT OAuth flow: Direct browser redirect to /auth/google + // This allows the backend to set session cookie and redirect to Google + // DO NOT use fetch() - OAuth requires browser redirects to preserve cookies + console.log('Redirecting to Google OAuth...') + window.location.href = `${API_URL}/auth/google` } return ( diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8b0f57b..e23eec4 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -4,4 +4,34 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + proxy: { + // Proxy API requests to backend - enables same-origin for session cookies + '/auth': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + '/login': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + '/register': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + '/logout': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + }, + }, })