2026-07-07 10:40:26 +08:00

223 lines
7.0 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { loginDingtalkUserByH5AuthCode } from '@/lib/dingtalk-login'
import {
buildDingtalkAutoLoginPath,
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)
for (const cookie of cookies) {
target.headers.append('set-cookie', cookie)
}
}
function createCookieLandingResponse({
redirectPath,
loginResponse,
title = '登录成功',
message = '正在进入考核调研...',
}: {
redirectPath: string
loginResponse: Response
title?: string
message?: string
}) {
const escapedPath = JSON.stringify(redirectPath).replace(/</g, '\\u003c')
const escapedTitle = JSON.stringify(title).replace(/</g, '\\u003c')
const escapedMessage = JSON.stringify(message).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}
h1{font-size:22px;margin:0 0 10px}
p{margin:0;color:#64748b;line-height:1.8}
</style>
</head>
<body>
<main>
<h1 id="title"></h1>
<p id="message"></p>
</main>
<script>
document.getElementById('title').textContent = ${escapedTitle};
document.getElementById('message').textContent = ${escapedMessage};
setTimeout(function () {
window.location.replace(${escapedPath});
}, 180);
</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 createH5LoginErrorPage(message: string, redirectPath: string) {
const safeMessage = JSON.stringify(message || '钉钉免登失败,请联系管理员检查配置').replace(/</g, '\\u003c')
const fallbackPath = buildDingtalkAutoLoginPath(redirectPath)
const safeFallbackPath = JSON.stringify(fallbackPath).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}
a{display:inline-block;margin-top:18px;color:#2563eb;text-decoration:none}
</style>
</head>
<body>
<main>
<h1>钉钉免登失败</h1>
<p id="message"></p>
<a id="fallback">改用网页授权登录</a>
</main>
<script>
document.getElementById('message').textContent = ${safeMessage};
document.getElementById('fallback').href = ${safeFallbackPath};
</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('缺少钉钉免登授权码,请从钉钉工作通知重新打开', redirectPath)
}
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 || '钉钉登录未能创建素材库会话', redirectPath)
}
if (data.needCompleteProfile) {
const userInfo = encodeURIComponent(JSON.stringify(data.user || {}))
return createCookieLandingResponse({
redirectPath: `/complete-profile?userInfo=${userInfo}`,
loginResponse,
title: '首次登录',
message: '正在进入资料完善页面...',
})
}
return createCookieLandingResponse({ redirectPath, loginResponse })
} catch (error: any) {
console.error('钉钉H5免登页面跳转失败:', error)
return createH5LoginErrorPage(error.message || '钉钉免登失败,请稍后再试', redirectPath)
}
}
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 },
)
}
}