112 lines
3.3 KiB
TypeScript
112 lines
3.3 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { getPKCE, clearPKCE, storeToken, getState, clearState } from '../auth/oidc'
|
|
import { api } from '../api/client'
|
|
import { useAuthStore } from '../stores/auth'
|
|
|
|
const CLIENT_ID = import.meta.env.VITE_OIDC_CLIENT_ID
|
|
const REDIRECT_URI = import.meta.env.VITE_OIDC_REDIRECT_URI
|
|
|
|
export default function CallbackPage() {
|
|
const navigate = useNavigate()
|
|
const { setUser, setError } = useAuthStore()
|
|
const [status, setStatus] = useState('Processing authentication...')
|
|
const isMounted = useRef(true)
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
isMounted.current = false
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const handleCallback = async () => {
|
|
try {
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
const code = urlParams.get('code')
|
|
const error = urlParams.get('error')
|
|
const state = urlParams.get('state')
|
|
const storedState = getState()
|
|
|
|
if (error) {
|
|
throw new Error(`Authentication error: ${error}`)
|
|
}
|
|
|
|
if (!code) {
|
|
throw new Error('No authorization code received')
|
|
}
|
|
|
|
if (!state || state !== storedState) {
|
|
throw new Error('Invalid or missing state parameter')
|
|
}
|
|
|
|
const verifier = getPKCE()
|
|
if (!verifier) {
|
|
throw new Error('PKCE verifier not found')
|
|
}
|
|
|
|
if (isMounted.current) {
|
|
setStatus('Exchanging code for token...')
|
|
}
|
|
|
|
const tokenEndpoint = 'https://auth.commumedia.org/application/o/token/'
|
|
const tokenResponse = await fetch(tokenEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: 'authorization_code',
|
|
client_id: CLIENT_ID,
|
|
code,
|
|
redirect_uri: REDIRECT_URI,
|
|
code_verifier: verifier,
|
|
}),
|
|
})
|
|
|
|
if (!tokenResponse.ok) {
|
|
const errorData = await tokenResponse.json().catch(() => ({}))
|
|
throw new Error(errorData.error_description || errorData.error || 'Token exchange failed')
|
|
}
|
|
|
|
const tokenData = await tokenResponse.json()
|
|
storeToken(tokenData.access_token)
|
|
clearPKCE()
|
|
clearState()
|
|
|
|
if (isMounted.current) {
|
|
setStatus('Fetching user information...')
|
|
}
|
|
|
|
const user = await api.getCurrentUser()
|
|
|
|
if (isMounted.current) {
|
|
setUser(user)
|
|
navigate('/')
|
|
}
|
|
} catch (error) {
|
|
console.error('Callback error:', error)
|
|
clearPKCE()
|
|
clearState()
|
|
|
|
if (isMounted.current) {
|
|
setError(error instanceof Error ? error : new Error('Authentication failed'))
|
|
setStatus('Authentication failed')
|
|
setTimeout(() => navigate('/login'), 3000)
|
|
}
|
|
}
|
|
}
|
|
|
|
handleCallback()
|
|
}, [navigate, setUser, setError])
|
|
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
<div className="text-center">
|
|
<h1 className="text-2xl font-bold mb-4">{status}</h1>
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accent mx-auto"></div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|