Files
esengine/packages/framework/database/src/UserRepository.ts

336 lines
9.3 KiB
TypeScript
Raw Normal View History

/**
* @zh
* @en User repository
*
* @zh
* @en Provides common user management methods including registration, login, role management
*/
import type { IMongoConnection } from '@esengine/database-drivers'
import { Repository } from './Repository.js'
import { hashPassword, verifyPassword } from './password.js'
import type { UserEntity } from './types.js'
/**
* @zh
* @en Create user parameters
*/
export interface CreateUserParams {
/**
* @zh
* @en Username
*/
username: string
/**
* @zh
* @en Plain text password
*/
password: string
/**
* @zh
* @en Email
*/
email?: string
/**
* @zh
* @en Role list
*/
roles?: string[]
/**
* @zh
* @en Additional metadata
*/
metadata?: Record<string, unknown>
}
/**
* @zh
* @en User info (without password)
*/
export type SafeUser = Omit<UserEntity, 'passwordHash'>
/**
* @zh
* @en User repository
*
* @example
* ```typescript
* const mongo = createMongoConnection({ uri: '...', database: 'game' })
* await mongo.connect()
*
* const userRepo = new UserRepository(mongo)
*
* // 注册用户
* const user = await userRepo.register({
* username: 'player1',
* password: 'securePassword123',
* email: 'player1@example.com',
* })
*
* // 验证登录
* const result = await userRepo.authenticate('player1', 'securePassword123')
* if (result) {
* console.log('登录成功:', result.username)
* }
* ```
*/
export class UserRepository extends Repository<UserEntity> {
constructor(connection: IMongoConnection, collectionName = 'users') {
super(connection, collectionName, true)
}
// =========================================================================
// 查询 | Query
// =========================================================================
/**
* @zh
* @en Find user by username
*/
async findByUsername(username: string): Promise<UserEntity | null> {
return this.findOne({ where: { username } })
}
/**
* @zh
* @en Find user by email
*/
async findByEmail(email: string): Promise<UserEntity | null> {
return this.findOne({ where: { email } })
}
/**
* @zh
* @en Check if username exists
*/
async usernameExists(username: string): Promise<boolean> {
return this.exists({ where: { username } })
}
/**
* @zh
* @en Check if email exists
*/
async emailExists(email: string): Promise<boolean> {
return this.exists({ where: { email } })
}
// =========================================================================
// 注册与认证 | Registration & Authentication
// =========================================================================
/**
* @zh
* @en Register new user
*
* @param params - @zh @en Create user parameters
* @returns @zh @en Created user (without password hash)
* @throws @zh @en If username already exists
*/
async register(params: CreateUserParams): Promise<SafeUser> {
const { username, password, email, roles, metadata } = params
if (await this.usernameExists(username)) {
throw new Error('Username already exists')
}
if (email && (await this.emailExists(email))) {
throw new Error('Email already exists')
}
const passwordHash = await hashPassword(password)
const user = await this.create({
username,
passwordHash,
email,
roles: roles ?? ['user'],
isActive: true,
metadata
})
return this.toSafeUser(user)
}
/**
* @zh
* @en Authenticate user login
*
* @param username - @zh @en Username
* @param password - @zh @en Plain text password
* @returns @zh null @en Returns user info on success, null on failure
*/
async authenticate(username: string, password: string): Promise<SafeUser | null> {
const user = await this.findByUsername(username)
if (!user || !user.isActive) {
return null
}
const isValid = await verifyPassword(password, user.passwordHash)
if (!isValid) {
return null
}
await this.update(user.id, { lastLoginAt: new Date() })
return this.toSafeUser(user)
}
// =========================================================================
// 密码管理 | Password Management
// =========================================================================
/**
* @zh
* @en Change password
*
* @param userId - @zh ID @en User ID
* @param oldPassword - @zh @en Old password
* @param newPassword - @zh @en New password
* @returns @zh @en Whether change was successful
*/
async changePassword(
userId: string,
oldPassword: string,
newPassword: string
): Promise<boolean> {
const user = await this.findById(userId)
if (!user) {
return false
}
const isValid = await verifyPassword(oldPassword, user.passwordHash)
if (!isValid) {
return false
}
const newHash = await hashPassword(newPassword)
const result = await this.update(userId, { passwordHash: newHash })
return result !== null
}
/**
* @zh
* @en Reset password (admin operation)
*
* @param userId - @zh ID @en User ID
* @param newPassword - @zh @en New password
*/
async resetPassword(userId: string, newPassword: string): Promise<boolean> {
const user = await this.findById(userId)
if (!user) {
return false
}
const newHash = await hashPassword(newPassword)
const result = await this.update(userId, { passwordHash: newHash })
return result !== null
}
// =========================================================================
// 角色管理 | Role Management
// =========================================================================
/**
* @zh
* @en Add role to user
*/
async addRole(userId: string, role: string): Promise<boolean> {
const user = await this.findById(userId)
if (!user) {
return false
}
const roles = user.roles ?? []
if (!roles.includes(role)) {
roles.push(role)
await this.update(userId, { roles })
}
return true
}
/**
* @zh
* @en Remove role from user
*/
async removeRole(userId: string, role: string): Promise<boolean> {
const user = await this.findById(userId)
if (!user) {
return false
}
const roles = (user.roles ?? []).filter(r => r !== role)
await this.update(userId, { roles })
return true
}
/**
* @zh
* @en Check if user has role
*/
async hasRole(userId: string, role: string): Promise<boolean> {
const user = await this.findById(userId)
return user?.roles?.includes(role) ?? false
}
/**
* @zh
* @en Check if user has any of the roles
*/
async hasAnyRole(userId: string, roles: string[]): Promise<boolean> {
const user = await this.findById(userId)
if (!user?.roles) return false
return roles.some(role => user.roles.includes(role))
}
// =========================================================================
// 状态管理 | Status Management
// =========================================================================
/**
* @zh
* @en Deactivate user
*/
async deactivate(userId: string): Promise<boolean> {
const result = await this.update(userId, { isActive: false })
return result !== null
}
/**
* @zh
* @en Activate user
*/
async activate(userId: string): Promise<boolean> {
const result = await this.update(userId, { isActive: true })
return result !== null
}
// =========================================================================
// 内部方法 | Internal Methods
// =========================================================================
/**
* @zh
* @en Remove password hash
*/
private toSafeUser(user: UserEntity): SafeUser {
const { passwordHash, ...safeUser } = user
return safeUser
}
}
/**
* @zh
* @en Create user repository
*/
export function createUserRepository(
connection: IMongoConnection,
collectionName = 'users'
): UserRepository {
return new UserRepository(connection, collectionName)
}