144 lines
3.9 KiB
TypeScript
144 lines
3.9 KiB
TypeScript
import 'server-only'
|
||
import { NextAuthOptions } from "next-auth"
|
||
import type { User as NextAuthUser } from "next-auth"
|
||
import CredentialsProvider from "next-auth/providers/credentials"
|
||
import bcrypt from "bcryptjs"
|
||
import { db } from "./db"
|
||
import { clearSessionInvalidation, isSessionInvalidated } from "./service/session"
|
||
|
||
/** 用户查询时需要 include 的关联 */
|
||
export const userAuthInclude = {
|
||
roles: { include: { permissions: true } },
|
||
dept: true,
|
||
} as const
|
||
|
||
/** 从数据库用户对象构建 JWT payload(供密码登录和 IAAA 登录共用) */
|
||
export function buildUserJwtPayload(user: {
|
||
id: string
|
||
name: string | null
|
||
status: string | null
|
||
deptCode: string | null
|
||
isSuperAdmin: boolean
|
||
roles: Array<{ name: string; permissions: Array<{ name: string }> }>
|
||
}) {
|
||
const roles = user.roles.map((r) => r.name)
|
||
const permissions = Array.from(
|
||
new Set(user.roles.flatMap((r) => r.permissions.map((p) => p.name)))
|
||
)
|
||
return {
|
||
id: user.id,
|
||
name: user.name,
|
||
status: user.status,
|
||
deptCode: user.deptCode,
|
||
roles,
|
||
permissions,
|
||
isSuperAdmin: user.isSuperAdmin,
|
||
}
|
||
}
|
||
|
||
export const authOptions: NextAuthOptions = {
|
||
providers: [
|
||
CredentialsProvider({
|
||
name: "credentials",
|
||
credentials: {
|
||
id: { label: "用户ID", type: "text" },
|
||
password: { label: "密码", type: "password" }
|
||
},
|
||
async authorize(credentials, req): Promise<NextAuthUser | null> {
|
||
if (!credentials?.id || !credentials?.password) {
|
||
return null
|
||
}
|
||
|
||
try {
|
||
// 查找用户
|
||
const user = await db.user.findUnique({
|
||
where: { id: credentials.id },
|
||
include: userAuthInclude,
|
||
})
|
||
|
||
if (!user) {
|
||
return null
|
||
}
|
||
|
||
// 验证密码
|
||
const isPasswordValid = await bcrypt.compare(credentials.password, user.password)
|
||
|
||
if (!isPasswordValid) {
|
||
return null
|
||
}
|
||
|
||
// 更新最近登录时间
|
||
await db.user.update({
|
||
where: { id: user.id },
|
||
data: { lastLoginAt: new Date() }
|
||
})
|
||
|
||
// 清除会话失效标记(用户已重新登录,获得最新权限)
|
||
await clearSessionInvalidation(user.id)
|
||
|
||
return buildUserJwtPayload(user) as any
|
||
} catch (error) {
|
||
console.error("Auth error:", error)
|
||
return null
|
||
}
|
||
}
|
||
})
|
||
],
|
||
session: {
|
||
strategy: "jwt",
|
||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||
},
|
||
jwt: {
|
||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||
},
|
||
pages: {
|
||
signIn: "/login",
|
||
},
|
||
callbacks: {
|
||
async jwt({ token, user }) {
|
||
// 初次登录时,将用户信息保存到JWT token中
|
||
if (user) {
|
||
const u = user as any
|
||
token = {
|
||
...token,
|
||
id: u.id,
|
||
name: u.name,
|
||
status: u.status,
|
||
deptCode: u.deptCode,
|
||
roles: u.roles,
|
||
permissions: u.permissions,
|
||
isSuperAdmin: u.isSuperAdmin,
|
||
}
|
||
} else if (token.id) {
|
||
// 后续请求:检查会话是否已被标记失效
|
||
const invalidated = await isSessionInvalidated(token.id as string)
|
||
if (invalidated) {
|
||
token.sessionInvalid = true
|
||
}
|
||
}
|
||
return token
|
||
},
|
||
async session({ session, token }) {
|
||
// 会话已被标记失效,返回不含用户信息的session
|
||
if (token.sessionInvalid) {
|
||
return { expires: session.expires } as any
|
||
}
|
||
// 将JWT token中的信息传递给session
|
||
if (session.user) {
|
||
const t = token as any
|
||
session.user = {
|
||
...session.user,
|
||
id: t.id,
|
||
name: t.name,
|
||
status: t.status,
|
||
deptCode: t.deptCode,
|
||
roles: t.roles,
|
||
permissions: t.permissions,
|
||
isSuperAdmin: t.isSuperAdmin,
|
||
}
|
||
}
|
||
return session
|
||
}
|
||
},
|
||
secret: process.env.NEXTAUTH_SECRET,
|
||
} |