refactor: P1-3/4/6 解耦修复 - 拆分 auth/users 文件 + notifications 反向依赖

This commit is contained in:
SpecialX
2026-06-18 02:21:44 +08:00
parent 62be0b9404
commit 2c8e229e00
11 changed files with 514 additions and 288 deletions

View File

@@ -9,125 +9,14 @@ import {
isAccountLocked,
} from "@/shared/lib/password-policy"
import { RATE_LIMIT_RULES, rateLimit, rateLimitKey, resetRateLimit } from "@/shared/lib/rate-limit"
const normalizeRole = (value: unknown) => {
const role = String(value ?? "").trim().toLowerCase()
if (role === "grade_head" || role === "teaching_head") return "teacher"
if (role === "admin" || role === "student" || role === "teacher" || role === "parent") return role
return "student"
}
const resolvePrimaryRole = (roleNames: string[]) => {
const mapped = roleNames.map((name) => normalizeRole(name)).filter(Boolean)
if (mapped.includes("admin")) return "admin"
if (mapped.includes("teacher")) return "teacher"
if (mapped.includes("parent")) return "parent"
if (mapped.includes("student")) return "student"
return "student"
}
const normalizeBcryptHash = (value: string) => {
if (value.startsWith("$2")) return value
if (value.startsWith("$")) return `$2b${value}`
return `$2b$${value}`
}
/**
* Resolve the client IP from request headers (best-effort, used for
* rate-limit keying only — not stored).
*/
const resolveClientIp = async (): Promise<string> => {
try {
const { headers } = await import("next/headers")
const headerList = await headers()
return (
headerList.get("x-forwarded-for")?.split(",")[0]?.trim() ??
headerList.get("x-real-ip") ??
"unknown"
)
} catch {
return "unknown"
}
}
/**
* Get or create a password_security row for a user.
*/
const getOrCreatePasswordSecurity = async (
db: typeof import("@/shared/db")["db"],
passwordSecurity: typeof import("@/shared/db/schema")["passwordSecurity"],
userId: string
) => {
const [existing] = await db
.select()
.from(passwordSecurity)
.where(eq(passwordSecurity.userId, userId))
.limit(1)
if (existing) return existing
const { createId } = await import("@paralleldrive/cuid2")
const id = createId()
await db.insert(passwordSecurity).values({
id,
userId,
failedLoginAttempts: 0,
})
const [created] = await db
.select()
.from(passwordSecurity)
.where(eq(passwordSecurity.userId, userId))
.limit(1)
return created
}
/**
* Increment failed login attempts and lock the account if the threshold
* is reached.
*/
const recordFailedLogin = async (
db: typeof import("@/shared/db")["db"],
passwordSecurity: typeof import("@/shared/db/schema")["passwordSecurity"],
userId: string
): Promise<{ locked: boolean; lockedUntil: Date | null }> => {
const current = await getOrCreatePasswordSecurity(db, passwordSecurity, userId)
const nextAttempts = current.failedLoginAttempts + 1
const shouldLock = nextAttempts >= PASSWORD_RULES.maxLoginAttempts
const lockedUntil = shouldLock
? new Date(Date.now() + PASSWORD_RULES.lockoutDurationMinutes * 60 * 1000)
: null
await db
.update(passwordSecurity)
.set({
failedLoginAttempts: nextAttempts,
lockedUntil,
updatedAt: new Date(),
})
.where(eq(passwordSecurity.userId, userId))
return { locked: shouldLock, lockedUntil }
}
/**
* Reset failed login attempts on successful login.
*/
const resetFailedLogin = async (
db: typeof import("@/shared/db")["db"],
passwordSecurity: typeof import("@/shared/db/schema")["passwordSecurity"],
userId: string
): Promise<void> => {
const current = await getOrCreatePasswordSecurity(db, passwordSecurity, userId)
if (current.failedLoginAttempts === 0 && !current.lockedUntil) return
await db
.update(passwordSecurity)
.set({
failedLoginAttempts: 0,
lockedUntil: null,
updatedAt: new Date(),
})
.where(eq(passwordSecurity.userId, userId))
}
import { normalizeBcryptHash } from "@/shared/lib/bcrypt-utils"
import { resolveClientIp } from "@/shared/lib/http-utils"
import {
getOrCreatePasswordSecurity,
recordFailedLogin,
resetFailedLogin,
} from "@/shared/lib/password-security-service"
import { normalizeRole, resolvePrimaryRole } from "@/shared/lib/role-utils"
export const { handlers, auth, signIn, signOut } = NextAuth({
trustHost: true,