修复钉钉端内免登会话写入
This commit is contained in:
parent
e2d3314e62
commit
17fe89f7fb
@ -12,6 +12,7 @@
|
||||
|
||||
- 默认使用中文说明,优先自然语言,少量必要代码示例。
|
||||
- 先做只读摸底,再决定是否修改。
|
||||
- 非必要不要使用兜底逻辑;优先把原生主链路和标准流程做好,只有在明确需要兼容、降级或应急,并说明原因和影响范围时,才增加兜底处理。
|
||||
- 修改后必须验证,先跑最相关的验证,再视情况扩大范围。
|
||||
- 不触碰其他应用配置,不把项目规则写入全局 `Codex` 配置。
|
||||
|
||||
|
||||
@ -1,13 +1,201 @@
|
||||
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 body = await request.json().catch(() => ({}))
|
||||
const code = String(body?.code || '').trim()
|
||||
const code = await readH5LoginCode(request)
|
||||
|
||||
if (isHtmlNavigationRequest(request)) {
|
||||
return handleH5LoginPage(request, code)
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.json(
|
||||
|
||||
@ -146,36 +146,19 @@ function createH5LoginPage({
|
||||
|
||||
function submitCode(code) {
|
||||
setStatus('已获取钉钉身份,正在创建登录状态...');
|
||||
fetch('/api/dingtalk/h5-login', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: code })
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.json().catch(function () { return {}; }).then(function (data) {
|
||||
return { ok: response.ok, data: data };
|
||||
});
|
||||
})
|
||||
.then(function (result) {
|
||||
if (!result.ok || !result.data || !result.data.success) {
|
||||
throw new Error((result.data && result.data.message) || '登录失败');
|
||||
}
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/api/dingtalk/h5-login?redirect=' + encodeURIComponent(target);
|
||||
form.style.display = 'none';
|
||||
|
||||
if (result.data.needCompleteProfile) {
|
||||
setStatus('首次登录,需要完善账号信息...');
|
||||
completeProfile(result.data.user);
|
||||
return;
|
||||
}
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'code';
|
||||
input.value = code;
|
||||
form.appendChild(input);
|
||||
|
||||
setStatus('登录成功,正在打开考核调研...');
|
||||
setTimeout(function () {
|
||||
window.location.replace(target);
|
||||
}, 350);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error && error.message ? error.message : '钉钉免登失败');
|
||||
});
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function resolveAuthCode(result) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user