import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { getPublicAppOrigin } from '@/lib/public-app-url'
import { consumeDingtalkSessionTicket } from '@/lib/dingtalk-session-ticket'
import {
deleteDingtalkLoginHandoff,
DINGTALK_LOGIN_HANDOFF_COOKIE,
} from '@/lib/dingtalk-login-handoff'
import { getTrustedDingtalkReturnOriginFromRequest } from '@/lib/dingtalk-return-origin'
import { setDingtalkAuthenticatedSessionCookies } from '@/lib/dingtalk-login'
import { resolveSecureAuthCookieFromRequest } from '@/lib/auth-cookie'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
const LOGIN_TRANSITION_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;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}
main{box-sizing:border-box;width:min(400px,100%);padding:28px 24px;text-align:center;border:1px solid #e3eaf3;border-radius:24px;background:#fff;box-shadow:0 18px 48px rgba(15,23,42,.10)}
.brand{font-size:14px;font-weight:650;color:#1677ff}
h1{margin:12px 0 0;font-size:24px;line-height:1.25;font-weight:700;letter-spacing:-.01em;color:#101828}
#message{margin:10px 0 0;color:#667085;font-size:14px;font-weight:500;line-height:1.7}
.progress{width:72px;height:4px;margin:22px auto 0;overflow:hidden;border-radius:999px;background:#e8f1ff}
.progress span{display:block;width:65%;height:100%;border-radius:inherit;background:#1677ff;animation:pulse 1.1s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:.55;transform:translateX(-20%)}50%{opacity:1;transform:translateX(55%)}}
`
const LOGIN_TRANSITION_PAGE_MARKUP = `
由壹点「素材库」
登录成功
正在进入素材库...
`
function stringifyForInlineScript(value: string) {
return JSON.stringify(value).replace(/
登录失败
登录失败
`,
{
status,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
'Referrer-Policy': 'no-referrer',
},
},
)
}
function createSessionRedirectResponse({
request,
redirectPath,
cookies,
targetOrigin,
}: {
request: NextRequest
redirectPath: string
cookies: string[]
targetOrigin?: string | null
}) {
const redirectUrl = buildRedirectUrl(redirectPath, request, targetOrigin)
const response = new NextResponse(
`
正在进入素材库
${LOGIN_TRANSITION_PAGE_MARKUP}
`,
{
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
Pragma: 'no-cache',
Expires: '0',
'Referrer-Policy': 'no-referrer',
},
},
)
const cookieNames = appendSetCookies(response, cookies)
console.log('[Dingtalk H5] 通过一次性票据写入会话Cookie并返回落地页:', {
redirectPath,
redirectUrl,
cookieNames,
requestHeaders: getRequestHeaderSnapshot(request),
})
return response
}
async function resolveActiveTicketUser(userId: string) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
username: true,
realName: true,
role: true,
department: true,
managedDepartment: true,
avatar: true,
isSuperAdmin: true,
status: true,
isActive: true,
},
})
if (!user) return null
const status = user.status || (user.isActive ? 'active' : 'pending')
if (status !== 'active') {
return null
}
return user
}
export async function GET(request: NextRequest) {
const requestOrigin = getTrustedDingtalkReturnOriginFromRequest(request)
const handoffBinding = request.cookies.get(DINGTALK_LOGIN_HANDOFF_COOKIE)?.value || null
if (!requestOrigin) {
return createErrorPage('登录交接来源校验失败,请重新登录。')
}
let ticket
try {
ticket = await consumeDingtalkSessionTicket(request.nextUrl.searchParams.get('ticket'), {
expectedTargetOrigin: requestOrigin,
expectedHandoffBinding: handoffBinding,
})
} catch (error) {
console.error('[Dingtalk] 消费登录会话交接票据失败:', error)
return createErrorPage('暂时无法完成登录交接,请重新登录。', 503)
}
if (!ticket) {
return createErrorPage('登录交接已失效或不是当前浏览器发起,请重新登录。')
}
if (ticket.targetOrigin && ticket.targetOrigin !== requestOrigin) {
return createErrorPage('登录交接来源校验失败,请重新登录。')
}
let ticketUser = null
if (ticket.userId) {
ticketUser = await resolveActiveTicketUser(ticket.userId)
if (!ticketUser) {
return createErrorPage('账号状态已变化,请重新登录。', 403)
}
}
const response = createSessionRedirectResponse({
request,
redirectPath: ticket.redirectPath,
cookies: ticket.cookies,
targetOrigin: ticket.targetOrigin,
})
if (ticketUser) {
await setDingtalkAuthenticatedSessionCookies(response, request, ticketUser)
response.cookies.set(DINGTALK_LOGIN_HANDOFF_COOKIE, '', {
httpOnly: true,
sameSite: 'lax',
secure: resolveSecureAuthCookieFromRequest(request),
path: '/api/dingtalk/session-ticket',
maxAge: 0,
})
if (ticket.handoffId) {
await deleteDingtalkLoginHandoff(ticket.handoffId).catch((error) => {
console.warn('[Dingtalk] 清理已完成的登录交接请求失败:', error)
})
}
}
return response
}