2026-05-25 16:20:26 +08:00

362 lines
11 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import bcrypt from 'bcryptjs'
import { randomBytes } from 'crypto'
import { DingtalkConfigError, requireDingtalkAppConfig } from '@/lib/dingtalk-config'
import { createDingtalkLoginToken } from '@/lib/dingtalk-login-token'
function getDingtalkConfig() {
return requireDingtalkAppConfig()
}
// 获取用户访问令牌
async function getUserAccessToken(code: string) {
const config = getDingtalkConfig()
const url = 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken'
const params = {
clientId: config.clientId,
clientSecret: config.clientSecret,
code: code,
grantType: 'authorization_code'
}
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
})
if (!response.ok) {
const errorText = await response.text()
console.error('获取用户访问令牌HTTP错误:', response.status, errorText)
throw new Error(`获取用户访问令牌失败: HTTP ${response.status}`)
}
const data = await response.json()
// 新版API可能返回错误信息
if (data.code && data.code !== '0') {
console.error('获取用户访问令牌API错误:', data)
throw new Error(`获取用户访问令牌失败: ${data.message || '未知错误'}`)
}
return data
}
// 获取应用访问令牌
async function getAppAccessToken() {
const config = getDingtalkConfig()
const url = 'https://oapi.dingtalk.com/gettoken'
const response = await fetch(`${url}?appkey=${config.clientId}&appsecret=${config.clientSecret}`)
if (!response.ok) {
throw new Error('获取应用访问令牌失败')
}
const data = await response.json()
// 检查钉钉 API 返回的错误码
if (data.errcode !== 0) {
console.error('钉钉API错误:', data)
throw new Error(`获取应用访问令牌失败: ${data.errmsg || '未知错误'}`)
}
// 检查 access_token 是否存在
if (!data.access_token) {
console.error('获取access_token返回数据异常:', data)
throw new Error('获取应用访问令牌失败: 返回数据中没有access_token')
}
return data.access_token
}
// 根据token获取用户unionId
async function getUnionId(token: string) {
const url = 'https://api.dingtalk.com/v1.0/contact/users/me'
const response = await fetch(url, {
headers: { 'x-acs-dingtalk-access-token': token }
})
if (!response.ok) {
throw new Error('获取unionId失败')
}
const data = await response.json()
// 检查 unionId 是否存在
if (!data.unionId) {
console.error('获取unionId返回数据异常:', data)
throw new Error('获取unionId失败: 返回数据中没有unionId')
}
return data.unionId
}
// 根据unionId获取userId
async function getUserIdByUnionId(unionId: string, appToken: string) {
const url = `https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=${appToken}`
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unionid: unionId })
})
if (!response.ok) {
throw new Error('根据unionId获取userId失败')
}
const data = await response.json()
// 检查钉钉 API 返回的错误码
if (data.errcode !== 0) {
console.error('钉钉API错误:', data)
throw new Error(`获取userId失败: ${data.errmsg || '未知错误'}`)
}
// 检查 result 是否存在
if (!data.result || !data.result.userid) {
console.error('钉钉API返回数据异常:', data)
throw new Error('获取userId失败: 返回数据格式错误')
}
return data.result.userid
}
// 根据userId获取用户详细信息
async function getUserInfo(userId: string, appToken: string) {
const url = `https://oapi.dingtalk.com/topapi/v2/user/get?access_token=${appToken}`
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userid: userId })
})
if (!response.ok) {
throw new Error('获取用户信息失败')
}
const data = await response.json()
// 检查钉钉 API 返回的错误码
if (data.errcode !== 0) {
console.error('钉钉API错误:', data)
throw new Error(`获取用户信息失败: ${data.errmsg || '未知错误'}`)
}
// 检查 result 是否存在
if (!data.result) {
console.error('钉钉API返回数据异常:', data)
throw new Error('获取用户信息失败: 返回数据格式错误')
}
return data.result
}
// 生成随机密码
function generateRandomPassword(length: number = 12): string {
return randomBytes(Math.max(length, 24)).toString('base64url')
}
// 生成随机邮箱
function generateRandomEmail(): string {
const randomStr = Math.random().toString(36).substring(2, 10)
return `dingtalk_${randomStr}@temp.com`
}
export async function POST(request: NextRequest) {
try {
const { code } = await request.json()
if (!code) {
return NextResponse.json(
{ success: false, message: '缺少授权码' },
{ status: 400 }
)
}
console.log('开始钉钉登录流程')
// 1. 获取用户访问令牌
const tokenData = await getUserAccessToken(code)
// 检查返回的数据
if (!tokenData || !tokenData.accessToken) {
console.error('获取用户访问令牌返回数据异常:', tokenData)
return NextResponse.json(
{ success: false, message: '获取用户访问令牌失败:返回数据格式错误' },
{ status: 500 }
)
}
const userAccessToken = tokenData.accessToken
console.log('获取用户访问令牌成功')
// 2. 获取应用访问令牌
const appAccessToken = await getAppAccessToken()
console.log('获取应用访问令牌成功')
// 3. 获取unionId
const unionId = await getUnionId(userAccessToken)
console.log('获取unionId成功:', unionId)
// 4. 根据unionId获取userId
const userId = await getUserIdByUnionId(unionId, appAccessToken)
console.log('获取userId成功:', userId)
// 5. 获取用户详细信息
const userInfo = await getUserInfo(userId, appAccessToken)
console.log('获取用户信息成功:', userInfo.name)
// 6. 在数据库中查找或创建用户
let user = await prisma.user.findUnique({
where: { dingtalkUserId: userId }
})
let needCompleteProfile = false
if (!user) {
// 首先检查是否有用户名为钉钉真实姓名的用户
const existingUserByName = await prisma.user.findUnique({
where: { username: userInfo.name }
})
if (existingUserByName && !existingUserByName.dingtalkUserId) {
// 找到了同名用户且未绑定钉钉,直接关联
console.log('找到同名用户,自动关联钉钉账号:', existingUserByName.username)
user = await prisma.user.update({
where: { id: existingUserByName.id },
data: {
dingtalkUserId: userId,
loginType: 'dingtalk',
realName: userInfo.name,
avatar: userInfo.avatar || existingUserByName.avatar,
phone: userInfo.mobile || existingUserByName.phone,
}
})
console.log('账号关联成功:', user.username)
} else {
// 创建新用户,直接使用钉钉真实姓名作为用户名
console.log('创建新用户(使用钉钉姓名):', userInfo.name)
let emailToUse = userInfo.email || generateRandomEmail()
// 检查邮箱是否已存在
let emailExists = await prisma.user.findUnique({ where: { email: emailToUse } })
if (emailExists) {
emailToUse = generateRandomEmail()
}
const randomPasswordHash = await bcrypt.hash(generateRandomPassword(32), 10)
user = await prisma.user.create({
data: {
username: userInfo.name, // 直接使用钉钉真实姓名
email: emailToUse,
password: randomPasswordHash,
realName: userInfo.name,
avatar: userInfo.avatar || null,
phone: userInfo.mobile || null,
department: '', // 待用户选择
role: 'USER',
isActive: false, // 新用户默认未激活,需要管理员激活
loginType: 'dingtalk',
dingtalkUserId: userId
}
})
needCompleteProfile = true // 需要选择部门
console.log('用户创建成功:', user.id)
}
} else {
// 更新现有用户信息
console.log('更新现有用户:', user.username)
user = await prisma.user.update({
where: { id: user.id },
data: {
realName: userInfo.name,
avatar: userInfo.avatar || user.avatar,
phone: userInfo.mobile || user.phone,
}
})
}
// 如果需要完善信息(选择部门)
if (needCompleteProfile) {
const tokenResult = await createDingtalkLoginToken(user.id, 'complete_profile')
return NextResponse.json({
success: true,
needCompleteProfile: true,
message: '请选择您的部门',
profileToken: tokenResult.token,
user: {
id: user.id,
username: user.username,
email: user.email,
realName: user.realName,
avatar: user.avatar,
role: user.role,
department: user.department,
isActive: user.isActive
}
})
}
// 检查用户是否已激活
if (!user.isActive) {
return NextResponse.json({
success: false,
message: '您的账号尚未激活,请联系管理员激活后再登录'
}, { status: 403 })
}
const tokenResult = await createDingtalkLoginToken(user.id, 'login')
// 返回用户信息(不包含敏感信息)
return NextResponse.json({
success: true,
needCompleteProfile: false,
message: '登录成功',
loginToken: tokenResult.token,
user: {
id: user.id,
username: user.username,
email: user.email,
realName: user.realName,
avatar: user.avatar,
role: user.role,
department: user.department
}
})
} catch (error: any) {
if (error instanceof DingtalkConfigError) {
return NextResponse.json(
{
success: false,
message: '当前环境未配置钉钉登录,请联系管理员',
},
{ status: 503 }
)
}
console.error('钉钉登录失败:', error)
return NextResponse.json(
{
success: false,
message: error.message || '登录失败,请稍后再试'
},
{ status: 500 }
)
}
}