diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index ad76aafb61..32f2ac0379 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -358,6 +358,24 @@ class TerminalServerPolicyForm(BaseModel): policy_data: dict +class TerminalServerLifecycleForm(BaseModel): + url: str + key: str | None = '' + auth_type: str | None = 'bearer' + policy_id: str + lifecycle_data: dict + + +class TerminalServerRefreshForm(BaseModel): + url: str + key: str | None = '' + auth_type: str | None = 'bearer' + user_id: str | None = None + policy_id: str | None = None + only_idle: bool = True + reset: bool = False + + @router.post('/terminal_servers/policy') async def put_terminal_server_policy( request: Request, form_data: TerminalServerPolicyForm, user=Depends(get_admin_user) @@ -393,6 +411,91 @@ async def put_terminal_server_policy( raise HTTPException(status_code=400, detail='Failed to save policy to terminal server') +@router.post('/terminal_servers/lifecycle') +async def put_terminal_server_lifecycle( + request: Request, form_data: TerminalServerLifecycleForm, user=Depends(get_admin_user) +): + """ + Proxy a policy lifecycle PUT to an orchestrator terminal server. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {'Content-Type': 'application/json'} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + lifecycle_url = f'{base_url}/api/v1/policies/{form_data.policy_id}/lifecycle' + async with session.put( + lifecycle_url, + headers=headers, + json=form_data.lifecycle_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as resp: + if resp.ok: + return await resp.json() + detail = await resp.text() + raise HTTPException(status_code=resp.status, detail=detail) + except HTTPException: + raise + except Exception as e: + log.debug(f'Failed to save lifecycle to terminal server: {e}') + raise HTTPException(status_code=400, detail='Failed to save lifecycle to terminal server') + + +@router.post('/terminal_servers/refresh') +async def refresh_terminal_server_terminals( + request: Request, form_data: TerminalServerRefreshForm, user=Depends(get_admin_user) +): + """ + Proxy a terminal refresh request to an orchestrator terminal server. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {'Content-Type': 'application/json'} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + body = { + 'only_idle': form_data.only_idle, + 'reset': form_data.reset, + } + if form_data.user_id: + body['user_id'] = form_data.user_id + if form_data.policy_id: + body['policy_id'] = form_data.policy_id + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + refresh_url = f'{base_url}/api/v1/terminals/refresh' + async with session.post( + refresh_url, + headers=headers, + json=body, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as resp: + if resp.ok: + return await resp.json() + detail = await resp.text() + raise HTTPException(status_code=resp.status, detail=detail) + except HTTPException: + raise + except Exception as e: + log.debug(f'Failed to refresh terminals: {e}') + raise HTTPException(status_code=400, detail='Failed to refresh terminals') + + @router.post('/tool_servers/verify') async def verify_tool_servers_config(request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)): """ diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index 7859a4e787..f890b9edf2 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -1,7 +1,7 @@ import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import type { Banner } from '$lib/types'; -export const importConfig = async (token: string, config) => { +export const importConfig = async (token: string, config: object) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/configs/import`, { @@ -275,7 +275,8 @@ export const putOrchestratorPolicy = async ( url: string, key: string, policyId: string, - policyData: object + policyData: object, + authType: string = 'bearer' ): Promise => { let error = null; @@ -288,6 +289,7 @@ export const putOrchestratorPolicy = async ( body: JSON.stringify({ url: url.replace(/\/$/, ''), key, + auth_type: authType, policy_id: policyId, policy_data: policyData }) @@ -309,6 +311,91 @@ export const putOrchestratorPolicy = async ( return res; }; +export const putOrchestratorLifecycle = async ( + token: string, + url: string, + key: string, + policyId: string, + lifecycleData: object, + authType: string = 'bearer' +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/lifecycle`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url.replace(/\/$/, ''), + key, + auth_type: authType, + policy_id: policyId, + lifecycle_data: lifecycleData + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const refreshOrchestratorTerminals = async ( + token: string, + url: string, + key: string, + body: { + user_id?: string; + policy_id?: string; + only_idle?: boolean; + reset?: boolean; + }, + authType: string = 'bearer' +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/refresh`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url.replace(/\/$/, ''), + key, + auth_type: authType, + ...body + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + /** * Verify a terminal server connection via the backend proxy. * Used for system/admin connections to avoid CORS issues and API key exposure. diff --git a/src/lib/components/AddTerminalServerModal.svelte b/src/lib/components/AddTerminalServerModal.svelte index e7c0236eb6..3ba36e6f15 100644 --- a/src/lib/components/AddTerminalServerModal.svelte +++ b/src/lib/components/AddTerminalServerModal.svelte @@ -1,7 +1,7 @@