diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 4d5f35e2c3..238434c81b 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -925,6 +925,34 @@ async def revoke_user_sessions(request: Request, user_id: str, session_user=Depe return True +@router.delete('/{user_id}/api_key', response_model=bool) +async def delete_user_api_key_by_id( + request: Request, + user_id: str, + session_user: UserModel = Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + target_user = await Users.get_user_by_id(user_id, db=db) + if target_user is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.USER_NOT_FOUND) + + first_user = await Users.get_first_user(db=db) + if first_user and first_user.id == user_id and session_user.id != user_id: + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACTION_PROHIBITED) + + # Revocation must remain available when API keys or the user's permission are disabled. + await Users.delete_user_api_key_by_id(user_id, db=db) + await publish_event( + request, + EVENTS.AUTH_API_KEY_DELETED, + actor=session_user, + subject_id=user_id, + subject_type='user', + source='admin', + ) + return True + + @router.post('/{user_id}/update', response_model=UserModel | None) async def update_user_by_id( request: Request, diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index d72714862d..e14777382c 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -757,3 +757,13 @@ export const revokeUserSessions = async (token: string, userId: string) => { if (!response.ok) throw new Error(result.detail || 'Failed to revoke sessions'); return result; }; + +export const deleteUserAPIKeyById = async (token: string, userId: string) => { + const response = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/api_key`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` } + }); + const result = await response.json(); + if (!response.ok) throw new Error(result.detail || 'Failed to delete API key'); + return result; +}; diff --git a/src/lib/components/admin/Users/UserList/EditUserModal.svelte b/src/lib/components/admin/Users/UserList/EditUserModal.svelte index 5d5893b0c5..094bd14f9f 100644 --- a/src/lib/components/admin/Users/UserList/EditUserModal.svelte +++ b/src/lib/components/admin/Users/UserList/EditUserModal.svelte @@ -6,11 +6,18 @@ import { goto } from '$app/navigation'; - import { updateUserById, getUserGroupsById, revokeUserSessions } from '$lib/apis/users'; + import { + updateUserById, + getUserGroupsById, + revokeUserSessions, + deleteUserAPIKeyById + } from '$lib/apis/users'; import Modal from '$lib/components/common/Modal.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; let showRevokeConfirmation = false; + let showDeleteAPIKeyConfirmation = false; + let deletingAPIKey = false; import localizedFormat from 'dayjs/plugin/localizedFormat'; import XMark from '$lib/components/icons/XMark.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; @@ -67,6 +74,24 @@ }; + { + deletingAPIKey = true; + try { + await deleteUserAPIKeyById(localStorage.token, selectedUser.id); + toast.success($i18n.t('API Key deleted.')); + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)); + } finally { + deletingAPIKey = false; + } + }} +/> + +
+
{$i18n.t('API Key')}
+ +