91 lines
2.5 KiB
TypeScript
91 lines
2.5 KiB
TypeScript
'use client'
|
|
|
|
import { Suspense } from 'react'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { useSearchParams } from 'next/navigation'
|
|
import { Stack, Text, Loader } from '@mantine/core'
|
|
|
|
const SSO_STATE_STORAGE_KEY = 'sso_login_state'
|
|
|
|
function CallbackContent() {
|
|
const searchParams = useSearchParams()
|
|
const [error, setError] = useState<string | null>(null)
|
|
const callbackStarted = useRef(false)
|
|
|
|
useEffect(() => {
|
|
if (callbackStarted.current) return
|
|
|
|
const code = searchParams.get('sso_code')
|
|
const state = searchParams.get('state')
|
|
const expectedState = window.sessionStorage.getItem(SSO_STATE_STORAGE_KEY)
|
|
|
|
if (!code) {
|
|
setError('缺少 sso_code 参数')
|
|
return
|
|
}
|
|
if (!state || !expectedState || state !== expectedState) {
|
|
window.sessionStorage.removeItem(SSO_STATE_STORAGE_KEY)
|
|
setError('state 无效或已过期,请重新登录')
|
|
return
|
|
}
|
|
|
|
callbackStarted.current = true
|
|
window.sessionStorage.removeItem(SSO_STATE_STORAGE_KEY)
|
|
|
|
// 用 code 调用本应用的 /api/callback 换取用户信息
|
|
fetch('/api/callback', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ code }),
|
|
})
|
|
.then(async (res) => {
|
|
if (!res.ok) {
|
|
const data = await res.json()
|
|
throw new Error(data.error || '认证失败')
|
|
}
|
|
// 成功 -> 跳转到首页
|
|
window.location.href = '/'
|
|
})
|
|
.catch((err) => {
|
|
setError(err instanceof Error ? err.message : '认证失败')
|
|
})
|
|
}, [searchParams])
|
|
|
|
if (error) {
|
|
return (
|
|
<Stack align="center" justify="center" h="100vh" gap="md">
|
|
<Text size="lg" c="red">认证失败</Text>
|
|
<Text size="sm" c="dimmed">{error}</Text>
|
|
<Text
|
|
size="sm"
|
|
c="brand"
|
|
style={{ cursor: 'pointer' }}
|
|
onClick={() => (window.location.href = '/')}>
|
|
返回首页重试
|
|
</Text>
|
|
</Stack>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Stack align="center" justify="center" h="100vh" gap="md">
|
|
<Loader size="lg" />
|
|
<Text size="sm" c="dimmed">正在验证 SSO 登录...</Text>
|
|
</Stack>
|
|
)
|
|
}
|
|
|
|
export default function CallbackPage() {
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<Stack align="center" justify="center" h="100vh" gap="md">
|
|
<Loader size="lg" />
|
|
<Text size="sm" c="dimmed">正在验证 SSO 登录...</Text>
|
|
</Stack>
|
|
}>
|
|
<CallbackContent />
|
|
</Suspense>
|
|
)
|
|
}
|