Files
mdm-ordinis/ordinis-admin-ui/src/auth/RequireAuth.tsx
T
Zimin A.N. ab6f6cb860 feat(admin-ui): RequireAuth gate + RTL component тесты
RequireAuth: гейтит весь UI по аутентификации. Anonymous юзер вместо
красного "AxiosError 401" получает immediate signinRedirect на Keycloak;
authenticated юзер с истёкшим токеном получает silent renew → fallback на
full redirect. Guards против infinite loop:
- auth.isLoading / activeNavigator — bootstrap или signin/signout уже идёт
- ?error= в URL — вернулись с провалом из Keycloak, показываем сообщение
- auth.error без isAuthenticated — Keycloak недоступен, показываем сообщение

main.tsx: RequireAuth обёрнут вокруг Router/Query, чтобы tan-stack-router и
React Query не делали запросов до auth (иначе 401-каскад на старте).

TokenSync: 401 interceptor теперь редиректит и anonymous (раньше срабатывал
только для authenticated с истёкшим токеном), с теми же guards.

RTL component тесты — 22 теста на 3 ключевых компонента:
- SchemaBuilder (7): empty state, add/remove/reorder properties
- PropertyEditor (10): kind change, required/unique toggle, kind-specific
  поля (string→pattern, integer→min/max, enum→values, array_string→uniqueItems),
  move up/down disabled на boundaries
- SchemaDrivenForm (5): identity tab с required, переключение на extra,
  isPending → save disabled, cancel callback, serverError → Alert

Setup:
- vite.config.ts: test block (jsdom env, setup file, css off)
- src/test/setup.ts: jest-dom matchers, cleanup, HTMLCanvasElement.getContext
  stub (lottie-web из @nstart/ui крашит в jsdom без canvas)
- src/test/render.tsx: renderWithI18n helper, форсит ru-RU
  (LanguageDetector в node по умолчанию даёт en-US)

Deps: +@testing-library/react +@testing-library/jest-dom
+@testing-library/user-event +jsdom
2026-05-06 00:11:07 +03:00

80 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, type ReactNode } from 'react'
import { useAuth } from 'react-oidc-context'
/**
* Гейтит всё содержимое приложения по аутентификации. Если юзер не залогинен —
* сразу редиректим на Keycloak (без промежуточной страницы "Вы не авторизованы"
* и без красного 401 от axios). Пока auth state ещё определяется — показываем
* нейтральный spinner.
*
* Сценарии:
* - First visit, нет токена → signinRedirect → Keycloak login → redirect_uri →
* onSigninCallback чистит URL → дети рендерятся.
* - Истёк access_token, silent renew не успел → 401 от backend ловится в
* {@link TokenSync} interceptor, который дёргает signinSilent/signinRedirect.
* - Возврат с ошибкой OIDC (?error=access_denied) → не редиректим в цикле,
* показываем сообщение.
*/
export function RequireAuth({ children }: { children: ReactNode }) {
const auth = useAuth()
const oidcError =
typeof window !== 'undefined' &&
new URLSearchParams(window.location.search).get('error')
useEffect(() => {
if (oidcError) return
if (auth.isLoading || auth.activeNavigator) return
if (auth.isAuthenticated) return
auth.signinRedirect().catch((err) => {
// Не уйти на signinRedirect = Keycloak недоступен. Логируем, дальше
// покажем сообщение об ошибке (auth.error будет выставлен).
console.error('[auth] signinRedirect failed', err)
})
}, [auth.isAuthenticated, auth.isLoading, auth.activeNavigator, oidcError])
if (oidcError) {
return (
<div className="min-h-screen flex items-center justify-center p-8">
<div className="max-w-md text-center space-y-2">
<h1 className="text-lg font-primary">Ошибка входа</h1>
<p className="text-sm text-carbon/70">
Keycloak вернул ошибку:{' '}
<code className="font-mono">{oidcError}</code>
</p>
<button
type="button"
className="mt-4 inline-flex items-center justify-center rounded-sm border border-ultramarain text-ultramarain px-4 py-2 text-sm hover:bg-orbit"
onClick={() => {
window.location.href = '/'
}}
>
Попробовать снова
</button>
</div>
</div>
)
}
if (auth.error && !auth.isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center p-8">
<div className="max-w-md text-center space-y-2">
<h1 className="text-lg font-primary">Не удалось подключиться к авторизации</h1>
<p className="text-sm text-carbon/70">{auth.error.message}</p>
</div>
</div>
)
}
if (!auth.isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-sm text-carbon/60 font-secondary">Перенаправление на вход</div>
</div>
)
}
return <>{children}</>
}