|
| 1 | +import os |
| 2 | +from typing import Optional |
| 3 | +from fastapi import APIRouter, Request, HTTPException, Form |
| 4 | +from fastapi.responses import HTMLResponse |
| 5 | +from fastapi.templating import Jinja2Templates |
| 6 | +import firebase_admin.auth |
| 7 | +import requests |
| 8 | + |
| 9 | +from database.apps import get_app_by_id_db |
| 10 | +from database.redis_db import enable_app, increase_app_installs_count |
| 11 | +from utils.apps import is_user_app_enabled, get_is_user_paid_app, is_tester |
| 12 | +from models.app import App as AppModel, ActionType |
| 13 | + |
| 14 | +router = APIRouter( |
| 15 | + tags=["oauth"], |
| 16 | +) |
| 17 | + |
| 18 | +# Ensure the templates directory exists |
| 19 | +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 20 | +templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates")) |
| 21 | + |
| 22 | +@router.get("/v1/oauth/authorize", response_class=HTMLResponse) |
| 23 | +async def oauth_authorize( |
| 24 | + request: Request, |
| 25 | + app_id: str, |
| 26 | + state: Optional[str] = None, |
| 27 | +): |
| 28 | + app_data = get_app_by_id_db(app_id) |
| 29 | + if not app_data: |
| 30 | + raise HTTPException(status_code=404, detail="App not found") |
| 31 | + |
| 32 | + app = AppModel(**app_data) |
| 33 | + |
| 34 | + if not app.external_integration: |
| 35 | + raise HTTPException(status_code=400, detail="App does not support external integration") |
| 36 | + |
| 37 | + if not app.external_integration.app_home_url: |
| 38 | + raise HTTPException(status_code=400, detail="App home URL not configured for this app.") |
| 39 | + |
| 40 | + # Prepare permission strings |
| 41 | + permissions = [] |
| 42 | + if app.capabilities: |
| 43 | + if "chat" in app.capabilities: |
| 44 | + permissions.append({"icon": "💬", "text": "Engage in chat conversations with Omi."}) |
| 45 | + if "memories" in app.capabilities: |
| 46 | + permissions.append({"icon": "📝", "text": "Access and manage your conversations."}) |
| 47 | + |
| 48 | + if "external_integration" in app.capabilities and app.external_integration: |
| 49 | + if app.external_integration.triggers_on == 'audio_bytes': |
| 50 | + permissions.append({"icon": "🎤", "text": "Process audio data in real-time."}) |
| 51 | + elif app.external_integration.triggers_on == 'memory_creation': |
| 52 | + permissions.append({"icon": "🔔", "text": "Trigger actions when new conversations are created."}) |
| 53 | + elif app.external_integration.triggers_on == 'transcript_processed': |
| 54 | + permissions.append({"icon": "🎧", "text": "Trigger actions when live transcripts are processed."}) |
| 55 | + |
| 56 | + if app.external_integration.actions: |
| 57 | + for action_item in app.external_integration.actions: |
| 58 | + action_type_value = action_item.action.value |
| 59 | + if action_type_value == ActionType.CREATE_MEMORY.value: |
| 60 | + permissions.append({"icon": "✍️", "text": "Create new conversations on your behalf."}) |
| 61 | + elif action_type_value == ActionType.CREATE_FACTS.value: |
| 62 | + permissions.append({"icon": "➕", "text": "Create new memories for you."}) |
| 63 | + elif action_type_value == ActionType.READ_CONVERSATIONS.value: |
| 64 | + permissions.append({"icon": "📖", "text": "Access and read your conversation history."}) |
| 65 | + elif action_type_value == ActionType.READ_MEMORIES.value: |
| 66 | + permissions.append({"icon": "🔍", "text": "Access and read your stored memories."}) |
| 67 | + |
| 68 | + if "proactive_notification" in app.capabilities and app.proactive_notification and app.proactive_notification.scopes: |
| 69 | + if "user_name" in app.proactive_notification.scopes: |
| 70 | + permissions.append({"icon": "📛", "text": "Access your user name for notifications."}) |
| 71 | + if "user_facts" in app.proactive_notification.scopes: |
| 72 | + permissions.append({"icon": "💡", "text": "Access your facts for notifications."}) |
| 73 | + if "user_context" in app.proactive_notification.scopes: |
| 74 | + permissions.append({"icon": "📜", "text": "Access your conversation context for notifications."}) |
| 75 | + if "user_chat" in app.proactive_notification.scopes: |
| 76 | + permissions.append({"icon": "🗣️", "text": "Access your chat history for notifications."}) |
| 77 | + |
| 78 | + if not permissions: |
| 79 | + permissions.append({"icon": "✅", "text": "Access your basic Omi profile information."}) |
| 80 | + |
| 81 | + # Remove duplicate permissions (based on text) |
| 82 | + unique_permissions = [] |
| 83 | + seen_texts = set() |
| 84 | + for perm in permissions: |
| 85 | + if perm["text"] not in seen_texts: |
| 86 | + unique_permissions.append(perm) |
| 87 | + seen_texts.add(perm["text"]) |
| 88 | + permissions = unique_permissions |
| 89 | + |
| 90 | + return templates.TemplateResponse("oauth_authenticate.html", { |
| 91 | + "request": request, |
| 92 | + "app_id": app_id, |
| 93 | + "app_name": app.name, |
| 94 | + "app_image": app.image, |
| 95 | + "state": state, |
| 96 | + "permissions": permissions, |
| 97 | + "firebase_api_key": os.getenv("FIREBASE_API_KEY"), |
| 98 | + "firebase_auth_domain": os.getenv("FIREBASE_AUTH_DOMAIN"), |
| 99 | + "firebase_project_id": os.getenv("FIREBASE_PROJECT_ID"), |
| 100 | + }) |
| 101 | + |
| 102 | + |
| 103 | +@router.post("/v1/oauth/token") |
| 104 | +async def oauth_token( |
| 105 | + firebase_id_token: str = Form(...), |
| 106 | + app_id: str = Form(...), |
| 107 | + state: Optional[str] = Form(None) |
| 108 | +): |
| 109 | + try: |
| 110 | + decoded_token = firebase_admin.auth.verify_id_token(firebase_id_token) |
| 111 | + uid = decoded_token['uid'] |
| 112 | + except firebase_admin.auth.InvalidIdTokenError as e: |
| 113 | + raise HTTPException(status_code=401, detail=f"Invalid Firebase ID token: {e}") |
| 114 | + except Exception as e: |
| 115 | + raise HTTPException(status_code=401, detail=f"Error verifying Firebase ID token: {e}") |
| 116 | + |
| 117 | + app_data = get_app_by_id_db(app_id) |
| 118 | + if not app_data: |
| 119 | + raise HTTPException(status_code=404, detail="App not found") |
| 120 | + |
| 121 | + app = AppModel(**app_data) |
| 122 | + |
| 123 | + if not app.external_integration or not app.external_integration.app_home_url: |
| 124 | + raise HTTPException(status_code=400, detail="App not configured for OAuth or app home URL not set") |
| 125 | + |
| 126 | + # Validate if the user has enabled this app, if not, try to enable it automatically |
| 127 | + if not is_user_app_enabled(uid, app_id): |
| 128 | + if app.private is not None: |
| 129 | + if app.private and app.uid != uid and not is_tester(uid): |
| 130 | + raise HTTPException(status_code=403, |
| 131 | + detail="This app is private and you are not authorized to enable it.") |
| 132 | + |
| 133 | + # Check Setup completes |
| 134 | + if app.works_externally() and app.external_integration.setup_completed_url: |
| 135 | + try: |
| 136 | + res = requests.get(app.external_integration.setup_completed_url + f'?uid={uid}') |
| 137 | + res.raise_for_status() |
| 138 | + if not res.json().get('is_setup_completed', False): |
| 139 | + raise HTTPException(status_code=400, |
| 140 | + detail='App setup is not completed. Please complete app setup before authorizing.') |
| 141 | + except requests.RequestException as e: |
| 142 | + raise HTTPException(status_code=503, |
| 143 | + detail=f'Failed to verify app setup completion. Please try again later or contact support.') |
| 144 | + except ValueError: |
| 145 | + raise HTTPException(status_code=503, |
| 146 | + detail='Could not verify app setup due to an invalid response from the app. Please contact app developer or support.') |
| 147 | + |
| 148 | + # Check payment status |
| 149 | + if app.is_paid and not get_is_user_paid_app(app.id, uid): |
| 150 | + raise HTTPException(status_code=403, |
| 151 | + detail='This is a paid app. Please purchase the app before authorizing.') |
| 152 | + |
| 153 | + try: |
| 154 | + enable_app(uid, app_id) |
| 155 | + if (app.private is None or not app.private) and \ |
| 156 | + (app.uid is None or app.uid != uid) and \ |
| 157 | + not is_tester(uid): |
| 158 | + increase_app_installs_count(app_id) |
| 159 | + except Exception as e: |
| 160 | + raise HTTPException(status_code=500, detail=f"Could not automatically enable the app. Please try again or enable it manually.") |
| 161 | + |
| 162 | + redirect_url = app.external_integration.app_home_url |
| 163 | + |
| 164 | + return { |
| 165 | + "uid": uid, |
| 166 | + "redirect_url": redirect_url, |
| 167 | + "state": state |
| 168 | + } |
0 commit comments