gongxiangp2/lib/auth.ts

190 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { prisma } from '@/lib/prisma'
import bcrypt from 'bcryptjs'
import { resolveSecureAuthCookieForCurrentRequest } from '@/lib/auth-cookie'
import { authorizeDingtalkH5UserForNextAuth } from '@/lib/dingtalk-login'
const LEGACY_DINGTALK_AUTO_LOGIN_PASSWORD = 'dingtalk_auto_login'
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
loginType: { label: 'Login Type', type: 'text' },
authCode: { label: 'Auth Code', type: 'text' },
},
async authorize(credentials) {
const input = credentials as Record<string, string> | undefined
if (input?.loginType === 'dingtalk_inapp') {
const authCode = String(input.authCode || '').trim()
if (!authCode) {
throw new Error('钉钉免登授权码不能为空')
}
const user = await authorizeDingtalkH5UserForNextAuth(authCode, {
appSource: 'workNotice',
})
return {
id: user.id,
email: user.email,
name: user.username,
role: user.role,
department: user.department,
managedDepartment: user.managedDepartment,
avatar: user.avatar,
isSuperAdmin: user.isSuperAdmin ?? false,
}
}
if (!credentials?.email || !credentials?.password) {
throw new Error('请输入邮箱和密码')
}
if (credentials.password === LEGACY_DINGTALK_AUTO_LOGIN_PASSWORD) {
throw new Error('邮箱或密码错误')
}
const user = await prisma.user.findUnique({
where: { email: credentials.email },
})
if (!user) {
throw new Error('用户不存在')
}
const isPasswordValid = await bcrypt.compare(
credentials.password,
user.password
)
if (!isPasswordValid) {
throw new Error('密码错误')
}
// 检查用户状态(优先使用 status 字段,兼容 isActive)
const userStatus = (user as any).status || (user.isActive ? 'active' : 'pending');
const isUserActive = userStatus === 'active';
// 管理员可以登录(即使未激活),但普通用户必须激活
// 兼容所有管理员角色:ADMIN, SYSTEM_ADMIN, DEPT_ADMIN
const isAdminRole = user.role === 'ADMIN' || user.role === 'SYSTEM_ADMIN' || user.role === 'DEPT_ADMIN';
if (!isUserActive && !isAdminRole) {
throw new Error('账号未激活,请联系管理员')
}
// 检查是否被暂停或归档
if (userStatus === 'suspended' || userStatus === 'archived') {
throw new Error('账号已被暂停或归档,无法登录')
}
// 记录登录审计信息(异步,不阻塞登录)
const clientIp = (credentials as any).ip || null;
prisma.user.update({
where: { id: user.id },
data: {
lastLoginAt: new Date(),
lastLoginIp: clientIp,
loginCount: { increment: 1 },
// 双写:同时更新 status 和 isActive(如果 status 为 active)
...(isUserActive ? {
status: 'active',
isActive: true
} : {})
}
}).catch(err => {
console.error('记录登录审计信息失败:', err);
// 不阻塞登录流程
});
return {
id: user.id,
email: user.email,
name: user.username,
role: user.role,
department: user.department,
managedDepartment: user.managedDepartment,
avatar: user.avatar, // 添加头像字段
isSuperAdmin: (user as any).isSuperAdmin ?? false, // 添加超级管理员标识
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = (user as any).role;
token.id = user.id;
token.avatar = (user as any).avatar; // 添加头像到token
token.isSuperAdmin = (user as any).isSuperAdmin; // 添加超级管理员标识到token
token.department = (user as any).department || '';
token.managedDepartment = (user as any).managedDepartment || null;
token.accountStatus = 'active';
token.authCheckedAt = Date.now();
} else if (token.id) {
const lastCheckedAt = Number((token as any).authCheckedAt || 0)
if (Date.now() - lastCheckedAt >= 30_000) {
const currentUser = await prisma.user.findUnique({
where: { id: String(token.id) },
select: {
role: true,
status: true,
isActive: true,
avatar: true,
department: true,
managedDepartment: true,
isSuperAdmin: true,
},
})
const currentStatus = currentUser?.status || (currentUser?.isActive ? 'active' : 'pending')
const accountDisabled = !currentUser || currentStatus === 'suspended' || currentStatus === 'archived'
;(token as any).accountStatus = accountDisabled ? 'disabled' : currentStatus
;(token as any).authCheckedAt = Date.now()
if (currentUser && !accountDisabled) {
token.role = currentUser.role
token.avatar = currentUser.avatar
token.isSuperAdmin = currentUser.isSuperAdmin
token.department = currentUser.department || ''
token.managedDepartment = currentUser.managedDepartment || null
}
}
}
return token;
},
async session({ session, token }) {
if ((token as any).accountStatus === 'disabled') {
;(session as any).user = null
return session
}
if (session?.user && token) {
(session.user as any).role = token.role || 'USER';
(session.user as any).id = token.id;
(session.user as any).avatar = token.avatar; // 添加头像到session
(session.user as any).isSuperAdmin = token.isSuperAdmin || false; // 添加超级管理员标识到session
(session.user as any).department = (token as any).department || '';
(session.user as any).managedDepartment = (token as any).managedDepartment || null;
}
return session;
},
},
pages: {
signIn: '/login',
},
session: {
strategy: 'jwt',
// R43修复:缩短会话过期时间从30天到7天,提高安全性
maxAge: 7 * 24 * 60 * 60, // 7天
},
get useSecureCookies() {
return resolveSecureAuthCookieForCurrentRequest()
},
secret: process.env.NEXTAUTH_SECRET,
}