sendio/backend/app/api/google.py
2026-01-13 05:17:57 +02:00

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)}")