Files
vps-tracker/apps/web/src/routes/auth.callback.tsx
T
DenozordecandCursor a61b352f0a
Docker / build (push) Failing after 19s
fix(auth): остановить SSO-цикл редиректов после handoff
Cooldown 12с между portal handoff и страница sso_loop вместо бесконечного редиректа.

Co-authored-by: Cursor <[email protected]>
2026-07-18 16:08:50 +07:00

77 lines
2.4 KiB
TypeScript

import { createFileRoute, redirect } from '@tanstack/react-router'
import {
authPortalUrl,
clearPortalHandoffFlag,
clearToken,
ensureAuthConfig,
firstAllowedPath,
getClaims,
getToken,
parseHashToken,
redirectToPortalLogin,
setToken,
} from '@/lib/auth'
export const Route = createFileRoute('/auth/callback')({
validateSearch: (search: Record<string, unknown>) => ({
error: typeof search.error === 'string' ? search.error : undefined,
}),
beforeLoad: async ({ search }) => {
await ensureAuthConfig()
if (search.error === 'sso_loop') {
return
}
const { accessToken } = parseHashToken(window.location.hash)
if (accessToken) {
setToken(accessToken)
// Keep handoff timestamp for cooldown; only clear the per-load flag.
clearPortalHandoffFlag()
const claims = getClaims()
if (!claims) {
clearToken()
// Expired/invalid token from portal — force interactive login (no return_to storm)
window.location.assign(authPortalUrl())
await new Promise(() => {})
return
}
throw redirect({ to: firstAllowedPath() })
}
// Already stored from a previous parse (e.g. remount) — finish handoff.
if (getToken() && getClaims()) {
clearPortalHandoffFlag()
throw redirect({ to: firstAllowedPath() })
}
const ok = redirectToPortalLogin(`${window.location.origin}/auth/callback`)
if (!ok) {
throw redirect({ to: '/auth/callback', search: { error: 'sso_loop' } })
}
await new Promise(() => {})
},
component: AuthCallbackPage,
})
function AuthCallbackPage() {
const { error } = Route.useSearch()
if (error === 'sso_loop') {
return (
<div className="flex min-h-svh flex-col items-center justify-center gap-3 p-6 text-center">
<h1 className="text-lg font-semibold">Сессия не принята</h1>
<p className="text-muted-foreground max-w-md text-sm">
Повторный вход через portal остановлен (защита от цикла редиректов).
Обычно это несовпадение JWT_SECRET / ISSUER или просроченный токен.
Войдите заново на portal, затем откройте VPS Tracker.
</p>
<a
className="text-primary text-sm underline"
href={authPortalUrl()}
>
Открыть Auth Portal
</a>
</div>
)
}
return null
}