import { NextRequest, NextResponse } from "next/server"; import { db } from "@/lib/db"; import { getAuthSettings, getUser } from "@/lib/db/queries"; import { loginToken, user } from "@/lib/db/schema"; import { generateUUID } from "@/lib/utils"; import { signIn } from "@/app/(auth)/auth"; import { eq } from "drizzle-orm"; const normalizeCallbackUrl = (value: unknown) => { if (typeof value !== "string" || !value) return "/"; try { const parsed = new URL(value, "https://local.invalid"); if (parsed.origin !== "https://local.invalid") return "/"; return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/"; } catch { return "/"; } }; const parseDingTalkState = (state: string | null) => { if (!state) { return { org: undefined as string | undefined, callbackUrl: "/" }; } if (state.startsWith("v1.")) { try { const payload = JSON.parse( Buffer.from(state.slice(3), "base64url").toString("utf8"), ) as { org?: unknown; callbackUrl?: unknown }; return { org: typeof payload.org === "string" && payload.org !== "default" ? payload.org : undefined, callbackUrl: normalizeCallbackUrl(payload.callbackUrl), }; } catch (error) { console.warn("[DingTalk Callback] Failed to parse state payload:", error); } } return { org: state !== "default" ? state : undefined, callbackUrl: "/", }; }; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get("code"); const state = searchParams.get("state"); const dingTalkState = parseDingTalkState(state); console.log( "[DingTalk Callback] Received code:", code?.substring(0, 10) + "...", "state:", state, ); if (!code) { return NextResponse.json({ success: false, error: "Missing code" }, { status: 400 }); } const settings = await getAuthSettings(); if (!settings.isDingTalkEnabled) { return NextResponse.json( { success: false, error: "DingTalk login is not configured" }, { status: 400 }, ); } const organizations = settings.dingTalkOrganizations ?? []; const selectedOrg = dingTalkState.org ? organizations.find((org) => org.name === dingTalkState.org) : organizations[0]; const appKey = selectedOrg?.appKey ?? settings.dingTalkAppKey; const appSecret = selectedOrg?.appSecret ?? settings.dingTalkAppSecret; if (!appKey || !appSecret) { return NextResponse.json( { success: false, error: "DingTalk appKey/appSecret is not configured" }, { status: 400 }, ); } try { // 1. 获取 userAccessToken console.log("[DingTalk Callback] Getting user access token..."); const tokenResponse = await fetch( "https://api.dingtalk.com/v1.0/oauth2/userAccessToken", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId: appKey, clientSecret: appSecret, code, grantType: "authorization_code", }), }, ); const tokenData = await tokenResponse.json(); console.log("[DingTalk Callback] Token response:", tokenData.accessToken ? "success" : "failed"); if (!tokenData.accessToken) { console.error("[DingTalk Callback] Token Error:", tokenData); return NextResponse.json({ success: false, error: "Failed to get access token: " + (tokenData.message || JSON.stringify(tokenData)) }, { status: 400 }); } // 2. 获取用户 unionId console.log("[DingTalk Callback] Getting user unionId..."); const userMeResponse = await fetch("https://api.dingtalk.com/v1.0/contact/users/me", { headers: { "x-acs-dingtalk-access-token": tokenData.accessToken, }, }); const userMeData = await userMeResponse.json(); console.log("[DingTalk Callback] User me response:", userMeData.unionId ? "success" : "failed"); if (!userMeData.unionId) { console.error("[DingTalk Callback] User Me Error:", userMeData); return NextResponse.json({ success: false, error: "Failed to get user unionId" }, { status: 400 }); } // 3. 获取应用访问令牌 (用于获取更详细的用户信息) console.log("[DingTalk Callback] Getting app access token..."); const appTokenResponse = await fetch( `https://oapi.dingtalk.com/gettoken?appkey=${encodeURIComponent( appKey, )}&appsecret=${encodeURIComponent(appSecret)}`, ); const appTokenData = await appTokenResponse.json(); if (!appTokenData.access_token) { console.error("[DingTalk Callback] App Token Error:", appTokenData); return NextResponse.json({ success: false, error: "Failed to get app access token" }, { status: 400 }); } // 4. 根据 unionId 获取 userId console.log("[DingTalk Callback] Getting userId by unionId..."); const userIdResponse = await fetch(`https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=${appTokenData.access_token}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ unionid: userMeData.unionId }), }); const userIdData = await userIdResponse.json(); console.log("[DingTalk Callback] UserId response:", userIdData); let userInfo: any = { unionId: userMeData.unionId, nick: userMeData.nick, avatarUrl: userMeData.avatarUrl, }; // 如果能获取到 userId,则获取更详细的用户信息 if (userIdData.result && userIdData.result.userid) { console.log("[DingTalk Callback] Getting detailed user info..."); const detailResponse = await fetch(`https://oapi.dingtalk.com/topapi/v2/user/get?access_token=${appTokenData.access_token}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userid: userIdData.result.userid }), }); const detailData = await detailResponse.json(); if (detailData.result) { // 获取部门名称 let departmentNames: string[] = []; const deptIdList = detailData.result.dept_id_list || []; if (deptIdList.length > 0) { console.log("[DingTalk Callback] Getting department info for dept_ids:", deptIdList); try { // 获取每个部门的详细信息 for (const deptId of deptIdList) { const deptResponse = await fetch(`https://oapi.dingtalk.com/topapi/v2/department/get?access_token=${appTokenData.access_token}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dept_id: deptId }), }); const deptData = await deptResponse.json(); if (deptData.result && deptData.result.name) { departmentNames.push(deptData.result.name); } } console.log("[DingTalk Callback] Department names:", departmentNames); } catch (deptError) { console.error("[DingTalk Callback] Failed to get department info:", deptError); } } userInfo = { ...userInfo, userId: userIdData.result.userid, name: detailData.result.name, email: detailData.result.email, mobile: detailData.result.mobile, avatar: detailData.result.avatar, jobNumber: detailData.result.job_number, title: detailData.result.title, // 职位 deptIdList: deptIdList, departmentNames: departmentNames, // 部门名称列表 }; } } console.log("[DingTalk Callback] Final user info:", { ...userInfo, mobile: "***", departments: userInfo.departmentNames?.join(", ") || "无部门信息" }); // 5. 查找或创建用户 // 使用 unionId 作为唯一标识,email 可能为空 const email = userInfo.email || `dingtalk_${userInfo.unionId}@dingtalk.local`; console.log("[DingTalk Callback] Checking user:", email); // 检查用户是否存在(直接使用 Drizzle 查询,不通过 getUser) let existingUsers; try { existingUsers = await db .select() .from(user) .where(eq(user.email, email)); console.log("[DingTalk Callback] User found:", existingUsers.length > 0); if (existingUsers.length > 0) { console.log("[DingTalk Callback] User details:", { id: existingUsers[0].id, email: existingUsers[0].email, name: existingUsers[0].name, isActive: existingUsers[0].isActive }); } } catch (error) { console.error("[DingTalk Callback] Error querying user:", error); throw error; } // 组合部门信息为字符串 const departmentStr = userInfo.departmentNames?.length > 0 ? userInfo.departmentNames.join(" / ") : null; console.log("[DingTalk Callback] Department string:", departmentStr); // 获取钉钉用户ID const dingtalkUserId = userInfo.userId || null; console.log("[DingTalk Callback] DingTalk userId:", dingtalkUserId); if (existingUsers.length === 0) { // 创建新用户 console.log("[DingTalk Callback] Creating new user:", email, "with name:", userInfo.name || userInfo.nick); try { await db.insert(user).values({ email, password: "", // 钉钉用户无密码 name: userInfo.name || userInfo.nick, // 保存真实姓名或昵称 department: departmentStr, // 保存部门信息 dingtalkUserId: dingtalkUserId, // 保存钉钉用户ID role: "regular", isActive: true, }); console.log("[DingTalk Callback] User created successfully with department:", departmentStr, ", dingtalkUserId:", dingtalkUserId); // 重新查询用户 existingUsers = await db .select() .from(user) .where(eq(user.email, email)); } catch (error) { console.error("[DingTalk Callback] Error creating user:", error); throw error; } } else { // 更新现有用户的姓名、部门信息和钉钉用户ID const existingUser = existingUsers[0]; console.log("[DingTalk Callback] Existing user found, current name:", existingUser.name, ", department:", (existingUser as any).department); const updateData: Record = {}; // 更新姓名(如果数据库中为空但钉钉有数据) if (!existingUser.name && (userInfo.name || userInfo.nick)) { updateData.name = userInfo.name || userInfo.nick; } // 每次登录都更新部门信息(部门可能变化) if (departmentStr) { updateData.department = departmentStr; } // 更新钉钉用户ID(如果数据库中为空) if (!(existingUser as any).dingtalkUserId && dingtalkUserId) { updateData.dingtalkUserId = dingtalkUserId; } if (Object.keys(updateData).length > 0) { console.log("[DingTalk Callback] Updating user info:", updateData); try { await db.update(user) .set(updateData) .where(eq(user.email, email)); console.log("[DingTalk Callback] User info updated successfully"); } catch (error) { console.error("[DingTalk Callback] Error updating user info:", error); // 不抛出错误,继续登录流程 } } } // 6. 生成临时登录令牌 const token = generateUUID(); await db.insert(loginToken).values({ token, email, expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5分钟有效期 }); console.log("[DingTalk Callback] Login successful for:", email); // 7. 返回成功响应(给 iframe 中的回调页面) return NextResponse.json({ success: true, message: "登录成功", user: { email, name: userInfo.name || userInfo.nick, avatar: userInfo.avatar || userInfo.avatarUrl, title: userInfo.title, departments: userInfo.departmentNames || [], }, token, callbackUrl: dingTalkState.callbackUrl, }); } catch (error) { console.error("[DingTalk Callback] Error:", error); return NextResponse.json({ success: false, error: "Internal server error: " + (error as Error).message }, { status: 500 }); } }