Automated Action 4cc9c41fe8 Add comprehensive error handling and debugging for signup issues
- Enhanced login endpoint with detailed logging for debugging JWT token issues
- Added explicit is_google_user and email_verified flags during registration
- Added test endpoints for JWT token creation verification
- Added debug endpoint to check user authentication status
- Improved error handling and logging throughout authentication flow
- Better validation for password vs Google OAuth users

Debug endpoints added:
- POST /auth/test-token - Test JWT token creation
- GET /auth/debug/user/{email} - Check user authentication status

This will help identify why JWT tokens are not being returned during signin.
2025-06-25 06:41:11 +00:00

372 lines
13 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel, EmailStr
from typing import Optional
from datetime import datetime
import logging
from app.db.session import get_db
from app.models.user import User
from app.utils.auth import get_password_hash, verify_password, create_access_token
from app.services.google_oauth_service import GoogleOAuthService
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/test")
async def test_auth():
logger.info("Auth test endpoint called")
return {
"message": "Auth router is working",
"status": "success"
}
@router.post("/test-token")
async def test_token_creation():
"""Test endpoint to verify JWT token creation is working"""
try:
test_token = create_access_token(data={"sub": "test@example.com"})
return {
"message": "Token creation successful",
"token_length": len(test_token),
"token_preview": test_token[:50] + "..." if len(test_token) > 50 else test_token
}
except Exception as e:
logger.error(f"Token creation test failed: {e}")
return {
"message": "Token creation failed",
"error": str(e)
}
@router.get("/debug/user/{email}")
async def debug_user_status(email: str, db: Session = Depends(get_db)):
"""Debug endpoint to check user status - REMOVE IN PRODUCTION"""
try:
db_user = db.query(User).filter(User.email == email).first()
if not db_user:
return {"message": "User not found", "email": email}
return {
"email": db_user.email,
"has_password_hash": db_user.password_hash is not None,
"password_hash_length": len(db_user.password_hash) if db_user.password_hash else 0,
"is_google_user": db_user.is_google_user,
"email_verified": db_user.email_verified,
"can_login_with_password": db_user.can_login_with_password(),
"created_at": str(db_user.created_at),
"google_id": db_user.google_id
}
except Exception as e:
return {"error": str(e)}
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserLogin(BaseModel):
email: EmailStr
password: str
class Token(BaseModel):
access_token: str
token_type: str
class UserResponse(BaseModel):
id: int
email: str
created_at: str
class Config:
orm_mode = True
@router.post("/register", response_model=UserResponse)
async def register(user: UserCreate, db: Session = Depends(get_db)):
try:
logger.info(f"Registration attempt for email: {user.email}")
# Check if user already exists
db_user = db.query(User).filter(User.email == user.email).first()
if db_user:
logger.warning(f"Registration failed - email already exists: {user.email}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Hash password and create user
hashed_password = get_password_hash(user.password)
logger.info(f"Password hashed successfully for user: {user.email}")
db_user = User(
email=user.email,
password_hash=hashed_password,
is_google_user=False, # Explicitly set for password users
email_verified=False # Email not verified for regular registration
)
db.add(db_user)
db.commit()
db.refresh(db_user)
logger.info(f"User registered successfully: {user.email}, can_login_with_password: {db_user.can_login_with_password()}")
return UserResponse(
id=db_user.id,
email=db_user.email,
created_at=str(db_user.created_at)
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Registration error for {user.email}: {str(e)}")
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error during registration"
)
@router.post("/login", response_model=Token)
async def login(user: UserLogin, db: Session = Depends(get_db)):
try:
logger.info(f"Login attempt for email: {user.email}")
db_user = db.query(User).filter(User.email == user.email).first()
# Check if user exists
if not db_user:
logger.warning(f"Login failed - user not found: {user.email}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
logger.info(f"User found: {user.email}, is_google_user: {db_user.is_google_user}, has_password: {db_user.password_hash is not None}")
# Check if user can login with password
if not db_user.can_login_with_password():
logger.warning(f"Login failed - Google user attempted password login: {user.email}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="This account uses Google sign-in. Please use Google to login.",
headers={"WWW-Authenticate": "Bearer"},
)
# Verify password
if not verify_password(user.password, db_user.password_hash):
logger.warning(f"Login failed - incorrect password: {user.email}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Create token
access_token = create_access_token(data={"sub": db_user.email})
logger.info(f"Login successful for user: {user.email}")
return {"access_token": access_token, "token_type": "bearer"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Login error for {user.email}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error during login"
)
# Google OAuth Models
class GoogleTokenLogin(BaseModel):
id_token: str
class GoogleCodeLogin(BaseModel):
code: str
redirect_uri: str
class GoogleOAuthURL(BaseModel):
oauth_url: str
class GoogleUserResponse(BaseModel):
id: int
email: str
first_name: Optional[str]
last_name: Optional[str]
full_name: str
is_google_user: bool
email_verified: bool
profile_picture: Optional[str]
created_at: str
is_new_user: bool
class Config:
orm_mode = True
# Google OAuth Endpoints
@router.get("/google/oauth-url", response_model=GoogleOAuthURL)
async def get_google_oauth_url():
"""Get Google OAuth URL for frontend redirect"""
oauth_url = GoogleOAuthService.get_google_oauth_url()
if not oauth_url:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Google OAuth not configured"
)
return {"oauth_url": oauth_url}
@router.post("/google/login-with-token", response_model=Token)
async def google_login_with_token(
google_data: GoogleTokenLogin,
db: Session = Depends(get_db)
):
"""Login/Register with Google ID token"""
try:
logger.info("Google OAuth login attempt with ID token")
# Verify Google token
user_info = await GoogleOAuthService.verify_google_token(google_data.id_token)
if not user_info:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Google token"
)
# Check if user exists by email
db_user = db.query(User).filter(User.email == user_info['email']).first()
if db_user:
# Existing user - update Google info if not already a Google user
if not db_user.is_google_user:
db_user.google_id = user_info['google_id']
db_user.is_google_user = True
db_user.email_verified = user_info['email_verified']
if user_info.get('picture'):
db_user.profile_picture = user_info['picture']
if user_info.get('first_name') and not db_user.first_name:
db_user.first_name = user_info['first_name']
if user_info.get('last_name') and not db_user.last_name:
db_user.last_name = user_info['last_name']
db_user.updated_at = datetime.utcnow()
db.commit()
db.refresh(db_user)
logger.info(f"Updated existing user with Google OAuth: {db_user.email}")
else:
# New user - create account
db_user = User(
email=user_info['email'],
google_id=user_info['google_id'],
is_google_user=True,
email_verified=user_info['email_verified'],
first_name=user_info.get('first_name'),
last_name=user_info.get('last_name'),
profile_picture=user_info.get('picture'),
preferred_language=user_info.get('locale', 'en')
)
db.add(db_user)
db.commit()
db.refresh(db_user)
logger.info(f"Created new Google user: {db_user.email}")
# Create JWT token
access_token = create_access_token(data={"sub": db_user.email})
return {"access_token": access_token, "token_type": "bearer"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Google OAuth error: {str(e)}")
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error during Google authentication"
)
@router.post("/google/login-with-code", response_model=Token)
async def google_login_with_code(
google_data: GoogleCodeLogin,
db: Session = Depends(get_db)
):
"""Login/Register with Google authorization code"""
try:
logger.info("Google OAuth login attempt with authorization code")
# Exchange code for ID token
id_token = await GoogleOAuthService.exchange_code_for_token(
google_data.code,
google_data.redirect_uri
)
if not id_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Failed to exchange authorization code"
)
# Verify the ID token and process user
user_info = await GoogleOAuthService.verify_google_token(id_token)
if not user_info:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Google token"
)
# Same logic as token login
db_user = db.query(User).filter(User.email == user_info['email']).first()
if db_user:
if not db_user.is_google_user:
db_user.google_id = user_info['google_id']
db_user.is_google_user = True
db_user.email_verified = user_info['email_verified']
if user_info.get('picture'):
db_user.profile_picture = user_info['picture']
if user_info.get('first_name') and not db_user.first_name:
db_user.first_name = user_info['first_name']
if user_info.get('last_name') and not db_user.last_name:
db_user.last_name = user_info['last_name']
db_user.updated_at = datetime.utcnow()
db.commit()
db.refresh(db_user)
else:
db_user = User(
email=user_info['email'],
google_id=user_info['google_id'],
is_google_user=True,
email_verified=user_info['email_verified'],
first_name=user_info.get('first_name'),
last_name=user_info.get('last_name'),
profile_picture=user_info.get('picture'),
preferred_language=user_info.get('locale', 'en')
)
db.add(db_user)
db.commit()
db.refresh(db_user)
access_token = create_access_token(data={"sub": db_user.email})
return {"access_token": access_token, "token_type": "bearer"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Google OAuth code exchange error: {str(e)}")
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error during Google authentication"
)