from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from google.oauth2 import id_token from google.auth.transport import requests as google_requests from app.db.base import get_db from app.models.user import User, AuthProvider, UserRole from app.schemas.auth import ( RegisterRequest, LoginRequest, GoogleLoginRequest, RefreshRequest, TokenResponse, UserResponse, AuthResponse, ) from app.core.security import ( hash_password, verify_password, create_access_token, create_refresh_token, decode_token, get_current_user, get_optional_current_user, ) from app.core.config import settings router = APIRouter(prefix="/auth", tags=["Authentication"]) def _generate_tokens(user_id: int) -> dict: access_token = create_access_token({"sub": str(user_id)}) refresh_token = create_refresh_token({"sub": str(user_id)}) return { "access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer", } @router.post("/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED) async def register(request: RegisterRequest, db: Session = Depends(get_db)): existing = db.query(User).filter(User.email == request.email).first() if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Email already registered", ) user = User( name=request.name, email=request.email, password_hash=hash_password(request.password), phone=request.phone, role=UserRole.CUSTOMER, auth_provider=AuthProvider.LOCAL, ) db.add(user) db.commit() db.refresh(user) tokens = _generate_tokens(user.id) user_data = UserResponse.model_validate(user).model_dump() user_data["created_at"] = user_data["created_at"].isoformat() return AuthResponse( isSuccess=True, value={"tokens": tokens, "user": user_data}, statusCode=201, ) @router.post("/login", response_model=AuthResponse) async def login(request: LoginRequest, db: Session = Depends(get_db)): user = db.query(User).filter(User.email == request.email).first() if not user or not user.password_hash: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password", ) if not verify_password(request.password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password", ) tokens = _generate_tokens(user.id) user_data = UserResponse.model_validate(user).model_dump() user_data["created_at"] = user_data["created_at"].isoformat() return AuthResponse( isSuccess=True, value={"tokens": tokens, "user": user_data}, statusCode=200, ) @router.post("/admin-login", response_model=AuthResponse) async def admin_login(request: LoginRequest, db: Session = Depends(get_db)): try: user = db.query(User).filter(User.email == request.email).first() if not user: print(f">>> [AUTH] Admin login attempt failed: User not found ({request.email})") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password", ) if not user.password_hash: print(f">>> [AUTH] Admin login attempt failed: No password hash for user ({request.email})") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password", ) if not verify_password(request.password, user.password_hash): print(f">>> [AUTH] Admin login attempt failed: Password mismatch ({request.email})") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password", ) # Crucial step: Verify admin role if user.role != UserRole.ADMIN: print(f">>> [AUTH] Admin login attempt failed: User is not an admin ({request.email}, role: {user.role})") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied. Admin privileges required.", ) print(f">>> [AUTH] Admin login successful: {request.email}") tokens = _generate_tokens(user.id) user_data = UserResponse.model_validate(user).model_dump() user_data["created_at"] = user_data["created_at"].isoformat() return AuthResponse( isSuccess=True, value={"tokens": tokens, "user": user_data}, statusCode=200, ) except HTTPException: raise except Exception as e: import traceback traceback.print_exc() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Login error: {str(e)}", ) @router.post("/google", response_model=AuthResponse) async def google_login(request: GoogleLoginRequest, db: Session = Depends(get_db)): """Authenticate user with Google ID token.""" if not settings.GOOGLE_CLIENT_ID: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Google authentication is not configured", ) try: idinfo = id_token.verify_oauth2_token( request.credential, google_requests.Request(), settings.GOOGLE_CLIENT_ID, ) except ValueError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Google token", ) google_id = idinfo.get("sub") email = idinfo.get("email") name = idinfo.get("name", "") avatar_url = idinfo.get("picture", "") if not email: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Google account does not have an email", ) # Check if user exists by google_id user = db.query(User).filter(User.google_id == google_id).first() if not user: # Check if user exists by email (link accounts) user = db.query(User).filter(User.email == email).first() if user: # Link existing account to Google user.google_id = google_id user.auth_provider = AuthProvider.GOOGLE if not user.avatar_url: user.avatar_url = avatar_url else: # Create new user user = User( name=name, email=email, google_id=google_id, avatar_url=avatar_url, auth_provider=AuthProvider.GOOGLE, role=UserRole.CUSTOMER, ) db.add(user) db.commit() db.refresh(user) tokens = _generate_tokens(user.id) user_data = UserResponse.model_validate(user).model_dump() user_data["created_at"] = user_data["created_at"].isoformat() return AuthResponse( isSuccess=True, value={"tokens": tokens, "user": user_data}, statusCode=200, ) @router.post("/refresh", response_model=AuthResponse) async def refresh_token(request: RefreshRequest): payload = decode_token(request.refresh_token) if payload.get("type") != "refresh": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type — expected refresh token", ) user_id = payload.get("sub") tokens = _generate_tokens(int(user_id)) return AuthResponse( isSuccess=True, value={"tokens": tokens}, statusCode=200, ) @router.get("/me", response_model=AuthResponse) async def get_me( user_id: int = Depends(get_current_user), db: Session = Depends(get_db), ): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) user_data = UserResponse.model_validate(user).model_dump() user_data["created_at"] = user_data["created_at"].isoformat() return AuthResponse( isSuccess=True, value={"user": user_data}, statusCode=200, ) async def get_current_admin_user( user_id: int = Depends(get_current_user), db: Session = Depends(get_db), ) -> User: user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) if user.role != UserRole.ADMIN: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) return user