256 lines
8.7 KiB
Python
256 lines
8.7 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
|
|
)
|
|
|
|
|
|
# Public endpoint for guests to update their info
|
|
@app.get("/public/guest/{phone_number}")
|
|
def get_guest_by_phone(phone_number: str, db: Session = Depends(get_db)):
|
|
"""
|
|
Public endpoint: Get guest info by phone number
|
|
Returns guest if found, or None to allow new registration
|
|
"""
|
|
guest = db.query(models.Guest).filter(models.Guest.phone_number == phone_number).first()
|
|
if not guest:
|
|
# Return structure indicating not found, but don't raise error
|
|
return {"found": False, "phone_number": phone_number}
|
|
return {"found": True, **guest.__dict__}
|
|
|
|
|
|
@app.put("/public/guest/{phone_number}")
|
|
def update_guest_by_phone(
|
|
phone_number: str,
|
|
guest_update: schemas.GuestPublicUpdate,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Public endpoint: Allow guests to update their own info using phone number
|
|
Creates new guest if not found (marked as 'self-service')
|
|
"""
|
|
guest = db.query(models.Guest).filter(models.Guest.phone_number == phone_number).first()
|
|
|
|
if not guest:
|
|
# Create new guest from link (not imported from contacts)
|
|
guest = models.Guest(
|
|
first_name=guest_update.first_name or "Guest",
|
|
last_name=guest_update.last_name or "",
|
|
phone_number=phone_number,
|
|
rsvp_status=guest_update.rsvp_status or "pending",
|
|
meal_preference=guest_update.meal_preference,
|
|
has_plus_one=guest_update.has_plus_one or False,
|
|
plus_one_name=guest_update.plus_one_name,
|
|
owner="self-service" # Mark as self-registered via link
|
|
)
|
|
db.add(guest)
|
|
else:
|
|
# Update existing guest
|
|
# Always update names if provided (override contact names)
|
|
if guest_update.first_name is not None:
|
|
guest.first_name = guest_update.first_name
|
|
if guest_update.last_name is not None:
|
|
guest.last_name = guest_update.last_name
|
|
|
|
# Update other fields
|
|
if guest_update.rsvp_status is not None:
|
|
guest.rsvp_status = guest_update.rsvp_status
|
|
if guest_update.meal_preference is not None:
|
|
guest.meal_preference = guest_update.meal_preference
|
|
if guest_update.has_plus_one is not None:
|
|
guest.has_plus_one = guest_update.has_plus_one
|
|
if guest_update.plus_one_name is not None:
|
|
guest.plus_one_name = guest_update.plus_one_name
|
|
|
|
db.commit()
|
|
db.refresh(guest)
|
|
return guest
|
|
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) |