from fastapi import FastAPI, Depends, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse, HTMLResponse from sqlalchemy.orm import Session import uvicorn from typing import List import os from dotenv import load_dotenv import httpx import models import schemas import crud from database import engine, get_db # Load environment variables load_dotenv() # Create database tables models.Base.metadata.create_all(bind=engine) app = FastAPI(title="Wedding Guest List API") # Configure CORS app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173"], # Vite default port allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/") def read_root(): return {"message": "Wedding Guest List API"} # Guest endpoints @app.post("/guests/", response_model=schemas.Guest) def create_guest(guest: schemas.GuestCreate, db: Session = Depends(get_db)): return crud.create_guest(db=db, guest=guest) @app.get("/guests/", response_model=List[schemas.Guest]) def read_guests(skip: int = 0, limit: int = 1000, db: Session = Depends(get_db)): guests = crud.get_guests(db, skip=skip, limit=limit) return guests @app.get("/guests/{guest_id}", response_model=schemas.Guest) def read_guest(guest_id: int, db: Session = Depends(get_db)): db_guest = crud.get_guest(db, guest_id=guest_id) if db_guest is None: raise HTTPException(status_code=404, detail="Guest not found") return db_guest @app.put("/guests/{guest_id}", response_model=schemas.Guest) def update_guest(guest_id: int, guest: schemas.GuestUpdate, db: Session = Depends(get_db)): db_guest = crud.update_guest(db, guest_id=guest_id, guest=guest) if db_guest is None: raise HTTPException(status_code=404, detail="Guest not found") return db_guest @app.delete("/guests/{guest_id}") def delete_guest(guest_id: int, db: Session = Depends(get_db)): success = crud.delete_guest(db, guest_id=guest_id) if not success: raise HTTPException(status_code=404, detail="Guest not found") return {"message": "Guest deleted successfully"} @app.post("/guests/bulk-delete") def delete_guests_bulk(guest_ids: List[int], db: Session = Depends(get_db)): deleted_count = crud.delete_guests_bulk(db, guest_ids=guest_ids) return {"message": f"Successfully deleted {deleted_count} guests"} @app.delete("/guests/undo-import/{owner}") def undo_import(owner: str, db: Session = Depends(get_db)): """ Delete all guests imported by a specific owner """ deleted_count = crud.delete_guests_by_owner(db, owner=owner) return {"message": f"Successfully deleted {deleted_count} guests from {owner}"} @app.get("/guests/owners/") def get_owners(db: Session = Depends(get_db)): """ Get list of unique owners """ owners = crud.get_unique_owners(db) return {"owners": owners} # Search and filter endpoints @app.get("/guests/search/", response_model=List[schemas.Guest]) def search_guests( query: str = "", rsvp_status: str = None, meal_preference: str = None, owner: str = None, db: Session = Depends(get_db) ): guests = crud.search_guests( db, query=query, rsvp_status=rsvp_status, meal_preference=meal_preference, owner=owner ) return guests # Google OAuth configuration GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") GOOGLE_REDIRECT_URI = "http://localhost:8000/auth/google/callback" # Google OAuth endpoints @app.get("/auth/google") async def google_auth(): """ Initiate Google OAuth flow - redirects to Google """ auth_url = ( f"https://accounts.google.com/o/oauth2/v2/auth?" f"client_id={GOOGLE_CLIENT_ID}&" f"redirect_uri={GOOGLE_REDIRECT_URI}&" f"response_type=code&" f"scope=https://www.googleapis.com/auth/contacts.readonly%20https://www.googleapis.com/auth/userinfo.email&" f"access_type=offline&" f"prompt=consent" ) return RedirectResponse(url=auth_url) @app.get("/auth/google/callback") async def google_callback(code: str, db: Session = Depends(get_db)): """ Handle Google OAuth callback and import contacts Owner will be extracted from the user's email """ try: # Exchange code for access token async with httpx.AsyncClient() as client: token_response = await client.post( "https://oauth2.googleapis.com/token", data={ "code": code, "client_id": GOOGLE_CLIENT_ID, "client_secret": GOOGLE_CLIENT_SECRET, "redirect_uri": GOOGLE_REDIRECT_URI, "grant_type": "authorization_code", }, ) if token_response.status_code != 200: raise HTTPException(status_code=400, detail="Failed to get access token") token_data = token_response.json() access_token = token_data.get("access_token") # Get user info to extract email user_info_response = await client.get( "https://www.googleapis.com/oauth2/v2/userinfo", headers={"Authorization": f"Bearer {access_token}"} ) if user_info_response.status_code != 200: raise HTTPException(status_code=400, detail="Failed to get user info") user_info = user_info_response.json() user_email = user_info.get("email", "unknown") # Use full email as owner owner = user_email # Import contacts from google_contacts import import_contacts_from_google imported_count = await import_contacts_from_google(access_token, db, owner) # Redirect back to frontend with success message return RedirectResponse( url=f"http://localhost:5173?imported={imported_count}&owner={owner}", status_code=302 ) except Exception as e: # Redirect back with error return RedirectResponse( url=f"http://localhost:5173?error={str(e)}", status_code=302 ) if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)