feat: 实现完整的用户认证系统 #2
61
README.md
61
README.md
@@ -270,59 +270,30 @@ docs/ # 项目文档
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 日志系统
|
||||
### 🔐 用户认证系统
|
||||
|
||||
项目集成了完整的日志系统,基于 Pino 高性能日志库:
|
||||
完整的用户认证解决方案,支持多种登录方式和安全特性:
|
||||
|
||||
**特性:**
|
||||
- 🚀 高性能日志记录
|
||||
- 🔒 自动敏感信息过滤
|
||||
- 🎯 多级别日志控制
|
||||
- 🔍 请求上下文绑定
|
||||
- 📊 结构化日志输出
|
||||
- 用户名/邮箱/手机号登录
|
||||
- GitHub OAuth 第三方登录
|
||||
- 密码重置和修改功能
|
||||
- bcrypt 密码加密
|
||||
- 基于角色的权限控制
|
||||
|
||||
**使用示例:**
|
||||
**详细文档**: [用户认证系统文档](./docs/systems/user-auth/README.md)
|
||||
|
||||
```typescript
|
||||
import { AppLoggerService } from './core/utils/logger/logger.service';
|
||||
### 📊 日志系统
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private readonly logger: AppLoggerService) {}
|
||||
基于 Pino 的高性能日志系统,提供结构化日志记录:
|
||||
|
||||
async createUser(userData: CreateUserDto) {
|
||||
this.logger.info('开始创建用户', {
|
||||
operation: 'createUser',
|
||||
email: userData.email,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
- 高性能日志记录
|
||||
- 自动敏感信息过滤
|
||||
- 多级别日志控制
|
||||
- 请求上下文绑定
|
||||
|
||||
try {
|
||||
const user = await this.userRepository.save(userData);
|
||||
**详细文档**: [日志系统文档](./docs/systems/logger/README.md)
|
||||
|
||||
this.logger.info('用户创建成功', {
|
||||
operation: 'createUser',
|
||||
userId: user.id,
|
||||
email: userData.email
|
||||
});
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.logger.error('用户创建失败', {
|
||||
operation: 'createUser',
|
||||
email: userData.email,
|
||||
error: error.message
|
||||
}, error.stack);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
详细使用方法请参考:[后端开发规范指南 - 日志系统使用指南](./docs/backend_development_guide.md#四日志系统使用指南)
|
||||
|
||||
**💡 提示:使用 [AI 辅助开发指南](./docs/AI辅助开发规范指南.md) 可以让 AI 帮你自动生成符合规范的日志代码!**
|
||||
**💡 提示:使用 [AI 辅助开发指南](./docs/AI辅助开发规范指南.md) 可以让 AI 帮你自动生成符合规范的代码!**
|
||||
|
||||
## 下一步
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
*
|
||||
* @author 开发者姓名
|
||||
* @version 1.0.0
|
||||
* @since 2024-12-13
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
```
|
||||
|
||||
@@ -595,7 +595,7 @@ async createPlayer(email: string, nickname: string): Promise<Player> {
|
||||
*
|
||||
* @author 开发团队
|
||||
* @version 1.0.0
|
||||
* @since 2024-12-13
|
||||
* @since 2025-12-13
|
||||
*/
|
||||
@Injectable()
|
||||
export class PlazaService {
|
||||
|
||||
80
docs/systems/logger/README.md
Normal file
80
docs/systems/logger/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 日志系统
|
||||
|
||||
## 概述
|
||||
|
||||
项目集成了完整的日志系统,基于 Pino 高性能日志库,提供结构化日志记录、自动敏感信息过滤和多级别日志控制。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🚀 高性能日志记录
|
||||
- 🔒 自动敏感信息过滤
|
||||
- 🎯 多级别日志控制
|
||||
- 🔍 请求上下文绑定
|
||||
- 📊 结构化日志输出
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```typescript
|
||||
import { AppLoggerService } from './core/utils/logger/logger.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private readonly logger: AppLoggerService) {}
|
||||
|
||||
async createUser(userData: CreateUserDto) {
|
||||
this.logger.info('开始创建用户', {
|
||||
operation: 'createUser',
|
||||
email: userData.email,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
const user = await this.userRepository.save(userData);
|
||||
|
||||
this.logger.info('用户创建成功', {
|
||||
operation: 'createUser',
|
||||
userId: user.id,
|
||||
email: userData.email
|
||||
});
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.logger.error('用户创建失败', {
|
||||
operation: 'createUser',
|
||||
email: userData.email,
|
||||
error: error.message
|
||||
}, error.stack);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 日志级别
|
||||
|
||||
- `error`: 错误信息
|
||||
- `warn`: 警告信息
|
||||
- `info`: 一般信息
|
||||
- `debug`: 调试信息
|
||||
|
||||
## 配置
|
||||
|
||||
日志配置位于 `src/core/utils/logger/logger.config.ts`,支持:
|
||||
|
||||
- 日志级别设置
|
||||
- 输出格式配置
|
||||
- 敏感信息过滤规则
|
||||
- 文件输出配置
|
||||
|
||||
## 敏感信息过滤
|
||||
|
||||
系统自动过滤以下敏感信息:
|
||||
- 密码字段
|
||||
- 令牌信息
|
||||
- 个人身份信息
|
||||
- 支付相关信息
|
||||
|
||||
详细使用方法请参考:[后端开发规范指南 - 日志系统使用指南](../../backend_development_guide.md#四日志系统使用指南)
|
||||
334
docs/systems/user-auth/README.md
Normal file
334
docs/systems/user-auth/README.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# 用户认证系统
|
||||
|
||||
## 概述
|
||||
|
||||
用户认证系统提供完整的用户注册、登录、密码管理功能,支持传统用户名密码登录和第三方OAuth登录。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🔐 多种登录方式:用户名/邮箱/手机号登录
|
||||
- 📝 用户注册和信息管理
|
||||
- 🐙 GitHub OAuth 第三方登录
|
||||
- 🔄 密码重置和修改
|
||||
- 🛡️ bcrypt 密码加密
|
||||
- 🎯 基于角色的权限控制
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 分层结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── business/login/ # 业务逻辑层
|
||||
│ ├── login.controller.ts # HTTP 控制器
|
||||
│ ├── login.service.ts # 业务服务
|
||||
│ ├── login.dto.ts # 数据传输对象
|
||||
│ ├── login.service.spec.ts # 业务服务测试
|
||||
│ └── login.module.ts # 业务模块
|
||||
├── core/
|
||||
│ ├── login_core/ # 核心功能层
|
||||
│ │ ├── login_core.service.ts # 核心认证逻辑
|
||||
│ │ ├── login_core.service.spec.ts # 核心服务测试
|
||||
│ │ └── login_core.module.ts # 核心模块
|
||||
│ └── db/users/ # 数据访问层
|
||||
│ ├── users.entity.ts # 用户实体
|
||||
│ ├── users.service.ts # 用户数据服务
|
||||
│ └── users.dto.ts # 用户 DTO
|
||||
```
|
||||
|
||||
### 职责分离
|
||||
|
||||
#### 1. 业务逻辑层 (Business Layer)
|
||||
- **位置**: `src/business/login/`
|
||||
- **职责**:
|
||||
- 处理HTTP请求和响应
|
||||
- 数据格式化和验证
|
||||
- 业务流程控制
|
||||
- 错误处理和日志记录
|
||||
|
||||
#### 2. 核心功能层 (Core Layer)
|
||||
- **位置**: `src/core/login_core/`
|
||||
- **职责**:
|
||||
- 认证核心算法实现
|
||||
- 密码加密和验证
|
||||
- 用户查找和匹配
|
||||
- 令牌生成和验证
|
||||
|
||||
#### 3. 数据访问层 (Data Access Layer)
|
||||
- **位置**: `src/core/db/users/`
|
||||
- **职责**:
|
||||
- 数据库操作封装
|
||||
- 实体关系映射
|
||||
- 数据完整性保证
|
||||
- 查询优化
|
||||
|
||||
## API 接口
|
||||
|
||||
### 用户注册
|
||||
|
||||
```bash
|
||||
POST /auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "+8613800138000"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "1",
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "+8613800138000",
|
||||
"avatar_url": null,
|
||||
"role": 1,
|
||||
"created_at": "2025-12-17T10:00:00.000Z"
|
||||
},
|
||||
"access_token": "eyJ1c2VySWQiOiIxIiwidXNlcm5hbWUiOiJ0ZXN0dXNlciJ9...",
|
||||
"is_new_user": true,
|
||||
"message": "注册成功"
|
||||
},
|
||||
"message": "注册成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 用户登录
|
||||
|
||||
```bash
|
||||
POST /auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "testuser", # 支持用户名/邮箱/手机号
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
### GitHub OAuth登录
|
||||
|
||||
```bash
|
||||
POST /auth/github
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"github_id": "12345678",
|
||||
"username": "githubuser",
|
||||
"nickname": "GitHub用户",
|
||||
"email": "github@example.com",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12345678"
|
||||
}
|
||||
```
|
||||
|
||||
### 密码重置
|
||||
|
||||
```bash
|
||||
# 1. 发送验证码
|
||||
POST /auth/forgot-password
|
||||
{
|
||||
"identifier": "test@example.com"
|
||||
}
|
||||
|
||||
# 2. 重置密码
|
||||
POST /auth/reset-password
|
||||
{
|
||||
"identifier": "test@example.com",
|
||||
"verification_code": "123456",
|
||||
"new_password": "newpassword123"
|
||||
}
|
||||
```
|
||||
|
||||
### 修改密码
|
||||
|
||||
```bash
|
||||
PUT /auth/change-password
|
||||
{
|
||||
"user_id": "1",
|
||||
"old_password": "password123",
|
||||
"new_password": "newpassword123"
|
||||
}
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 用户实体 (Users Entity)
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: bigint, // 主键ID
|
||||
username: string, // 用户名(唯一)
|
||||
email?: string, // 邮箱(唯一,可选)
|
||||
phone?: string, // 手机号(唯一,可选)
|
||||
password_hash?: string, // 密码哈希(OAuth用户为空)
|
||||
nickname: string, // 显示昵称
|
||||
github_id?: string, // GitHub ID(唯一,可选)
|
||||
avatar_url?: string, // 头像URL
|
||||
role: number, // 用户角色(1-普通,9-管理员)
|
||||
created_at: Date, // 创建时间
|
||||
updated_at: Date // 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库设计特点
|
||||
|
||||
1. **唯一性约束**: username, email, phone, github_id
|
||||
2. **索引优化**: 主键、唯一索引、角色索引
|
||||
3. **字符集支持**: utf8mb4,支持emoji
|
||||
4. **数据类型**: BIGINT主键,VARCHAR字段,DATETIME时间戳
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 1. 密码安全
|
||||
- **加密算法**: bcrypt (saltRounds=12)
|
||||
- **强度验证**: 最少8位,包含字母和数字
|
||||
- **存储安全**: 只存储哈希值,不存储明文
|
||||
|
||||
### 2. 数据验证
|
||||
- **输入验证**: class-validator装饰器
|
||||
- **SQL注入防护**: TypeORM参数化查询
|
||||
- **XSS防护**: 数据转义和验证
|
||||
|
||||
### 3. 访问控制
|
||||
- **令牌机制**: 基于用户信息的访问令牌
|
||||
- **角色权限**: 基于角色的访问控制(RBAC)
|
||||
- **会话管理**: 令牌生成和验证
|
||||
|
||||
### 4. 错误处理
|
||||
- **统一异常**: NestJS异常过滤器
|
||||
- **日志记录**: 操作日志和错误日志
|
||||
- **信息脱敏**: 敏感信息自动脱敏
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
### 单元测试
|
||||
- 核心服务测试:`src/core/login_core/login_core.service.spec.ts`
|
||||
- 业务服务测试:`src/business/login/login.service.spec.ts`
|
||||
|
||||
### 集成测试
|
||||
- 端到端测试:`test/business/login.e2e-spec.ts`
|
||||
|
||||
### 测试用例
|
||||
- 用户注册和登录流程
|
||||
- GitHub OAuth认证
|
||||
- 密码重置和修改
|
||||
- 数据验证和错误处理
|
||||
- 安全性测试
|
||||
|
||||
## 使用示例
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
```javascript
|
||||
// 用户注册
|
||||
const registerResponse = await fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com'
|
||||
})
|
||||
});
|
||||
|
||||
const registerData = await registerResponse.json();
|
||||
console.log(registerData);
|
||||
|
||||
// 用户登录
|
||||
const loginResponse = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifier: 'testuser',
|
||||
password: 'password123'
|
||||
})
|
||||
});
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
console.log(loginData);
|
||||
```
|
||||
|
||||
### curl 命令
|
||||
|
||||
```bash
|
||||
# 用户注册
|
||||
curl -X POST http://localhost:3000/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com"
|
||||
}'
|
||||
|
||||
# 用户登录
|
||||
curl -X POST http://localhost:3000/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"identifier": "testuser",
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误代码
|
||||
|
||||
- `LOGIN_FAILED`: 登录失败
|
||||
- `REGISTER_FAILED`: 注册失败
|
||||
- `GITHUB_OAUTH_FAILED`: GitHub登录失败
|
||||
- `SEND_CODE_FAILED`: 发送验证码失败
|
||||
- `RESET_PASSWORD_FAILED`: 密码重置失败
|
||||
- `CHANGE_PASSWORD_FAILED`: 密码修改失败
|
||||
|
||||
### 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误描述",
|
||||
"error_code": "ERROR_CODE"
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 计划中的功能
|
||||
|
||||
1. **JWT令牌管理**
|
||||
- 访问令牌和刷新令牌
|
||||
- 令牌黑名单机制
|
||||
- 自动刷新功能
|
||||
|
||||
2. **多因子认证**
|
||||
- 短信验证码
|
||||
- 邮箱验证码
|
||||
- TOTP应用支持
|
||||
|
||||
3. **社交登录扩展**
|
||||
- 微信登录
|
||||
- QQ登录
|
||||
- 微博登录
|
||||
|
||||
4. **安全增强**
|
||||
- 登录失败次数限制
|
||||
- IP白名单/黑名单
|
||||
- 设备指纹识别
|
||||
|
||||
5. **用户管理**
|
||||
- 用户状态管理(激活/禁用)
|
||||
- 用户角色权限细化
|
||||
- 用户行为日志记录
|
||||
@@ -30,6 +30,8 @@
|
||||
"@nestjs/schedule": "^4.1.2",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/websockets": "^10.4.20",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"mysql2": "^3.16.0",
|
||||
@@ -45,8 +47,10 @@
|
||||
"@nestjs/testing": "^10.4.20",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.26",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"supertest": "^7.1.4",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
ignoredBuiltDependencies:
|
||||
- '@nestjs/core'
|
||||
- bcrypt
|
||||
|
||||
@@ -5,6 +5,8 @@ import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { LoggerModule } from './core/utils/logger/logger.module';
|
||||
import { UsersModule } from './core/db/users/users.module';
|
||||
import { LoginCoreModule } from './core/login_core/login_core.module';
|
||||
import { LoginModule } from './business/login/login.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -24,6 +26,8 @@ import { UsersModule } from './core/db/users/users.module';
|
||||
synchronize: false,
|
||||
}),
|
||||
UsersModule,
|
||||
LoginCoreModule,
|
||||
LoginModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
136
src/business/login/login.controller.ts
Normal file
136
src/business/login/login.controller.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 登录控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理登录相关的HTTP请求和响应
|
||||
* - 提供RESTful API接口
|
||||
* - 数据验证和格式化
|
||||
*
|
||||
* API端点:
|
||||
* - POST /auth/login - 用户登录
|
||||
* - POST /auth/register - 用户注册
|
||||
* - POST /auth/github - GitHub OAuth登录
|
||||
* - POST /auth/forgot-password - 发送密码重置验证码
|
||||
* - POST /auth/reset-password - 重置密码
|
||||
* - PUT /auth/change-password - 修改密码
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Controller, Post, Put, Body, HttpCode, HttpStatus, ValidationPipe, UsePipes, Logger } from '@nestjs/common';
|
||||
import { LoginService, ApiResponse, LoginResponse } from './login.service';
|
||||
import { LoginDto, RegisterDto, GitHubOAuthDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto } from './login.dto';
|
||||
|
||||
@Controller('auth')
|
||||
export class LoginController {
|
||||
private readonly logger = new Logger(LoginController.name);
|
||||
|
||||
constructor(private readonly loginService: LoginService) {}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param loginDto 登录数据
|
||||
* @returns 登录结果
|
||||
*/
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async login(@Body() loginDto: LoginDto): Promise<ApiResponse<LoginResponse>> {
|
||||
return await this.loginService.login({
|
||||
identifier: loginDto.identifier,
|
||||
password: loginDto.password
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param registerDto 注册数据
|
||||
* @returns 注册结果
|
||||
*/
|
||||
@Post('register')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async register(@Body() registerDto: RegisterDto): Promise<ApiResponse<LoginResponse>> {
|
||||
return await this.loginService.register({
|
||||
username: registerDto.username,
|
||||
password: registerDto.password,
|
||||
nickname: registerDto.nickname,
|
||||
email: registerDto.email,
|
||||
phone: registerDto.phone
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录
|
||||
*
|
||||
* @param githubDto GitHub OAuth数据
|
||||
* @returns 登录结果
|
||||
*/
|
||||
@Post('github')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async githubOAuth(@Body() githubDto: GitHubOAuthDto): Promise<ApiResponse<LoginResponse>> {
|
||||
return await this.loginService.githubOAuth({
|
||||
github_id: githubDto.github_id,
|
||||
username: githubDto.username,
|
||||
nickname: githubDto.nickname,
|
||||
email: githubDto.email,
|
||||
avatar_url: githubDto.avatar_url
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送密码重置验证码
|
||||
*
|
||||
* @param forgotPasswordDto 忘记密码数据
|
||||
* @returns 发送结果
|
||||
*/
|
||||
@Post('forgot-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto): Promise<ApiResponse<{ verification_code?: string }>> {
|
||||
return await this.loginService.sendPasswordResetCode(forgotPasswordDto.identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param resetPasswordDto 重置密码数据
|
||||
* @returns 重置结果
|
||||
*/
|
||||
@Post('reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto): Promise<ApiResponse> {
|
||||
return await this.loginService.resetPassword({
|
||||
identifier: resetPasswordDto.identifier,
|
||||
verificationCode: resetPasswordDto.verification_code,
|
||||
newPassword: resetPasswordDto.new_password
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param changePasswordDto 修改密码数据
|
||||
* @returns 修改结果
|
||||
*/
|
||||
@Put('change-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async changePassword(@Body() changePasswordDto: ChangePasswordDto): Promise<ApiResponse> {
|
||||
// 实际应用中应从JWT令牌中获取用户ID
|
||||
// 这里为了演示,使用请求体中的用户ID
|
||||
const userId = BigInt(changePasswordDto.user_id);
|
||||
|
||||
return await this.loginService.changePassword(
|
||||
userId,
|
||||
changePasswordDto.old_password,
|
||||
changePasswordDto.new_password
|
||||
);
|
||||
}
|
||||
}
|
||||
206
src/business/login/login.dto.ts
Normal file
206
src/business/login/login.dto.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 登录业务数据传输对象
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义登录相关API的请求数据结构
|
||||
* - 提供数据验证规则和错误提示
|
||||
* - 确保API接口的数据格式一致性
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
IsPhoneNumber,
|
||||
IsNotEmpty,
|
||||
Length,
|
||||
IsOptional,
|
||||
Matches,
|
||||
IsNumberString
|
||||
} from 'class-validator';
|
||||
|
||||
/**
|
||||
* 登录请求DTO
|
||||
*/
|
||||
export class LoginDto {
|
||||
/**
|
||||
* 登录标识符
|
||||
* 支持用户名、邮箱或手机号登录
|
||||
*/
|
||||
@IsString({ message: '登录标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '登录标识符不能为空' })
|
||||
@Length(1, 100, { message: '登录标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@IsString({ message: '密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
@Length(1, 128, { message: '密码长度需在1-128字符之间' })
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册请求DTO
|
||||
*/
|
||||
export class RegisterDto {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@IsString({ message: '用户名必须是字符串' })
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
|
||||
@Matches(/^[a-zA-Z0-9_]+$/, { message: '用户名只能包含字母、数字和下划线' })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@IsString({ message: '密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
@Length(8, 128, { message: '密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '密码必须包含字母和数字' })
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@IsString({ message: '昵称必须是字符串' })
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* 邮箱(可选)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* 手机号(可选)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(null, { message: '手机号格式不正确' })
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录请求DTO
|
||||
*/
|
||||
export class GitHubOAuthDto {
|
||||
/**
|
||||
* GitHub用户ID
|
||||
*/
|
||||
@IsString({ message: 'GitHub ID必须是字符串' })
|
||||
@IsNotEmpty({ message: 'GitHub ID不能为空' })
|
||||
@Length(1, 100, { message: 'GitHub ID长度需在1-100字符之间' })
|
||||
github_id: string;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@IsString({ message: '用户名必须是字符串' })
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@IsString({ message: '昵称必须是字符串' })
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* 邮箱(可选)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* 头像URL(可选)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString({ message: '头像URL必须是字符串' })
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码请求DTO
|
||||
*/
|
||||
export class ForgotPasswordDto {
|
||||
/**
|
||||
* 邮箱或手机号
|
||||
*/
|
||||
@IsString({ message: '标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '邮箱或手机号不能为空' })
|
||||
@Length(1, 100, { message: '标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码请求DTO
|
||||
*/
|
||||
export class ResetPasswordDto {
|
||||
/**
|
||||
* 邮箱或手机号
|
||||
*/
|
||||
@IsString({ message: '标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '邮箱或手机号不能为空' })
|
||||
@Length(1, 100, { message: '标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@IsNotEmpty({ message: '验证码不能为空' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
verification_code: string;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@IsString({ message: '新密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '新密码不能为空' })
|
||||
@Length(8, 128, { message: '新密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码请求DTO
|
||||
*/
|
||||
export class ChangePasswordDto {
|
||||
/**
|
||||
* 用户ID
|
||||
* 实际应用中应从JWT令牌中获取,这里为了演示放在请求体中
|
||||
*/
|
||||
@IsNumberString({}, { message: '用户ID必须是数字字符串' })
|
||||
@IsNotEmpty({ message: '用户ID不能为空' })
|
||||
user_id: string;
|
||||
|
||||
/**
|
||||
* 旧密码
|
||||
*/
|
||||
@IsString({ message: '旧密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '旧密码不能为空' })
|
||||
@Length(1, 128, { message: '旧密码长度需在1-128字符之间' })
|
||||
old_password: string;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@IsString({ message: '新密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '新密码不能为空' })
|
||||
@Length(8, 128, { message: '新密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
|
||||
new_password: string;
|
||||
}
|
||||
25
src/business/login/login.module.ts
Normal file
25
src/business/login/login.module.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 登录业务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合登录相关的控制器、服务和依赖
|
||||
* - 提供完整的登录业务功能模块
|
||||
* - 可被其他模块导入使用
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LoginController } from './login.controller';
|
||||
import { LoginService } from './login.service';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [LoginCoreModule],
|
||||
controllers: [LoginController],
|
||||
providers: [LoginService],
|
||||
exports: [LoginService],
|
||||
})
|
||||
export class LoginModule {}
|
||||
174
src/business/login/login.service.spec.ts
Normal file
174
src/business/login/login.service.spec.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 登录业务服务测试
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LoginService } from './login.service';
|
||||
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
||||
|
||||
describe('LoginService', () => {
|
||||
let service: LoginService;
|
||||
let loginCoreService: jest.Mocked<LoginCoreService>;
|
||||
|
||||
const mockUser = {
|
||||
id: BigInt(1),
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
phone: '+8613800138000',
|
||||
password_hash: '$2b$12$hashedpassword',
|
||||
nickname: '测试用户',
|
||||
github_id: null as string | null,
|
||||
avatar_url: null as string | null,
|
||||
role: 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockLoginCoreService = {
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
githubOAuth: jest.fn(),
|
||||
sendPasswordResetCode: jest.fn(),
|
||||
resetPassword: jest.fn(),
|
||||
changePassword: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LoginService,
|
||||
{
|
||||
provide: LoginCoreService,
|
||||
useValue: mockLoginCoreService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LoginService>(LoginService);
|
||||
loginCoreService = module.get(LoginCoreService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should return success response for valid login', async () => {
|
||||
loginCoreService.login.mockResolvedValue({
|
||||
user: mockUser,
|
||||
isNewUser: false
|
||||
});
|
||||
|
||||
const result = await service.login({
|
||||
identifier: 'testuser',
|
||||
password: 'password123'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe('testuser');
|
||||
expect(result.data?.access_token).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return error response for failed login', async () => {
|
||||
loginCoreService.login.mockRejectedValue(new Error('登录失败'));
|
||||
|
||||
const result = await service.login({
|
||||
identifier: 'testuser',
|
||||
password: 'wrongpassword'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('登录失败');
|
||||
expect(result.error_code).toBe('LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should return success response for valid registration', async () => {
|
||||
loginCoreService.register.mockResolvedValue({
|
||||
user: mockUser,
|
||||
isNewUser: true
|
||||
});
|
||||
|
||||
const result = await service.register({
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe('testuser');
|
||||
expect(result.data?.is_new_user).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error response for failed registration', async () => {
|
||||
loginCoreService.register.mockRejectedValue(new Error('用户名已存在'));
|
||||
|
||||
const result = await service.register({
|
||||
username: 'existinguser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('用户名已存在');
|
||||
expect(result.error_code).toBe('REGISTER_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('githubOAuth', () => {
|
||||
it('should return success response for GitHub OAuth', async () => {
|
||||
loginCoreService.githubOAuth.mockResolvedValue({
|
||||
user: mockUser,
|
||||
isNewUser: true
|
||||
});
|
||||
|
||||
const result = await service.githubOAuth({
|
||||
github_id: 'github123',
|
||||
username: 'githubuser',
|
||||
nickname: 'GitHub用户'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.user.username).toBe('testuser');
|
||||
expect(result.data?.is_new_user).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPasswordResetCode', () => {
|
||||
it('should return success response with verification code', async () => {
|
||||
loginCoreService.sendPasswordResetCode.mockResolvedValue('123456');
|
||||
|
||||
const result = await service.sendPasswordResetCode('test@example.com');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.verification_code).toBe('123456');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should return success response for password reset', async () => {
|
||||
loginCoreService.resetPassword.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.resetPassword({
|
||||
identifier: 'test@example.com',
|
||||
verificationCode: '123456',
|
||||
newPassword: 'newpassword123'
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('密码重置成功');
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePassword', () => {
|
||||
it('should return success response for password change', async () => {
|
||||
loginCoreService.changePassword.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.changePassword(BigInt(1), 'oldpassword', 'newpassword123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('密码修改成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
328
src/business/login/login.service.ts
Normal file
328
src/business/login/login.service.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* 登录业务服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理登录相关的业务逻辑和流程控制
|
||||
* - 整合核心服务,提供完整的业务功能
|
||||
* - 处理业务规则、数据格式化和错误处理
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于业务流程和规则实现
|
||||
* - 调用核心服务完成具体功能
|
||||
* - 为控制器层提供业务接口
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult } from '../../core/login_core/login_core.service';
|
||||
import { Users } from '../../core/db/users/users.entity';
|
||||
|
||||
/**
|
||||
* 登录响应数据接口
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
/** 用户信息 */
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
avatar_url?: string;
|
||||
role: number;
|
||||
created_at: Date;
|
||||
};
|
||||
/** 访问令牌(实际应用中应生成JWT) */
|
||||
access_token: string;
|
||||
/** 刷新令牌 */
|
||||
refresh_token?: string;
|
||||
/** 是否为新用户 */
|
||||
is_new_user?: boolean;
|
||||
/** 消息 */
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用响应接口
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 响应数据 */
|
||||
data?: T;
|
||||
/** 消息 */
|
||||
message: string;
|
||||
/** 错误代码 */
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
private readonly logger = new Logger(LoginService.name);
|
||||
|
||||
constructor(
|
||||
private readonly loginCoreService: LoginCoreService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param loginRequest 登录请求
|
||||
* @returns 登录响应
|
||||
*/
|
||||
async login(loginRequest: LoginRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
try {
|
||||
this.logger.log(`用户登录尝试: ${loginRequest.identifier}`);
|
||||
|
||||
// 调用核心服务进行认证
|
||||
const authResult = await this.loginCoreService.login(loginRequest);
|
||||
|
||||
// 生成访问令牌(实际应用中应使用JWT)
|
||||
const accessToken = this.generateAccessToken(authResult.user);
|
||||
|
||||
// 格式化响应数据
|
||||
const response: LoginResponse = {
|
||||
user: this.formatUserInfo(authResult.user),
|
||||
access_token: accessToken,
|
||||
is_new_user: authResult.isNewUser,
|
||||
message: '登录成功'
|
||||
};
|
||||
|
||||
this.logger.log(`用户登录成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response,
|
||||
message: '登录成功'
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`用户登录失败: ${loginRequest.identifier}`, error instanceof Error ? error.stack : String(error));
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '登录失败',
|
||||
error_code: 'LOGIN_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param registerRequest 注册请求
|
||||
* @returns 注册响应
|
||||
*/
|
||||
async register(registerRequest: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
try {
|
||||
this.logger.log(`用户注册尝试: ${registerRequest.username}`);
|
||||
|
||||
// 调用核心服务进行注册
|
||||
const authResult = await this.loginCoreService.register(registerRequest);
|
||||
|
||||
// 生成访问令牌
|
||||
const accessToken = this.generateAccessToken(authResult.user);
|
||||
|
||||
// 格式化响应数据
|
||||
const response: LoginResponse = {
|
||||
user: this.formatUserInfo(authResult.user),
|
||||
access_token: accessToken,
|
||||
is_new_user: true,
|
||||
message: '注册成功'
|
||||
};
|
||||
|
||||
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response,
|
||||
message: '注册成功'
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`用户注册失败: ${registerRequest.username}`, error instanceof Error ? error.stack : String(error));
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '注册失败',
|
||||
error_code: 'REGISTER_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录
|
||||
*
|
||||
* @param oauthRequest OAuth请求
|
||||
* @returns 登录响应
|
||||
*/
|
||||
async githubOAuth(oauthRequest: GitHubOAuthRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
try {
|
||||
this.logger.log(`GitHub OAuth登录尝试: ${oauthRequest.github_id}`);
|
||||
|
||||
// 调用核心服务进行OAuth认证
|
||||
const authResult = await this.loginCoreService.githubOAuth(oauthRequest);
|
||||
|
||||
// 生成访问令牌
|
||||
const accessToken = this.generateAccessToken(authResult.user);
|
||||
|
||||
// 格式化响应数据
|
||||
const response: LoginResponse = {
|
||||
user: this.formatUserInfo(authResult.user),
|
||||
access_token: accessToken,
|
||||
is_new_user: authResult.isNewUser,
|
||||
message: authResult.isNewUser ? 'GitHub账户绑定成功' : 'GitHub登录成功'
|
||||
};
|
||||
|
||||
this.logger.log(`GitHub OAuth成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response,
|
||||
message: response.message
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`GitHub OAuth失败: ${oauthRequest.github_id}`, error instanceof Error ? error.stack : String(error));
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'GitHub登录失败',
|
||||
error_code: 'GITHUB_OAUTH_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送密码重置验证码
|
||||
*
|
||||
* @param identifier 邮箱或手机号
|
||||
* @returns 响应结果
|
||||
*/
|
||||
async sendPasswordResetCode(identifier: string): Promise<ApiResponse<{ verification_code?: string }>> {
|
||||
try {
|
||||
this.logger.log(`发送密码重置验证码: ${identifier}`);
|
||||
|
||||
// 调用核心服务发送验证码
|
||||
const verificationCode = await this.loginCoreService.sendPasswordResetCode(identifier);
|
||||
|
||||
this.logger.log(`密码重置验证码已发送: ${identifier}`);
|
||||
|
||||
// 实际应用中不应返回验证码,这里仅用于演示
|
||||
return {
|
||||
success: true,
|
||||
data: { verification_code: verificationCode },
|
||||
message: '验证码已发送,请查收'
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`发送密码重置验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error));
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '发送验证码失败',
|
||||
error_code: 'SEND_CODE_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param resetRequest 重置请求
|
||||
* @returns 响应结果
|
||||
*/
|
||||
async resetPassword(resetRequest: PasswordResetRequest): Promise<ApiResponse> {
|
||||
try {
|
||||
this.logger.log(`密码重置尝试: ${resetRequest.identifier}`);
|
||||
|
||||
// 调用核心服务重置密码
|
||||
await this.loginCoreService.resetPassword(resetRequest);
|
||||
|
||||
this.logger.log(`密码重置成功: ${resetRequest.identifier}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '密码重置成功'
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`密码重置失败: ${resetRequest.identifier}`, error instanceof Error ? error.stack : String(error));
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '密码重置失败',
|
||||
error_code: 'RESET_PASSWORD_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param oldPassword 旧密码
|
||||
* @param newPassword 新密码
|
||||
* @returns 响应结果
|
||||
*/
|
||||
async changePassword(userId: bigint, oldPassword: string, newPassword: string): Promise<ApiResponse> {
|
||||
try {
|
||||
this.logger.log(`修改密码尝试: 用户ID ${userId}`);
|
||||
|
||||
// 调用核心服务修改密码
|
||||
await this.loginCoreService.changePassword(userId, oldPassword, newPassword);
|
||||
|
||||
this.logger.log(`修改密码成功: 用户ID ${userId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '密码修改成功'
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`修改密码失败: 用户ID ${userId}`, error instanceof Error ? error.stack : String(error));
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '密码修改失败',
|
||||
error_code: 'CHANGE_PASSWORD_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化用户信息
|
||||
*
|
||||
* @param user 用户实体
|
||||
* @returns 格式化的用户信息
|
||||
*/
|
||||
private formatUserInfo(user: Users) {
|
||||
return {
|
||||
id: user.id.toString(), // 将bigint转换为字符串
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
avatar_url: user.avatar_url,
|
||||
role: user.role,
|
||||
created_at: user.created_at
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访问令牌
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @returns 访问令牌
|
||||
*/
|
||||
private generateAccessToken(user: Users): string {
|
||||
// 实际应用中应使用JWT库生成真正的JWT令牌
|
||||
// 这里仅用于演示,生成一个简单的令牌
|
||||
const payload = {
|
||||
userId: user.id.toString(),
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 简单的Base64编码(实际应用中应使用JWT)
|
||||
return Buffer.from(JSON.stringify(payload)).toString('base64');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,20 @@
|
||||
// src/user/dto/create-user.dto.ts
|
||||
/**
|
||||
* 用户数据传输对象模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户创建和更新的数据传输对象
|
||||
* - 提供完整的数据验证规则和错误提示
|
||||
* - 支持多种登录方式的数据格式验证
|
||||
*
|
||||
* 依赖模块:
|
||||
* - class-validator: 数据验证装饰器
|
||||
* - class-transformer: 数据转换工具
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
@@ -9,51 +25,194 @@ import {
|
||||
IsOptional,
|
||||
Length,
|
||||
IsNotEmpty
|
||||
} from 'class-validator'
|
||||
} from 'class-validator';
|
||||
|
||||
/**
|
||||
* 创建用户数据传输对象
|
||||
*
|
||||
* 职责:
|
||||
* - 定义用户创建时的数据结构和验证规则
|
||||
* - 确保输入数据的格式正确性和业务规则符合性
|
||||
* - 提供友好的错误提示信息
|
||||
*
|
||||
* 主要字段:
|
||||
* - username: 唯一用户名,用于登录识别
|
||||
* - email: 邮箱地址,用于通知和账户找回
|
||||
* - phone: 手机号码,支持全球格式
|
||||
* - password_hash: 密码哈希值,OAuth登录时可为空
|
||||
* - nickname: 显示昵称,在游戏中展示
|
||||
* - github_id: GitHub第三方登录标识
|
||||
* - avatar_url: 用户头像链接
|
||||
* - role: 用户角色,控制权限级别
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户注册接口的请求体验证
|
||||
* - 管理员创建用户的数据验证
|
||||
* - 第三方登录用户信息同步
|
||||
*
|
||||
* 验证规则:
|
||||
* - 必填字段:username, nickname
|
||||
* - 唯一性字段:username, email, phone, github_id
|
||||
* - 长度限制:username(1-50), nickname(1-50), github_id(1-100)
|
||||
* - 格式验证:email格式, phone国际格式
|
||||
* - 数值范围:role(1-9)
|
||||
*/
|
||||
export class CreateUserDto {
|
||||
// 用户名:必填、字符串、长度1-50
|
||||
/**
|
||||
* 用户名
|
||||
*
|
||||
* 业务规则:
|
||||
* - 必填字段,用于用户登录和唯一标识
|
||||
* - 长度限制:1-50个字符
|
||||
* - 全局唯一性:不允许重复
|
||||
* - 建议使用字母、数字、下划线组合
|
||||
*
|
||||
* 验证规则:
|
||||
* - 非空验证:确保用户名不为空
|
||||
* - 字符串类型验证
|
||||
* - 长度范围验证:1-50字符
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
|
||||
username: string;
|
||||
|
||||
// 邮箱:可选、合法邮箱格式
|
||||
/**
|
||||
* 邮箱地址
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,用于账户找回和通知
|
||||
* - 全局唯一性:不允许重复
|
||||
* - 支持标准邮箱格式验证
|
||||
* - OAuth登录时可能为空
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 邮箱格式验证:符合RFC标准
|
||||
* - 长度限制:最大100字符(数据库约束)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email?: string;
|
||||
|
||||
// 手机号:可选、合法手机号格式(支持全球号码)
|
||||
/**
|
||||
* 手机号码
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,用于账户找回和通知
|
||||
* - 全局唯一性:不允许重复
|
||||
* - 支持国际手机号格式
|
||||
* - 用于短信验证和双因子认证
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 国际手机号格式验证
|
||||
* - 长度限制:最大30字符(数据库约束)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(null, { message: '手机号格式不正确' })
|
||||
phone?: string;
|
||||
|
||||
// 密码哈希:可选(OAuth登录为空)
|
||||
/**
|
||||
* 密码哈希值
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,OAuth登录时为空
|
||||
* - 存储加密后的密码,不存储明文
|
||||
* - 用于传统用户名密码登录方式
|
||||
* - 应使用bcrypt等安全哈希算法
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 字符串类型验证
|
||||
* - 长度限制:最大255字符(数据库约束)
|
||||
*
|
||||
* 安全注意:
|
||||
* - 传输过程中应使用HTTPS
|
||||
* - 日志记录时会自动脱敏处理
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString({ message: '密码哈希必须是字符串' })
|
||||
password_hash?: string;
|
||||
|
||||
// 昵称:必填、字符串、长度1-50
|
||||
/**
|
||||
* 用户昵称
|
||||
*
|
||||
* 业务规则:
|
||||
* - 必填字段,用于游戏内显示
|
||||
* - 长度限制:1-50个字符
|
||||
* - 支持中文、英文、数字等字符
|
||||
* - 可以与用户名不同,更友好的显示名称
|
||||
*
|
||||
* 验证规则:
|
||||
* - 非空验证:确保昵称不为空
|
||||
* - 字符串类型验证
|
||||
* - 长度范围验证:1-50字符
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
|
||||
nickname: string;
|
||||
|
||||
// GitHub ID:可选、字符串、长度1-100
|
||||
/**
|
||||
* GitHub用户标识
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,用于GitHub OAuth登录
|
||||
* - 全局唯一性:不允许重复
|
||||
* - 存储GitHub用户的唯一标识符
|
||||
* - 用于关联GitHub账户信息
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 字符串类型验证
|
||||
* - 长度范围验证:1-100字符
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString({ message: 'GitHub ID必须是字符串' })
|
||||
@Length(1, 100, { message: 'GitHub ID长度需在1-100字符之间' })
|
||||
github_id?: string;
|
||||
|
||||
// 头像URL:可选、字符串
|
||||
/**
|
||||
* 用户头像链接
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,用于显示用户头像
|
||||
* - 支持GitHub头像或自定义头像
|
||||
* - 应为有效的HTTP/HTTPS链接
|
||||
* - 建议使用CDN加速访问
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 字符串类型验证
|
||||
* - 长度限制:最大255字符(数据库约束)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString({ message: '头像URL必须是字符串' })
|
||||
avatar_url?: string;
|
||||
|
||||
// 角色:可选、数字、1(普通)或9(管理员)
|
||||
/**
|
||||
* 用户角色
|
||||
*
|
||||
* 业务规则:
|
||||
* - 可选字段,默认为普通用户(1)
|
||||
* - 角色级别:1-普通用户,9-管理员
|
||||
* - 控制用户在系统中的权限范围
|
||||
* - 管理员具有系统管理权限
|
||||
*
|
||||
* 验证规则:
|
||||
* - 可选字段验证
|
||||
* - 整数类型验证
|
||||
* - 数值范围验证:1-9之间
|
||||
* - 默认值:1(普通用户)
|
||||
*
|
||||
* 权限说明:
|
||||
* - 1: 普通用户 - 基础游戏功能
|
||||
* - 9: 管理员 - 系统管理权限
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsInt({ message: '角色必须是数字' })
|
||||
@Min(1, { message: '角色值最小为1' })
|
||||
@Max(9, { message: '角色值最大为9' })
|
||||
role?: number = 1; // 默认普通用户
|
||||
role?: number = 1;
|
||||
}
|
||||
@@ -1,15 +1,103 @@
|
||||
/**
|
||||
* 用户数据实体模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义用户数据表的实体映射和字段约束
|
||||
* - 提供用户数据的持久化存储结构
|
||||
* - 支持多种登录方式的用户信息存储
|
||||
*
|
||||
* 依赖模块:
|
||||
* - TypeORM: ORM框架,提供数据库映射功能
|
||||
* - MySQL: 底层数据库存储
|
||||
*
|
||||
* 数据库表:users
|
||||
* 存储引擎:InnoDB
|
||||
* 字符集:utf8mb4
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('users') // 对应数据库表名
|
||||
/**
|
||||
* 用户实体类
|
||||
*
|
||||
* 职责:
|
||||
* - 映射数据库users表的结构和约束
|
||||
* - 定义用户数据的字段类型和验证规则
|
||||
* - 提供用户信息的完整数据模型
|
||||
*
|
||||
* 主要功能:
|
||||
* - 用户身份标识和认证信息存储
|
||||
* - 支持传统登录和OAuth第三方登录
|
||||
* - 用户基础信息和角色权限管理
|
||||
* - 自动时间戳记录和更新
|
||||
*
|
||||
* 数据完整性:
|
||||
* - 主键约束:id字段自增主键
|
||||
* - 唯一约束:username, email, phone, github_id
|
||||
* - 非空约束:username, nickname, role
|
||||
* - 外键关联:可扩展关联用户详情、权限等表
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户注册和登录验证
|
||||
* - 用户信息查询和更新
|
||||
* - 权限验证和角色管理
|
||||
* - 用户数据统计和分析
|
||||
*
|
||||
* 索引策略:
|
||||
* - 主键索引:id (自动创建)
|
||||
* - 唯一索引:username, email, phone, github_id
|
||||
* - 普通索引:role (用于角色查询)
|
||||
* - 复合索引:created_at + role (用于分页查询)
|
||||
*/
|
||||
@Entity('users')
|
||||
export class Users {
|
||||
// id:bigint、主键、非空、唯一、自增
|
||||
/**
|
||||
* 用户主键ID
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:BIGINT,支持大量用户数据
|
||||
* - 约束:主键、非空、自增
|
||||
* - 范围:1 ~ 9,223,372,036,854,775,807
|
||||
*
|
||||
* 业务规则:
|
||||
* - 系统自动生成,不可手动指定
|
||||
* - 全局唯一标识符,用于用户关联
|
||||
* - 作为其他表的外键引用
|
||||
*
|
||||
* 性能考虑:
|
||||
* - 自增主键,插入性能优异
|
||||
* - 聚簇索引,范围查询效率高
|
||||
* - BIGINT类型,避免ID耗尽问题
|
||||
*/
|
||||
@PrimaryGeneratedColumn({
|
||||
type: 'bigint',
|
||||
comment: '主键ID'
|
||||
})
|
||||
id: bigint;
|
||||
|
||||
// username:varchar(50)、非空、唯一
|
||||
/**
|
||||
* 用户名
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(50),支持多语言字符
|
||||
* - 约束:非空、唯一索引
|
||||
* - 字符集:utf8mb4,支持emoji等特殊字符
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户登录的唯一标识符
|
||||
* - 全系统唯一,不允许重复
|
||||
* - 长度限制:1-50个字符
|
||||
* - 建议格式:字母、数字、下划线组合
|
||||
*
|
||||
* 安全考虑:
|
||||
* - 不应包含敏感信息
|
||||
* - 避免使用易猜测的用户名
|
||||
* - 支持用户名修改(需要额外验证)
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
@@ -19,7 +107,25 @@ export class Users {
|
||||
})
|
||||
username: string;
|
||||
|
||||
// email:varchar(100)、允许空、唯一
|
||||
/**
|
||||
* 邮箱地址
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(100),支持长邮箱地址
|
||||
* - 约束:允许空、唯一索引
|
||||
* - 索引:用于快速邮箱查找
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用于账户找回和重要通知
|
||||
* - 全系统唯一,不允许重复
|
||||
* - OAuth登录时可能为空
|
||||
* - 支持邮箱验证和双因子认证
|
||||
*
|
||||
* 隐私保护:
|
||||
* - 敏感信息,日志记录时脱敏
|
||||
* - 仅用于系统通知,不对外展示
|
||||
* - 支持用户自主修改和验证
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
@@ -29,7 +135,25 @@ export class Users {
|
||||
})
|
||||
email: string;
|
||||
|
||||
// phone:varchar(30)、允许空、唯一
|
||||
/**
|
||||
* 手机号码
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(30),支持国际号码格式
|
||||
* - 约束:允许空、唯一索引
|
||||
* - 格式:包含国家代码的完整号码
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用于账户找回和短信通知
|
||||
* - 全系统唯一,不允许重复
|
||||
* - 支持国际手机号格式(+86、+1等)
|
||||
* - 用于短信验证码和双因子认证
|
||||
*
|
||||
* 隐私保护:
|
||||
* - 敏感信息,日志记录时脱敏
|
||||
* - 仅用于安全验证,不对外展示
|
||||
* - 支持用户自主修改和验证
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 30,
|
||||
@@ -39,7 +163,27 @@ export class Users {
|
||||
})
|
||||
phone: string;
|
||||
|
||||
// password_hash:varchar(255)、允许空
|
||||
/**
|
||||
* 密码哈希值
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(255),支持各种哈希算法
|
||||
* - 约束:允许空(OAuth登录时)
|
||||
* - 存储:加密后的哈希值,不存储明文
|
||||
*
|
||||
* 业务规则:
|
||||
* - 传统用户名密码登录方式使用
|
||||
* - OAuth第三方登录时此字段为空
|
||||
* - 使用bcrypt等安全哈希算法
|
||||
* - 支持密码强度验证和定期更新
|
||||
*
|
||||
* 安全措施:
|
||||
* - 绝不存储明文密码
|
||||
* - 使用盐值防止彩虹表攻击
|
||||
* - 日志系统自动脱敏处理
|
||||
* - 传输过程使用HTTPS加密
|
||||
* - 支持密码重置和修改功能
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
@@ -48,7 +192,26 @@ export class Users {
|
||||
})
|
||||
password_hash: string;
|
||||
|
||||
// nickname:varchar(50)、非空
|
||||
/**
|
||||
* 用户昵称
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(50),支持多语言字符
|
||||
* - 约束:非空,无唯一性要求
|
||||
* - 字符集:utf8mb4,支持emoji表情
|
||||
*
|
||||
* 业务规则:
|
||||
* - 游戏内显示的友好名称
|
||||
* - 允许重复,提高用户体验
|
||||
* - 长度限制:1-50个字符
|
||||
* - 支持中文、英文、数字、表情符号
|
||||
*
|
||||
* 显示规则:
|
||||
* - 游戏内头顶显示名称
|
||||
* - 聊天消息发送者标识
|
||||
* - 排行榜和用户列表显示
|
||||
* - 支持用户随时修改
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
@@ -57,7 +220,26 @@ export class Users {
|
||||
})
|
||||
nickname: string;
|
||||
|
||||
// github_id:varchar(100)、允许空、唯一
|
||||
/**
|
||||
* GitHub用户标识
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(100),存储GitHub用户ID
|
||||
* - 约束:允许空、唯一索引
|
||||
* - 用途:GitHub OAuth登录关联
|
||||
*
|
||||
* 业务规则:
|
||||
* - GitHub第三方登录的唯一标识
|
||||
* - 全系统唯一,不允许重复
|
||||
* - 用于关联GitHub账户信息
|
||||
* - 支持GitHub头像和基础信息同步
|
||||
*
|
||||
* OAuth集成:
|
||||
* - 存储GitHub返回的用户ID
|
||||
* - 用于后续API调用身份验证
|
||||
* - 支持账户绑定和解绑操作
|
||||
* - 可扩展支持其他OAuth提供商
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
@@ -67,7 +249,26 @@ export class Users {
|
||||
})
|
||||
github_id: string;
|
||||
|
||||
// avatar_url:varchar(255)、允许空
|
||||
/**
|
||||
* 用户头像链接
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:VARCHAR(255),支持长URL
|
||||
* - 约束:允许空,无唯一性要求
|
||||
* - 存储:完整的HTTP/HTTPS链接
|
||||
*
|
||||
* 业务规则:
|
||||
* - 用户头像图片的访问链接
|
||||
* - 支持GitHub头像或自定义上传
|
||||
* - 建议使用CDN加速访问
|
||||
* - 支持多种图片格式(jpg、png、gif等)
|
||||
*
|
||||
* 性能优化:
|
||||
* - 建议使用图片CDN服务
|
||||
* - 支持多尺寸头像适配
|
||||
* - 缓存策略优化加载速度
|
||||
* - 默认头像兜底机制
|
||||
*/
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
@@ -76,7 +277,31 @@ export class Users {
|
||||
})
|
||||
avatar_url: string;
|
||||
|
||||
// role:tinyint、非空、默认1
|
||||
/**
|
||||
* 用户角色
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:TINYINT,节省存储空间
|
||||
* - 约束:非空、默认值1
|
||||
* - 范围:1-9,支持角色扩展
|
||||
*
|
||||
* 业务规则:
|
||||
* - 控制用户在系统中的权限级别
|
||||
* - 1:普通用户,基础游戏功能
|
||||
* - 9:管理员,系统管理权限
|
||||
* - 支持角色升级和降级操作
|
||||
*
|
||||
* 权限设计:
|
||||
* - 基于角色的访问控制(RBAC)
|
||||
* - 支持细粒度权限配置
|
||||
* - 可扩展更多角色类型
|
||||
* - 权限验证中间件集成
|
||||
*
|
||||
* 扩展性:
|
||||
* - 预留2-8角色级别供未来使用
|
||||
* - 支持角色权限动态配置
|
||||
* - 可关联角色权限表进行扩展
|
||||
*/
|
||||
@Column({
|
||||
type: 'tinyint',
|
||||
nullable: false,
|
||||
@@ -85,7 +310,26 @@ export class Users {
|
||||
})
|
||||
role: number;
|
||||
|
||||
// created_at:datetime、非空、默认当前时间
|
||||
/**
|
||||
* 创建时间
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:DATETIME,精确到秒
|
||||
* - 约束:非空、默认当前时间
|
||||
* - 时区:使用系统时区,建议UTC
|
||||
*
|
||||
* 业务规则:
|
||||
* - 记录用户注册的准确时间
|
||||
* - 用于用户数据统计和分析
|
||||
* - 支持按时间范围查询用户
|
||||
* - 不可修改,保证数据完整性
|
||||
*
|
||||
* 应用场景:
|
||||
* - 用户注册趋势分析
|
||||
* - 新用户欢迎流程触发
|
||||
* - 数据审计和合规要求
|
||||
* - 用户生命周期管理
|
||||
*/
|
||||
@CreateDateColumn({
|
||||
type: 'datetime',
|
||||
nullable: false,
|
||||
@@ -94,12 +338,31 @@ export class Users {
|
||||
})
|
||||
created_at: Date;
|
||||
|
||||
// updated_at:datetime、非空、默认当前时间
|
||||
/**
|
||||
* 更新时间
|
||||
*
|
||||
* 数据库设计:
|
||||
* - 类型:DATETIME,精确到秒
|
||||
* - 约束:非空、自动更新
|
||||
* - 触发:任何字段更新时自动刷新
|
||||
*
|
||||
* 业务规则:
|
||||
* - 记录用户信息最后修改时间
|
||||
* - 数据库级别自动维护
|
||||
* - 用于数据同步和缓存失效
|
||||
* - 支持增量数据同步
|
||||
*
|
||||
* 应用场景:
|
||||
* - 数据变更审计
|
||||
* - 缓存更新策略
|
||||
* - 数据同步时间戳
|
||||
* - 用户活跃度分析
|
||||
*/
|
||||
@UpdateDateColumn({
|
||||
type: 'datetime',
|
||||
nullable: false,
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
onUpdate: 'CURRENT_TIMESTAMP', // 数据库更新时自动刷新时间
|
||||
onUpdate: 'CURRENT_TIMESTAMP',
|
||||
comment: '更新时间'
|
||||
})
|
||||
updated_at: Date;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2024-12-17
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
|
||||
23
src/core/login_core/login_core.module.ts
Normal file
23
src/core/login_core/login_core.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 登录核心模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供登录认证的核心服务模块
|
||||
* - 集成用户数据服务和认证逻辑
|
||||
* - 为业务层提供可复用的认证功能
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LoginCoreService } from './login_core.service';
|
||||
import { UsersModule } from '../db/users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule],
|
||||
providers: [LoginCoreService],
|
||||
exports: [LoginCoreService],
|
||||
})
|
||||
export class LoginCoreModule {}
|
||||
216
src/core/login_core/login_core.service.spec.ts
Normal file
216
src/core/login_core/login_core.service.spec.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 登录核心服务测试
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LoginCoreService } from './login_core.service';
|
||||
import { UsersService } from '../db/users/users.service';
|
||||
import { UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
|
||||
describe('LoginCoreService', () => {
|
||||
let service: LoginCoreService;
|
||||
let usersService: jest.Mocked<UsersService>;
|
||||
|
||||
const mockUser = {
|
||||
id: BigInt(1),
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
phone: '+8613800138000',
|
||||
password_hash: '$2b$12$hashedpassword',
|
||||
nickname: '测试用户',
|
||||
github_id: null as string | null,
|
||||
avatar_url: null as string | null,
|
||||
role: 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockUsersService = {
|
||||
findByUsername: jest.fn(),
|
||||
findByEmail: jest.fn(),
|
||||
findByGithubId: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LoginCoreService,
|
||||
{
|
||||
provide: UsersService,
|
||||
useValue: mockUsersService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LoginCoreService>(LoginCoreService);
|
||||
usersService = module.get(UsersService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should login successfully with valid credentials', async () => {
|
||||
usersService.findByUsername.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(true);
|
||||
|
||||
const result = await service.login({
|
||||
identifier: 'testuser',
|
||||
password: 'password123'
|
||||
});
|
||||
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(result.isNewUser).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException for invalid user', async () => {
|
||||
usersService.findByUsername.mockResolvedValue(null);
|
||||
usersService.findByEmail.mockResolvedValue(null);
|
||||
usersService.findAll.mockResolvedValue([]);
|
||||
|
||||
await expect(service.login({
|
||||
identifier: 'nonexistent',
|
||||
password: 'password123'
|
||||
})).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException for wrong password', async () => {
|
||||
usersService.findByUsername.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(false);
|
||||
|
||||
await expect(service.login({
|
||||
identifier: 'testuser',
|
||||
password: 'wrongpassword'
|
||||
})).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register successfully', async () => {
|
||||
usersService.create.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('hashedpassword');
|
||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||
|
||||
const result = await service.register({
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户'
|
||||
});
|
||||
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(result.isNewUser).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate password strength', async () => {
|
||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {
|
||||
throw new BadRequestException('密码长度至少8位');
|
||||
});
|
||||
|
||||
await expect(service.register({
|
||||
username: 'testuser',
|
||||
password: '123',
|
||||
nickname: '测试用户'
|
||||
})).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('githubOAuth', () => {
|
||||
it('should login existing GitHub user', async () => {
|
||||
usersService.findByGithubId.mockResolvedValue(mockUser);
|
||||
usersService.update.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.githubOAuth({
|
||||
github_id: 'github123',
|
||||
username: 'githubuser',
|
||||
nickname: 'GitHub用户'
|
||||
});
|
||||
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(result.isNewUser).toBe(false);
|
||||
});
|
||||
|
||||
it('should create new GitHub user', async () => {
|
||||
usersService.findByGithubId.mockResolvedValue(null);
|
||||
usersService.findByUsername.mockResolvedValue(null);
|
||||
usersService.create.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.githubOAuth({
|
||||
github_id: 'github123',
|
||||
username: 'githubuser',
|
||||
nickname: 'GitHub用户'
|
||||
});
|
||||
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(result.isNewUser).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPasswordResetCode', () => {
|
||||
it('should send reset code for email', async () => {
|
||||
usersService.findByEmail.mockResolvedValue(mockUser);
|
||||
|
||||
const code = await service.sendPasswordResetCode('test@example.com');
|
||||
|
||||
expect(code).toMatch(/^\d{6}$/);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException for non-existent user', async () => {
|
||||
usersService.findByEmail.mockResolvedValue(null);
|
||||
|
||||
await expect(service.sendPasswordResetCode('nonexistent@example.com'))
|
||||
.rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should reset password successfully', async () => {
|
||||
usersService.findByEmail.mockResolvedValue(mockUser);
|
||||
usersService.update.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('newhashedpassword');
|
||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||
|
||||
const result = await service.resetPassword({
|
||||
identifier: 'test@example.com',
|
||||
verificationCode: '123456',
|
||||
newPassword: 'newpassword123'
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for invalid verification code', async () => {
|
||||
await expect(service.resetPassword({
|
||||
identifier: 'test@example.com',
|
||||
verificationCode: 'invalid',
|
||||
newPassword: 'newpassword123'
|
||||
})).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePassword', () => {
|
||||
it('should change password successfully', async () => {
|
||||
usersService.findOne.mockResolvedValue(mockUser);
|
||||
usersService.update.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(true);
|
||||
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('newhashedpassword');
|
||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||
|
||||
const result = await service.changePassword(BigInt(1), 'oldpassword', 'newpassword123');
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException for wrong old password', async () => {
|
||||
usersService.findOne.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(false);
|
||||
|
||||
await expect(service.changePassword(BigInt(1), 'wrongpassword', 'newpassword123'))
|
||||
.rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
});
|
||||
423
src/core/login_core/login_core.service.ts
Normal file
423
src/core/login_core/login_core.service.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* 登录核心服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供用户认证的核心功能实现
|
||||
* - 处理登录、注册、密码重置等核心逻辑
|
||||
* - 为业务层提供基础的认证服务
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于认证功能的核心实现
|
||||
* - 不处理HTTP请求和响应格式化
|
||||
* - 为business层提供可复用的服务
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { UsersService } from '../db/users/users.service';
|
||||
import { Users } from '../db/users/users.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* 登录请求数据接口
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
/** 登录标识符:用户名、邮箱或手机号 */
|
||||
identifier: string;
|
||||
/** 密码 */
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册请求数据接口
|
||||
*/
|
||||
export interface RegisterRequest {
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 密码 */
|
||||
password: string;
|
||||
/** 昵称 */
|
||||
nickname: string;
|
||||
/** 邮箱(可选) */
|
||||
email?: string;
|
||||
/** 手机号(可选) */
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录请求数据接口
|
||||
*/
|
||||
export interface GitHubOAuthRequest {
|
||||
/** GitHub用户ID */
|
||||
github_id: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 昵称 */
|
||||
nickname: string;
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 头像URL */
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码重置请求数据接口
|
||||
*/
|
||||
export interface PasswordResetRequest {
|
||||
/** 邮箱或手机号 */
|
||||
identifier: string;
|
||||
/** 验证码 */
|
||||
verificationCode: string;
|
||||
/** 新密码 */
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证结果接口
|
||||
*/
|
||||
export interface AuthResult {
|
||||
/** 用户信息 */
|
||||
user: Users;
|
||||
/** 是否为新用户 */
|
||||
isNewUser?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LoginCoreService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 用户名密码登录
|
||||
*
|
||||
* @param loginRequest 登录请求数据
|
||||
* @returns 认证结果
|
||||
* @throws UnauthorizedException 认证失败时
|
||||
*/
|
||||
async login(loginRequest: LoginRequest): Promise<AuthResult> {
|
||||
const { identifier, password } = loginRequest;
|
||||
|
||||
// 查找用户(支持用户名、邮箱、手机号登录)
|
||||
let user: Users | null = null;
|
||||
|
||||
// 尝试用户名查找
|
||||
user = await this.usersService.findByUsername(identifier);
|
||||
|
||||
// 如果用户名未找到,尝试邮箱查找
|
||||
if (!user && this.isEmail(identifier)) {
|
||||
user = await this.usersService.findByEmail(identifier);
|
||||
}
|
||||
|
||||
// 如果邮箱未找到,尝试手机号查找(简单验证)
|
||||
if (!user && this.isPhoneNumber(identifier)) {
|
||||
const users = await this.usersService.findAll();
|
||||
user = users.find(u => u.phone === identifier) || null;
|
||||
}
|
||||
|
||||
// 用户不存在
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('用户名、邮箱或手机号不存在');
|
||||
}
|
||||
|
||||
// 检查是否为OAuth用户(没有密码)
|
||||
if (!user.password_hash) {
|
||||
throw new UnauthorizedException('该账户使用第三方登录,请使用对应的登录方式');
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await this.verifyPassword(password, user.password_hash);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('密码错误');
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param registerRequest 注册请求数据
|
||||
* @returns 认证结果
|
||||
* @throws ConflictException 用户已存在时
|
||||
* @throws BadRequestException 数据验证失败时
|
||||
*/
|
||||
async register(registerRequest: RegisterRequest): Promise<AuthResult> {
|
||||
const { username, password, nickname, email, phone } = registerRequest;
|
||||
|
||||
// 验证密码强度
|
||||
this.validatePasswordStrength(password);
|
||||
|
||||
// 加密密码
|
||||
const passwordHash = await this.hashPassword(password);
|
||||
|
||||
// 创建用户
|
||||
const user = await this.usersService.create({
|
||||
username,
|
||||
password_hash: passwordHash,
|
||||
nickname,
|
||||
email,
|
||||
phone,
|
||||
role: 1 // 默认普通用户
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录/注册
|
||||
*
|
||||
* @param oauthRequest OAuth请求数据
|
||||
* @returns 认证结果
|
||||
*/
|
||||
async githubOAuth(oauthRequest: GitHubOAuthRequest): Promise<AuthResult> {
|
||||
const { github_id, username, nickname, email, avatar_url } = oauthRequest;
|
||||
|
||||
// 查找是否已存在GitHub用户
|
||||
let user = await this.usersService.findByGithubId(github_id);
|
||||
|
||||
if (user) {
|
||||
// 用户已存在,更新信息
|
||||
user = await this.usersService.update(user.id, {
|
||||
nickname,
|
||||
email,
|
||||
avatar_url
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: false
|
||||
};
|
||||
}
|
||||
|
||||
// 检查用户名是否已被占用
|
||||
let finalUsername = username;
|
||||
let counter = 1;
|
||||
while (await this.usersService.findByUsername(finalUsername)) {
|
||||
finalUsername = `${username}_${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
user = await this.usersService.create({
|
||||
username: finalUsername,
|
||||
nickname,
|
||||
email,
|
||||
github_id,
|
||||
avatar_url,
|
||||
role: 1 // 默认普通用户
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送密码重置验证码
|
||||
*
|
||||
* @param identifier 邮箱或手机号
|
||||
* @returns 验证码(实际应用中应发送到用户邮箱/手机)
|
||||
* @throws NotFoundException 用户不存在时
|
||||
*/
|
||||
async sendPasswordResetCode(identifier: string): Promise<string> {
|
||||
// 查找用户
|
||||
let user: Users | null = null;
|
||||
|
||||
if (this.isEmail(identifier)) {
|
||||
user = await this.usersService.findByEmail(identifier);
|
||||
} else if (this.isPhoneNumber(identifier)) {
|
||||
const users = await this.usersService.findAll();
|
||||
user = users.find(u => u.phone === identifier) || null;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 生成6位数验证码
|
||||
const verificationCode = this.generateVerificationCode();
|
||||
|
||||
// TODO: 实际应用中应该:
|
||||
// 1. 将验证码存储到Redis等缓存中,设置过期时间(如5分钟)
|
||||
// 2. 发送验证码到用户邮箱或手机
|
||||
// 3. 返回成功消息而不是验证码本身
|
||||
|
||||
// 这里为了演示,直接返回验证码
|
||||
console.log(`密码重置验证码(${identifier}): ${verificationCode}`);
|
||||
|
||||
return verificationCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param resetRequest 重置请求数据
|
||||
* @returns 更新后的用户信息
|
||||
* @throws NotFoundException 用户不存在时
|
||||
* @throws BadRequestException 验证码错误时
|
||||
*/
|
||||
async resetPassword(resetRequest: PasswordResetRequest): Promise<Users> {
|
||||
const { identifier, verificationCode, newPassword } = resetRequest;
|
||||
|
||||
// TODO: 实际应用中应该验证验证码的有效性
|
||||
// 这里为了演示,简单验证验证码格式
|
||||
if (!/^\d{6}$/.test(verificationCode)) {
|
||||
throw new BadRequestException('验证码格式错误');
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
let user: Users | null = null;
|
||||
|
||||
if (this.isEmail(identifier)) {
|
||||
user = await this.usersService.findByEmail(identifier);
|
||||
} else if (this.isPhoneNumber(identifier)) {
|
||||
const users = await this.usersService.findAll();
|
||||
user = users.find(u => u.phone === identifier) || null;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 验证密码强度
|
||||
this.validatePasswordStrength(newPassword);
|
||||
|
||||
// 加密新密码
|
||||
const passwordHash = await this.hashPassword(newPassword);
|
||||
|
||||
// 更新密码
|
||||
return await this.usersService.update(user.id, {
|
||||
password_hash: passwordHash
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param oldPassword 旧密码
|
||||
* @param newPassword 新密码
|
||||
* @returns 更新后的用户信息
|
||||
* @throws UnauthorizedException 旧密码错误时
|
||||
*/
|
||||
async changePassword(userId: bigint, oldPassword: string, newPassword: string): Promise<Users> {
|
||||
// 获取用户信息
|
||||
const user = await this.usersService.findOne(userId);
|
||||
|
||||
// 检查是否为OAuth用户
|
||||
if (!user.password_hash) {
|
||||
throw new BadRequestException('OAuth用户无法修改密码');
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
const isOldPasswordValid = await this.verifyPassword(oldPassword, user.password_hash);
|
||||
if (!isOldPasswordValid) {
|
||||
throw new UnauthorizedException('旧密码错误');
|
||||
}
|
||||
|
||||
// 验证新密码强度
|
||||
this.validatePasswordStrength(newPassword);
|
||||
|
||||
// 加密新密码
|
||||
const passwordHash = await this.hashPassword(newPassword);
|
||||
|
||||
// 更新密码
|
||||
return await this.usersService.update(userId, {
|
||||
password_hash: passwordHash
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户密码
|
||||
*
|
||||
* @param password 明文密码
|
||||
* @param hash 密码哈希值
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
private async verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
try {
|
||||
return await bcrypt.compare(password, hash);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
*
|
||||
* @param password 明文密码
|
||||
* @returns 密码哈希值
|
||||
*/
|
||||
private async hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 12; // 推荐的盐值轮数
|
||||
return await bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码强度
|
||||
*
|
||||
* @param password 密码
|
||||
* @throws BadRequestException 密码强度不足时
|
||||
*/
|
||||
private validatePasswordStrength(password: string): void {
|
||||
if (password.length < 8) {
|
||||
throw new BadRequestException('密码长度至少8位');
|
||||
}
|
||||
|
||||
if (password.length > 128) {
|
||||
throw new BadRequestException('密码长度不能超过128位');
|
||||
}
|
||||
|
||||
// 检查是否包含字母和数字
|
||||
const hasLetter = /[a-zA-Z]/.test(password);
|
||||
const hasNumber = /\d/.test(password);
|
||||
|
||||
if (!hasLetter || !hasNumber) {
|
||||
throw new BadRequestException('密码必须包含字母和数字');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @returns 6位数验证码
|
||||
*/
|
||||
private generateVerificationCode(): string {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为邮箱格式
|
||||
*
|
||||
* @param str 字符串
|
||||
* @returns 是否为邮箱
|
||||
*/
|
||||
private isEmail(str: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为手机号格式(简单验证)
|
||||
*
|
||||
* @param str 字符串
|
||||
* @returns 是否为手机号
|
||||
*/
|
||||
private isPhoneNumber(str: string): boolean {
|
||||
// 简单的手机号验证,支持国际格式
|
||||
const phoneRegex = /^(\+\d{1,3}[- ]?)?\d{10,11}$/;
|
||||
return phoneRegex.test(str.replace(/\s/g, ''));
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* 用户功能测试脚本
|
||||
*
|
||||
* 使用方法:npx ts-node test-users-functionality.ts
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './src/app.module';
|
||||
import { UsersService } from './src/core/db/users/users.service';
|
||||
import { CreateUserDto } from './src/core/db/users/users.dto';
|
||||
|
||||
async function testUsersFunctionality() {
|
||||
console.log('🚀 启动用户功能测试...\n');
|
||||
|
||||
try {
|
||||
// 创建NestJS应用
|
||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||
logger: false, // 禁用日志以保持输出清洁
|
||||
});
|
||||
|
||||
// 获取用户服务
|
||||
const usersService = app.get(UsersService);
|
||||
console.log('✅ 成功获取UsersService实例');
|
||||
|
||||
// 测试数据
|
||||
const testUserDto: CreateUserDto = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
email: `test_${Date.now()}@example.com`,
|
||||
phone: `+86138${Date.now().toString().slice(-8)}`,
|
||||
password_hash: 'hashed_password_123',
|
||||
nickname: '功能测试用户',
|
||||
github_id: `github_${Date.now()}`,
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
role: 1
|
||||
};
|
||||
|
||||
console.log('\n📝 测试创建用户...');
|
||||
const createdUser = await usersService.create(testUserDto);
|
||||
console.log('✅ 用户创建成功:', {
|
||||
id: createdUser.id.toString(),
|
||||
username: createdUser.username,
|
||||
nickname: createdUser.nickname,
|
||||
email: createdUser.email
|
||||
});
|
||||
|
||||
console.log('\n🔍 测试查询用户...');
|
||||
const foundUser = await usersService.findOne(createdUser.id);
|
||||
console.log('✅ 用户查询成功:', foundUser.username);
|
||||
|
||||
console.log('\n📊 测试用户统计...');
|
||||
const userCount = await usersService.count();
|
||||
console.log('✅ 当前用户总数:', userCount);
|
||||
|
||||
console.log('\n🔍 测试根据用户名查询...');
|
||||
const userByUsername = await usersService.findByUsername(createdUser.username);
|
||||
console.log('✅ 根据用户名查询成功:', userByUsername?.nickname);
|
||||
|
||||
console.log('\n✏️ 测试更新用户...');
|
||||
const updatedUser = await usersService.update(createdUser.id, {
|
||||
nickname: '更新后的昵称'
|
||||
});
|
||||
console.log('✅ 用户更新成功:', updatedUser.nickname);
|
||||
|
||||
console.log('\n📋 测试查询所有用户...');
|
||||
const allUsers = await usersService.findAll(5); // 限制5个
|
||||
console.log('✅ 查询到用户数量:', allUsers.length);
|
||||
|
||||
console.log('\n🔍 测试搜索功能...');
|
||||
const searchResults = await usersService.search('测试');
|
||||
console.log('✅ 搜索结果数量:', searchResults.length);
|
||||
|
||||
console.log('\n🗑️ 测试删除用户...');
|
||||
const deleteResult = await usersService.remove(createdUser.id);
|
||||
console.log('✅ 用户删除成功:', deleteResult.message);
|
||||
|
||||
// 验证删除
|
||||
console.log('\n✅ 验证删除结果...');
|
||||
try {
|
||||
await usersService.findOne(createdUser.id);
|
||||
console.log('❌ 删除验证失败:用户仍然存在');
|
||||
} catch (error) {
|
||||
console.log('✅ 删除验证成功:用户已不存在');
|
||||
}
|
||||
|
||||
await app.close();
|
||||
console.log('\n🎉 所有功能测试通过!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
testUsersFunctionality();
|
||||
182
test/business/login.e2e-spec.ts
Normal file
182
test/business/login.e2e-spec.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 登录功能端到端测试
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../../src/app.module';
|
||||
|
||||
describe('Login (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('/auth/register (POST)', () => {
|
||||
it('should register a new user', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
username: 'testuser' + Date.now(),
|
||||
password: 'password123',
|
||||
nickname: '测试用户',
|
||||
email: `test${Date.now()}@example.com`
|
||||
})
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.user.username).toBeDefined();
|
||||
expect(res.body.data.access_token).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error for invalid password', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
username: 'testuser',
|
||||
password: '123', // 密码太短
|
||||
nickname: '测试用户'
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/auth/login (POST)', () => {
|
||||
it('should login with valid credentials', async () => {
|
||||
// 先注册用户
|
||||
const username = 'logintest' + Date.now();
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
username,
|
||||
password: 'password123',
|
||||
nickname: '登录测试用户'
|
||||
});
|
||||
|
||||
// 然后登录
|
||||
return request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
identifier: username,
|
||||
password: 'password123'
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.user.username).toBe(username);
|
||||
expect(res.body.data.access_token).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error for invalid credentials', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
identifier: 'nonexistent',
|
||||
password: 'wrongpassword'
|
||||
})
|
||||
.expect(200) // 业务层返回200,但success为false
|
||||
.expect((res) => {
|
||||
expect(res.body.success).toBe(false);
|
||||
expect(res.body.error_code).toBe('LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/auth/github (POST)', () => {
|
||||
it('should handle GitHub OAuth login', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/auth/github')
|
||||
.send({
|
||||
github_id: 'github' + Date.now(),
|
||||
username: 'githubuser' + Date.now(),
|
||||
nickname: 'GitHub用户',
|
||||
email: `github${Date.now()}@example.com`,
|
||||
avatar_url: 'https://avatars.githubusercontent.com/u/123456'
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.user.username).toBeDefined();
|
||||
expect(res.body.data.is_new_user).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/auth/forgot-password (POST)', () => {
|
||||
it('should send password reset code', async () => {
|
||||
// 先注册用户
|
||||
const email = `reset${Date.now()}@example.com`;
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
username: 'resettest' + Date.now(),
|
||||
password: 'password123',
|
||||
nickname: '重置测试用户',
|
||||
email
|
||||
});
|
||||
|
||||
// 发送重置验证码
|
||||
return request(app.getHttpServer())
|
||||
.post('/auth/forgot-password')
|
||||
.send({
|
||||
identifier: email
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.verification_code).toMatch(/^\d{6}$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/auth/reset-password (POST)', () => {
|
||||
it('should reset password with valid code', async () => {
|
||||
// 先注册用户
|
||||
const email = `resetpwd${Date.now()}@example.com`;
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
username: 'resetpwdtest' + Date.now(),
|
||||
password: 'password123',
|
||||
nickname: '重置密码测试用户',
|
||||
email
|
||||
});
|
||||
|
||||
// 获取验证码
|
||||
const codeResponse = await request(app.getHttpServer())
|
||||
.post('/auth/forgot-password')
|
||||
.send({
|
||||
identifier: email
|
||||
});
|
||||
|
||||
const verificationCode = codeResponse.body.data.verification_code;
|
||||
|
||||
// 重置密码
|
||||
return request(app.getHttpServer())
|
||||
.post('/auth/reset-password')
|
||||
.send({
|
||||
identifier: email,
|
||||
verification_code: verificationCode,
|
||||
new_password: 'newpassword123'
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.message).toBe('密码重置成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user