mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 06:30:21 +00:00
* fix: add server-side sorting to user list, prevent client-side sort on paged data The user management table applied client-side sorting to the current page slice while pagination was handled server-side, causing ID-asc views to show pages out of order (e.g. 23-42, 3-22, 1-2). Backend: add sort_by/sort_order query params to GetAllUsers and SearchUsers with a column whitelist for safe ORDER BY generation. Frontend: pass sorting state to the API and reset to page 1 on sort change. Generic useDataTable hook now disables client-side sorted row model and sort UI for tables with manual pagination but no server-side sort handler. * fix: add id tie-breaker to non-unique sort columns, remove side effect from state updater Append secondary ORDER BY id DESC when sorting by non-unique columns (quota, created_at, etc.) to prevent row duplication/skipping across OFFSET pages. Move onPaginationChange out of setSorting updater to avoid side effects inside a pure function (React Strict Mode double-invocation safety).
221 lines
5.5 KiB
TypeScript
Vendored
221 lines
5.5 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import type { PermissionCatalog } from '@/lib/admin-permissions'
|
|
import { api } from '@/lib/api'
|
|
|
|
import type {
|
|
User,
|
|
GetUsersParams,
|
|
GetUsersResponse,
|
|
SearchUsersParams,
|
|
UserFormData,
|
|
ManageUserAction,
|
|
ManageUserQuotaPayload,
|
|
ApiResponse,
|
|
} from './types'
|
|
|
|
// ============================================================================
|
|
// User Management APIs
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get paginated users list
|
|
*/
|
|
export async function getUsers(
|
|
params: GetUsersParams = {}
|
|
): Promise<GetUsersResponse> {
|
|
const { p = 1, page_size = 10, sort_by, sort_order } = params
|
|
const res = await api.get('/api/user/', {
|
|
params: {
|
|
p,
|
|
page_size,
|
|
sort_by,
|
|
sort_order,
|
|
},
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Search users by keyword or group
|
|
*/
|
|
export async function searchUsers(
|
|
params: SearchUsersParams
|
|
): Promise<GetUsersResponse> {
|
|
const {
|
|
keyword = '',
|
|
group = '',
|
|
role = '',
|
|
status = '',
|
|
p = 1,
|
|
page_size = 10,
|
|
sort_by,
|
|
sort_order,
|
|
} = params
|
|
const queryParams = new URLSearchParams()
|
|
queryParams.set('keyword', keyword)
|
|
queryParams.set('group', group)
|
|
if (role) queryParams.set('role', role)
|
|
if (status) queryParams.set('status', status)
|
|
queryParams.set('p', String(p))
|
|
queryParams.set('page_size', String(page_size))
|
|
if (sort_by) queryParams.set('sort_by', sort_by)
|
|
if (sort_order) queryParams.set('sort_order', sort_order)
|
|
const res = await api.get(`/api/user/search?${queryParams.toString()}`)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Get single user by ID
|
|
*/
|
|
export async function getUser(id: number): Promise<ApiResponse<User>> {
|
|
const res = await api.get(`/api/user/${id}`)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Create a new user
|
|
*/
|
|
export async function createUser(
|
|
data: UserFormData
|
|
): Promise<ApiResponse<User>> {
|
|
const res = await api.post('/api/user/', data)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Update an existing user
|
|
*/
|
|
export async function updateUser(
|
|
data: UserFormData & { id: number }
|
|
): Promise<ApiResponse<Partial<User>>> {
|
|
const res = await api.put('/api/user/', data)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Delete a single user (hard delete)
|
|
*/
|
|
export async function deleteUser(id: number): Promise<ApiResponse> {
|
|
const res = await api.delete(`/api/user/${id}/`)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Manage user (promote, demote, enable, disable, delete)
|
|
*/
|
|
export async function manageUser(
|
|
id: number,
|
|
action: ManageUserAction
|
|
): Promise<ApiResponse<Partial<User>>> {
|
|
const res = await api.post('/api/user/manage', { id, action })
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Adjust user quota atomically (add/subtract/override)
|
|
*/
|
|
export async function adjustUserQuota(
|
|
payload: ManageUserQuotaPayload
|
|
): Promise<ApiResponse<Partial<User>>> {
|
|
const res = await api.post('/api/user/manage', payload)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Reset user's Passkey registration
|
|
*/
|
|
export async function resetUserPasskey(id: number): Promise<ApiResponse> {
|
|
const res = await api.delete(`/api/user/${id}/reset_passkey`)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Reset user's Two-Factor Authentication setup
|
|
*/
|
|
export async function resetUserTwoFA(id: number): Promise<ApiResponse> {
|
|
const res = await api.delete(`/api/user/${id}/2fa`)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Get all available groups
|
|
*/
|
|
export async function getGroups(): Promise<ApiResponse<string[]>> {
|
|
const res = await api.get('/api/group/')
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Get the permission catalog (resources, actions, and role baselines).
|
|
* Source of truth lives in the backend authz package.
|
|
*/
|
|
export async function getPermissionCatalog(): Promise<PermissionCatalog> {
|
|
const res = await api.get('/api/authz/catalog')
|
|
return {
|
|
resources: res.data?.data?.resources ?? [],
|
|
roles: res.data?.data?.roles ?? [],
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Admin Binding Management APIs
|
|
// ============================================================================
|
|
|
|
export interface OAuthBinding {
|
|
provider_id: string
|
|
provider_name: string
|
|
user_id?: number
|
|
external_id?: string
|
|
}
|
|
|
|
/**
|
|
* Get user's custom OAuth bindings (admin)
|
|
*/
|
|
export async function getUserOAuthBindings(
|
|
userId: number
|
|
): Promise<ApiResponse<OAuthBinding[]>> {
|
|
const res = await api.get(`/api/user/${userId}/oauth/bindings`)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Clear a user's built-in binding (admin)
|
|
*/
|
|
export async function adminClearUserBinding(
|
|
userId: number,
|
|
bindingType: string
|
|
): Promise<ApiResponse> {
|
|
const res = await api.delete(`/api/user/${userId}/bindings/${bindingType}`)
|
|
return res.data
|
|
}
|
|
|
|
/**
|
|
* Unbind custom OAuth for a user (admin)
|
|
*/
|
|
export async function adminUnbindCustomOAuth(
|
|
userId: number,
|
|
providerId: string
|
|
): Promise<ApiResponse> {
|
|
const res = await api.delete(
|
|
`/api/user/${userId}/oauth/bindings/${providerId}`
|
|
)
|
|
return res.data
|
|
}
|