backend(oauth): persist oauth tokens and add Google Calendar demo endpoint
This commit is contained in:
parent
8aee66f658
commit
08a9c77b65
|
|
@ -3,6 +3,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
from . import models
|
from . import models
|
||||||
from .oauth import router as oauth_router
|
from .oauth import router as oauth_router
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
app = FastAPI(title='LifeRPG Modern Backend')
|
app = FastAPI(title='LifeRPG Modern Backend')
|
||||||
|
|
||||||
|
|
@ -41,3 +43,25 @@ def create_user(payload: dict):
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
db.close()
|
db.close()
|
||||||
return {'id': user.id, 'email': user.email}
|
return {'id': user.id, 'email': user.email}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/api/v1/integrations/{integration_id}/google/events')
|
||||||
|
def google_events(integration_id: int):
|
||||||
|
"""Demo endpoint: fetch upcoming Google Calendar events using stored access token.
|
||||||
|
|
||||||
|
Note: For production you must handle token refresh, errors, and rate limits. This is a demo.
|
||||||
|
"""
|
||||||
|
db = models.SessionLocal()
|
||||||
|
try:
|
||||||
|
token = db.query(models.OAuthToken).filter_by(integration_id=integration_id).order_by(models.OAuthToken.id.desc()).first()
|
||||||
|
if not token or not token.access_token:
|
||||||
|
raise HTTPException(status_code=404, detail='no token found for integration')
|
||||||
|
|
||||||
|
headers = {'Authorization': f'Bearer {token.access_token}'}
|
||||||
|
params = {'maxResults': 10, 'singleEvents': True, 'orderBy': 'startTime', 'timeMin': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
|
||||||
|
resp = requests.get('https://www.googleapis.com/calendar/v3/calendars/primary/events', headers=headers, params=params, timeout=10)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise HTTPException(status_code=502, detail=f'google api error: {resp.status_code}')
|
||||||
|
return resp.json()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Request
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
from authlib.integrations.starlette_client import OAuth
|
from authlib.integrations.starlette_client import OAuth
|
||||||
|
from . import models
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
oauth = OAuth()
|
oauth = OAuth()
|
||||||
|
|
||||||
# Register provider with placeholders; read from env at runtime
|
# Load config from env at runtime
|
||||||
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
|
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
|
||||||
GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET')
|
GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET')
|
||||||
BASE_URL = os.getenv('BASE_URL', 'http://localhost:8000')
|
BASE_URL = os.getenv('BASE_URL', 'http://localhost:8000')
|
||||||
|
|
@ -21,6 +22,7 @@ if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET:
|
||||||
client_kwargs={'scope': 'openid email profile https://www.googleapis.com/auth/calendar.events'}
|
client_kwargs={'scope': 'openid email profile https://www.googleapis.com/auth/calendar.events'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/oauth/google/login')
|
@router.get('/oauth/google/login')
|
||||||
async def google_login(request: Request):
|
async def google_login(request: Request):
|
||||||
if 'google' not in oauth:
|
if 'google' not in oauth:
|
||||||
|
|
@ -28,12 +30,62 @@ async def google_login(request: Request):
|
||||||
redirect_uri = BASE_URL + '/api/v1/oauth/google/callback'
|
redirect_uri = BASE_URL + '/api/v1/oauth/google/callback'
|
||||||
return await oauth.google.authorize_redirect(request, redirect_uri)
|
return await oauth.google.authorize_redirect(request, redirect_uri)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/oauth/google/callback')
|
@router.get('/oauth/google/callback')
|
||||||
async def google_callback(request: Request):
|
async def google_callback(request: Request):
|
||||||
|
"""Handle Google's OAuth callback, persist Integration and OAuthToken records.
|
||||||
|
|
||||||
|
This demo stores access/refresh tokens associated to a newly created `Integration` for
|
||||||
|
the (demo) user. In a real app, you'd associate the integration with the authenticated
|
||||||
|
user and secure storage for tokens.
|
||||||
|
"""
|
||||||
if 'google' not in oauth:
|
if 'google' not in oauth:
|
||||||
return {'error': 'google oauth not configured'}
|
return {'error': 'google oauth not configured'}
|
||||||
|
|
||||||
token = await oauth.google.authorize_access_token(request)
|
token = await oauth.google.authorize_access_token(request)
|
||||||
user = await oauth.google.parse_id_token(request, token)
|
# Try to get userinfo (sub/email) from id_token or userinfo endpoint
|
||||||
# token contains access_token and refresh_token; persist securely
|
userinfo = None
|
||||||
# For demo, return token info
|
try:
|
||||||
return {'token': token, 'user': user}
|
userinfo = await oauth.google.parse_id_token(request, token)
|
||||||
|
except Exception:
|
||||||
|
# fallback: try userinfo endpoint
|
||||||
|
try:
|
||||||
|
resp = await oauth.google.get('userinfo', token=token)
|
||||||
|
userinfo = resp.json()
|
||||||
|
except Exception:
|
||||||
|
userinfo = {}
|
||||||
|
|
||||||
|
# Persist integration + token into DB (demo uses `user_id` query param or 1)
|
||||||
|
db = models.SessionLocal()
|
||||||
|
try:
|
||||||
|
# For demo, allow passing ?user_id= to associate the integration
|
||||||
|
qs = dict(request.query_params)
|
||||||
|
user_id = int(qs.get('user_id')) if qs.get('user_id') else 1
|
||||||
|
|
||||||
|
# Create or reuse an Integration row for this user+provider
|
||||||
|
ext_id = userinfo.get('sub') or userinfo.get('id') or None
|
||||||
|
integration = db.query(models.Integration).filter_by(user_id=user_id, provider='google').first()
|
||||||
|
if not integration:
|
||||||
|
integration = models.Integration(user_id=user_id, provider='google', external_id=ext_id, config='{}')
|
||||||
|
db.add(integration)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(integration)
|
||||||
|
|
||||||
|
# Persist token (single latest token demo)
|
||||||
|
expires_at = None
|
||||||
|
if token.get('expires_in'):
|
||||||
|
expires_at = int(time.time()) + int(token.get('expires_in'))
|
||||||
|
|
||||||
|
oauth_token = models.OAuthToken(
|
||||||
|
integration_id=integration.id,
|
||||||
|
access_token=token.get('access_token'),
|
||||||
|
refresh_token=token.get('refresh_token'),
|
||||||
|
scope=token.get('scope'),
|
||||||
|
expires_at=expires_at
|
||||||
|
)
|
||||||
|
db.add(oauth_token)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {'ok': True, 'integration_id': integration.id, 'token_saved': bool(oauth_token.id)}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ uvicorn[standard]
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
authlib
|
authlib
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
requests
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user