Skip to content

Commit 1a6ee73

Browse files
authored
Apps new capabilities > auth (#2408)
* Add support omi oauth * Try to enable apps automatically on oauth * Add Omi Oauth Docs
1 parent 6055f4b commit 1a6ee73

File tree

7 files changed

+588
-4
lines changed

7 files changed

+588
-4
lines changed

backend/.env.template

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ STRIPE_CONNECT_WEBHOOK_SECRET=
3737
BASE_API_URL=
3838

3939
RAPID_API_HOST=
40-
RAPID_API_KEY=
40+
RAPID_API_KEY=
41+
42+
# Firebase OAuth
43+
FIREBASE_API_KEY=
44+
FIREBASE_AUTH_DOMAIN=
45+
FIREBASE_PROJECT_ID=

backend/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from modal import Image, App, asgi_app, Secret
88
from routers import workflow, chat, firmware, plugins, transcribe, notifications, \
99
speech_profile, agents, users, trends, sync, apps, custom_auth, \
10-
payment, integration, conversations, memories, mcp
10+
payment, integration, conversations, memories, mcp, oauth # Added oauth
1111

1212
from utils.other.timeout import TimeoutMiddleware
1313

@@ -38,6 +38,7 @@
3838

3939
app.include_router(apps.router)
4040
app.include_router(custom_auth.router)
41+
app.include_router(oauth.router) # Added oauth router
4142

4243
app.include_router(payment.router)
4344
app.include_router(mcp.router)

backend/routers/oauth.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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

Comments
 (0)