修复钉钉工作通知端内免登
This commit is contained in:
parent
06b9678fe1
commit
1a00ca9030
32
app/api/dingtalk/h5-login/route.ts
Normal file
32
app/api/dingtalk/h5-login/route.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { loginDingtalkUserByH5AuthCode } from '@/lib/dingtalk-login'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const code = String(body?.code || '').trim()
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: '缺少钉钉免登授权码' },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
const response = await loginDingtalkUserByH5AuthCode(request, code)
|
||||
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
return response
|
||||
} catch (error: any) {
|
||||
console.error('钉钉H5免登失败:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: error.message || '钉钉免登失败,请稍后再试',
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getDingtalkAppAccessToken } from '@/lib/dingtalk-client'
|
||||
import { ensurePerformanceSurveyEmployeeProfileForUser } from '@/lib/performance-survey-report-sync'
|
||||
import {
|
||||
getAuthSessionCookieName,
|
||||
resolveSecureAuthCookieFromRequest,
|
||||
} from '@/lib/auth-cookie'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { encode } from 'next-auth/jwt'
|
||||
import type { Role } from '@prisma/client'
|
||||
import { loginDingtalkUserByOAuthCode } from '@/lib/dingtalk-login'
|
||||
|
||||
/**
|
||||
* R25修复:移除硬编码的钉钉密钥,强制使用环境变量
|
||||
@ -18,481 +9,26 @@ import type { Role } from '@prisma/client'
|
||||
* - DINGTALK_CLIENT_SECRET: 钉钉应用的 AppSecret
|
||||
*/
|
||||
|
||||
// 钉钉配置验证标记(只在首次调用时检查一次)
|
||||
let configValidated = false
|
||||
|
||||
// 验证钉钉配置
|
||||
function validateDingtalkConfig(): { clientId: string; clientSecret: string } | null {
|
||||
const clientId = process.env.DINGTALK_CLIENT_ID
|
||||
const clientSecret = process.env.DINGTALK_CLIENT_SECRET
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
if (!configValidated) {
|
||||
console.error('[Dingtalk] ❌ 配置错误:缺少必需的环境变量')
|
||||
console.error('[Dingtalk] 请配置以下环境变量:')
|
||||
console.error('[Dingtalk] - DINGTALK_CLIENT_ID: 钉钉应用的 AppKey')
|
||||
console.error('[Dingtalk] - DINGTALK_CLIENT_SECRET: 钉钉应用的 AppSecret')
|
||||
configValidated = true
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (!configValidated) {
|
||||
console.log('[Dingtalk] ✅ 钉钉配置已加载')
|
||||
configValidated = true
|
||||
}
|
||||
|
||||
return { clientId, clientSecret }
|
||||
}
|
||||
|
||||
// 钉钉应用配置(从环境变量获取)
|
||||
function getDingtalkConfig(): { clientId: string; clientSecret: string } {
|
||||
const config = validateDingtalkConfig()
|
||||
if (!config) {
|
||||
throw new Error('钉钉登录未配置,请联系管理员配置 DINGTALK_CLIENT_ID 和 DINGTALK_CLIENT_SECRET')
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// 获取用户访问令牌
|
||||
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() {
|
||||
return getDingtalkAppAccessToken()
|
||||
}
|
||||
|
||||
function warmPerformanceSurveyProfile(user: {
|
||||
id: string
|
||||
dingtalkUserId: string | null
|
||||
email: string
|
||||
username: string
|
||||
realName: string | null
|
||||
}) {
|
||||
void ensurePerformanceSurveyEmployeeProfileForUser({
|
||||
userId: user.id,
|
||||
dingtalkUserId: user.dingtalkUserId,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
realName: user.realName,
|
||||
displayName: user.realName || user.username,
|
||||
}).catch((error) => {
|
||||
console.warn('[performance-survey] 钉钉登录后预热人员档案失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
async function createAuthenticatedResponse(
|
||||
request: NextRequest,
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
realName?: string | null
|
||||
role: Role
|
||||
department: string
|
||||
managedDepartment?: string | null
|
||||
avatar?: string | null
|
||||
isSuperAdmin?: boolean | null
|
||||
},
|
||||
) {
|
||||
const secret = process.env.NEXTAUTH_SECRET
|
||||
if (!secret) {
|
||||
throw new Error('登录会话未配置 NEXTAUTH_SECRET')
|
||||
}
|
||||
|
||||
const maxAge = 7 * 24 * 60 * 60
|
||||
const secureCookie = resolveSecureAuthCookieFromRequest(request)
|
||||
const cookieName = getAuthSessionCookieName(secureCookie)
|
||||
const sessionToken = await encode({
|
||||
secret,
|
||||
maxAge,
|
||||
token: {
|
||||
sub: user.id,
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.username,
|
||||
role: user.role,
|
||||
avatar: user.avatar || null,
|
||||
isSuperAdmin: user.isSuperAdmin ?? false,
|
||||
department: user.department || '',
|
||||
managedDepartment: user.managedDepartment || null,
|
||||
},
|
||||
})
|
||||
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
needCompleteProfile: false,
|
||||
message: '登录成功',
|
||||
sessionCreated: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
avatar: user.avatar,
|
||||
role: user.role,
|
||||
department: user.department,
|
||||
},
|
||||
})
|
||||
|
||||
response.cookies.set(cookieName, sessionToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: secureCookie,
|
||||
path: '/',
|
||||
maxAge,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// 根据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 {
|
||||
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'
|
||||
let password = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
password += charset.charAt(Math.floor(Math.random() * charset.length))
|
||||
}
|
||||
return password
|
||||
}
|
||||
|
||||
// 生成随机邮箱
|
||||
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, state } = await request.json()
|
||||
const { code } = await request.json()
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: '缺少授权码' },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// state 用于传递前端数据(如origin),不是公司标识
|
||||
console.log('开始钉钉登录流程:', { code })
|
||||
|
||||
// 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.findFirst({
|
||||
where: { dingtalkUserId: userId }
|
||||
})
|
||||
|
||||
let isNewUser = false
|
||||
let needCompleteProfile = false
|
||||
|
||||
if (!user) {
|
||||
// 首先检查是否有用户名为钉钉真实姓名的用户
|
||||
const existingUserByName = await prisma.user.findUnique({
|
||||
where: { username: userInfo.name }
|
||||
})
|
||||
|
||||
// 使用固定密码用于钉钉自动登录
|
||||
const hashedPassword = await bcrypt.hash('dingtalk_auto_login', 10)
|
||||
|
||||
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,
|
||||
password: hashedPassword, // 更新密码为固定值
|
||||
}
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
username: userInfo.name, // 直接使用钉钉真实姓名
|
||||
email: emailToUse,
|
||||
password: hashedPassword,
|
||||
realName: userInfo.name,
|
||||
avatar: userInfo.avatar || null,
|
||||
phone: userInfo.mobile || null,
|
||||
department: '', // 待用户选择
|
||||
role: 'USER',
|
||||
isActive: false, // 新用户默认未激活,需要管理员激活(向后兼容)
|
||||
status: 'pending', // 新用户默认状态为 pending(阶段一新增)
|
||||
loginType: 'dingtalk',
|
||||
dingtalkUserId: userId
|
||||
}
|
||||
})
|
||||
|
||||
isNewUser = true
|
||||
needCompleteProfile = true // 需要选择部门
|
||||
console.log('用户创建成功:', user.id)
|
||||
}
|
||||
} else {
|
||||
// 更新现有用户信息
|
||||
console.log('更新现有用户:', user.username)
|
||||
|
||||
// 确保密码也更新为固定值,以便前端能够自动登录
|
||||
const hashedPassword = await bcrypt.hash('dingtalk_auto_login', 10)
|
||||
|
||||
user = await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
realName: userInfo.name,
|
||||
avatar: userInfo.avatar || user.avatar,
|
||||
phone: userInfo.mobile || user.phone,
|
||||
password: hashedPassword, // 更新密码为固定值
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果需要完善信息(选择部门)
|
||||
if (needCompleteProfile) {
|
||||
warmPerformanceSurveyProfile(user)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
needCompleteProfile: true,
|
||||
message: '请选择您的部门',
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 检查用户状态(优先使用 status 字段,兼容 isActive)
|
||||
const userStatus = (user as any).status || (user.isActive ? 'active' : 'pending');
|
||||
const isUserActive = userStatus === 'active';
|
||||
|
||||
if (!isUserActive) {
|
||||
warmPerformanceSurveyProfile(user)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '您的账号尚未激活,请联系管理员激活后再登录'
|
||||
}, { status: 403 })
|
||||
}
|
||||
|
||||
// 检查是否被暂停或归档
|
||||
if (userStatus === 'suspended' || userStatus === 'archived') {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '您的账号已被暂停或归档,无法登录'
|
||||
}, { status: 403 })
|
||||
}
|
||||
|
||||
// 记录登录审计信息(获取客户端IP)
|
||||
const clientIp = request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
null;
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
lastLoginAt: new Date(),
|
||||
lastLoginIp: clientIp,
|
||||
loginCount: { increment: 1 },
|
||||
// 双写:确保 status 和 isActive 一致
|
||||
status: 'active',
|
||||
isActive: true
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('记录登录审计信息失败:', err);
|
||||
// 不阻塞登录流程
|
||||
});
|
||||
|
||||
// 返回用户信息(不包含敏感信息)
|
||||
warmPerformanceSurveyProfile(user)
|
||||
|
||||
return createAuthenticatedResponse(request, {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
realName: user.realName,
|
||||
role: user.role,
|
||||
department: user.department,
|
||||
managedDepartment: user.managedDepartment,
|
||||
avatar: user.avatar,
|
||||
isSuperAdmin: (user as any).isSuperAdmin ?? false,
|
||||
})
|
||||
|
||||
return loginDingtalkUserByOAuthCode(request, String(code))
|
||||
} catch (error: any) {
|
||||
console.error('钉钉登录失败:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: error.message || '登录失败,请稍后再试'
|
||||
message: error.message || '登录失败,请稍后再试',
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,10 @@ import {
|
||||
resolveSecureAuthCookieFromRequest,
|
||||
} from '@/lib/auth-cookie'
|
||||
import { DINGTALK_DIRECT_LOGIN_SOURCE, buildDingtalkOAuthUrl } from '@/lib/dingtalk-oauth'
|
||||
import {
|
||||
getConfiguredDingtalkCorpId,
|
||||
normalizeDingtalkCorpId,
|
||||
} from '@/lib/dingtalk-corp'
|
||||
import { getPublicAppOrigin } from '@/lib/public-app-url'
|
||||
import { normalizeInternalRedirectPath } from '@/lib/safe-redirect'
|
||||
|
||||
@ -26,6 +30,194 @@ async function resolveCurrentToken(request: NextRequest) {
|
||||
return null
|
||||
}
|
||||
|
||||
function isDingtalkBrowser(request: NextRequest) {
|
||||
return /DingTalk/i.test(request.headers.get('user-agent') || '')
|
||||
}
|
||||
|
||||
function stringifyForInlineScript(value: string) {
|
||||
return JSON.stringify(value).replace(/</g, '\\u003c')
|
||||
}
|
||||
|
||||
function createH5LoginPage({
|
||||
redirectPath,
|
||||
corpId,
|
||||
fallbackOAuthUrl,
|
||||
}: {
|
||||
redirectPath: string
|
||||
corpId: string
|
||||
fallbackOAuthUrl: string
|
||||
}) {
|
||||
return new NextResponse(
|
||||
`<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>正在进入素材库</title>
|
||||
<meta http-equiv="Cache-Control" content="no-store" />
|
||||
<style>
|
||||
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0f172a;color:#f8fafc;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||||
main{width:min(420px,calc(100vw - 48px));padding:32px 24px;text-align:center}
|
||||
h1{margin:0 0 12px;font-size:22px}
|
||||
p{margin:0;color:#cbd5e1;line-height:1.7}
|
||||
.spinner{width:38px;height:38px;margin:0 auto 20px;border:4px solid rgba(255,255,255,.24);border-top-color:#60a5fa;border-radius:999px;animation:spin .85s linear infinite}
|
||||
a{color:#93c5fd}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="spinner" aria-hidden="true"></div>
|
||||
<h1>正在进入素材库</h1>
|
||||
<p id="status">正在通过钉钉确认身份...</p>
|
||||
</main>
|
||||
<script src="https://g.alicdn.com/dingding/dingtalk-jsapi/2.15.6/dingtalk.open.js" onload="window.__startDingtalkLogin && window.__startDingtalkLogin()" onerror="window.__failDingtalkLogin && window.__failDingtalkLogin('钉钉 JSAPI 加载失败')"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var target = ${stringifyForInlineScript(redirectPath)};
|
||||
var corpId = ${stringifyForInlineScript(corpId)};
|
||||
var fallbackOAuthUrl = ${stringifyForInlineScript(fallbackOAuthUrl)};
|
||||
var started = false;
|
||||
var status = document.getElementById('status');
|
||||
|
||||
function setStatus(text) {
|
||||
if (status) status.textContent = text;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
if (status) {
|
||||
status.textContent = message + '。请返回钉钉重新打开通知,或联系管理员检查钉钉免登配置。';
|
||||
status.appendChild(document.createElement('br'));
|
||||
status.appendChild(document.createElement('br'));
|
||||
var link = document.createElement('a');
|
||||
link.href = fallbackOAuthUrl;
|
||||
link.textContent = '改用网页授权登录';
|
||||
status.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
window.__failDingtalkLogin = showError;
|
||||
|
||||
function completeProfile(user) {
|
||||
var userInfo = encodeURIComponent(JSON.stringify(user || {}));
|
||||
window.location.replace('/complete-profile?userInfo=' + userInfo);
|
||||
}
|
||||
|
||||
function submitCode(code) {
|
||||
setStatus('已获取钉钉身份,正在创建登录状态...');
|
||||
fetch('/api/dingtalk/h5-login', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: code })
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.json().catch(function () { return {}; }).then(function (data) {
|
||||
return { ok: response.ok, data: data };
|
||||
});
|
||||
})
|
||||
.then(function (result) {
|
||||
if (!result.ok || !result.data || !result.data.success) {
|
||||
throw new Error((result.data && result.data.message) || '登录失败');
|
||||
}
|
||||
|
||||
if (result.data.needCompleteProfile) {
|
||||
setStatus('首次登录,需要完善账号信息...');
|
||||
completeProfile(result.data.user);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('登录成功,正在打开考核调研...');
|
||||
setTimeout(function () {
|
||||
window.location.replace(target);
|
||||
}, 180);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error && error.message ? error.message : '钉钉免登失败');
|
||||
});
|
||||
}
|
||||
|
||||
window.__startDingtalkLogin = function () {
|
||||
if (started) return;
|
||||
started = true;
|
||||
|
||||
var dd = window.dd;
|
||||
if (!dd || !dd.ready || !dd.runtime || !dd.runtime.permission) {
|
||||
showError('当前页面未检测到钉钉客户端环境');
|
||||
return;
|
||||
}
|
||||
|
||||
if (dd.error) {
|
||||
dd.error(function (error) {
|
||||
console.log('DingTalk JSAPI error:', error);
|
||||
showError('钉钉免登初始化失败');
|
||||
});
|
||||
}
|
||||
|
||||
dd.ready(function () {
|
||||
setStatus('正在向钉钉获取免登授权码...');
|
||||
dd.runtime.permission.requestAuthCode({
|
||||
corpId: corpId,
|
||||
onSuccess: function (result) {
|
||||
if (!result || !result.code) {
|
||||
showError('钉钉未返回免登授权码');
|
||||
return;
|
||||
}
|
||||
submitCode(result.code);
|
||||
},
|
||||
onFail: function (error) {
|
||||
console.log('requestAuthCode failed:', error);
|
||||
showError('钉钉免登授权失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function createMissingCorpIdPage() {
|
||||
return new NextResponse(
|
||||
`<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>钉钉免登配置缺失</title>
|
||||
<style>
|
||||
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f8fafc;color:#0f172a;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||||
main{width:min(440px,calc(100vw - 48px));padding:28px}
|
||||
h1{font-size:22px;margin:0 0 12px}
|
||||
p{margin:0;color:#475569;line-height:1.8}
|
||||
code{background:#e2e8f0;border-radius:6px;padding:2px 6px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>钉钉免登配置缺失</h1>
|
||||
<p>当前缺少企业 <code>CorpId</code>,无法在钉钉内自动确认身份。请在服务端配置 <code>DINGTALK_CORP_ID</code>,或让入口链接携带 <code>corpId</code> 参数后重试。</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>`,
|
||||
{
|
||||
status: 503,
|
||||
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 redirectPath = normalizeInternalRedirectPath(request.nextUrl.searchParams.get('redirect'))
|
||||
const token = await resolveCurrentToken(request)
|
||||
@ -33,9 +225,28 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.redirect(new URL(redirectPath, getPublicAppOrigin()))
|
||||
}
|
||||
|
||||
return NextResponse.redirect(buildDingtalkOAuthUrl({
|
||||
const fallbackOAuthUrl = buildDingtalkOAuthUrl({
|
||||
redirectPath,
|
||||
callbackPath: '/dingtalk-callback',
|
||||
source: DINGTALK_DIRECT_LOGIN_SOURCE,
|
||||
}))
|
||||
})
|
||||
|
||||
if (!isDingtalkBrowser(request)) {
|
||||
return NextResponse.redirect(fallbackOAuthUrl)
|
||||
}
|
||||
|
||||
const queryCorpId =
|
||||
request.nextUrl.searchParams.get('corpId') ||
|
||||
request.nextUrl.searchParams.get('corpid')
|
||||
const corpId = normalizeDingtalkCorpId(queryCorpId) || getConfiguredDingtalkCorpId()
|
||||
|
||||
if (!corpId) {
|
||||
return createMissingCorpIdPage()
|
||||
}
|
||||
|
||||
return createH5LoginPage({
|
||||
redirectPath,
|
||||
corpId,
|
||||
fallbackOAuthUrl,
|
||||
})
|
||||
}
|
||||
|
||||
12
lib/dingtalk-corp.ts
Normal file
12
lib/dingtalk-corp.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export function normalizeDingtalkCorpId(value: unknown) {
|
||||
const corpId = String(value ?? '').trim()
|
||||
return corpId || ''
|
||||
}
|
||||
|
||||
export function getConfiguredDingtalkCorpId() {
|
||||
return normalizeDingtalkCorpId(
|
||||
process.env.DINGTALK_CORP_ID ||
|
||||
process.env.DINGTALK_CORPID ||
|
||||
process.env.NEXT_PUBLIC_DINGTALK_CORP_ID,
|
||||
)
|
||||
}
|
||||
466
lib/dingtalk-login.ts
Normal file
466
lib/dingtalk-login.ts
Normal file
@ -0,0 +1,466 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getDingtalkAppAccessToken } from '@/lib/dingtalk-client'
|
||||
import { ensurePerformanceSurveyEmployeeProfileForUser } from '@/lib/performance-survey-report-sync'
|
||||
import {
|
||||
getAuthSessionCookieName,
|
||||
resolveSecureAuthCookieFromRequest,
|
||||
} from '@/lib/auth-cookie'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { encode } from 'next-auth/jwt'
|
||||
import type { Role } from '@prisma/client'
|
||||
|
||||
type DingtalkUserInfo = {
|
||||
name?: string | null
|
||||
avatar?: string | null
|
||||
mobile?: string | null
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
let configValidated = false
|
||||
|
||||
function validateDingtalkConfig(): { clientId: string; clientSecret: string } | null {
|
||||
const clientId = process.env.DINGTALK_CLIENT_ID
|
||||
const clientSecret = process.env.DINGTALK_CLIENT_SECRET
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
if (!configValidated) {
|
||||
console.error('[Dingtalk] 配置错误:缺少必需的环境变量')
|
||||
console.error('[Dingtalk] 请配置 DINGTALK_CLIENT_ID 和 DINGTALK_CLIENT_SECRET')
|
||||
configValidated = true
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (!configValidated) {
|
||||
console.log('[Dingtalk] 钉钉配置已加载')
|
||||
configValidated = true
|
||||
}
|
||||
|
||||
return { clientId, clientSecret }
|
||||
}
|
||||
|
||||
function getDingtalkConfig(): { clientId: string; clientSecret: string } {
|
||||
const config = validateDingtalkConfig()
|
||||
if (!config) {
|
||||
throw new Error('钉钉登录未配置,请联系管理员配置 DINGTALK_CLIENT_ID 和 DINGTALK_CLIENT_SECRET')
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
async function getUserAccessToken(code: string) {
|
||||
const config = getDingtalkConfig()
|
||||
const response = await fetch('https://api.dingtalk.com/v1.0/oauth2/userAccessToken', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
clientId: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
code,
|
||||
grantType: 'authorization_code',
|
||||
}),
|
||||
})
|
||||
|
||||
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()
|
||||
if (data.code && data.code !== '0') {
|
||||
console.error('获取用户访问令牌API错误:', data)
|
||||
throw new Error(`获取用户访问令牌失败: ${data.message || '未知错误'}`)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async function getUnionId(token: string) {
|
||||
const response = await fetch('https://api.dingtalk.com/v1.0/contact/users/me', {
|
||||
headers: { 'x-acs-dingtalk-access-token': token },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取unionId失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (!data.unionId) {
|
||||
console.error('获取unionId返回数据异常:', data)
|
||||
throw new Error('获取unionId失败: 返回数据中没有unionId')
|
||||
}
|
||||
|
||||
return data.unionId
|
||||
}
|
||||
|
||||
async function getUserIdByUnionId(unionId: string, appToken: string) {
|
||||
const response = await fetch(
|
||||
`https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=${appToken}`,
|
||||
{
|
||||
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()
|
||||
if (data.errcode !== 0) {
|
||||
console.error('钉钉API错误:', data)
|
||||
throw new Error(`获取userId失败: ${data.errmsg || '未知错误'}`)
|
||||
}
|
||||
|
||||
if (!data.result?.userid) {
|
||||
console.error('钉钉API返回数据异常:', data)
|
||||
throw new Error('获取userId失败: 返回数据格式错误')
|
||||
}
|
||||
|
||||
return data.result.userid
|
||||
}
|
||||
|
||||
async function getUserIdByH5AuthCode(code: string, appToken: string) {
|
||||
const response = await fetch(
|
||||
`https://oapi.dingtalk.com/topapi/v2/user/getuserinfo?access_token=${appToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code }),
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`通过免登码获取用户失败: HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.errcode !== 0) {
|
||||
console.error('钉钉免登API错误:', data)
|
||||
throw new Error(`通过免登码获取用户失败: ${data.errmsg || '未知错误'}`)
|
||||
}
|
||||
|
||||
const userId = data.result?.userid || data.result?.userId || data.userid || data.userId
|
||||
if (!userId) {
|
||||
console.error('钉钉免登API返回数据异常:', data)
|
||||
throw new Error('通过免登码获取用户失败: 返回数据中没有userId')
|
||||
}
|
||||
|
||||
return String(userId)
|
||||
}
|
||||
|
||||
async function getUserInfo(userId: string, appToken: string): Promise<DingtalkUserInfo> {
|
||||
const response = await fetch(
|
||||
`https://oapi.dingtalk.com/topapi/v2/user/get?access_token=${appToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userid: userId }),
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取用户信息失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.errcode !== 0) {
|
||||
console.error('钉钉API错误:', data)
|
||||
throw new Error(`获取用户信息失败: ${data.errmsg || '未知错误'}`)
|
||||
}
|
||||
|
||||
if (!data.result) {
|
||||
console.error('钉钉API返回数据异常:', data)
|
||||
throw new Error('获取用户信息失败: 返回数据格式错误')
|
||||
}
|
||||
|
||||
return data.result
|
||||
}
|
||||
|
||||
function generateRandomEmail(): string {
|
||||
const randomStr = Math.random().toString(36).substring(2, 10)
|
||||
return `dingtalk_${randomStr}@temp.com`
|
||||
}
|
||||
|
||||
function warmPerformanceSurveyProfile(user: {
|
||||
id: string
|
||||
dingtalkUserId: string | null
|
||||
email: string
|
||||
username: string
|
||||
realName: string | null
|
||||
}) {
|
||||
void ensurePerformanceSurveyEmployeeProfileForUser({
|
||||
userId: user.id,
|
||||
dingtalkUserId: user.dingtalkUserId,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
realName: user.realName,
|
||||
displayName: user.realName || user.username,
|
||||
}).catch((error) => {
|
||||
console.warn('[performance-survey] 钉钉登录后预热人员档案失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
async function createAuthenticatedResponse(
|
||||
request: NextRequest,
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
realName?: string | null
|
||||
role: Role
|
||||
department: string
|
||||
managedDepartment?: string | null
|
||||
avatar?: string | null
|
||||
isSuperAdmin?: boolean | null
|
||||
},
|
||||
) {
|
||||
const secret = process.env.NEXTAUTH_SECRET
|
||||
if (!secret) {
|
||||
throw new Error('登录会话未配置 NEXTAUTH_SECRET')
|
||||
}
|
||||
|
||||
const maxAge = 7 * 24 * 60 * 60
|
||||
const secureCookie = resolveSecureAuthCookieFromRequest(request)
|
||||
const cookieName = getAuthSessionCookieName(secureCookie)
|
||||
const sessionToken = await encode({
|
||||
secret,
|
||||
maxAge,
|
||||
token: {
|
||||
sub: user.id,
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.username,
|
||||
role: user.role,
|
||||
avatar: user.avatar || null,
|
||||
isSuperAdmin: user.isSuperAdmin ?? false,
|
||||
department: user.department || '',
|
||||
managedDepartment: user.managedDepartment || null,
|
||||
},
|
||||
})
|
||||
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
needCompleteProfile: false,
|
||||
message: '登录成功',
|
||||
sessionCreated: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
avatar: user.avatar,
|
||||
role: user.role,
|
||||
department: user.department,
|
||||
},
|
||||
})
|
||||
|
||||
response.cookies.set(cookieName, sessionToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: secureCookie,
|
||||
path: '/',
|
||||
maxAge,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
async function createOrUpdateUserSession(
|
||||
request: NextRequest,
|
||||
userId: string,
|
||||
userInfo: DingtalkUserInfo,
|
||||
) {
|
||||
const displayName = String(userInfo.name || '').trim()
|
||||
if (!displayName) {
|
||||
throw new Error('钉钉返回的用户名称为空')
|
||||
}
|
||||
|
||||
let user = await prisma.user.findFirst({
|
||||
where: { dingtalkUserId: userId },
|
||||
})
|
||||
|
||||
let needCompleteProfile = false
|
||||
const hashedPassword = await bcrypt.hash('dingtalk_auto_login', 10)
|
||||
|
||||
if (!user) {
|
||||
const existingUserByName = await prisma.user.findUnique({
|
||||
where: { username: displayName },
|
||||
})
|
||||
|
||||
if (existingUserByName && !existingUserByName.dingtalkUserId) {
|
||||
console.log('找到同名用户,自动关联钉钉账号:', existingUserByName.username)
|
||||
user = await prisma.user.update({
|
||||
where: { id: existingUserByName.id },
|
||||
data: {
|
||||
dingtalkUserId: userId,
|
||||
loginType: 'dingtalk',
|
||||
realName: displayName,
|
||||
avatar: userInfo.avatar || existingUserByName.avatar,
|
||||
phone: userInfo.mobile || existingUserByName.phone,
|
||||
password: hashedPassword,
|
||||
},
|
||||
})
|
||||
console.log('账号关联成功:', user.username)
|
||||
} else {
|
||||
console.log('创建新用户(使用钉钉姓名):', displayName)
|
||||
|
||||
let emailToUse = userInfo.email || generateRandomEmail()
|
||||
const emailExists = await prisma.user.findUnique({ where: { email: emailToUse } })
|
||||
if (emailExists) {
|
||||
emailToUse = generateRandomEmail()
|
||||
}
|
||||
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
username: displayName,
|
||||
email: emailToUse,
|
||||
password: hashedPassword,
|
||||
realName: displayName,
|
||||
avatar: userInfo.avatar || null,
|
||||
phone: userInfo.mobile || null,
|
||||
department: '',
|
||||
role: 'USER',
|
||||
isActive: false,
|
||||
status: 'pending',
|
||||
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: displayName,
|
||||
avatar: userInfo.avatar || user.avatar,
|
||||
phone: userInfo.mobile || user.phone,
|
||||
password: hashedPassword,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (needCompleteProfile) {
|
||||
warmPerformanceSurveyProfile(user)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
needCompleteProfile: true,
|
||||
message: '请选择您的部门',
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const userStatus = (user as any).status || (user.isActive ? 'active' : 'pending')
|
||||
const isUserActive = userStatus === 'active'
|
||||
|
||||
if (!isUserActive) {
|
||||
warmPerformanceSurveyProfile(user)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: '您的账号尚未激活,请联系管理员激活后再登录',
|
||||
},
|
||||
{ status: 403 },
|
||||
)
|
||||
}
|
||||
|
||||
if (userStatus === 'suspended' || userStatus === 'archived') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: '您的账号已被暂停或归档,无法登录',
|
||||
},
|
||||
{ status: 403 },
|
||||
)
|
||||
}
|
||||
|
||||
const clientIp =
|
||||
request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
null
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
lastLoginAt: new Date(),
|
||||
lastLoginIp: clientIp,
|
||||
loginCount: { increment: 1 },
|
||||
status: 'active',
|
||||
isActive: true,
|
||||
},
|
||||
}).catch((error) => {
|
||||
console.error('记录登录审计信息失败:', error)
|
||||
})
|
||||
|
||||
warmPerformanceSurveyProfile(user)
|
||||
|
||||
return createAuthenticatedResponse(request, {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
realName: user.realName,
|
||||
role: user.role,
|
||||
department: user.department,
|
||||
managedDepartment: user.managedDepartment,
|
||||
avatar: user.avatar,
|
||||
isSuperAdmin: (user as any).isSuperAdmin ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
export async function loginDingtalkUserByOAuthCode(request: NextRequest, code: string) {
|
||||
console.log('开始钉钉OAuth登录流程')
|
||||
|
||||
const tokenData = await getUserAccessToken(code)
|
||||
if (!tokenData?.accessToken) {
|
||||
console.error('获取用户访问令牌返回数据异常:', tokenData)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: '获取用户访问令牌失败:返回数据格式错误' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
|
||||
console.log('获取用户访问令牌成功')
|
||||
const appAccessToken = await getDingtalkAppAccessToken()
|
||||
console.log('获取应用访问令牌成功')
|
||||
|
||||
const unionId = await getUnionId(tokenData.accessToken)
|
||||
console.log('获取unionId成功:', unionId)
|
||||
|
||||
const userId = await getUserIdByUnionId(unionId, appAccessToken)
|
||||
console.log('获取userId成功:', userId)
|
||||
|
||||
const userInfo = await getUserInfo(userId, appAccessToken)
|
||||
console.log('获取用户信息成功:', userInfo.name)
|
||||
|
||||
return createOrUpdateUserSession(request, userId, userInfo)
|
||||
}
|
||||
|
||||
export async function loginDingtalkUserByH5AuthCode(request: NextRequest, code: string) {
|
||||
console.log('开始钉钉H5免登流程')
|
||||
|
||||
const appAccessToken = await getDingtalkAppAccessToken()
|
||||
console.log('获取应用访问令牌成功')
|
||||
|
||||
const userId = await getUserIdByH5AuthCode(code, appAccessToken)
|
||||
console.log('通过免登码获取userId成功:', userId)
|
||||
|
||||
const userInfo = await getUserInfo(userId, appAccessToken)
|
||||
console.log('获取用户信息成功:', userInfo.name)
|
||||
|
||||
return createOrUpdateUserSession(request, userId, userInfo)
|
||||
}
|
||||
@ -66,6 +66,8 @@ const OPTIONAL_ENV_VARS = [
|
||||
'DINGTALK_APP_SECRET',
|
||||
'DINGTALK_CLIENT_ID',
|
||||
'DINGTALK_CLIENT_SECRET',
|
||||
'DINGTALK_CORP_ID',
|
||||
'DINGTALK_CORPID',
|
||||
'DINGTALK_WORK_NOTICE_CLIENT_ID',
|
||||
'DINGTALK_WORK_NOTICE_CLIENT_SECRET',
|
||||
'DINGTALK_WORK_NOTICE_APP_KEY',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user