166 lines
5.7 KiB
TypeScript
166 lines
5.7 KiB
TypeScript
import { NextResponse, type NextRequest } from 'next/server'
|
|
import { DINGTALK_DIRECT_LOGIN_SOURCE, parseDingtalkOAuthState } from '@/lib/dingtalk-oauth'
|
|
import { getPublicAppOrigin } from '@/lib/public-app-url'
|
|
import { buildDingtalkAutoLoginPath } from '@/lib/safe-redirect'
|
|
|
|
export const runtime = 'nodejs'
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
function appendLoginSetCookie(target: NextResponse, source: Response) {
|
|
const getSetCookie = (source.headers as any).getSetCookie
|
|
const cookies = typeof getSetCookie === 'function'
|
|
? getSetCookie.call(source.headers)
|
|
: [source.headers.get('set-cookie')].filter(Boolean)
|
|
|
|
for (const cookie of cookies) {
|
|
target.headers.append('set-cookie', cookie)
|
|
}
|
|
}
|
|
|
|
function getForwardedHeaders(request: NextRequest) {
|
|
const publicUrl = new URL(getPublicAppOrigin())
|
|
return {
|
|
'x-forwarded-proto': publicUrl.protocol.replace(':', ''),
|
|
'x-forwarded-host': publicUrl.host,
|
|
'x-real-ip': request.headers.get('x-real-ip') || '',
|
|
'x-forwarded-for': request.headers.get('x-forwarded-for') || '',
|
|
}
|
|
}
|
|
|
|
function getInternalLoginUrl() {
|
|
const port = process.env.PORT || '3000'
|
|
return `http://127.0.0.1:${port}/api/dingtalk/login`
|
|
}
|
|
|
|
function createCookieLandingResponse(redirectPath: string, loginResponse: Response) {
|
|
const escapedPath = JSON.stringify(redirectPath).replace(/</g, '\\u003c')
|
|
const response = new NextResponse(
|
|
`<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>正在进入素材库</title>
|
|
<meta http-equiv="Cache-Control" content="no-store" />
|
|
<style>
|
|
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f8fafc;color:#0f172a;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
main{padding:28px;text-align:center}
|
|
p{margin:8px 0 0;color:#64748b}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>登录成功</h1>
|
|
<p>正在进入考核调研...</p>
|
|
</main>
|
|
<script>
|
|
window.location.replace(${escapedPath});
|
|
</script>
|
|
</body>
|
|
</html>`,
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
|
},
|
|
},
|
|
)
|
|
|
|
appendLoginSetCookie(response, loginResponse)
|
|
return response
|
|
}
|
|
|
|
function createDingtalkDirectErrorPage(message: string) {
|
|
const safeMessage = JSON.stringify(message || '钉钉免登失败,请联系管理员检查配置').replace(/</g, '\\u003c')
|
|
return new NextResponse(
|
|
`<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>钉钉免登失败</title>
|
|
<meta http-equiv="Cache-Control" content="no-store" />
|
|
<style>
|
|
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f8fafc;color:#0f172a;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
main{width:min(440px,calc(100vw - 48px));padding:28px;text-align:center}
|
|
h1{font-size:22px;margin:0 0 12px}
|
|
p{margin:0;color:#475569;line-height:1.8}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>钉钉免登失败</h1>
|
|
<p id="message"></p>
|
|
</main>
|
|
<script>
|
|
document.getElementById('message').textContent = ${safeMessage};
|
|
</script>
|
|
</body>
|
|
</html>`,
|
|
{
|
|
status: 401,
|
|
headers: {
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const code = request.nextUrl.searchParams.get('code') || request.nextUrl.searchParams.get('authCode')
|
|
const { redirectPath, source } = parseDingtalkOAuthState(request.nextUrl.searchParams.get('state'))
|
|
const publicOrigin = getPublicAppOrigin()
|
|
|
|
if (!code) {
|
|
if (source === DINGTALK_DIRECT_LOGIN_SOURCE) {
|
|
return createDingtalkDirectErrorPage('钉钉授权缺少授权码,请从钉钉工作通知重新打开')
|
|
}
|
|
|
|
return NextResponse.redirect(new URL(buildDingtalkAutoLoginPath(redirectPath), publicOrigin))
|
|
}
|
|
|
|
try {
|
|
const loginResponse = await fetch(getInternalLoginUrl(), {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...getForwardedHeaders(request),
|
|
},
|
|
body: JSON.stringify({ code, state: request.nextUrl.searchParams.get('state') || '' }),
|
|
cache: 'no-store',
|
|
})
|
|
const data = await loginResponse.json().catch(() => null)
|
|
|
|
if (!loginResponse.ok || !data?.success || data.needCompleteProfile) {
|
|
if (source === DINGTALK_DIRECT_LOGIN_SOURCE) {
|
|
return createDingtalkDirectErrorPage(data?.message || '钉钉登录未能创建素材库会话')
|
|
}
|
|
|
|
const loginPath = buildDingtalkAutoLoginPath(redirectPath)
|
|
const url = new URL(loginPath, publicOrigin)
|
|
if (data?.message) {
|
|
url.searchParams.set('error', data.message)
|
|
}
|
|
return NextResponse.redirect(url)
|
|
}
|
|
|
|
if (source === DINGTALK_DIRECT_LOGIN_SOURCE) {
|
|
return createCookieLandingResponse(redirectPath, loginResponse)
|
|
}
|
|
|
|
const redirectResponse = NextResponse.redirect(new URL(redirectPath, publicOrigin))
|
|
appendLoginSetCookie(redirectResponse, loginResponse)
|
|
redirectResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
|
return redirectResponse
|
|
} catch (error) {
|
|
console.error('[Dingtalk] 服务端授权回调失败:', error)
|
|
if (source === DINGTALK_DIRECT_LOGIN_SOURCE) {
|
|
return createDingtalkDirectErrorPage('钉钉服务端授权失败,请稍后重试或联系管理员')
|
|
}
|
|
|
|
return NextResponse.redirect(new URL(buildDingtalkAutoLoginPath(redirectPath), publicOrigin))
|
|
}
|
|
}
|