Files
hair-keeper/src/server/auth.ts

144 lines
3.9 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 '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,
}