All files / features / auth CallbackPage.tsx

94.44% Statements 34/36
89.47% Branches 17/19
90.9% Functions 10/11
96.87% Lines 31/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96                        10x 34x 10x   10x 7x 7x 7x 7x   7x 2x 2x     5x   5x           5x 4x     4x 4x           4x 4x 4x     1x       10x 3x     7x               3x 3x 3x     3x                               4x 4x 4x          
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from './authStore'
 
interface TokenResponse {
  access_token: string
  token_type: string
  expires_in?: number
  id_token?: string
}
 
export function CallbackPage() {
  const navigate = useNavigate()
  const setAuth = useAuthStore((s) => s.setAuth)
  const [error, setError] = useState<string | null>(null)
 
  useEffect(() => {
    const code = sessionStorage.getItem('oidc-code')
    const codeVerifier = sessionStorage.getItem('oidc-code-verifier')
    sessionStorage.removeItem('oidc-code')
    sessionStorage.removeItem('oidc-code-verifier')
 
    if (!code || !codeVerifier) {
      setError('No authorization code found in callback')
      return
    }
 
    const redirectUri = `${window.location.protocol}//${window.location.hostname}/callback.html`
 
    fetch('/api/auth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code, code_verifier: codeVerifier, redirect_uri: redirectUri }),
    })
      .then((res) => {
        if (!res.ok) throw new Error('Token exchange failed')
        return res.json() as Promise<TokenResponse>
      })
      .then((data) => {
        const decoded = decodeJwtPayload(data.access_token)
        setAuth(data.access_token, {
          sub: decoded.sub ?? '',
          preferred_username: decoded.preferred_username ?? decoded.sub ?? '',
          email: decoded.email ?? '',
          groups: decoded.groups ?? [],
        })
        const returnUrl = sessionStorage.getItem('returnUrl')
        sessionStorage.removeItem('returnUrl')
        navigate(returnUrl ?? '/', { replace: true })
      })
      .catch(() => {
        setError('Failed to complete authentication. Please try again.')
      })
  }, [navigate, setAuth])
 
  if (error) {
    return <CallbackError error={error} navigate={navigate} />
  }
 
  return (
    <div className="flex min-h-screen items-center justify-center">
      <p className="text-muted-foreground">Signing in...</p>
    </div>
  )
}
 
function CallbackError({ error, navigate }: { error: string; navigate: (path: string, opts?: { replace?: boolean }) => void }) {
  useEffect(() => {
    const timer = setTimeout(() => navigate('/login', { replace: true }), 3000)
    return () => clearTimeout(timer)
  }, [navigate])
 
  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-4">
      <p className="text-destructive">{error}</p>
      <p className="text-sm text-muted-foreground">Redirecting to login...</p>
    </div>
  )
}
 
interface JwtPayload {
  sub?: string
  preferred_username?: string
  email?: string
  groups?: string[]
}
 
function decodeJwtPayload(token: string): JwtPayload {
  try {
    const base64 = token.split('.')[1]
    return JSON.parse(atob(base64)) as JwtPayload
  } catch {
    return {}
  }
}