167 lines
4.8 KiB
TypeScript
167 lines
4.8 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useRef, useState } from 'react'
|
||
import { signIn, useSession } from 'next-auth/react'
|
||
|
||
declare global {
|
||
interface Window {
|
||
dd?: any
|
||
}
|
||
}
|
||
|
||
type DingtalkEntryClientProps = {
|
||
corpId: string
|
||
redirectPath: string
|
||
}
|
||
|
||
function loadDingtalkJsApi() {
|
||
return new Promise<void>((resolve, reject) => {
|
||
if (window.dd?.runtime?.permission) {
|
||
resolve()
|
||
return
|
||
}
|
||
|
||
const existing = document.querySelector<HTMLScriptElement>('script[data-dingtalk-jsapi="true"]')
|
||
if (existing) {
|
||
existing.addEventListener('load', () => resolve(), { once: true })
|
||
existing.addEventListener('error', () => reject(new Error('钉钉JSAPI加载失败')), { once: true })
|
||
return
|
||
}
|
||
|
||
const script = document.createElement('script')
|
||
script.src = 'https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.39/dingtalk.open.js'
|
||
script.async = true
|
||
script.dataset.dingtalkJsapi = 'true'
|
||
script.onload = () => resolve()
|
||
script.onerror = () => reject(new Error('钉钉JSAPI加载失败'))
|
||
document.head.appendChild(script)
|
||
})
|
||
}
|
||
|
||
function requestDingtalkAuthCode(corpId: string) {
|
||
return new Promise<string>((resolve, reject) => {
|
||
const dd = window.dd
|
||
if (!dd?.ready || !dd?.runtime?.permission?.requestAuthCode) {
|
||
reject(new Error('当前页面未检测到钉钉免登环境'))
|
||
return
|
||
}
|
||
|
||
const timer = window.setTimeout(() => {
|
||
reject(new Error('钉钉免登初始化超时'))
|
||
}, 15000)
|
||
|
||
if (dd.error) {
|
||
dd.error((error: unknown) => {
|
||
window.clearTimeout(timer)
|
||
reject(error instanceof Error ? error : new Error(formatDingtalkError(error)))
|
||
})
|
||
}
|
||
|
||
dd.ready(() => {
|
||
window.clearTimeout(timer)
|
||
dd.runtime.permission.requestAuthCode({
|
||
corpId,
|
||
onSuccess: (result: { code?: string }) => {
|
||
const code = String(result?.code || '').trim()
|
||
if (!code) {
|
||
reject(new Error('钉钉未返回免登授权码'))
|
||
return
|
||
}
|
||
resolve(code)
|
||
},
|
||
onFail: (error: unknown) => {
|
||
reject(error instanceof Error ? error : new Error(formatDingtalkError(error)))
|
||
},
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
function formatDingtalkError(error: unknown) {
|
||
if (!error) return '钉钉免登失败'
|
||
if (typeof error === 'string') return error
|
||
if (error instanceof Error) return error.message
|
||
|
||
const record = error as Record<string, unknown>
|
||
const message =
|
||
record.message ||
|
||
record.errorMessage ||
|
||
record.errMsg ||
|
||
record.errmsg ||
|
||
record.submsg
|
||
|
||
if (message) return String(message)
|
||
|
||
try {
|
||
return JSON.stringify(error)
|
||
} catch {
|
||
return String(error)
|
||
}
|
||
}
|
||
|
||
export default function DingtalkEntryClient({ corpId, redirectPath }: DingtalkEntryClientProps) {
|
||
const { status } = useSession()
|
||
const startedRef = useRef(false)
|
||
const [message, setMessage] = useState('正在通过钉钉确认身份...')
|
||
|
||
useEffect(() => {
|
||
if (status === 'authenticated') {
|
||
window.location.replace(redirectPath)
|
||
}
|
||
}, [redirectPath, status])
|
||
|
||
useEffect(() => {
|
||
if (startedRef.current || status === 'loading' || status === 'authenticated') return
|
||
startedRef.current = true
|
||
|
||
const run = async () => {
|
||
if (!corpId) {
|
||
setMessage('钉钉免登配置缺少 CorpId,请联系管理员检查应用配置。')
|
||
return
|
||
}
|
||
|
||
if (!/DingTalk/i.test(window.navigator.userAgent || '')) {
|
||
setMessage('请在钉钉客户端内重新打开通知。')
|
||
return
|
||
}
|
||
|
||
try {
|
||
setMessage('正在加载钉钉免登组件...')
|
||
await loadDingtalkJsApi()
|
||
|
||
setMessage('正在向钉钉获取免登授权码...')
|
||
const authCode = await requestDingtalkAuthCode(corpId)
|
||
|
||
setMessage('正在写入素材库登录状态...')
|
||
const result = await signIn('credentials', {
|
||
loginType: 'dingtalk_inapp',
|
||
authCode,
|
||
redirect: false,
|
||
})
|
||
|
||
if (result?.error) {
|
||
setMessage(`钉钉免登失败:${result.error}`)
|
||
return
|
||
}
|
||
|
||
setMessage('登录成功,正在进入考核调研...')
|
||
window.location.replace(redirectPath)
|
||
} catch (error) {
|
||
setMessage(`钉钉免登失败:${formatDingtalkError(error)}`)
|
||
}
|
||
}
|
||
|
||
void run()
|
||
}, [corpId, redirectPath, status])
|
||
|
||
return (
|
||
<main className="flex min-h-screen items-center justify-center bg-slate-950 px-6 text-slate-50">
|
||
<section className="w-full max-w-sm text-center">
|
||
<div className="mx-auto mb-5 h-10 w-10 animate-spin rounded-full border-4 border-white/25 border-t-sky-400" />
|
||
<h1 className="mb-3 text-2xl font-semibold">正在进入素材库</h1>
|
||
<p className="text-sm leading-7 text-slate-300">{message}</p>
|
||
</section>
|
||
</main>
|
||
)
|
||
}
|