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 }