Merge branch 'feat/phase2-direct-kc-admin' into 'main'
feat(auth): Phase 2 — audited-users UserPicker + Tier-3 Keycloak Admin lookup See merge request 2-6/2-6-4/terravault/ordinis!272
This commit is contained in:
@@ -796,13 +796,20 @@ export type UserListItem = {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export const allUsersQuery = queryOptions({
|
||||
queryKey: ['users', 'all'] as const,
|
||||
/**
|
||||
* Юзеры, реально появлявшиеся в audit_log — для UserPicker'а в /audit.
|
||||
* Backend resolve'ит distinct user_id из audit_log JOIN с user_display_cache
|
||||
* → отдаёт только тех, кто реально что-то делал в ordinis (не всех юзеров
|
||||
* altum realm). Sub'ы которых нет в cache возвращаются с null display полями
|
||||
* — UserPicker покажет UUID-prefix.
|
||||
*/
|
||||
export const auditedUsersQuery = queryOptions({
|
||||
queryKey: ['users', 'audited'] as const,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
retry: false,
|
||||
queryFn: async (): Promise<UserListItem[]> => {
|
||||
try {
|
||||
const { data } = await apiClient.get<UserListItem[]>('/admin/users', {
|
||||
const { data } = await apiClient.get<UserListItem[]>('/admin/users/audited', {
|
||||
params: { limit: 500 },
|
||||
})
|
||||
return data
|
||||
@@ -814,7 +821,7 @@ export const allUsersQuery = queryOptions({
|
||||
},
|
||||
})
|
||||
|
||||
export const useAllUsers = () => useQuery(allUsersQuery)
|
||||
export const useAuditedUsers = () => useQuery(auditedUsersQuery)
|
||||
export const useRecords = (
|
||||
dictionaryName: string,
|
||||
scopeCsv: string,
|
||||
|
||||
@@ -3,16 +3,19 @@ import { useMemo, useState } from 'react'
|
||||
import { CaretDownIcon, MagnifyingGlassIcon, UserIcon, XIcon } from '@phosphor-icons/react'
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from 'cmdk'
|
||||
import { Popover, PopoverTrigger, PopoverContent, FieldLabel, TextInput } from '@/ui'
|
||||
import { useAllUsers, type UserListItem } from '@/api/queries'
|
||||
import { useAuditedUsers, type UserListItem } from '@/api/queries'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* Single-value user picker — cmdk-powered combobox для audit-фильтра
|
||||
* «Пользователь». Заменяет ручной TextInput на typeahead по username/email/UUID.
|
||||
*
|
||||
* <p>Источник: GET /api/v1/admin/users (cached user_display table).
|
||||
* INTERNAL+ scope требуется на backend; non-INTERNAL получит пустой
|
||||
* список и падает на input fallback (поле остаётся редактируемым).
|
||||
* <p>Источник: GET /api/v1/admin/users/audited — distinct user_id из audit_log
|
||||
* JOIN с user_display_cache. Возвращает только тех, кто реально засветился в
|
||||
* audit (не всех юзеров altum realm). INTERNAL+ scope требуется на backend;
|
||||
* non-INTERNAL получит пустой список и падает на input fallback (поле
|
||||
* остаётся редактируемым). Юзеры, которых ещё нет в user_display_cache,
|
||||
* возвращаются с null username/name/email — здесь рендерится UUID-prefix.
|
||||
*
|
||||
* <p>UX: trigger показывает username (или UUID prefix как fallback);
|
||||
* X сбрасывает selection inline. В popover'е — поиск по subname/email/UUID.
|
||||
@@ -36,7 +39,7 @@ export type UserPickerProps = {
|
||||
|
||||
export function UserPicker({ value, onChange, label, placeholder, disabled }: UserPickerProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const usersQ = useAllUsers()
|
||||
const usersQ = useAuditedUsers()
|
||||
|
||||
// Fallback на TextInput если cache пустой или endpoint вернул 403/404 (юзер
|
||||
// без INTERNAL scope, либо writer был только-что рестартован и cache не
|
||||
|
||||
Reference in New Issue
Block a user