From 17fe89f7fb19fd3eadd79ebc491e8548a190e62e Mon Sep 17 00:00:00 2001 From: chenpei Date: Tue, 7 Jul 2026 10:40:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=92=89=E9=92=89=E7=AB=AF?= =?UTF-8?q?=E5=86=85=E5=85=8D=E7=99=BB=E4=BC=9A=E8=AF=9D=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + app/api/dingtalk/h5-login/route.ts | 192 ++++++++++++++++++++++++++++- app/dingtalk-entry/route.ts | 39 ++---- 3 files changed, 202 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c54b854d..f5e8a22a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,7 @@ - 默认使用中文说明,优先自然语言,少量必要代码示例。 - 先做只读摸底,再决定是否修改。 +- 非必要不要使用兜底逻辑;优先把原生主链路和标准流程做好,只有在明确需要兼容、降级或应急,并说明原因和影响范围时,才增加兜底处理。 - 修改后必须验证,先跑最相关的验证,再视情况扩大范围。 - 不触碰其他应用配置,不把项目规则写入全局 `Codex` 配置。 diff --git a/app/api/dingtalk/h5-login/route.ts b/app/api/dingtalk/h5-login/route.ts index 6846d29d..ec9241e3 100644 --- a/app/api/dingtalk/h5-login/route.ts +++ b/app/api/dingtalk/h5-login/route.ts @@ -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(/ + + + + + 正在进入素材库 + + + + +
+

+

+
+ + +`, + { + 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(/ + + + + + 钉钉免登失败 + + + + +
+

钉钉免登失败

+

+ 改用网页授权登录 +
+ + +`, + { + 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( diff --git a/app/dingtalk-entry/route.ts b/app/dingtalk-entry/route.ts index 37d8c6b3..ff39799b 100644 --- a/app/dingtalk-entry/route.ts +++ b/app/dingtalk-entry/route.ts @@ -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) {