761 lines
26 KiB
Python
761 lines
26 KiB
Python
from fastapi import FastAPI, Depends, HTTPException, Query
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import RedirectResponse, HTMLResponse
|
|
from sqlalchemy.orm import Session
|
|
import uvicorn
|
|
from typing import List, Optional
|
|
from uuid import UUID
|
|
import os
|
|
from dotenv import load_dotenv
|
|
import httpx
|
|
from urllib.parse import urlencode, quote
|
|
|
|
import models
|
|
import schemas
|
|
import crud
|
|
import authz
|
|
import google_contacts
|
|
from database import engine, get_db
|
|
from whatsapp import get_whatsapp_service, WhatsAppError
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Create database tables
|
|
models.Base.metadata.create_all(bind=engine)
|
|
|
|
app = FastAPI(title="Multi-Event Invitation Management API")
|
|
|
|
# Get allowed origins from environment
|
|
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
allowed_origins = [FRONTEND_URL]
|
|
# Allow common localhost development ports
|
|
allowed_origins.extend([
|
|
"http://localhost:5173",
|
|
"http://localhost:5174",
|
|
"http://127.0.0.1:5173",
|
|
"http://127.0.0.1:5174",
|
|
])
|
|
|
|
# Configure CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=allowed_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
# ============================================
|
|
# Helper: Get current user (placeholder - implement with your auth)
|
|
# ============================================
|
|
def get_current_user_id() -> UUID:
|
|
"""
|
|
Extract current user from request
|
|
TODO: Implement with JWT, session, or your auth system
|
|
For now returns a test user - replace this with real auth
|
|
"""
|
|
# This is a placeholder - you need to implement authentication
|
|
# Options:
|
|
# 1. JWT tokens from Authorization header
|
|
# 2. Session cookies
|
|
# 3. API keys
|
|
# 4. OAuth2
|
|
|
|
# For development, use a test user
|
|
test_user_email = os.getenv("TEST_USER_EMAIL", "test@example.com")
|
|
db = SessionLocal()
|
|
user = crud.get_or_create_user(db, test_user_email)
|
|
db.close()
|
|
return user.id
|
|
|
|
|
|
from database import SessionLocal
|
|
|
|
|
|
# ============================================
|
|
# Root Endpoint
|
|
# ============================================
|
|
@app.get("/")
|
|
def read_root():
|
|
return {"message": "Multi-Event Invitation Management API"}
|
|
|
|
|
|
# ============================================
|
|
# Event Endpoints
|
|
# ============================================
|
|
@app.post("/events", response_model=schemas.Event)
|
|
def create_event(
|
|
event: schemas.EventCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Create a new event (creator becomes admin)"""
|
|
return crud.create_event(db, event, current_user_id)
|
|
|
|
|
|
@app.get("/events", response_model=List[schemas.Event])
|
|
def list_events(
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""List all events user is a member of"""
|
|
return crud.get_events_for_user(db, current_user_id)
|
|
|
|
|
|
@app.get("/events/{event_id}", response_model=schemas.EventWithMembers)
|
|
async def get_event(
|
|
event_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
authz_info: dict = Depends(lambda: None)
|
|
):
|
|
"""Get event details (only for members)"""
|
|
# First verify access
|
|
try:
|
|
current_user_id = UUID("00000000-0000-0000-0000-000000000000") # Placeholder
|
|
authz_info = await authz.verify_event_access(event_id, db, current_user_id)
|
|
except:
|
|
raise HTTPException(status_code=403, detail="Not authorized")
|
|
|
|
event = crud.get_event(db, event_id)
|
|
members = crud.get_event_members(db, event_id)
|
|
event.members = members
|
|
return event
|
|
|
|
|
|
@app.patch("/events/{event_id}", response_model=schemas.Event)
|
|
async def update_event(
|
|
event_id: UUID,
|
|
event_update: schemas.EventUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Update event (admin only)"""
|
|
authz_info = await authz.verify_event_admin(event_id, db, current_user_id)
|
|
return crud.update_event(db, event_id, event_update)
|
|
|
|
|
|
@app.delete("/events/{event_id}")
|
|
async def delete_event(
|
|
event_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Delete event (admin only)"""
|
|
authz_info = await authz.verify_event_admin(event_id, db, current_user_id)
|
|
success = crud.delete_event(db, event_id)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="Event not found")
|
|
return {"message": "Event deleted successfully"}
|
|
|
|
|
|
# ============================================
|
|
# Event Member Endpoints
|
|
# ============================================
|
|
@app.post("/events/{event_id}/invite-member", response_model=schemas.EventMember)
|
|
async def invite_event_member(
|
|
event_id: UUID,
|
|
invite: schemas.EventMemberCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Invite user to event by email (admin only)"""
|
|
authz_info = await authz.verify_event_admin(event_id, db, current_user_id)
|
|
|
|
# Find or create user
|
|
user = crud.get_or_create_user(db, invite.user_email)
|
|
|
|
# Add to event
|
|
member = crud.create_event_member(
|
|
db, event_id, user.id, invite.role, invite.display_name
|
|
)
|
|
return member
|
|
|
|
|
|
@app.get("/events/{event_id}/members", response_model=List[schemas.EventMember])
|
|
async def list_event_members(
|
|
event_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""List all members of an event"""
|
|
authz_info = await authz.verify_event_access(event_id, db, current_user_id)
|
|
return crud.get_event_members(db, event_id)
|
|
|
|
|
|
@app.patch("/events/{event_id}/members/{user_id}")
|
|
async def update_member_role(
|
|
event_id: UUID,
|
|
user_id: UUID,
|
|
role_update: dict,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Update member role (admin only)"""
|
|
authz_info = await authz.verify_event_admin(event_id, db, current_user_id)
|
|
|
|
member = crud.update_event_member_role(
|
|
db, event_id, user_id, role_update.get("role", "viewer")
|
|
)
|
|
if not member:
|
|
raise HTTPException(status_code=404, detail="Member not found")
|
|
return member
|
|
|
|
|
|
@app.delete("/events/{event_id}/members/{user_id}")
|
|
async def remove_member(
|
|
event_id: UUID,
|
|
user_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Remove member from event (admin only)"""
|
|
authz_info = await authz.verify_event_admin(event_id, db, current_user_id)
|
|
|
|
success = crud.remove_event_member(db, event_id, user_id)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="Member not found")
|
|
return {"message": "Member removed successfully"}
|
|
|
|
|
|
# ============================================
|
|
# Guest Endpoints (Event-Scoped)
|
|
# ============================================
|
|
@app.post("/events/{event_id}/guests", response_model=schemas.Guest)
|
|
async def create_guest(
|
|
event_id: UUID,
|
|
guest: schemas.GuestCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Add a single guest to event (editor+ only)"""
|
|
authz_info = await authz.verify_event_editor(event_id, db, current_user_id)
|
|
return crud.create_guest(db, event_id, guest, current_user_id)
|
|
|
|
|
|
@app.get("/events/{event_id}/guests", response_model=List[schemas.Guest])
|
|
async def list_guests(
|
|
event_id: UUID,
|
|
search: Optional[str] = Query(None),
|
|
status: Optional[str] = Query(None),
|
|
rsvp_status: Optional[str] = Query(None),
|
|
side: Optional[str] = Query(None),
|
|
owner: Optional[str] = Query(None), # Filter by owner email or 'self-service'
|
|
added_by_me: bool = Query(False),
|
|
skip: int = Query(0),
|
|
limit: int = Query(1000),
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""List guests for event with optional filters"""
|
|
authz_info = await authz.verify_event_access(event_id, db, current_user_id)
|
|
|
|
# Support both old (status) and new (rsvp_status) parameter names
|
|
filter_status = rsvp_status or status
|
|
|
|
added_by_user_id = current_user_id if added_by_me else None
|
|
guests = crud.search_guests(
|
|
db, event_id, search, filter_status, side, added_by_user_id, owner_email=owner
|
|
)
|
|
return guests[skip:skip+limit]
|
|
|
|
|
|
@app.get("/events/{event_id}/guest-owners")
|
|
async def get_guest_owners(
|
|
event_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Get list of unique owners/sources for guests in an event"""
|
|
authz_info = await authz.verify_event_access(event_id, db, current_user_id)
|
|
|
|
# Query distinct owner_email values
|
|
from sqlalchemy import distinct
|
|
owners = db.query(distinct(models.Guest.owner_email)).filter(
|
|
models.Guest.event_id == event_id
|
|
).all()
|
|
|
|
# Extract values and filter out None
|
|
owner_list = [owner[0] for owner in owners if owner[0]]
|
|
owner_list.sort()
|
|
|
|
# Check for self-service guests
|
|
self_service_count = db.query(models.Guest).filter(
|
|
models.Guest.event_id == event_id,
|
|
models.Guest.source == "self-service"
|
|
).count()
|
|
|
|
result = {
|
|
"owners": owner_list,
|
|
"has_self_service": self_service_count > 0,
|
|
"total_guests": db.query(models.Guest).filter(
|
|
models.Guest.event_id == event_id
|
|
).count()
|
|
}
|
|
|
|
return result
|
|
|
|
|
|
@app.get("/events/{event_id}/guests/{guest_id}", response_model=schemas.Guest)
|
|
async def get_guest(
|
|
event_id: UUID,
|
|
guest_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Get guest details"""
|
|
authz_info = await authz.verify_event_access(event_id, db, current_user_id)
|
|
guest = crud.get_guest(db, guest_id, event_id)
|
|
if not guest:
|
|
raise HTTPException(status_code=404, detail="Guest not found")
|
|
return guest
|
|
|
|
|
|
@app.patch("/events/{event_id}/guests/{guest_id}", response_model=schemas.Guest)
|
|
async def update_guest(
|
|
event_id: UUID,
|
|
guest_id: UUID,
|
|
guest_update: schemas.GuestUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Update guest details (editor+ only)"""
|
|
authz_info = await authz.verify_event_editor(event_id, db, current_user_id)
|
|
guest = crud.update_guest(db, guest_id, event_id, guest_update)
|
|
if not guest:
|
|
raise HTTPException(status_code=404, detail="Guest not found")
|
|
return guest
|
|
|
|
|
|
@app.delete("/events/{event_id}/guests/{guest_id}")
|
|
async def delete_guest(
|
|
event_id: UUID,
|
|
guest_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Delete guest (admin only)"""
|
|
authz_info = await authz.verify_event_admin(event_id, db, current_user_id)
|
|
success = crud.delete_guest(db, guest_id, event_id)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="Guest not found")
|
|
return {"message": "Guest deleted successfully"}
|
|
|
|
|
|
# ============================================
|
|
# Bulk Guest Import
|
|
# ============================================
|
|
@app.post("/events/{event_id}/guests/import", response_model=dict)
|
|
async def bulk_import_guests(
|
|
event_id: UUID,
|
|
import_data: schemas.GuestBulkImport,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Bulk import guests (editor+ only)"""
|
|
authz_info = await authz.verify_event_editor(event_id, db, current_user_id)
|
|
|
|
guests = crud.bulk_import_guests(db, event_id, import_data.guests, current_user_id)
|
|
return {
|
|
"imported_count": len(guests),
|
|
"guests": guests
|
|
}
|
|
|
|
|
|
# ============================================
|
|
# Event Statistics
|
|
# ============================================
|
|
@app.get("/events/{event_id}/stats")
|
|
async def get_event_stats(
|
|
event_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Get event statistics (members only)"""
|
|
authz_info = await authz.verify_event_access(event_id, db, current_user_id)
|
|
|
|
stats = crud.get_event_stats(db, event_id)
|
|
sides = crud.get_sides_summary(db, event_id)
|
|
|
|
return {
|
|
"stats": stats,
|
|
"sides": sides
|
|
}
|
|
|
|
|
|
# ============================================
|
|
# WhatsApp Messaging
|
|
# ============================================
|
|
@app.post("/events/{event_id}/guests/{guest_id}/whatsapp")
|
|
async def send_guest_message(
|
|
event_id: UUID,
|
|
guest_id: UUID,
|
|
message_req: schemas.WhatsAppMessage,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""Send WhatsApp message to guest (members only)"""
|
|
authz_info = await authz.verify_event_access(event_id, db, current_user_id)
|
|
|
|
# Get guest
|
|
guest = crud.get_guest(db, guest_id, event_id)
|
|
if not guest:
|
|
raise HTTPException(status_code=404, detail="Guest not found")
|
|
|
|
# Use override phone or guest's phone
|
|
phone = message_req.phone or guest.phone
|
|
|
|
try:
|
|
service = get_whatsapp_service()
|
|
result = await service.send_text_message(phone, message_req.message)
|
|
return result
|
|
except WhatsAppError as e:
|
|
raise HTTPException(status_code=400, detail=f"WhatsApp error: {str(e)}")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to send message: {str(e)}")
|
|
|
|
|
|
@app.post("/events/{event_id}/whatsapp/broadcast")
|
|
async def broadcast_whatsapp_message(
|
|
event_id: UUID,
|
|
broadcast_req: dict,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""
|
|
Broadcast WhatsApp message to multiple guests
|
|
|
|
Request body:
|
|
{
|
|
"message": "Your message here",
|
|
"guest_ids": ["uuid1", "uuid2"], // optional: if not provided, send to all
|
|
"filter_status": "confirmed" // optional: filter by status
|
|
}
|
|
"""
|
|
authz_info = await authz.verify_event_access(event_id, db, current_user_id)
|
|
|
|
message = broadcast_req.get("message", "")
|
|
if not message:
|
|
raise HTTPException(status_code=400, detail="Message is required")
|
|
|
|
# Get guests to send to
|
|
if broadcast_req.get("guest_ids"):
|
|
guest_ids = [UUID(gid) for gid in broadcast_req["guest_ids"]]
|
|
guests = []
|
|
for gid in guest_ids:
|
|
g = crud.get_guest(db, gid, event_id)
|
|
if g:
|
|
guests.append(g)
|
|
elif broadcast_req.get("filter_status"):
|
|
guests = crud.get_guests_by_status(db, event_id, broadcast_req["filter_status"])
|
|
else:
|
|
guests = crud.get_guests(db, event_id)
|
|
|
|
# Send to all guests
|
|
results = []
|
|
failed = []
|
|
|
|
try:
|
|
service = get_whatsapp_service()
|
|
for guest in guests:
|
|
try:
|
|
result = await service.send_text_message(guest.phone, message)
|
|
results.append({
|
|
"guest_id": str(guest.id),
|
|
"phone": guest.phone,
|
|
"status": "sent",
|
|
"message_id": result.get("message_id")
|
|
})
|
|
except Exception as e:
|
|
failed.append({
|
|
"guest_id": str(guest.id),
|
|
"phone": guest.phone,
|
|
"error": str(e)
|
|
})
|
|
except WhatsAppError as e:
|
|
raise HTTPException(status_code=400, detail=f"WhatsApp error: {str(e)}")
|
|
|
|
return {
|
|
"total": len(guests),
|
|
"sent": len(results),
|
|
"failed": len(failed),
|
|
"results": results,
|
|
"failures": failed
|
|
}
|
|
|
|
|
|
# ============================================
|
|
# Google OAuth Integration
|
|
# ============================================
|
|
@app.get("/auth/google")
|
|
async def get_google_auth_url(
|
|
event_id: Optional[str] = None
|
|
):
|
|
"""
|
|
Initiate Google OAuth flow - redirects to Google.
|
|
"""
|
|
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
|
redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "http://localhost:8000/auth/google/callback")
|
|
|
|
if not client_id:
|
|
raise HTTPException(status_code=500, detail="Google Client ID not configured")
|
|
|
|
# Google OAuth2 authorization endpoint
|
|
auth_url = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
|
|
params = {
|
|
"client_id": client_id,
|
|
"redirect_uri": redirect_uri,
|
|
"response_type": "code",
|
|
"scope": "https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/userinfo.email",
|
|
"access_type": "offline",
|
|
"state": event_id or "default" # Pass event_id as state for later use
|
|
}
|
|
|
|
full_url = f"{auth_url}?{urlencode(params)}"
|
|
|
|
# Redirect to Google OAuth endpoint
|
|
return RedirectResponse(url=full_url)
|
|
|
|
|
|
@app.get("/auth/google/callback")
|
|
async def google_callback(
|
|
code: str = Query(None),
|
|
state: str = Query(None),
|
|
error: str = Query(None),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Handle Google OAuth callback.
|
|
Exchanges authorization code for access token and imports contacts.
|
|
"""
|
|
if error:
|
|
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
error_url = f"{frontend_url}?error={quote(error)}"
|
|
return RedirectResponse(url=error_url)
|
|
|
|
if not code:
|
|
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
return RedirectResponse(url=f"{frontend_url}?error={quote('Missing authorization code')}")
|
|
|
|
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
|
client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
|
|
redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "http://localhost:8000/auth/google/callback")
|
|
|
|
if not client_id or not client_secret:
|
|
raise HTTPException(status_code=500, detail="Google OAuth credentials not configured")
|
|
|
|
try:
|
|
async with httpx.AsyncClient() as client_http:
|
|
# Exchange authorization code for access token
|
|
token_url = "https://oauth2.googleapis.com/token"
|
|
|
|
token_data = {
|
|
"client_id": client_id,
|
|
"client_secret": client_secret,
|
|
"code": code,
|
|
"grant_type": "authorization_code",
|
|
"redirect_uri": redirect_uri
|
|
}
|
|
|
|
response = await client_http.post(token_url, data=token_data)
|
|
|
|
if response.status_code != 200:
|
|
error_detail = response.json().get("error_description", response.text)
|
|
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
return RedirectResponse(url=f"{frontend_url}?error={quote(error_detail)}")
|
|
|
|
tokens = response.json()
|
|
access_token = tokens.get("access_token")
|
|
|
|
if not access_token:
|
|
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
return RedirectResponse(url=f"{frontend_url}?error={quote('No access token')}")
|
|
|
|
# Get user info to extract email
|
|
user_info_response = await client_http.get(
|
|
"https://www.googleapis.com/oauth2/v2/userinfo",
|
|
headers={"Authorization": f"Bearer {access_token}"}
|
|
)
|
|
|
|
if user_info_response.status_code != 200:
|
|
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
return RedirectResponse(url=f"{frontend_url}?error={quote('Failed to get user info')}")
|
|
|
|
user_info = user_info_response.json()
|
|
user_email = user_info.get("email", "unknown")
|
|
|
|
# Look up or create a User for this Google account
|
|
# This is needed because added_by_user_id is required
|
|
user = db.query(models.User).filter(models.User.email == user_email).first()
|
|
if not user:
|
|
# Create a new user with this email
|
|
user = models.User(email=user_email)
|
|
db.add(user)
|
|
db.commit()
|
|
db.refresh(user)
|
|
|
|
# Import contacts - get event_id from state parameter
|
|
event_id = state if state and state != "default" else None
|
|
|
|
try:
|
|
imported_count = await google_contacts.import_contacts_from_google(
|
|
access_token=access_token,
|
|
db=db,
|
|
owner_email=user_email,
|
|
added_by_user_id=str(user.id),
|
|
event_id=event_id
|
|
)
|
|
|
|
# Success - return HTML that sets sessionStorage and redirects
|
|
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
|
|
if event_id:
|
|
# Build the target URL
|
|
target_url = f"{frontend_url}/events/{event_id}/guests"
|
|
else:
|
|
target_url = frontend_url
|
|
|
|
# Return HTML that sets sessionStorage and redirects
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Import Complete</title>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
sessionStorage.setItem('googleImportJustCompleted', 'true');
|
|
sessionStorage.setItem('googleImportCount', '{imported_count}');
|
|
sessionStorage.setItem('googleImportEmail', '{user_email}');
|
|
window.location.href = '{target_url}';
|
|
</script>
|
|
<p>Redirecting...</p>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
return HTMLResponse(content=html_content)
|
|
|
|
except Exception as import_error:
|
|
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
return RedirectResponse(url=f"{frontend_url}?error={quote(f'Import failed: {str(import_error)}')}")
|
|
|
|
except Exception as e:
|
|
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
|
return RedirectResponse(url=f"{frontend_url}?error={quote('OAuth error')}")
|
|
|
|
|
|
@app.post("/events/{event_id}/import-google-contacts")
|
|
async def import_google_contacts(
|
|
event_id: UUID,
|
|
import_data: schemas.GoogleContactsImport,
|
|
db: Session = Depends(get_db),
|
|
current_user_id: UUID = Depends(get_current_user_id)
|
|
):
|
|
"""
|
|
Deprecated: Use /auth/google endpoint instead.
|
|
This endpoint is kept for backward compatibility.
|
|
"""
|
|
raise HTTPException(
|
|
status_code=410,
|
|
detail="Google import flow has been updated. Use the Google Import button instead."
|
|
)
|
|
|
|
|
|
# ============================================
|
|
# Public Guest Self-Service Endpoints
|
|
# ============================================
|
|
@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 (no authentication required)
|
|
Used for guest self-service lookup via shared link
|
|
|
|
Returns:
|
|
- {found: true, guest_data} if guest found
|
|
- {found: false, phone_number} if not found
|
|
"""
|
|
guest = db.query(models.Guest).filter(
|
|
models.Guest.phone_number == phone_number
|
|
).first()
|
|
|
|
if not guest:
|
|
return {"found": False, "phone_number": phone_number}
|
|
|
|
# Return guest data (exclude sensitive fields if needed)
|
|
guest_dict = {
|
|
"found": True,
|
|
"first_name": guest.first_name,
|
|
"last_name": guest.last_name,
|
|
"phone_number": guest.phone_number,
|
|
"email": guest.email,
|
|
"rsvp_status": guest.rsvp_status,
|
|
"meal_preference": guest.meal_preference,
|
|
"has_plus_one": guest.has_plus_one,
|
|
"plus_one_name": guest.plus_one_name,
|
|
}
|
|
return 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
|
|
No authentication required - guests can use shared URLs with phone number
|
|
|
|
Features:
|
|
- Updates existing guest fields (first_name, last_name override imported values)
|
|
- Used for self-service RSVP and preference collection
|
|
- Guest must exist (typically from Google import) - returns 404 if not found
|
|
"""
|
|
guest = db.query(models.Guest).filter(
|
|
models.Guest.phone_number == phone_number
|
|
).first()
|
|
|
|
if not guest:
|
|
# Guest not found - return 404
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Guest with phone number {phone_number} not found. Please check the number and try again."
|
|
)
|
|
|
|
# Update existing guest - override with provided values
|
|
# This allows guests to correct their names/preferences even if imported from contacts
|
|
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
|
|
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 {
|
|
"id": guest.id,
|
|
"first_name": guest.first_name,
|
|
"last_name": guest.last_name,
|
|
"phone_number": guest.phone_number,
|
|
"email": guest.email,
|
|
"rsvp_status": guest.rsvp_status,
|
|
"meal_preference": guest.meal_preference,
|
|
"has_plus_one": guest.has_plus_one,
|
|
"plus_one_name": guest.plus_one_name,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|