
- 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.
372 lines
13 KiB
Python
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"
|
|
) |