diff --git a/backend/open_webui/models/apps.py b/backend/open_webui/models/apps.py index ade9f9aa2e..2b118c2a75 100644 --- a/backend/open_webui/models/apps.py +++ b/backend/open_webui/models/apps.py @@ -102,6 +102,8 @@ class AppInteractionForm(StrictModel): class AppManifestForm(StrictModel): url: str = Field(max_length=2048) + api_key: SecretStr | None = Field(default=None, min_length=1, max_length=4096) + app_id: str | None = None class AppForm(StrictModel): diff --git a/backend/open_webui/routers/apps.py b/backend/open_webui/routers/apps.py index 0008346ee2..f3d582a6f5 100644 --- a/backend/open_webui/routers/apps.py +++ b/backend/open_webui/routers/apps.py @@ -76,13 +76,30 @@ async def get_apps(user=Depends(get_verified_user), db: AsyncSession = Depends(g @router.post('/manifest/preview', response_model=Manifest) -async def preview_app_manifest(form_data: AppManifestForm, user=Depends(get_admin_user)): +async def preview_app_manifest( + form_data: AppManifestForm, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) +): if not await Config.get('apps.enable'): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Apps are disabled') + api_key = form_data.api_key.get_secret_value() if form_data.api_key is not None else None + if form_data.app_id: + app = await get_app_by_id(form_data.app_id, db=db) + if form_data.url != app.data['url']: + raise HTTPException(400, 'App URL does not match the installed app') + if api_key is None and app.api_key: + api_key = get_app_encryption_cipher().decrypt(app.api_key.encode()).decode() + if not api_key: + raise HTTPException(400, 'An app API key is required') try: - status_code, raw = await send_app_request(form_data.url) + status_code, raw = await send_app_request( + form_data.url, headers={'Authorization': f'Bearer {api_key}'} + ) + if status_code in (401, 403): + raise HTTPException(400, 'App rejected the API key. Check the API key configured on the app server.') if status_code != 200: - raise HTTPException(502, f'App manifest endpoint returned HTTP {status_code}. Check the URL or enter details manually.') + raise HTTPException( + 502, f'App manifest endpoint returned HTTP {status_code}. Check the URL or enter details manually.' + ) manifest = Manifest.model_validate(raw) return manifest.model_dump() except HTTPException: diff --git a/src/lib/apis/apps/index.ts b/src/lib/apis/apps/index.ts index 658a36340c..5e7c5f6c05 100644 --- a/src/lib/apis/apps/index.ts +++ b/src/lib/apis/apps/index.ts @@ -157,7 +157,11 @@ export const deleteAppById = async (token: string = '', app_id: string) => { return res; }; -export const previewAppManifest = async (token: string = '', url: string) => { +export const previewAppManifest = async ( + token: string = '', + url: string, + data: { api_key?: string; app_id?: string } +) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/apps/manifest/preview`, { @@ -167,7 +171,7 @@ export const previewAppManifest = async (token: string = '', url: string) => { 'Content-Type': 'application/json', authorization: `Bearer ${token}` }, - body: JSON.stringify({ url }) + body: JSON.stringify({ url, ...data }) }) .then(async (res) => { if (!res.ok) throw await res.json(); diff --git a/src/lib/components/admin/Settings/Apps/AppModal.svelte b/src/lib/components/admin/Settings/Apps/AppModal.svelte index fa51d023c6..db42790a21 100644 --- a/src/lib/components/admin/Settings/Apps/AppModal.svelte +++ b/src/lib/components/admin/Settings/Apps/AppModal.svelte @@ -77,7 +77,10 @@ async function submit() { await run(async () => { if (!manifest) { - manifest = await previewAppManifest(localStorage.token, url); + manifest = await previewAppManifest(localStorage.token, url, { + ...(apiKey ? { api_key: apiKey } : {}), + ...(app ? { app_id: app.id } : {}) + }); manual = false; name = manifest.name; slug = @@ -288,6 +291,24 @@ placeholder="https://app.example.com/events" /> + + + {:else}
run(async () => { - manifest = await previewAppManifest(localStorage.token, url); + manifest = await previewAppManifest(localStorage.token, url, { + ...(apiKey ? { api_key: apiKey } : {}), + ...(app ? { app_id: app.id } : {}) + }); scopes = scopes.filter((scope) => manifest.requested_scopes.includes(scope)); manifestUpdated = true; showScopes = true; @@ -360,6 +384,7 @@ {$i18n.t('Saving approves the updated app identity and selected scopes.')}

{/if} +