invy/backend/main.py
2025-12-29 05:49:05 +02:00

193 lines
6.3 KiB
Python

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)