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"
/>
+