diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 69691a5d5..3b95d1609 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -10,6 +10,8 @@ from open_webui.models.groups import Groups from pydantic import BaseModel, ConfigDict from sqlalchemy import BigInteger, Column, String, Text +from sqlalchemy import or_ + #################### # User DB Schema @@ -67,6 +69,11 @@ class UserModel(BaseModel): #################### +class UserListResponse(BaseModel): + users: list[UserModel] + total: int + + class UserResponse(BaseModel): id: str name: str @@ -161,25 +168,62 @@ class UsersTable: def get_users( self, + filter: Optional[dict] = None, skip: Optional[int] = None, limit: Optional[int] = None, - query_key: Optional[int] = None - ) -> list[UserModel]: + ) -> UserListResponse: with get_db() as db: + query = db.query(User) - if not query_key: - query = db.query(User).order_by(User.created_at.desc()) - else: - query = ( - db.query(User) - .filter( + if filter: + query_key = filter.get("query") + if query_key: + query = query.filter( or_( - User.name.ilike(f'%{query_key}%'), - User.email.ilike(f'%{query_key}%') + User.name.ilike(f"%{query_key}%"), + User.email.ilike(f"%{query_key}%"), ) ) - .order_by(User.created_at.desc()) - ) + + order_by = filter.get("order_by") + direction = filter.get("direction") + + if order_by == "name": + if direction == "asc": + query = query.order_by(User.name.asc()) + else: + query = query.order_by(User.name.desc()) + elif order_by == "email": + if direction == "asc": + query = query.order_by(User.email.asc()) + else: + query = query.order_by(User.email.desc()) + + elif order_by == "created_at": + if direction == "asc": + query = query.order_by(User.created_at.asc()) + else: + query = query.order_by(User.created_at.desc()) + + elif order_by == "last_active_at": + if direction == "asc": + query = query.order_by(User.last_active_at.asc()) + else: + query = query.order_by(User.last_active_at.desc()) + + elif order_by == "updated_at": + if direction == "asc": + query = query.order_by(User.updated_at.asc()) + else: + query = query.order_by(User.updated_at.desc()) + elif order_by == "role": + if direction == "asc": + query = query.order_by(User.role.asc()) + else: + query = query.order_by(User.role.desc()) + + else: + query = query.order_by(User.created_at.desc()) if skip: query = query.offset(skip) @@ -187,8 +231,10 @@ class UsersTable: query = query.limit(limit) users = query.all() - - return [UserModel.model_validate(user) for user in users] + return { + "users": [UserModel.model_validate(user) for user in users], + "total": db.query(User).count(), + } def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]: with get_db() as db: diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index cac7215a4..0c348f51e 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -6,6 +6,7 @@ from open_webui.models.groups import Groups from open_webui.models.chats import Chats from open_webui.models.users import ( UserModel, + UserListResponse, UserRoleUpdateForm, Users, UserSettings, @@ -33,23 +34,38 @@ router = APIRouter() ############################ -@router.get("/", response_model=list[UserModel]) +PAGE_ITEM_COUNT = 10 + + +@router.get("/", response_model=UserListResponse) async def get_users( - page: Optional[int] = None, - limit: Optional[int] = None, - q: Optional[str] = None, + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, user=Depends(get_admin_user), ): - if q: - skip: Optional[int] = None - if page: - skip = (page - 1) * limit - return Users.get_users(skip=skip, limit=limit, query_key=q) - else: - skip: Optional[int] = None - if page: - skip = (page - 1) * limit - return Users.get_users(skip=skip, limit=limit) + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if query: + filter["query"] = query + if order_by: + filter["order_by"] = order_by + if direction: + filter["direction"] = direction + + return Users.get_users(filter=filter, skip=skip, limit=limit) + + +@router.get("/all", response_model=UserListResponse) +async def get_all_users( + user=Depends(get_admin_user), +): + return Users.get_users() ############################ diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index d8a7bb871..85f5f2252 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -116,65 +116,54 @@ export const updateUserRole = async (token: string, id: string, role: string) => return res; }; -export const getUsers = async (token: string, page?: number, limit: number = 10, q?: string) => { +export const getUsers = async ( + token: string, + query?: string, + orderBy?: string, + direction?: string, + page = 1 +) => { let error = null; let res = null; - if (q !== undefined) { - res = await fetch(`${WEBUI_API_BASE_URL}/users/?q=${q}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - } - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); - } else if (page !== undefined) { - res = await fetch(`${WEBUI_API_BASE_URL}/users/?page=${page}&limit=${limit}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - } - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); - } else { - res = await fetch(`${WEBUI_API_BASE_URL}/users/`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - } - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + + let searchParams = new URLSearchParams(); + + searchParams.set('page', `${page}`); + + if (query) { + searchParams.set('query', query); } + + if (orderBy) { + searchParams.set('order_by', orderBy); + } + + if (direction) { + searchParams.set('direction', direction); + } + + res = await fetch(`${WEBUI_API_BASE_URL}/users/?${searchParams.toString()}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + if (error) { throw error; } - return res ? res : []; + + return res; }; export const getUserSettings = async (token: string) => { diff --git a/src/lib/components/admin/Users.svelte b/src/lib/components/admin/Users.svelte index 7abde9dc5..e777757e0 100644 --- a/src/lib/components/admin/Users.svelte +++ b/src/lib/components/admin/Users.svelte @@ -5,34 +5,19 @@ import { goto } from '$app/navigation'; import { user } from '$lib/stores'; - import { getUsers } from '$lib/apis/users'; - import UserList from './Users/UserList.svelte'; import Groups from './Users/Groups.svelte'; const i18n = getContext('i18n'); - let users = []; - let totalUsers = 0; - let selectedTab = 'overview'; let loaded = false; - $: if (selectedTab) { - getUsersHandler(); - } - - const getUsersHandler = async () => { - users = await getUsers(localStorage.token); - }; - onMount(async () => { if ($user?.role !== 'admin') { await goto('/'); - } else { - users = await getUsers(localStorage.token); - totalUsers = users.length; } + loaded = true; const containerElement = document.getElementById('users-tabs-container'); @@ -104,9 +89,9 @@
{#if selectedTab === 'overview'} - + {:else if selectedTab === 'groups'} - + {/if}
diff --git a/src/lib/components/admin/Users/UserList.svelte b/src/lib/components/admin/Users/UserList.svelte index b36940dae..738489c55 100644 --- a/src/lib/components/admin/Users/UserList.svelte +++ b/src/lib/components/admin/Users/UserList.svelte @@ -33,14 +33,17 @@ const i18n = getContext('i18n'); - export let totalUsers = 0; - let users = [] - - let search = ''; - let selectedUser = null; - let page = 1; + let users = []; + let total = 0; + + let query = ''; + let orderBy = 'created_at'; // default sort key + let direction = 'asc'; // default sort order + + let selectedUser = null; + let showDeleteConfirmDialog = false; let showAddUserModal = false; @@ -54,7 +57,7 @@ }); if (res) { - users = await getUsers(localStorage.token); + getUserList(); } }; @@ -64,48 +67,43 @@ return null; }); if (res) { - users = await getUsers(localStorage.token); + getUserList(); } }; - const fetchUserPage = async () => { - try { - users = await getUsers(localStorage.token, page); - } catch (err) { - console.error("Error fetching users: " + err); - } - }; - let sortKey = 'created_at'; // default sort key - let sortOrder = 'asc'; // default sort order - - function setSortKey(key) { - if (sortKey === key) { - sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; + const setSortKey = (key) => { + if (orderBy === key) { + direction = direction === 'asc' ? 'desc' : 'asc'; } else { - sortKey = key; - sortOrder = 'asc'; + orderBy = key; + direction = 'asc'; } - } + }; - const queryUser = async (q) => { + const getUserList = async () => { try { - const result = await getUsers(localStorage.token, undefined, 10, q); - filteredUsers = result.slice((page - 1) * 10, page * 10); + const res = await getUsers(localStorage.token, query, orderBy, direction, page).catch( + (error) => { + toast.error(`${error}`); + return null; + } + ); + + if (res) { + users = res.users; + total = res.total; + } } catch (err) { console.error(err); } }; - let filteredUsers; - $: if (search.trim() === '') { - filteredUsers = users - .sort((a, b) => { - if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1; - if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1; - return 0; - }) - } else { - queryUser(search); + $: if (page) { + getUserList(); + } + + $: if (query !== null && orderBy && direction) { + getUserList(); } @@ -122,7 +120,7 @@ {selectedUser} sessionUser={$user} on:save={async () => { - users = await getUsers(localStorage.token); + getUserList(); }} /> {/key} @@ -130,7 +128,7 @@ { - users = await getUsers(localStorage.token); + getUserList(); }} /> @@ -193,7 +191,7 @@ @@ -232,9 +230,9 @@
{$i18n.t('Role')} - {#if sortKey === 'role'} + {#if orderBy === 'role'} {#if sortOrder === 'asc'} + >{#if direction === 'asc'} {:else} @@ -255,9 +253,9 @@
{$i18n.t('Name')} - {#if sortKey === 'name'} + {#if orderBy === 'name'} {#if sortOrder === 'asc'} + >{#if direction === 'asc'} {:else} @@ -278,9 +276,9 @@
{$i18n.t('Email')} - {#if sortKey === 'email'} + {#if orderBy === 'email'} {#if sortOrder === 'asc'} + >{#if direction === 'asc'} {:else} @@ -302,9 +300,9 @@
{$i18n.t('Last Active')} - {#if sortKey === 'last_active_at'} + {#if orderBy === 'last_active_at'} {#if sortOrder === 'asc'} + >{#if direction === 'asc'} {:else} @@ -324,9 +322,9 @@ >
{$i18n.t('Created at')} - {#if sortKey === 'created_at'} + {#if orderBy === 'created_at'} {#if sortOrder === 'asc'} + >{#if direction === 'asc'} {:else} @@ -348,9 +346,9 @@
{$i18n.t('OAuth ID')} - {#if sortKey === 'oauth_sub'} + {#if orderBy === 'oauth_sub'} {#if sortOrder === 'asc'} + >{#if direction === 'asc'} {:else} @@ -368,7 +366,7 @@ - {#each filteredUsers as user, userIdx} + {#each users as user, userIdx}