Files
BYW-VoiceHub/server/services/userService.ts
T

90 lines
2.7 KiB
TypeScript

import bcrypt from 'bcryptjs'
import { db } from '~/drizzle/db'
import { passwordAuditLogs, users } from '~/drizzle/schema'
import { and, eq, sql } from 'drizzle-orm'
import { getBeijingTime } from '~/utils/timeUtils'
import { createApiError } from '~~/server/utils/apiError'
interface PasswordAuditContext {
action: string
ipAddress: string
userAgent: string | null
actorId?: number | null
}
interface UpdateUserPasswordOptions {
forceReset?: boolean
expectedTokenVersion?: number
auditContext?: PasswordAuditContext
}
/**
* 更新用户密码
* 包含:加密密码、更新密码修改时间
* @param userId 用户ID
* @param newPassword 新密码
* @param options 密码更新选项,可用 tokenVersion 做并发条件更新
*/
export async function updateUserPassword(
userId: number,
newPassword: string,
options: UpdateUserPasswordOptions = {}
): Promise<{ passwordChangedAt: Date; tokenVersion: number }> {
// 1. 加密新密码
const hashedNewPassword = await bcrypt.hash(newPassword, 12)
// 2. 更新数据库
const passwordChangedAt = getBeijingTime()
const updated = await db.transaction(async (tx) => {
const result = await tx
.update(users)
.set({
password: hashedNewPassword,
passwordChangedAt,
forcePasswordChange: options.forceReset ?? false,
tokenVersion: sql`${users.tokenVersion} + 1`
})
.where(
options.expectedTokenVersion === undefined
? eq(users.id, userId)
: and(eq(users.id, userId), eq(users.tokenVersion, options.expectedTokenVersion))
)
.returning({
passwordChangedAt: users.passwordChangedAt,
tokenVersion: users.tokenVersion
})
const updatedUser = result[0]
if (!updatedUser) {
if (options.expectedTokenVersion !== undefined) {
throw createApiError(409, 'AUTH_PASSWORD_STATE_CHANGED', '密码状态已变化,请重新验证后再试')
}
throw createApiError(404, 'USER_NOT_FOUND', '用户不存在')
}
return updatedUser
})
// 3. 审计日志在事务外写入,失败不影响密码修改主流程
if (options.auditContext) {
try {
await db.insert(passwordAuditLogs).values({
userId,
actorId: options.auditContext.actorId ?? null,
action: options.auditContext.action,
success: true,
ipAddress: options.auditContext.ipAddress,
userAgent: options.auditContext.userAgent,
failureReason: null
})
} catch (error) {
console.error('[PasswordAudit] 写入密码审计日志失败:', error)
}
}
return {
passwordChangedAt: updated.passwordChangedAt || passwordChangedAt,
tokenVersion: updated.tokenVersion
}
}