Files
mdm-ordinis/ordinis-admin-ui/src/auth/useAuth.ts
T
Zimin A.N. 7e435e07b7 feat(auth): миграция oidc-client-ts → keycloak-js (altum-pattern)
Phase 1a из плана: только swap библиотеки, realm/URL остаются на
auth.nstart.space/realms/nstart. Phase 2 (intgr с altum auth-service)
— отдельный эпик.

Зачем. oidc-client-ts с automaticSilentRenew триггерил бесконечный
POST /token loop когда refresh_token истекал (см. MR !269 revert,
~30+ 400'к в console, страница залипала на 403). Altum давно ушёл
с oidc-client-ts на keycloak-js по той же причине.

Новая модель.
- src/lib/keycloak.ts — singleton + initKeycloak(check-sso, PKCE) +
  ensureFreshToken(N) wrapper над kc.updateToken(). Зеркало
  altum/src/lib/keycloak.ts.
- src/stores/authStore.ts — Zustand store: {user, isAuthenticated,
  isLoading, error}. checkAuth() ждёт initKeycloak, wireKeycloakEvents()
  → onAuthSuccess/Refresh/Logout/TokenExpired re-снэпшотят store.
- src/auth/useAuth.ts — compat shim для react-oidc-context API
  (auth.user.profile, isAuthenticated, signinSilent, signinRedirect,
  signoutRedirect, removeUser, error). Все 11 callsites продолжают
  работать без правок (только сменили путь import'а).
- src/api/client.ts — request interceptor дёргает ensureFreshToken(30)
  ПЕРЕД каждым запросом + attaches Bearer ${getToken()}. 401 interceptor
  делает ensureFreshToken(0) + retry один раз. Никаких таймеров, никаких
  setAccessToken/setUnauthorizedHandler holder'ов — keycloak-js сам source
  of truth.
- src/main.tsx — убран <AuthProvider>, <TokenSync />. Просто
  useAuthStore.getState().checkAuth() на старте, потом обычный render.
- public/silent-check-sso.html — endpoint для silentCheckSsoRedirectUri.

Удалены.
- src/auth/TokenSync.tsx — не нужен, ensureFreshToken на запросах.
- src/auth/oidcConfig.ts — не нужен, конфиг внутри lib/keycloak.ts.
- npm deps: react-oidc-context, oidc-client-ts.

Добавлены deps: keycloak-js@26.2.4, zustand@5.0.13.

Tests: 171/171 passed, TSC чистый.

Migration callsites — все unchanged по семантике благодаря compat shim.
2026-05-26 11:18:30 +03:00

54 lines
2.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 { useAuthStore } from '@/stores/authStore'
import { ensureFreshToken, startLogin, startLogout } from '@/lib/keycloak'
/**
* Compat-shim для `useAuth` API из react-oidc-context. Минимальный
* superset того, что использовалось в codebase (см. RequireAuth,
* useUserDisplay, Sidebar, MyDraftsPanel, reviews, etc.):
* - `auth.user.profile.sub` / `preferred_username` / `email` / `name`
* - `auth.user.expired`
* - `auth.user.access_token`
* - `auth.isAuthenticated` / `auth.isLoading`
* - `auth.signinSilent()` / `auth.signinRedirect()` / `auth.signoutRedirect()`
* - `auth.activeNavigator` (всегда undefined в keycloak-js модели)
*
* <p>Поверх zustand `useAuthStore` — реактивно обновляется на
* keycloak-js callbacks (onAuthSuccess, onAuthRefreshSuccess, onTokenExpired,
* onAuthLogout). Permissions / role checks остаются работать без изменений.
*/
export const useAuth = () => {
const user = useAuthStore((s) => s.user)
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
const isLoading = useAuthStore((s) => s.isLoading)
const error = useAuthStore((s) => s.error)
return {
user,
isAuthenticated,
isLoading,
error,
activeNavigator: undefined,
/** Forced silent refresh — wraps ensureFreshToken(0). На failure
* keycloak-js делает kc.login() автоматически → user редиректится. */
signinSilent: async () => {
await ensureFreshToken(0)
return useAuthStore.getState().user
},
signinRedirect: async () => {
await startLogin()
},
signoutRedirect: async () => {
await startLogout()
},
/** Local-only logout (clear store без redirect на Keycloak). Зеркало
* `react-oidc-context removeUser` — callsites используют для "забыть
* user" без full logout flow. В keycloak-js модели соответствует
* очистке локального state — следующий ensureFreshToken покажет
* Login. */
removeUser: async () => {
const { user, ...rest } = useAuthStore.getState()
void user
useAuthStore.setState({ ...rest, user: null, isAuthenticated: false })
},
}
}