用户资料/重置密码/用户管理

This commit is contained in:
Yeomai committed 2026-10-18 14:29:49 +08:00
1 parent 5c1f640a2b
commit 813407de30
14 files changed
+591 -2

No files matched your search

@@ -0,0 +1,29 @@
import { requireAdminAuth } from '../../../../utils/access'
import { createApiError } from '../../../../utils/api-error'
import { clearUserPassword, findUserById } from '../../../../utils/db'
import { logActivity } from '../../../../utils/activity-log'
export default defineEventHandler(async (event) => {
const operator = await requireAdminAuth(event)
const id = Number(getRouterParam(event, 'id'))
const target = Number.isInteger(id) ? findUserById(id) : null
if (!target) {
createApiError(event, 'INVALID_REQUEST', '用户不存在', 404)
}
if (target.id === operator.id) {
createApiError(event, 'INVALID_REQUEST', '不能清除自己的密码', 400)
}
clearUserPassword(target.id)
logActivity(event, {
action: 'settings',
key: `users/${target.id}/clear-password`,
originalName: target.username,
userId: operator.id,
source: 'admin'
})
return { success: true }
})
+7
View File
@@ -0,0 +1,7 @@
import { requireAdminAuth } from '../../../utils/access'
import { listAdminUsers } from '../../../utils/db'
export default defineEventHandler(async (event) => {
await requireAdminAuth(event)
return { users: listAdminUsers() }
})
+7 -1
View File
@@ -9,7 +9,7 @@ import {
rowToAuthUser,
verifyPassword
} from '../../utils/auth'
import { findUserByUsername } from '../../utils/db'
import { findUserByUsername, isPasswordResetRequired } from '../../utils/db'
import { getAdminSecret } from '../../utils/env'
import { verifyLoginVerification, type VerificationBody } from '../../utils/login-verification'
import { logActivity } from '../../utils/activity-log'
@@ -56,6 +56,12 @@ export default defineEventHandler(async (event) => {
}
const user = findUserByUsername(username)
// 管理员已清除密码:下次登录时直接进入设置新密码流程
if (user && isPasswordResetRequired(user.id)) {
return { success: false, requiresPasswordReset: true, username: user.username }
}
if (!user || !verifyPassword(password, user.password_hash)) {
logWarn('login failed: invalid credentials', { ip, username })
createApiError(event, 'UNAUTHORIZED', '用户名或密码错误', 401)
+34
View File
@@ -0,0 +1,34 @@
import { createApiError } from '../../utils/api-error'
import { createSession, hashPassword } from '../../utils/auth'
import {
findUserByUsername,
isPasswordResetRequired,
setUserPassword
} from '../../utils/db'
import { logInfo } from '../../utils/logger'
interface ResetBody {
username?: string
password?: string
}
export default defineEventHandler(async (event) => {
const body = await readBody<ResetBody>(event).catch((): ResetBody => ({}))
const username = body.username?.trim() ?? ''
const password = body.password ?? ''
if (!username || password.length < 6) {
createApiError(event, 'INVALID_REQUEST', '请输入用户名和至少 6 位的新密码', 400)
}
const user = findUserByUsername(username)
if (!user || !isPasswordResetRequired(user.id)) {
createApiError(event, 'UNAUTHORIZED', '该账号无需重置密码', 401)
}
setUserPassword(user.id, await hashPassword(password))
await createSession(event, user.id)
logInfo('password reset completed', { username })
return { success: true }
})
+23
View File
@@ -0,0 +1,23 @@
import { requireUserAuth } from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import { findUserByUsername, updateUsername } from '../../utils/db'
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
const body = await readBody<{ username?: unknown }>(event).catch(
(): { username?: unknown } => ({})
)
const username = typeof body.username === 'string' ? body.username.trim() : ''
if (!/^[a-zA-Z0-9_.-]{2,32}$/.test(username)) {
createApiError(event, 'INVALID_REQUEST', '用户名需为 2–32 位字母、数字、下划线、点或短横线', 400)
}
const existing = findUserByUsername(username)
if (existing && existing.id !== user.id) {
createApiError(event, 'CONFLICT', '用户名已被占用', 409)
}
updateUsername(user.id, username)
return { success: true, user: { ...user, username } }
})
+56
View File
@@ -439,6 +439,62 @@ export function updateUserPassword(userId: number, passwordHash: string): void {
`).run(passwordHash, userId)
}
/** 懒迁移:users 表新增「需重置密码」标记 */
function ensurePasswordResetColumn(): void {
const columns = getDb().prepare('PRAGMA table_info(users)').all() as Array<{
name: string
}>
if (!columns.some(column => column.name === 'password_reset')) {
getDb().exec(`
ALTER TABLE users ADD COLUMN password_reset INTEGER NOT NULL DEFAULT 0
`)
}
}
export interface AdminUserRow {
id: number
username: string
role: UserRole
created_at: string
password_reset: number
}
/** 管理员用户列表 */
export function listAdminUsers(): AdminUserRow[] {
ensurePasswordResetColumn()
return getDb().prepare(`
SELECT id, username, role, created_at, password_reset
FROM users ORDER BY id ASC
`).all() as unknown as AdminUserRow[]
}
/** 清空密码并标记需要重置 */
export function clearUserPassword(userId: number): void {
ensurePasswordResetColumn()
getDb().prepare(`
UPDATE users SET password_hash = '', password_reset = 1 WHERE id = ?
`).run(userId)
}
/** 设置新密码并解除重置标记 */
export function setUserPassword(userId: number, passwordHash: string): void {
ensurePasswordResetColumn()
getDb().prepare(`
UPDATE users SET password_hash = ?, password_reset = 0 WHERE id = ?
`).run(passwordHash, userId)
}
export function updateUsername(userId: number, username: string): void {
getDb().prepare('UPDATE users SET username = ? WHERE id = ?').run(username, userId)
}
export function isPasswordResetRequired(userId: number): boolean {
ensurePasswordResetColumn()
const row = getDb().prepare('SELECT password_reset FROM users WHERE id = ?')
.get(userId) as { password_reset?: number } | undefined
return Number(row?.password_reset ?? 0) === 1
}
export function ensureUserApiToken(userId: number): string {
const existing = getUserApiToken(userId)
if (existing) return existing