diff --git a/modern/backend/app.py b/modern/backend/app.py index bef3489..be7d33f 100644 --- a/modern/backend/app.py +++ b/modern/backend/app.py @@ -3,6 +3,8 @@ from fastapi.middleware.cors import CORSMiddleware from . import models from .oauth import router as oauth_router import os +import requests +import time app = FastAPI(title='LifeRPG Modern Backend') @@ -41,3 +43,25 @@ def create_user(payload: dict): db.refresh(user) db.close() 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() diff --git a/modern/backend/oauth.py b/modern/backend/oauth.py index 8e86403..3890741 100644 --- a/modern/backend/oauth.py +++ b/modern/backend/oauth.py @@ -1,13 +1,14 @@ import os +import time from fastapi import APIRouter, Request from starlette.responses import RedirectResponse from authlib.integrations.starlette_client import OAuth +from . import models router = APIRouter() - 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_SECRET = os.getenv('GOOGLE_CLIENT_SECRET') 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'} ) + @router.get('/oauth/google/login') async def google_login(request: Request): if 'google' not in oauth: @@ -28,12 +30,62 @@ async def google_login(request: Request): redirect_uri = BASE_URL + '/api/v1/oauth/google/callback' return await oauth.google.authorize_redirect(request, redirect_uri) + @router.get('/oauth/google/callback') 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: return {'error': 'google oauth not configured'} + token = await oauth.google.authorize_access_token(request) - user = await oauth.google.parse_id_token(request, token) - # token contains access_token and refresh_token; persist securely - # For demo, return token info - return {'token': token, 'user': user} + # Try to get userinfo (sub/email) from id_token or userinfo endpoint + userinfo = None + try: + 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() diff --git a/modern/backend/requirements_full.txt b/modern/backend/requirements_full.txt index bc78894..9586333 100644 --- a/modern/backend/requirements_full.txt +++ b/modern/backend/requirements_full.txt @@ -3,3 +3,4 @@ uvicorn[standard] sqlalchemy authlib python-dotenv +requests