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.
This commit is contained in:
Zimin A.N.
2026-05-26 11:18:30 +03:00
parent 0aaae9cb2b
commit 7e435e07b7
21 changed files with 394 additions and 269 deletions
+35 -32
View File
@@ -83,15 +83,15 @@ importers:
i18next-browser-languagedetector:
specifier: ^8.0.2
version: 8.2.1
keycloak-js:
specifier: ^26.2.4
version: 26.2.4
leaflet:
specifier: ^1.9.4
version: 1.9.4
lucide-react:
specifier: ^1.14.0
version: 1.14.0(react@19.2.5)
oidc-client-ts:
specifier: ^3.5.0
version: 3.5.0
react:
specifier: ^19.0.0
version: 19.2.5
@@ -113,9 +113,6 @@ importers:
react-leaflet:
specifier: ^5.0.0
version: 5.0.0(leaflet@1.9.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react-oidc-context:
specifier: ^3.3.1
version: 3.3.1(oidc-client-ts@3.5.0)(react@19.2.5)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -134,6 +131,9 @@ importers:
world-atlas:
specifier: ^2.0.2
version: 2.0.2
zustand:
specifier: ^5.0.13
version: 5.0.13(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
devDependencies:
'@semantic-release/changelog':
specifier: ^6.0.3
@@ -2691,14 +2691,13 @@ packages:
jsonfile@6.2.1:
resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
jwt-decode@4.0.0:
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
engines: {node: '>=18'}
kapsule@1.16.3:
resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==}
engines: {node: '>=12'}
keycloak-js@26.2.4:
resolution: {integrity: sha512-PnXpR3ubETGOt0B/Qt2lxmPbkZr5bc3vlQsOqDoTPPQsZRp7JjhTKxlJ187uWh8qJhvBab6Gsjb06a8ayOPfuw==}
keyv@5.6.0:
resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
@@ -3065,10 +3064,6 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
oidc-client-ts@3.5.0:
resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==}
engines: {node: '>=18'}
onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
@@ -3322,13 +3317,6 @@ packages:
react: ^19.0.0
react-dom: ^19.0.0
react-oidc-context@3.3.1:
resolution: {integrity: sha512-/Azvm9W4DhhOtSDBE73kFInh1b6zZRRfILKbgmk2syExMF0PCYJOn/dGdOOi2BFX8x0rCeUe45NXHU+/+xDcrQ==}
engines: {node: '>=18'}
peerDependencies:
oidc-client-ts: ^3.1.0
react: '>=16.14.0'
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
@@ -3986,6 +3974,24 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zustand@5.0.13:
resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots:
3d-force-graph@1.80.0:
@@ -6363,12 +6369,12 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jwt-decode@4.0.0: {}
kapsule@1.16.3:
dependencies:
lodash-es: 4.18.1
keycloak-js@26.2.4: {}
keyv@5.6.0:
dependencies:
'@keyv/serialize': 1.1.1
@@ -6605,10 +6611,6 @@ snapshots:
object-assign@4.1.1: {}
oidc-client-ts@3.5.0:
dependencies:
jwt-decode: 4.0.0
onetime@5.1.2:
dependencies:
mimic-fn: 2.1.0
@@ -6820,11 +6822,6 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
react-oidc-context@3.3.1(oidc-client-ts@3.5.0)(react@19.2.5):
dependencies:
oidc-client-ts: 3.5.0
react: 19.2.5
react-refresh@0.17.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5):
@@ -7440,3 +7437,9 @@ snapshots:
yoctocolors@2.1.2: {}
zod@3.25.76: {}
zustand@5.0.13(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)):
optionalDependencies:
'@types/react': 19.2.14
react: 19.2.5
use-sync-external-store: 1.6.0(react@19.2.5)