from fastapi import FastAPI, Depends, HTTPException, status, Request, APIRouter from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm import firebase_admin from firebase_admin import credentials, auth from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models.api.user import UserCreate, UserSignIn, PasswordReset, TokenVerify, UserResponse from app.models.database.DBUser import DBUser import datetime import os from app.core.database.session_manager import get_db_session as get_db from pydantic import BaseModel, EmailStr router = APIRouter(prefix="/FirebaseAuth", tags=["FirebaseAuth related APIs"]) # Initialize Firebase Admin SDK with better error handling try: current_dir = os.path.dirname(os.path.abspath(__file__)) # Try multiple possible paths for the service account file service_account_paths = [ "/opt/MailPilot/MailPilot_ai_agents/app/serviceAccountKey/mailpoilt-firebase-adminsdk-fbsvc-26bb455f79.json", os.path.join(current_dir, "../serviceAccountKey/mailpoilt-firebase-adminsdk-fbsvc-26bb455f79.json"), os.path.join(current_dir, "../../serviceAccountKey/mailpoilt-firebase-adminsdk-fbsvc-26bb455f79.json") ] cred = None for path in service_account_paths: if os.path.exists(path): cred = credentials.Certificate(path) break if cred is None: raise FileNotFoundError("Firebase service account key not found") if not firebase_admin._apps: firebase_admin.initialize_app(cred) except Exception as e: print(f"Firebase initialization error: {str(e)}") # Continue without crashing, but auth functions will fail oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/FirebaseAuth/signin") async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): try: decoded_token = auth.verify_id_token(token) user_id = decoded_token["uid"] # Get the Firebase user firebase_user = auth.get_user(user_id) result = await db.execute(select(DBUser).filter(DBUser.firebase_uid == user_id)) db_user = result.scalar_one_or_none() if db_user is None: raise HTTPException(status_code=404, detail="User not found in database") return UserResponse( firebase_uid=db_user.firebase_uid, email=db_user.email, display_name=db_user.display_name, is_active=db_user.is_active, created_at=db_user.created_at, last_login=db_user.last_login, provider=db_user.provider, email_verified=firebase_user.email_verified ) except Exception as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid authentication credentials: {str(e)}", headers={"WWW-Authenticate": "Bearer"}, ) @router.post("/signup", response_model=dict) async def create_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)): """Create a new user with email and password and store in database""" try: # Check if user already exists try: existing_user = auth.get_user_by_email(user_data.email) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"User with email {user_data.email} already exists" ) except auth.UserNotFoundError: # This is what we want - user doesn't exist yet pass # Create Firebase user firebase_user = auth.create_user( email=user_data.email, password=user_data.password, display_name=user_data.display_name, email_verified=False # Explicitly set to false ) # Generate email verification link action_code_settings = auth.ActionCodeSettings( url=f"https://mailpoilt.web.app/verify-email?email={user_data.email}", handle_code_in_app=True ) verification_link = auth.generate_email_verification_link( user_data.email, action_code_settings ) # Firebase will handle sending the verification email automatically current_time = datetime.datetime.utcnow() db_user = DBUser( firebase_uid=firebase_user.uid, email=user_data.email, display_name=user_data.display_name, is_active=True, created_at=current_time, last_login=current_time, provider="email" ) db.add(db_user) await db.commit() await db.refresh(db_user) return { "message": "User created successfully. Please check your email to verify your account.", "verification_link": verification_link, # In production, you might not return this "user": { "firebase_uid": db_user.firebase_uid, "email": db_user.email, "display_name": db_user.display_name, "is_active": db_user.is_active, "created_at": db_user.created_at.isoformat() if db_user.created_at else None, "last_login": db_user.last_login.isoformat() if db_user.last_login else None, "provider": db_user.provider, "email_verified": firebase_user.email_verified } } except Exception as e: await db.rollback() try: if 'firebase_user' in locals(): auth.delete_user(firebase_user.uid) except: pass raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error creating user: {str(e)}" ) @router.post("/signin", response_model=dict) async def signin_user(user_data: UserSignIn, db: AsyncSession = Depends(get_db)): """Sign in a user with email and password""" try: try: firebase_user = auth.get_user_by_email(user_data.email) except auth.UserNotFoundError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"No user found with email: {user_data.email}" ) # Generate a custom token that can be exchanged for an ID token custom_token = auth.create_custom_token(firebase_user.uid) # Update last login time result = await db.execute(select(DBUser).filter(DBUser.firebase_uid == firebase_user.uid)) db_user = result.scalar_one_or_none() if not db_user: # Create db user if not exists db_user = DBUser( firebase_uid=firebase_user.uid, email=firebase_user.email, display_name=firebase_user.display_name or user_data.email.split('@')[0], is_active=True, created_at=datetime.datetime.utcnow(), last_login=datetime.datetime.utcnow(), provider="email" ) db.add(db_user) else: db_user.last_login = datetime.datetime.utcnow() await db.commit() await db.refresh(db_user) user_info = { "firebase_uid": db_user.firebase_uid, "email": db_user.email, "display_name": db_user.display_name, "is_active": db_user.is_active, "created_at": db_user.created_at.isoformat() if db_user.created_at else None, "last_login": db_user.last_login.isoformat() if db_user.last_login else None, "provider": db_user.provider, "email_verified": firebase_user.email_verified, "custom_token": custom_token.decode("utf-8") if isinstance(custom_token, bytes) else custom_token } return { "message": "Login successful", "user": user_info, "custom_token": custom_token.decode("utf-8") if isinstance(custom_token, bytes) else custom_token, "email_verified": firebase_user.email_verified } except Exception as e: if isinstance(e, HTTPException): raise e await db.rollback() raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Authentication failed: {str(e)}" ) class EmailVerifyRequest(BaseModel): email: EmailStr @router.post("/resend-verification", status_code=status.HTTP_200_OK) async def resend_verification_email( email_data: EmailVerifyRequest = None, current_user: UserResponse = Depends(get_current_user) ): """ Resend verification email to a user If user is logged in, uses their email. Otherwise, uses the email provided in the request body. """ try: # If email is provided in request body, use that # Otherwise use logged in user's email email = email_data.email if email_data else current_user.email # Check if user exists try: firebase_user = auth.get_user_by_email(email) except auth.UserNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No user found with email: {email}" ) # Check if email is already verified if firebase_user.email_verified: return {"message": "Email is already verified"} # Generate a new verification link action_code_settings = auth.ActionCodeSettings( url=f"https://mailpoilt.web.app/verify-email?email={email}", handle_code_in_app=True ) verification_link = auth.generate_email_verification_link( email, action_code_settings ) return { "message": "Verification email sent successfully", "verification_link": verification_link } except Exception as e: if isinstance(e, HTTPException): raise e raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to resend verification email: {str(e)}" ) email: EmailStr @router.post("/check-email-verified") async def check_email_verified(email_data: EmailVerifyRequest): """Check if a user's email is verified""" try: # Check if user exists try: firebase_user = auth.get_user_by_email(email_data.email) except auth.UserNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No user found with email: {email_data.email}" ) return { "email": email_data.email, "email_verified": firebase_user.email_verified } except Exception as e: if isinstance(e, HTTPException): raise e raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to check email verification status: {str(e)}" ) @router.post("/verify-token", response_model=UserResponse) async def verify_token(token_data: TokenVerify, db: AsyncSession = Depends(get_db)): """Verify a Firebase ID token or UID and return user data""" try: # First try to verify as an ID token try: decoded_token = auth.verify_id_token(token_data.token) user_id = decoded_token["uid"] except: # If that fails, treat it as a UID user_id = token_data.token try: firebase_user = auth.get_user(user_id) except auth.UserNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) result = await db.execute(select(DBUser).filter(DBUser.firebase_uid == user_id)) db_user = result.scalar_one_or_none() if not db_user: # Create DB user if it doesn't exist db_user = DBUser( firebase_uid=user_id, email=firebase_user.email, display_name=firebase_user.display_name or firebase_user.email.split('@')[0], is_active=True, created_at=datetime.datetime.utcnow(), last_login=datetime.datetime.utcnow(), provider="firebase" ) db.add(db_user) await db.commit() await db.refresh(db_user) else: # Update last_login time db_user.last_login = datetime.datetime.utcnow() await db.commit() await db.refresh(db_user) return UserResponse( firebase_uid=db_user.firebase_uid, email=db_user.email, display_name=db_user.display_name, is_active=db_user.is_active, created_at=db_user.created_at, last_login=db_user.last_login, provider=db_user.provider, email_verified=firebase_user.email_verified ) except Exception as e: if isinstance(e, HTTPException): raise e raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Token verification failed: {str(e)}" ) @router.post("/token") async def get_token(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)): return await signin_user( UserSignIn(email=form_data.username, password=form_data.password), db )