import { useQuery } from '@tanstack/react-query' import { useAuth } from 'react-oidc-context' import { apiClient } from '@/api/client' /** * Lookup user display info по UUID (JWT sub). Backed by backend in-memory * cache (заполняется на каждом authenticated request через * JwtUserCaptureFilter). Returns null если sub не в кэше — frontend * fallback'нёт на short UUID. * *
Stale-while-revalidate: TanStack Query кэширует 5 минут, refetch * on focus. Cheap — endpoint = O(1) hash lookup, no DB hit. * *
404 → данных нет; не считаем error (через retry: false + handle null).
*/
type UserDisplayInfo = {
sub: string
preferredUsername?: string | null
name?: string | null
email?: string | null
updatedAt: string
}
const useResolveUserDisplay = (uuid: string | null | undefined) =>
useQuery({
queryKey: ['user-display', uuid] as const,
// Не дёргаем endpoint для литерала "anonymous" — это не UUID, а маркер
// отсутствия JWT subject (см. backend currentUserId fallback когда
// ORDINIS_AUTH_REQUIRED=false). Резолв даёт 404, в консоли мусор.
// Вместо запроса фронт сам подменит на «anonymous» в useUserDisplay ниже.
enabled: Boolean(uuid) && uuid !== 'anonymous',
staleTime: 5 * 60 * 1000,
retry: false,
queryFn: async (): Promise Cases:
* Backend endpoint: {@code GET /api/v1/admin/users/{sub}/display} — backed
* by in-memory cache (заполняется JwtUserCaptureFilter на каждом authenticated
* request). Если backend pod restart'нулся, кэш пуст — postepenно заполняется
* обратно. Frontend gracefully fallback'ит на short UUID.
*
* Используется везде где UI показывает actor: AUTHOR в таблицах
* /reviews (record + schema), USER в /audit log, AUTHOR в schema draft
* drawer.
*/
export function useUserDisplay(uuid: string | null | undefined): {
display: string
isMe: boolean
fullId: string
preferredUsername?: string | null
name?: string | null
email?: string | null
} {
const auth = useAuth()
const fullId = uuid ?? ''
const resolved = useResolveUserDisplay(fullId)
if (!fullId) {
return { display: 'anonymous', isMe: false, fullId: '' }
}
// "anonymous" — литерал из backend currentUserId() когда нет JWT
// (ORDINIS_AUTH_REQUIRED=false и токен не дошёл). Не UUID, не резолвим
// через /admin/users/{sub}/display — там будет 404, шум в консоли.
if (fullId === 'anonymous') {
return { display: 'anonymous', isMe: false, fullId }
}
const currentSub = auth.user?.profile?.sub
const isMe = Boolean(currentSub) && currentSub === fullId
if (isMe) {
const me =
(auth.user?.profile?.preferred_username as string | undefined) ||
(auth.user?.profile?.name as string | undefined) ||
(auth.user?.profile?.email as string | undefined)
return { display: me ? `Я • ${me}` : 'Я', isMe, fullId }
}
const info = resolved.data
if (info?.preferredUsername || info?.name) {
return {
display: info.preferredUsername || info.name || '—',
isMe,
fullId,
preferredUsername: info.preferredUsername,
name: info.name,
email: info.email,
}
}
// Fallback — кэш empty или user ещё не появлялся на этом backend'е.
// Показываем em dash вместо raw UUID prefix; full ID доступен в tooltip
// через fullId (см. UserCell title=...).
return { display: '—', isMe, fullId }
}
/**
* UserCell — convenience component для тех мест где нужен readonly displaу
* с tooltip. Используется в таблицах.
*
* Tooltip содержит full UUID + email + name (если есть в кэше) для
* disambiguation: short UUID prefix может collide между users.
*/
export function UserCell({ uuid }: { uuid: string | null | undefined }) {
const { display, isMe, fullId, name, email } = useUserDisplay(uuid)
if (!fullId) return anonymous
const tooltip = [fullId, name, email].filter(Boolean).join('\n')
return (
{display}
)
}
*
*
*