Files
external-app/app/callback/page.tsx
2026-07-14 15:21:28 +08:00

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>
)
}