186 lines
5.6 KiB
TypeScript
186 lines
5.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { loginDingtalkUserByH5AuthCode } from '@/lib/dingtalk-login'
|
|
import { normalizeInternalRedirectPath } 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)
|
|
|
|
const cookieNames: string[] = []
|
|
for (const cookie of cookies) {
|
|
target.headers.append('set-cookie', cookie)
|
|
const cookieName = String(cookie).split('=', 1)[0]?.trim()
|
|
if (cookieName) {
|
|
cookieNames.push(cookieName)
|
|
}
|
|
}
|
|
|
|
return cookieNames
|
|
}
|
|
|
|
function createCookieRedirectResponse({
|
|
request,
|
|
redirectPath,
|
|
loginResponse,
|
|
}: {
|
|
request: NextRequest
|
|
redirectPath: string
|
|
loginResponse: Response
|
|
}) {
|
|
const redirectUrl = new URL(redirectPath, request.url)
|
|
const response = NextResponse.redirect(redirectUrl, { status: 303 })
|
|
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
|
|
|
const cookieNames = appendLoginSetCookie(response, loginResponse)
|
|
console.log('[Dingtalk H5] 写入免登会话Cookie并跳转:', {
|
|
redirectPath,
|
|
cookieNames,
|
|
})
|
|
return response
|
|
}
|
|
|
|
function createH5LoginErrorPage(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',
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
async function readH5LoginCode(request: NextRequest) {
|
|
const contentType = request.headers.get('content-type') || ''
|
|
if (contentType.includes('application/json')) {
|
|
const body = await request.json().catch(() => ({}))
|
|
return String(body?.code || '').trim()
|
|
}
|
|
|
|
if (
|
|
contentType.includes('application/x-www-form-urlencoded') ||
|
|
contentType.includes('multipart/form-data')
|
|
) {
|
|
const body = await request.formData().catch(() => null)
|
|
return String(body?.get('code') || '').trim()
|
|
}
|
|
|
|
const body = await request.text().catch(() => '')
|
|
return new URLSearchParams(body).get('code')?.trim() || ''
|
|
}
|
|
|
|
function isHtmlNavigationRequest(request: NextRequest) {
|
|
const accept = request.headers.get('accept') || ''
|
|
const fetchMode = request.headers.get('sec-fetch-mode') || ''
|
|
return accept.includes('text/html') || fetchMode === 'navigate'
|
|
}
|
|
|
|
async function handleH5LoginPage(request: NextRequest, code: string) {
|
|
const redirectPath = normalizeInternalRedirectPath(
|
|
request.nextUrl.searchParams.get('redirect'),
|
|
'/performance-survey',
|
|
)
|
|
|
|
if (!code) {
|
|
return createH5LoginErrorPage('缺少钉钉免登授权码,请从钉钉工作通知重新打开')
|
|
}
|
|
|
|
try {
|
|
const loginResponse = await loginDingtalkUserByH5AuthCode(request, code, {
|
|
appSource: 'workNotice',
|
|
})
|
|
const data = await loginResponse.json().catch(() => null)
|
|
|
|
if (!loginResponse.ok || !data?.success) {
|
|
return createH5LoginErrorPage(data?.message || '钉钉登录未能创建素材库会话')
|
|
}
|
|
|
|
if (data.needCompleteProfile) {
|
|
const userInfo = encodeURIComponent(JSON.stringify(data.user || {}))
|
|
return createCookieRedirectResponse({
|
|
request,
|
|
redirectPath: `/complete-profile?userInfo=${userInfo}`,
|
|
loginResponse,
|
|
})
|
|
}
|
|
|
|
return createCookieRedirectResponse({ request, redirectPath, loginResponse })
|
|
} catch (error: any) {
|
|
console.error('钉钉H5免登页面跳转失败:', error)
|
|
return createH5LoginErrorPage(error.message || '钉钉免登失败,请稍后再试')
|
|
}
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const code = String(
|
|
request.nextUrl.searchParams.get('code') ||
|
|
request.nextUrl.searchParams.get('authCode') ||
|
|
'',
|
|
).trim()
|
|
|
|
return handleH5LoginPage(request, code)
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const code = await readH5LoginCode(request)
|
|
|
|
if (isHtmlNavigationRequest(request)) {
|
|
return handleH5LoginPage(request, code)
|
|
}
|
|
|
|
if (!code) {
|
|
return NextResponse.json(
|
|
{ success: false, message: '缺少钉钉免登授权码' },
|
|
{ status: 400 },
|
|
)
|
|
}
|
|
|
|
const response = await loginDingtalkUserByH5AuthCode(request, code, {
|
|
appSource: 'workNotice',
|
|
})
|
|
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
|
return response
|
|
} catch (error: any) {
|
|
console.error('钉钉H5免登失败:', error)
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
message: error.message || '钉钉免登失败,请稍后再试',
|
|
},
|
|
{ status: 500 },
|
|
)
|
|
}
|
|
}
|