Fix-auth-hashing-update-worklog
This commit is contained in:
@@ -1,5 +1,20 @@
|
|||||||
# Work Log
|
# Work Log
|
||||||
|
|
||||||
|
## 2026-02-24
|
||||||
|
|
||||||
|
### 1. Credentials 登录与密码安全修复
|
||||||
|
* **密码哈希规范化**:
|
||||||
|
* 注册时统一生成标准 bcrypt 前缀(`$2a/$2b/$2y`),确保数据库内哈希格式一致。
|
||||||
|
* 登录时对非标准前缀(如 `$10$...`)自动补全为 `$2b$...` 再校验,避免格式不一致导致登录失败。
|
||||||
|
* **登录一致性修复**:
|
||||||
|
* 登录校验不再对密码做 `trim()`,避免前后空格导致的 bcrypt 比对失败。
|
||||||
|
* **调试排障(已回收)**:
|
||||||
|
* 临时加入安全日志定位失败原因(不输出明文),定位后已移除。
|
||||||
|
|
||||||
|
### 2. 验证
|
||||||
|
* `npm run typecheck`:通过
|
||||||
|
* `npm run lint`:存在既有告警(未新增)
|
||||||
|
|
||||||
## 2026-01-15
|
## 2026-01-15
|
||||||
|
|
||||||
### 1. Schedule Module Optimization
|
### 1. Schedule Module Optimization
|
||||||
|
|||||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -36,6 +36,7 @@
|
|||||||
"@tiptap/pm": "^3.15.3",
|
"@tiptap/pm": "^3.15.3",
|
||||||
"@tiptap/react": "^3.15.3",
|
"@tiptap/react": "^3.15.3",
|
||||||
"@tiptap/starter-kit": "^3.15.3",
|
"@tiptap/starter-kit": "^3.15.3",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
"@faker-js/faker": "^10.1.0",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -5397,6 +5399,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcryptjs": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-array": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
@@ -6461,6 +6470,12 @@
|
|||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/bignumber.js": {
|
"node_modules/bignumber.js": {
|
||||||
"version": "9.3.1",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"@tiptap/starter-kit": "^3.15.3",
|
"@tiptap/starter-kit": "^3.15.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
"@faker-js/faker": "^10.1.0",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Metadata } from "next"
|
import { Metadata } from "next"
|
||||||
|
import { hash } from "bcryptjs"
|
||||||
import { createId } from "@paralleldrive/cuid2"
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
|
|
||||||
@@ -10,6 +11,12 @@ export const metadata: Metadata = {
|
|||||||
description: "Create an account",
|
description: "Create an account",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeBcryptHash = (value: string) => {
|
||||||
|
if (value.startsWith("$2")) return value
|
||||||
|
if (value.startsWith("$")) return `$2b${value}`
|
||||||
|
return `$2b$${value}`
|
||||||
|
}
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
async function registerAction(formData: FormData): Promise<ActionState> {
|
async function registerAction(formData: FormData): Promise<ActionState> {
|
||||||
"use server"
|
"use server"
|
||||||
@@ -37,11 +44,12 @@ export default function RegisterPage() {
|
|||||||
})
|
})
|
||||||
if (existing) return { success: false, message: "该邮箱已注册" }
|
if (existing) return { success: false, message: "该邮箱已注册" }
|
||||||
|
|
||||||
|
const hashedPassword = normalizeBcryptHash(await hash(password, 10))
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
name: name.length ? name : null,
|
name: name.length ? name : null,
|
||||||
email,
|
email,
|
||||||
password,
|
password: hashedPassword,
|
||||||
role: "student",
|
role: "student",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
31
src/auth.ts
31
src/auth.ts
@@ -1,6 +1,19 @@
|
|||||||
|
import { compare, hash } from "bcryptjs"
|
||||||
import NextAuth from "next-auth"
|
import NextAuth from "next-auth"
|
||||||
import Credentials from "next-auth/providers/credentials"
|
import Credentials from "next-auth/providers/credentials"
|
||||||
|
|
||||||
|
const normalizeRole = (value: unknown) => {
|
||||||
|
const role = String(value ?? "").trim().toLowerCase()
|
||||||
|
if (role === "admin" || role === "student" || role === "teacher" || role === "parent") return role
|
||||||
|
return "student"
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeBcryptHash = (value: string) => {
|
||||||
|
if (value.startsWith("$2")) return value
|
||||||
|
if (value.startsWith("$")) return `$2b${value}`
|
||||||
|
return `$2b$${value}`
|
||||||
|
}
|
||||||
|
|
||||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||||
trustHost: true,
|
trustHost: true,
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
@@ -29,17 +42,17 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
||||||
const storedPassword = user.password ?? null
|
const storedPassword = user.password ?? null
|
||||||
if (storedPassword) {
|
if (!storedPassword) return null
|
||||||
if (storedPassword !== password) return null
|
const normalizedPassword = normalizeBcryptHash(storedPassword)
|
||||||
} else if (process.env.NODE_ENV === "production") {
|
if (!normalizedPassword.startsWith("$2")) return null
|
||||||
return null
|
const ok = await compare(password, normalizedPassword)
|
||||||
}
|
if (!ok) return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name ?? undefined,
|
name: user.name ?? undefined,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: (user.role ?? "student") as string,
|
role: normalizeRole(user.role),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -48,7 +61,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
jwt: async ({ token, user }) => {
|
jwt: async ({ token, user }) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.id = (user as { id: string }).id
|
token.id = (user as { id: string }).id
|
||||||
token.role = (user as { role?: string }).role ?? "student"
|
token.role = normalizeRole((user as { role?: string }).role)
|
||||||
token.name = (user as { name?: string }).name
|
token.name = (user as { name?: string }).name
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +79,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (fresh) {
|
if (fresh) {
|
||||||
token.role = fresh.role ?? token.role ?? "student"
|
token.role = normalizeRole(fresh.role ?? token.role)
|
||||||
token.name = fresh.name ?? token.name
|
token.name = fresh.name ?? token.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +89,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
session: async ({ session, token }) => {
|
session: async ({ session, token }) => {
|
||||||
if (session.user) {
|
if (session.user) {
|
||||||
session.user.id = String(token.id ?? "")
|
session.user.id = String(token.id ?? "")
|
||||||
session.user.role = String(token.role ?? "student")
|
session.user.role = normalizeRole(token.role)
|
||||||
if (typeof token.name === "string") {
|
if (typeof token.name === "string") {
|
||||||
session.user.name = token.name
|
session.user.name = token.name
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user