Files
NextEdu/src/auth.ts
SpecialX 56c3f32e2d chore(config): update auth, env, i18n, proxy, and dependencies
- Update src/auth.ts auth configuration

- Update src/env.mjs environment variable validation

- Update src/i18n/request.ts locale handling

- Update src/proxy.ts middleware

- Update src/next-auth.d.ts type declarations

- Update package.json and package-lock.json dependencies
2026-07-03 10:30:28 +08:00

162 lines
6.0 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 NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { resolvePermissions } from "@/shared/lib/permissions"
import { isRole } from "@/shared/types/permissions"
import { logLoginEvent } from "@/shared/lib/login-logger"
import { normalizeRole, resolvePrimaryRole } from "@/shared/lib/role-utils"
import {
encodePermissionsBitmap,
decodePermissionsBitmap,
} from "@/shared/lib/permission-bitmap"
import { trackAuthEvent } from "@/shared/lib/track-event"
export const { handlers, auth, signIn, signOut } = NextAuth({
trustHost: true,
secret: process.env.NEXTAUTH_SECRET,
session: { strategy: "jwt" },
pages: { signIn: "/login" },
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
totpCode: { label: "2FA Code", type: "text" },
},
// audit-P1-3 重构authorize 回调原混合 7 类职责107 行),
// 已抽取至 modules/auth/services/login-service.ts 的 authenticateUser 函数。
// 此处为薄包装,仅负责动态 import避免 server-only 模块在模块评估期加载)
// 与委托调用。所有登录逻辑(速率限制/账户锁定/密码校验/2FA/角色加载)均在
// login-service 中实现,可独立测试。
authorize: async (credentials) => {
const { authenticateUser } = await import("@/modules/auth/services/login-service")
return authenticateUser(credentials)
},
}),
],
callbacks: {
jwt: async ({ token, user }) => {
// P0-5 修复User 接口已在 next-auth.d.ts 中扩展了 role/roles 字段,
// 无需 `as` 断言即可安全访问。
if (user && user.id) {
token.id = user.id
token.role = normalizeRole(user.role)
token.name = user.name ?? undefined
// Store all roles (not just primary) and resolved permissions
const allRoles = (user.roles ?? [user.role ?? "student"]).filter(isRole)
token.roles = allRoles
// audit-P1-7JWT 中仅存位图(~14 字符),不存数组(~1.1KB)。
// Session callback 解码还原为 Permission[] 供业务使用。
const permissions = await resolvePermissions(allRoles)
token.permissionsBitmap = encodePermissionsBitmap(permissions)
// Onboarding status is resolved from DB below; default to false on first login
token.onboarded = false
}
// Refresh roles/permissions from DB on each JWT refresh
const userId = String(token.id ?? "").trim()
if (userId) {
const [{ eq }, { db }, { roles, users, usersToRoles }] = await Promise.all([
import("drizzle-orm"),
import("@/shared/db"),
import("@/shared/db/schema"),
])
const [fresh, roleRows] = await Promise.all([
db.query.users.findFirst({
where: eq(users.id, userId),
columns: { name: true, onboardedAt: true },
}),
db
.select({ name: roles.name })
.from(usersToRoles)
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
.where(eq(usersToRoles.userId, userId)),
])
if (fresh) {
const allRoles = roleRows.map((r) => r.name).filter(isRole)
token.role = resolvePrimaryRole(allRoles)
token.name = fresh.name ?? token.name
token.roles = allRoles
// audit-P1-7刷新位图不再写 token.permissions 数组
const permissions = await resolvePermissions(allRoles)
token.permissionsBitmap = encodePermissionsBitmap(permissions)
token.onboarded = Boolean(fresh.onboardedAt)
}
}
return token
},
session: async ({ session, token }) => {
// audit-P1-7session.user.permissions 由位图解码得到,
// 业务代码getAuthContext / usePermission无需感知位图存在。
if (session.user) {
session.user.id = String(token.id ?? "")
session.user.role = normalizeRole(token.role)
session.user.roles = (token.roles ?? []).filter(isRole)
session.user.permissions = decodePermissionsBitmap(
token.permissionsBitmap ?? "",
)
session.user.onboarded = Boolean(token.onboarded)
if (typeof token.name === "string") {
session.user.name = token.name
}
}
return session
},
},
events: {
async signIn({ user }) {
await logLoginEvent({
userId: user.id,
userEmail: user.email ?? "",
action: "signin",
status: "success",
})
// audit-P1-9登录成功埋点用于登录成功率、异常登录地理/设备告警)
// 非阻塞trackEvent 内部已吞掉异常
await trackAuthEvent("auth.signin_success", {
userId: user.id,
properties: {
// user.role 是单一主角色user.roles 是全部角色(可能未填充)
roles: (user.roles ?? (user.role ? [user.role] : [])),
},
})
},
async signOut(message) {
// P0-5 修复:使用 `in` 操作符对联合类型进行类型收窄,替代 `as` 断言。
// NextAuth v5 signOut 事件 message 为联合类型:
// { session: ... } | { token: JWT }
// JWT 策略下实际收到 { token: JWT },其中 JWT 已在 next-auth.d.ts 扩展。
let userId = ""
let userEmail = ""
let roles: string[] = []
if ("token" in message) {
const token = message.token
if (token) {
userId = String(token.id ?? "")
userEmail = String(token.email ?? "")
roles = (token.roles ?? []).filter(isRole)
}
}
if (userEmail) {
await logLoginEvent({
userId: userId || undefined,
userEmail,
action: "signout",
status: "success",
})
}
// audit-P1-9登出埋点
if (userId) {
await trackAuthEvent("auth.signout", {
userId,
properties: { roles },
})
}
},
},
})