243 lines
8.2 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, useRef, useState, Suspense } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { normalizeInternalRedirectPath } from '@/lib/safe-redirect'
const DINGTALK_DIRECT_LOGIN_SOURCE = 'work_notice_direct'
function parseDingtalkStateSource(value: string) {
if (!value) return ''
let raw = value
try {
raw = decodeURIComponent(raw)
} catch {
// URLSearchParams 通常已经解码,这里仅兼容手工拼接的 URL。
}
try {
const data = JSON.parse(raw)
return typeof data?.source === 'string' ? data.source : ''
} catch {
return ''
}
}
function DingtalkCallbackContent() {
const router = useRouter()
const searchParams = useSearchParams()
const handledRef = useRef(false)
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')
const [message, setMessage] = useState('正在处理钉钉登录...')
useEffect(() => {
const handleDingtalkLogin = async () => {
try {
if (handledRef.current) {
return
}
handledRef.current = true
const code = searchParams?.get('code') || searchParams?.get('authCode')
const state = searchParams?.get('state') || 'default'
if (!code) {
throw new Error('授权失败:缺少授权码')
}
// 解析state参数,获取原始访问地址
let originUrl = ''
let redirectPath = '/'
try {
const stateData = JSON.parse(decodeURIComponent(state))
originUrl = stateData.origin || ''
redirectPath = normalizeInternalRedirectPath(stateData.redirect)
} 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.opener) {
// 新窗口模式:通知打开窗口的父页面
window.opener.postMessage(
{
type: 'dingtalk_need_complete_profile',
userInfo: data.user,
},
'*'
)
window.close()
} else if (window.parent !== window) {
// iframe模式:通知父窗口跳转(兼容旧代码)
window.parent.postMessage(
{
type: 'dingtalk_need_complete_profile',
userInfo: data.user,
},
'*'
)
} else {
router.push(`/complete-profile?userInfo=${userInfoParam}`)
}
}, 1000)
return
}
setMessage('登录成功,正在跳转...')
setStatus('success')
// 通知父窗口登录成功(如果在 iframe 或新窗口中)
if (window.opener) {
// 新窗口模式:通知打开窗口的父页面
window.opener.postMessage(
{
type: 'dingtalk_login_success',
userInfo: data.user,
originUrl: originUrl, // 携带原始地址
redirectUrl: redirectPath,
},
'*'
)
} else if (window.parent !== window) {
// iframe模式:通知父窗口(兼容旧代码)
window.parent.postMessage(
{
type: 'dingtalk_login_success',
userInfo: data.user,
originUrl: originUrl, // 携带原始地址
redirectUrl: redirectPath,
},
'*'
)
} else {
await new Promise((resolve) => setTimeout(resolve, 200))
const sessionResponse = await fetch('/api/auth/session', {
cache: 'no-store',
credentials: 'include',
})
const sessionData = await sessionResponse.json().catch(() => null)
if (!sessionData?.user) {
throw new Error('登录会话写入失败,请重新使用钉钉登录')
}
setTimeout(() => {
window.location.href = redirectPath
}, 300)
}
} catch (error: any) {
console.error('钉钉登录错误:', error)
setStatus('error')
setMessage(error.message || '登录失败,请稍后再试')
const state = searchParams?.get('state') || ''
const isDirectWorkNoticeLogin = parseDingtalkStateSource(state) === DINGTALK_DIRECT_LOGIN_SOURCE
// 工作通知直达入口不再退回登录页自动重试,避免重复消费钉钉 code 后形成循环。
if (isDirectWorkNoticeLogin) {
return
}
// 3秒后关闭或跳转
setTimeout(() => {
if (window.opener) {
// 新窗口模式:通知打开窗口的父页面
window.opener.postMessage({ type: 'dingtalk_login_error', message: error.message }, '*')
window.close()
} else if (window.parent !== window) {
// iframe模式:通知父窗口(兼容旧代码)
window.parent.postMessage({ type: 'dingtalk_login_error', message: error.message }, '*')
} else {
router.push('/login')
}
}, 3000)
}
}
handleDingtalkLogin()
}, [searchParams, router])
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100">
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8">
<div className="text-center">
{status === 'loading' && (
<div className="mb-4">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
</div>
)}
{status === 'success' && (
<div className="mb-4">
<svg className="mx-auto h-12 w-12 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
)}
{status === 'error' && (
<div className="mb-4">
<svg className="mx-auto h-12 w-12 text-red-500" 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={`text-xl font-semibold mb-2 ${
status === 'error' ? 'text-red-600' : 'text-gray-800'
}`}>
{status === 'loading' && '钉钉登录'}
{status === 'success' && '登录成功'}
{status === 'error' && '登录失败'}
</h2>
<p className="text-gray-600">{message}</p>
</div>
</div>
</div>
)
}
export default function DingtalkCallbackPage() {
return (
<Suspense fallback={
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100">
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8">
<div className="text-center">
<div className="mb-4">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
</div>
<h2 className="text-xl font-semibold mb-2 text-gray-800">钉钉登录</h2>
<p className="text-gray-600">正在加载...</p>
</div>
</div>
</div>
}>
<DingtalkCallbackContent />
</Suspense>
)
}