Spaces:
Sleeping
Sleeping
| """ | |
| Authentication routes for HuggingFace OAuth. | |
| Feature: 012-profile-contact-ui | |
| """ | |
| import os | |
| from flask import Blueprint, redirect, session, url_for, flash, request | |
| from ..services.auth_service import auth_service | |
| from ..services.storage_service import create_or_update_user, get_user_profile | |
| from ..services.backend_client import backend_client | |
| bp = Blueprint("auth", __name__) | |
| def login(): | |
| """Initiate HuggingFace OAuth login flow.""" | |
| import logging | |
| from flask import current_app | |
| logger = logging.getLogger(__name__) | |
| # Generate redirect_uri from current request to ensure it matches the callback | |
| # This handles both localhost and deployed environments automatically | |
| redirect_uri = url_for("auth.callback", _external=True) | |
| logger.info(f"[OAUTH] Starting login flow") | |
| logger.info(f"[OAUTH] Generated redirect_uri: {redirect_uri}") | |
| logger.info(f"[OAUTH] Request URL: {request.url}") | |
| logger.info(f"[OAUTH] Request host: {request.host}") | |
| logger.info(f"[OAUTH] Request scheme: {request.scheme}") | |
| logger.info(f"[OAUTH] Session interface type: {type(current_app.session_interface).__name__}") | |
| logger.info(f"[OAUTH] Session interface module: {type(current_app.session_interface).__module__}") | |
| # Store redirect_uri in session for callback verification | |
| session["oauth_redirect_uri"] = redirect_uri | |
| session["test_marker"] = "login_triggered" # Test value to verify session persistence | |
| # Make session permanent to ensure cookie gets set with proper expiry | |
| session.permanent = True | |
| logger.info(f"[OAUTH] Session data stored: oauth_redirect_uri={redirect_uri}") | |
| logger.info(f"[OAUTH] Session keys before authorize_redirect: {list(session.keys())}") | |
| logger.info(f"[OAUTH] Session.modified: {session.modified}") | |
| logger.info(f"[OAUTH] Session.permanent: {session.permanent}") | |
| logger.info(f"[OAUTH] Session ID (if server-side): {getattr(session, 'sid', 'N/A')}") | |
| # Call authorize_redirect - Authlib will add _state_* key to session | |
| # Flask will automatically save the complete session (including state) via after_request | |
| response = auth_service.hf.authorize_redirect(redirect_uri) | |
| logger.info(f"[OAUTH] Session keys after authorize_redirect: {list(session.keys())}") | |
| logger.info(f"[OAUTH] OAuth redirect location: {response.location if hasattr(response, 'location') else 'N/A'}") | |
| logger.info(f"[OAUTH] Response type: {type(response).__name__}") | |
| logger.info(f"[OAUTH] Response Set-Cookie preview: {[h[:80] for h in response.headers.getlist('Set-Cookie')]}") | |
| # Flask's after_request middleware will automatically save session with ALL data including state | |
| return response | |
| def callback(): | |
| """ | |
| Handle OAuth callback from HuggingFace. | |
| Exchange code for token, fetch user info, create/update user profile. | |
| """ | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.info(f"[OAUTH] === CALLBACK ENDPOINT HIT ===") | |
| logger.info(f"[OAUTH] Request URL: {request.url}") | |
| logger.info(f"[OAUTH] Request path: {request.path}") | |
| logger.info(f"[OAUTH] Request args: {dict(request.args)}") | |
| try: | |
| # Debug session state and cookies | |
| from flask import current_app | |
| logger.info(f"[OAUTH] Cookies received: {list(request.cookies.keys())}") | |
| logger.info(f"[OAUTH] Cookie values (redacted): {[(k, v[:20] + '...' if len(v) > 20 else v) for k, v in request.cookies.items()]}") | |
| logger.info(f"[OAUTH] Session interface type: {type(current_app.session_interface).__name__}") | |
| logger.info(f"[OAUTH] Session interface module: {type(current_app.session_interface).__module__}") | |
| logger.info(f"[OAUTH] Session keys: {list(session.keys())}") | |
| logger.info(f"[OAUTH] Session ID (if server-side): {getattr(session, 'sid', 'N/A')}") | |
| logger.info(f"[OAUTH] Test marker from login: {session.get('test_marker')}") | |
| # Check for state key (Authlib uses _state_huggingface_<random>) | |
| state_keys = [k for k in session.keys() if k.startswith('_state_huggingface')] | |
| logger.info(f"[OAUTH] State keys in session: {state_keys}") | |
| logger.info(f"[OAUTH] State from URL: {request.args.get('state')}") | |
| # Additional diagnostics | |
| if not state_keys: | |
| logger.error(f"[OAUTH] CRITICAL: No state keys in session! Session data lost!") | |
| logger.error(f"[OAUTH] All session keys: {dict(session)}") | |
| if 'flask_session' not in type(current_app.session_interface).__module__: | |
| logger.error(f"[OAUTH] ERROR: Using client-side sessions instead of Flask-Session!") | |
| logger.error(f"[OAUTH] Session interface: {type(current_app.session_interface)}") | |
| # Exchange authorization code for access token | |
| # Authlib automatically validates the state and uses the redirect_uri from the session | |
| # If state validation fails, it will raise an exception that we catch below | |
| token = auth_service.fetch_token() | |
| # Fetch user information | |
| userinfo = auth_service.fetch_userinfo(token) | |
| # Extract user data | |
| user_id = userinfo.get("preferred_username") | |
| display_name = userinfo.get("name", user_id) | |
| profile_picture_url = userinfo.get("picture") | |
| if not user_id: | |
| flash("Failed to retrieve user information from HuggingFace", "danger") | |
| return redirect(url_for("auth.login")) | |
| # Check if user already exists | |
| existing_profile = get_user_profile(user_id) | |
| if existing_profile: | |
| # Update existing user (last_login, display_name) | |
| user_profile = create_or_update_user(user_id, display_name, profile_picture_url) | |
| else: | |
| # New user - create backend session first, then store with returned session_id | |
| try: | |
| backend_response = backend_client.create_session( | |
| user_id=user_id, | |
| contact_id="user-profile", | |
| description=f"{display_name}'s Profile", | |
| is_reference=False | |
| ) | |
| backend_session_id = backend_response.get("session_id") | |
| if not backend_session_id: | |
| raise ValueError("Backend did not return session_id") | |
| # Create user profile in SQLite with backend's session_id | |
| user_profile = create_or_update_user( | |
| user_id, | |
| display_name, | |
| profile_picture_url, | |
| session_id=backend_session_id | |
| ) | |
| except Exception as e: | |
| import logging | |
| logging.error(f"Failed to create backend session: {str(e)}", exc_info=True) | |
| flash(f"Failed to initialize profile: {str(e)}", "danger") | |
| return redirect(url_for("auth.login")) | |
| # Store user info in Flask session | |
| session.permanent = True | |
| session["user_id"] = user_id | |
| session["display_name"] = display_name | |
| session["profile_picture_url"] = profile_picture_url | |
| session["session_id"] = user_profile.session_id | |
| session["profile_session_id"] = user_profile.session_id # Feature: 001-contact-session-fixes - cache for facts/messages | |
| session["access_token"] = token.get("access_token") | |
| # CRITICAL: Manually save session before redirect | |
| # Same issue as login - redirect response bypasses automatic session save | |
| response = redirect(url_for("profile.view_profile")) | |
| from flask import current_app | |
| current_app.session_interface.save_session(current_app, session, response) | |
| logger.info(f"[OAUTH] Session saved with user_id={user_id}, redirecting to profile") | |
| return response | |
| except Exception as e: | |
| # Log the full error for debugging | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.error(f"Authentication failed: {str(e)}", exc_info=True) | |
| logger.debug(f"Session keys at error: {list(session.keys())}") | |
| logger.debug(f"Request args: {request.args}") | |
| # Clear any stale OAuth state | |
| for key in list(session.keys()): | |
| if key.startswith('_state_'): | |
| session.pop(key) | |
| flash(f"Authentication failed: {str(e)}", "danger") | |
| return redirect(url_for("auth.login")) | |
| def logout(): | |
| """Clear session and log out user.""" | |
| user_name = session.get("display_name", "User") | |
| session.clear() | |
| return redirect(url_for("auth.login")) | |