131 lines
4.5 KiB
TypeScript
131 lines
4.5 KiB
TypeScript
import { NextAuthOptions } from 'next-auth'
|
||
import CredentialsProvider from 'next-auth/providers/credentials'
|
||
import { prisma } from '@/lib/prisma'
|
||
import bcrypt from 'bcryptjs'
|
||
|
||
export const authOptions: NextAuthOptions = {
|
||
providers: [
|
||
CredentialsProvider({
|
||
name: 'Credentials',
|
||
credentials: {
|
||
email: { label: 'Email', type: 'email' },
|
||
password: { label: 'Password', type: 'password' },
|
||
},
|
||
async authorize(credentials) {
|
||
if (!credentials?.email || !credentials?.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;
|
||
}
|
||
return token;
|
||
},
|
||
async session({ session, token }) {
|
||
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',
|
||
maxAge: 30 * 24 * 60 * 60, // 30天
|
||
},
|
||
cookies: {
|
||
sessionToken: {
|
||
name: `next-auth.session-token`,
|
||
options: {
|
||
httpOnly: true,
|
||
sameSite: 'lax', // 允许跨站请求携带cookie(钉钉登录需要)
|
||
path: '/',
|
||
// 根据 NEXTAUTH_URL 的协议来判断是否使用 secure
|
||
// 如果 NEXTAUTH_URL 是 HTTPS,则使用 secure: true
|
||
// 如果是 HTTP(内网环境),则使用 secure: false
|
||
secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false,
|
||
},
|
||
},
|
||
},
|
||
secret: process.env.NEXTAUTH_SECRET,
|
||
}
|