from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from google_auth_oauthlib.flow import Flow from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from typing import Optional from app.db.base import get_db from app.core.deps import get_current_user from app.core.config import settings from app.models.user import User from app.models.contact import Contact, DNDList from app.models.google_token import GoogleToken from app.schemas.imports import GoogleAuthURL, GoogleSyncResponse from app.utils.encryption import encrypt_token, decrypt_token from app.utils.phone import normalize_phone import json router = APIRouter() SCOPES = ['https://www.googleapis.com/auth/contacts.readonly'] def get_google_flow(): """Create Google OAuth flow""" return Flow.from_client_config( { "web": { "client_id": settings.GOOGLE_CLIENT_ID, "client_secret": settings.GOOGLE_CLIENT_SECRET, "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "redirect_uris": [settings.GOOGLE_REDIRECT_URI] } }, scopes=SCOPES, redirect_uri=settings.GOOGLE_REDIRECT_URI ) @router.get("/google/start", response_model=GoogleAuthURL) def google_auth_start(current_user: User = Depends(get_current_user)): """Start Google OAuth flow""" if not settings.GOOGLE_CLIENT_ID or not settings.GOOGLE_CLIENT_SECRET: raise HTTPException(status_code=500, detail="Google OAuth not configured") flow = get_google_flow() authorization_url, state = flow.authorization_url( access_type='offline', include_granted_scopes='true', prompt='consent' ) return GoogleAuthURL(auth_url=authorization_url) @router.get("/google/callback") async def google_auth_callback( code: str = Query(...), current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Handle Google OAuth callback""" try: flow = get_google_flow() flow.fetch_token(code=code) credentials = flow.credentials # Store refresh token existing_token = db.query(GoogleToken).filter( GoogleToken.user_id == current_user.id ).first() token_data = { 'token': credentials.token, 'refresh_token': credentials.refresh_token, 'token_uri': credentials.token_uri, 'client_id': credentials.client_id, 'client_secret': credentials.client_secret, 'scopes': credentials.scopes } encrypted = encrypt_token(json.dumps(token_data)) if existing_token: existing_token.encrypted_token = encrypted else: token_obj = GoogleToken( user_id=current_user.id, encrypted_token=encrypted ) db.add(token_obj) db.commit() return {"status": "success", "message": "Google account connected"} except Exception as e: raise HTTPException(status_code=400, detail=f"OAuth error: {str(e)}") @router.post("/google/sync", response_model=GoogleSyncResponse) def google_sync_contacts( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Sync contacts from Google""" # Get stored token token_obj = db.query(GoogleToken).filter( GoogleToken.user_id == current_user.id ).first() if not token_obj: raise HTTPException(status_code=400, detail="Google account not connected") try: # Decrypt token token_data = json.loads(decrypt_token(token_obj.encrypted_token)) # Create credentials credentials = Credentials( token=token_data.get('token'), refresh_token=token_data.get('refresh_token'), token_uri=token_data.get('token_uri'), client_id=token_data.get('client_id'), client_secret=token_data.get('client_secret'), scopes=token_data.get('scopes') ) # Build People API service service = build('people', 'v1', credentials=credentials) # Fetch contacts results = service.people().connections().list( resourceName='people/me', pageSize=1000, personFields='names,phoneNumbers,emailAddresses' ).execute() connections = results.get('connections', []) imported_count = 0 for person in connections: names = person.get('names', []) phones = person.get('phoneNumbers', []) emails = person.get('emailAddresses', []) if not phones: continue first_name = names[0].get('givenName') if names else None last_name = names[0].get('familyName') if names else None email = emails[0].get('value') if emails else None for phone_obj in phones: phone_value = phone_obj.get('value', '').strip() if not phone_value: continue normalized_phone = normalize_phone(phone_value) if not normalized_phone: continue # Check DND dnd_entry = db.query(DNDList).filter( DNDList.user_id == current_user.id, DNDList.phone_e164 == normalized_phone ).first() if dnd_entry: continue # Check existing existing = db.query(Contact).filter( Contact.user_id == current_user.id, Contact.phone_e164 == normalized_phone ).first() if existing: if first_name: existing.first_name = first_name if last_name: existing.last_name = last_name if email: existing.email = email else: contact = Contact( user_id=current_user.id, phone_e164=normalized_phone, first_name=first_name, last_name=last_name, email=email, opted_in=False, source="google" ) db.add(contact) imported_count += 1 db.commit() return GoogleSyncResponse( status="success", contacts_imported=imported_count ) except Exception as e: raise HTTPException(status_code=400, detail=f"Sync error: {str(e)}")