203 lines
6.9 KiB
Python
203 lines
6.9 KiB
Python
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)}")
|