2026-05-25 16:20:26 +08:00

189 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useEffect, useState, Suspense } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { signIn } from 'next-auth/react'
type CallbackStatus = 'loading' | 'success' | 'error'
function CallbackShell({
status,
message,
}: {
status: CallbackStatus
message: string
}) {
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 px-4 py-10 dark:bg-slate-950">
<div className="w-full max-w-sm rounded-lg border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex flex-col items-center text-center">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-slate-50 dark:border-slate-700 dark:bg-slate-800">
{status === 'loading' && (
<div className="h-5 w-5 animate-spin rounded-full border-2 border-slate-400 border-t-transparent" />
)}
{status === 'success' && (
<svg className="h-5 w-5 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
{status === 'error' && (
<svg className="h-5 w-5 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
<h2 className={`mb-2 text-lg font-semibold ${
status === 'error' ? 'text-red-600 dark:text-red-400' : 'text-slate-900 dark:text-slate-100'
}`}>
{status === 'loading' && '钉钉登录'}
{status === 'success' && '登录成功'}
{status === 'error' && '登录失败'}
</h2>
<p className="text-sm leading-6 text-slate-600 dark:text-slate-400">{message}</p>
</div>
</div>
</div>
)
}
function DingtalkCallbackContent() {
const router = useRouter()
const searchParams = useSearchParams()
const [status, setStatus] = useState<CallbackStatus>('loading')
const [message, setMessage] = useState('正在处理钉钉登录...')
const postToParent = (payload: Record<string, unknown>) => {
window.parent.postMessage(payload, window.location.origin)
}
useEffect(() => {
const handleDingtalkLogin = async () => {
try {
const code = searchParams.get('code')
const state = searchParams.get('state') || 'default'
if (!code) {
throw new Error('授权失败:缺少授权码')
}
// 解析state参数,获取原始访问地址
let originUrl = ''
try {
const stateData = JSON.parse(decodeURIComponent(state))
originUrl = stateData.origin || ''
} catch (e) {
// state解析失败,忽略
}
setMessage('正在验证授权信息...')
// 调用钉钉登录接口
const response = await fetch('/api/dingtalk/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, state }),
})
const data = await response.json()
if (!response.ok || !data.success) {
throw new Error(data.message || '登录失败')
}
// 检查是否需要完善信息
if (data.needCompleteProfile) {
setMessage('首次登录,正在跳转到信息完善页面...')
setStatus('success')
// 跳转到信息完善页面
const userInfoParam = encodeURIComponent(JSON.stringify(data.user))
setTimeout(() => {
if (window.parent !== window) {
// 在 iframe 中,通知父窗口跳转
postToParent({
type: 'dingtalk_need_complete_profile',
userInfo: data.user,
profileToken: data.profileToken,
})
} else {
router.push(`/complete-profile?userInfo=${userInfoParam}&profileToken=${encodeURIComponent(data.profileToken || '')}`)
}
}, 1000)
return
}
setMessage('登录成功,正在跳转...')
setStatus('success')
// 检查来源:优先使用state中的origin,其次使用referrer
const isFromIntranet = originUrl && originUrl.includes('192.168.')
// 通知父窗口登录成功(如果在 iframe 中)
if (window.parent !== window) {
postToParent({
type: 'dingtalk_login_success',
userInfo: data.user,
loginToken: data.loginToken,
originUrl: originUrl // 携带原始地址
})
} else {
// 不在 iframe 中(直接访问回调页面)
if (isFromIntranet) {
// 从内网来的:不在这里创建session,跳转回内网后由内网页面创建
setMessage('登录成功,正在返回内网...')
// 将一次性登录凭证传递回内网页面,让内网页面创建 session
window.location.href = `${originUrl}/login?dingtalk_success=true&token=${encodeURIComponent(data.loginToken)}`
} else {
// 从外网来的:直接创建session并跳转
const signInResult = await signIn('credentials', {
dingtalkToken: data.loginToken,
redirect: false,
})
if (signInResult?.error) {
throw new Error(signInResult.error)
}
setTimeout(() => {
router.push('/')
router.refresh()
}, 1000)
}
}
} catch (error: any) {
console.error('钉钉登录错误:', error)
setStatus('error')
setMessage(error.message || '登录失败,请稍后再试')
// 3秒后关闭或跳转
setTimeout(() => {
if (window.parent !== window) {
postToParent({ type: 'dingtalk_login_error', message: error.message })
} else {
router.push('/login')
}
}, 3000)
}
}
handleDingtalkLogin()
}, [searchParams, router])
return <CallbackShell status={status} message={message} />
}
export default function DingtalkCallbackPage() {
return (
<Suspense fallback={
<CallbackShell status="loading" message="正在加载..." />
}>
<DingtalkCallbackContent />
</Suspense>
)
}