This commit is contained in:
Timothy Jaeryang Baek 2026-10-29 06:32:47 +09:00
parent 0a4be809c4
commit 360d81dfe8
3 changed files with 75 additions and 1 deletions

View file

@ -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,

View file

@ -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;
};

View file

@ -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 @@
};
</script>
<ConfirmDialog
bind:show={showDeleteAPIKeyConfirmation}
title={$i18n.t('Delete API Key?')}
message={$i18n.t('This will revoke the current API key.')}
confirmLabel={$i18n.t('Delete')}
on:confirm={async () => {
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;
}
}}
/>
<ConfirmDialog
bind:show={showRevokeConfirmation}
title={$i18n.t('Sign out all devices?')}
@ -231,6 +256,17 @@
/>
</div>
</div>
<div class="flex items-center justify-between w-full">
<div class="text-xs text-gray-500">{$i18n.t('API Key')}</div>
<button
type="button"
class="text-xs text-gray-500 transition-colors hover:text-gray-700 disabled:opacity-50 dark:text-gray-400 dark:hover:text-gray-200"
disabled={deletingAPIKey}
on:click={() => {
showDeleteAPIKeyConfirmation = true;
}}>{$i18n.t('Delete')}</button
>
</div>
</div>
</div>
</div>