03973a943e
TokenSync.tsx был удалён в Phase 1a (keycloak-js migration, MR !270). В JSDoc на RequireAuth.tsx осталась ссылка на его поведение — обновляем описание под текущую модель: apiClient request interceptor + ensureFreshToken на 401 (см. api/client.ts:64). Минор-долг из checkpoint'а keycloak-js-altum-migration.
82 lines
4.3 KiB
TypeScript
82 lines
4.3 KiB
TypeScript
import { type ReactNode } from 'react'
|
||
import { useAuth } from '@/auth/useAuth'
|
||
|
||
/**
|
||
* Auth gate с гостевым режимом view-only.
|
||
*
|
||
* <p>До v1.1.1 этот компонент force-redirect'ил неаутентифицированных юзеров
|
||
* на Keycloak. Теперь — guest-friendly: anonymous user видит UI с PUBLIC scope
|
||
* данными (backend отдаёт PUBLIC dictionaries без auth, см. SecurityConfig
|
||
* permissive mode + ScopeContext anonymous → PUBLIC).
|
||
*
|
||
* <p>Логин теперь пользовательское действие: клик на кнопку «Войти» в top bar
|
||
* → {@link useAuth().signinRedirect()}. После возврата из Keycloak пользователь
|
||
* получает scope по ролям JWT и видит INTERNAL/RESTRICTED данные.
|
||
*
|
||
* <p>Сценарии:
|
||
* <ul>
|
||
* <li>First visit, нет токена → render children, top bar показывает «Войти».</li>
|
||
* <li>Click «Войти» → signinRedirect → Keycloak → callback → authenticated.</li>
|
||
* <li>OIDC error в URL (?error=...) → error screen (не редиректим в цикле).</li>
|
||
* <li>auth.error (Keycloak недоступен) → error screen с сообщением.</li>
|
||
* <li>Mutations пытаются 401 → apiClient interceptor ловит, делает
|
||
* ensureFreshToken + retry; на refresh failure keycloak-js сам
|
||
* редиректит на /login (full reload). Inline ошибка остаётся для
|
||
* anonymous users без CTA-сценария.</li>
|
||
* </ul>
|
||
*/
|
||
export function RequireAuth({ children }: { children: ReactNode }) {
|
||
const auth = useAuth()
|
||
|
||
const oidcError =
|
||
typeof window !== 'undefined' &&
|
||
new URLSearchParams(window.location.search).get('error')
|
||
|
||
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-title-lg font-sans">Ошибка входа</h1>
|
||
<p className="text-body text-ink-2">
|
||
Keycloak вернул ошибку:{' '}
|
||
<code className="font-mono">{oidcError}</code>
|
||
</p>
|
||
<button
|
||
type="button"
|
||
className="mt-4 inline-flex items-center justify-center rounded-sm border border-accent text-accent px-4 py-2 text-body hover:bg-accent-bg"
|
||
onClick={() => {
|
||
window.location.href = '/'
|
||
}}
|
||
>
|
||
Попробовать снова
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Keycloak unreachable / OIDC discovery failed — раньше показывали amber
|
||
// banner вверху страницы, но per user feedback ("Авторизация недоступна:
|
||
// Failed to fetch.?") это noise: anonymous mode работает без auth, error
|
||
// banner лишь пугает. Тихо рендерим children в guest mode — AuthBadge
|
||
// в TopBar показывает Sign in для retry.
|
||
if (auth.error && !auth.isAuthenticated) {
|
||
return <>{children}</>
|
||
}
|
||
|
||
// ВАЖНО: НЕ блокируем UI пока {@code auth.isLoading=true}. Initial OIDC
|
||
// bootstrap может зависать на 10-20 сек если Keycloak silent SSO iframe
|
||
// блокируется X-Frame-Options или сеть медленная. Anonymous user в это
|
||
// время видит только spinner и может подумать что сайт сломан.
|
||
// Решение: рендерим children immediately. AuthBadge в top bar сам
|
||
// показывает loading state своим маленьким индикатором, а PUBLIC данные
|
||
// (которые backend отдаёт без auth) грузятся параллельно с silent SSO.
|
||
// Если silent SSO succeed позже — useAuth() обновит scope, кнопки
|
||
// create/edit активируются автоматически (canMutate реактивен).
|
||
|
||
// Anonymous OR authenticated OR loading — рендерим children всегда. UI в
|
||
// компонентах сам адаптируется (через useAuth().isAuthenticated):
|
||
// hide mutation buttons, show «Войти» CTA, тонировать blocked actions.
|
||
return <>{children}</>
|
||
}
|