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' const SURVEY_OPENING_PAGE_STYLE = ` html,body{margin:0;min-height:100%;background:#f6f8fb;color:#111827;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} body{min-height:100vh;min-height:100dvh;overflow:hidden} .topbar{height:64px;box-sizing:border-box;display:flex;align-items:center;padding:0 20px;color:#fff;background:#000} .topbar span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .topbar strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:18px;font-weight:650;line-height:1.15;letter-spacing:-.01em} main{box-sizing:border-box;width:min(430px,100%);margin:0 auto;padding:32px 20px 0} .shell{padding:24px;border:1px solid #e3eaf3;border-radius:26px;background:#fff;box-shadow:0 18px 48px rgba(15,23,42,.10)} .kicker{font-size:14px;font-weight:650;color:#1677ff} h1{margin:12px 0 0;font-size:28px;line-height:1.18;font-weight:700;letter-spacing:-.01em;color:#101828} .message{margin:12px 0 0;color:#667085;font-size:14px;font-weight:500;line-height:1.7} .open-box{margin-top:24px;padding:16px;border-radius:22px;background:#f6f8fb} .open-row{display:flex;align-items:center;justify-content:space-between;gap:16px} .open-row strong{font-size:15px;font-weight:650;color:#101828} .open-row em{font-style:normal;color:#1677ff;font-size:14px;font-weight:650} .progress{height:6px;margin-top:16px;overflow:hidden;border-radius:999px;background:#e8f1ff} .progress span{display:block;width:66%;height:100%;border-radius:inherit;background:#1677ff;animation:pulse 1.1s ease-in-out infinite} .ghost-card{margin-top:28px;padding:20px;border:1px solid #eef2f7;border-radius:18px;background:#fff} .ghost-title{font-size:15px;font-weight:600;color:#7b8494} .ghost-line{height:12px;border-radius:999px;background:#eef3f9} .ghost-line.one{margin-top:24px} .ghost-line.two{width:74%;margin-top:12px} .ghost-list{margin-top:32px;display:grid;gap:20px} .ghost-row{display:flex;align-items:center;gap:12px} .ghost-dot{width:16px;height:16px;border-radius:999px;border:1px solid #e3eaf3} .ghost-row .ghost-line{flex:1;margin:0} .ghost-row:last-child .ghost-line{flex:.67} @keyframes pulse{0%,100%{opacity:.55}50%{opacity:1}} ` const SURVEY_OPENING_PAGE_MARKUP = `
由壹点「素材库」
考核调研

正在打开问卷

正在打开问卷...

准备问卷内容 马上进入
` 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(/ 正在打开问卷 ${SURVEY_OPENING_PAGE_MARKUP} `, { 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(/ 无法打开问卷

无法打开问卷

`, { 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) { if (source === DINGTALK_DIRECT_LOGIN_SOURCE) { return createDingtalkDirectErrorPage('暂时无法打开问卷,请稍后重试或联系管理员。') } const loginPath = buildDingtalkAutoLoginPath(redirectPath) const url = new URL(loginPath, publicOrigin) if (data?.message) { url.searchParams.set('error', data.message) } return NextResponse.redirect(url) } if (data.needCompleteProfile) { const userInfo = encodeURIComponent(JSON.stringify(data.user || {})) const profileResponse = NextResponse.redirect( new URL(`/complete-profile?userInfo=${userInfo}`, publicOrigin), ) appendLoginSetCookie(profileResponse, loginResponse) profileResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') return profileResponse } 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)) } }