From fdaa240d441de02a687f3ea069409f5ff7851a74 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 29 Oct 2026 06:49:47 +0900 Subject: [PATCH] refac --- backend/open_webui/routers/users.py | 39 ++++++++- src/lib/apis/users/index.ts | 10 +++ .../admin/Users/UserList/EditUserModal.svelte | 79 ++++++++++++++++--- src/lib/i18n/locales/en-US/translation.json | 4 + 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 238434c81b..4c3c5bf7e6 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -16,7 +16,7 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.events import EVENTS, publish_event from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES, STATIC_DIR from open_webui.internal.db import get_async_session -from open_webui.models.auths import Auths +from open_webui.models.auths import ApiKey, Auths from open_webui.models.config import Config from open_webui.models.chat_messages import ChatMessages from open_webui.models.chats import Chats @@ -41,6 +41,7 @@ from open_webui.models.models import Models from open_webui.models.tools import Tools from open_webui.utils.access_control import fill_missing_permissions, get_permissions, has_permission from open_webui.utils.auth import ( + create_api_key, get_admin_user, get_password_hash, get_verified_user, @@ -925,6 +926,42 @@ async def revoke_user_sessions(request: Request, user_id: str, session_user=Depe return True +@router.post('/{user_id}/api_key/rotate', response_model=ApiKey) +async def rotate_user_api_key_by_id( + request: Request, + response: Response, + 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) + + if not await Config.get('auth.enable_api_keys') or ( + target_user.role != 'admin' + and not await has_permission(target_user.id, 'features.api_keys', await Config.get('user.permissions'), db=db) + ): + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_CREATION_NOT_ALLOWED) + + api_key = create_api_key() + await Users.update_user_api_key_by_id(user_id, api_key, db=db) + await publish_event( + request, + EVENTS.AUTH_API_KEY_CREATED, + actor=session_user, + subject_id=user_id, + subject_type='user', + source='admin', + ) + response.headers['Cache-Control'] = 'no-store' + return {'api_key': api_key} + + @router.delete('/{user_id}/api_key', response_model=bool) async def delete_user_api_key_by_id( request: Request, diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index e14777382c..6e3780e044 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -758,6 +758,16 @@ export const revokeUserSessions = async (token: string, userId: string) => { return result; }; +export const rotateUserAPIKeyById = async (token: string, userId: string): Promise => { + const response = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/api_key/rotate`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` } + }); + const result = await response.json(); + if (!response.ok) throw new Error(result.detail || 'Failed to rotate API key'); + return result.api_key; +}; + export const deleteUserAPIKeyById = async (token: string, userId: string) => { const response = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/api_key`, { method: 'DELETE', diff --git a/src/lib/components/admin/Users/UserList/EditUserModal.svelte b/src/lib/components/admin/Users/UserList/EditUserModal.svelte index 094bd14f9f..d63f636e1f 100644 --- a/src/lib/components/admin/Users/UserList/EditUserModal.svelte +++ b/src/lib/components/admin/Users/UserList/EditUserModal.svelte @@ -10,14 +10,17 @@ updateUserById, getUserGroupsById, revokeUserSessions, + rotateUserAPIKeyById, deleteUserAPIKeyById } from '$lib/apis/users'; + import { copyToClipboard } from '$lib/utils'; 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; + let showRotateAPIKeyConfirmation = false; + let rotatedAPIKey = ''; import localizedFormat from 'dayjs/plugin/localizedFormat'; import XMark from '$lib/components/icons/XMark.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; @@ -31,11 +34,14 @@ export let selectedUser; export let sessionUser; - $: if (show) { + $: if (show && selectedUser) { init(); + } else { + rotatedAPIKey = ''; } const init = () => { + rotatedAPIKey = ''; if (selectedUser) { _user = { ...selectedUser, password: '' }; loadUserGroups(); @@ -74,20 +80,38 @@ }; + { + const target = _user; + try { + const key = await rotateUserAPIKeyById(localStorage.token, selectedUser.id); + if (show && target === _user) rotatedAPIKey = key; + toast.success($i18n.t('API Key created.')); + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error)); + } + }} +/> + { - deletingAPIKey = true; + const target = _user; try { await deleteUserAPIKeyById(localStorage.token, selectedUser.id); + if (target === _user) rotatedAPIKey = ''; toast.success($i18n.t('API Key deleted.')); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); - } finally { - deletingAPIKey = false; } }} /> @@ -258,15 +282,44 @@
{$i18n.t('API Key')}
- +
+ + +
+ {#if rotatedAPIKey} +
+
+ + +
+

+ {$i18n.t( + 'Copy this key now. It will not be shown here again after you close this dialog.' + )} +

+
+ {/if} diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index 962988ce05..6d1a047ad7 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -660,6 +660,7 @@ "Copy Prompt": "", "Copy proposed revision to clipboard": "", "Copy Share Link": "", + "Copy this key now. It will not be shown here again after you close this dialog.": "", "Copy to clipboard": "", "Copy Token": "", "Copy URL": "", @@ -2328,6 +2329,8 @@ "Revised": "", "Rewrite the selected text.": "", "Role": "", + "Rotate": "", + "Rotate API Key?": "", "RTL": "", "Run": "", "Run AI on your own terms. Connect any model, extend with code, and protect what matters without compromise. Your models, your data, your machine, wherever you open it.": "", @@ -3887,6 +3890,7 @@ "This will delete the cached model and cannot be undone.": "", "This will permanently delete the calendar \"{{name}}\" and all its events. This action cannot be undone.": "", "This will remove all files and directories from this knowledge base. This action cannot be undone.": "", + "This will replace the current API key. Integrations using the old key will stop working.": "", "This will reset the saved settings for this base model to their defaults. The model will remain available.": "", "This will revoke the current API key.": "", "Thorough explanation": "",