- compass-service 2.0
- 项目启动
- 特性
- Monorepo 结构,易于扩展服务及公共资源沉淀
- Typescript/Jest/Airbnb Eslint/Prettier
- 支持环境变量控制
- 支持Google reCAPTCHA v3 人机校验
- 接口多版本支持
- 接口限流保护
- 约束接口进参,移除非白名单属性,自动转换数据为符合预期的类型
- PrismaORM 数据库管理
- 统一的响应拦截器,规范返回数据
- 支持JWT校验 + 用户权限集验证
- EMail邮件服务支持
- 支持连接redis服务
- 添加日志中间件,监控入站请求
- 支持Swagger API文档
- helmet 安全的响应头设置
- 默认启用 express json,urlencoded 中间件
- 集成circleciCI自动集成部署
- 业务功能
- 2.0 移除的特性
- 更多文档信息
本项目会以具体业务为实例不断完善内容,如果你希望使用本项目作为基础模板快速搭建项目,可使用Compass Template内的Nest模板
首次启动请先参考下方 Prisma ORM 管理 部分同步数据库架构
如果您不具备可用的mysql,redis或是可选的postgres环境,可以参考部署基础环境一键启动基础环境
npm install
安装依赖
根目录新建 .env 文件, 复制 .env.example 内容到 .env 文件,并按需调整配置内容.
npm run start:dev
开发模式启动
npm run start:prod
生产模式启动
npm run format
执行代码格式化
npm run lint
执行代码检查
根据自身业务实际情况,修改项目内 "FIXME: " 标记部分的逻辑
npm run build
构建项目
nest g app [project_name]
创建一个子应用到monorepo
nest g lib [library_name]
创建一个包到monorepo
./shared
文件夹内直接添加可被共享的资源文件
- 支持Typescript环境
npm run format
进行代码格式化npm run lint
进行代码检查,默认基于Airbnb规范npm run test
进行单元测试npm run test:e2e
进行端到端测试
支持.env文件控制环境变量,示例可见: .env.example,复制示例文件进入.env文件后按需配置即可
客户端:
<!-- 插入recaptcha脚本并指定key,如果是国内host需要替换为www.recaptcha.net -->
<script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>
<script>
// 当点击某个提交按钮时进行人机静默校验,否则应该进行双重认证或拒绝认证
function onClick(e) {
e.preventDefault();
// 提示: reCAPTCHA_site_key为您在Google ReCaptcha注册的网站key
grecaptcha.ready(function() {
// action各种含义参考: https://developers.google.com/recaptcha/docs/v3?hl=zh-cn#interpreting_the_score
grecaptcha.execute('reCAPTCHA_site_key', {action: 'login'}).then(function(token) {
// 在此处添加您的逻辑,把表单数据跟token一起提供给后端校验
fetch('/api/v1/recaptcha/validate', {
method: 'POST',
body: JSON.stringify({ token }),
})
.then(resp => resp.json())
.then(result => {
if (result.statusCode === 100200 && result.data) {
console.log('签发的临时许可 token: %s, 请在五分钟内使用此token登录', result.data);
}
});
});
});
}
</script>
服务端: .env文件内 设置 COMPASS_RECAPTCHA_SECRET 环境变量为您在Google ReCaptcha注册的后台key
- 接口
/api/v1/recaptcha/validate
收到{ token }
后会提交google验证 - 验证通过后会下发一个五分钟有效的临时token
- 用户在登录时可将用户信息与临时token一并提交登录接口
- 登录接口验证token属于签发的授权token并账号密码正确即登录成功
用法示例如下:
@Controller('example')
export class ExampleController {
// 访问地址: /api/v1/example/test
@Get('test')
test(): string {
return 'This is v1 endpoint.';
}
// 访问地址: /api/v2/example/test
@Version('2')
@Get('test')
test2(): string {
return 'This is v2 endpoint.';
}
}
默认一个IP一个端点每分钟仅允许调用20次,特例场景可以通过装饰器跳过限流或局部修改限流,示例如下:
@Controller('example')
export class ExampleController {
// 该接口跳过节流保护
@SkipThrottle()
@Get('test')
test(): string {
return 'Hello world.';
}
// 该接口每分钟调用不超过3次
@Throttle(3, 60)
@Get('test2')
test(): string {
return 'Hello world.';
}
// 默认采用全局节流配置
@Get('test3')
test(): string {
return 'Hello world.';
}
}
通过shared/config/index.ts
下的validationOption可调整选项
当遇见多个Dto联合类型时,内置ValidationPipe失效,可按照下列示例处理:
import { IsNumber, IsString, IsOptional } from 'class-validator';
import { Body } from '@nestjs/common';
import { validateMultipleDto } from '@shared';
class ADto {
@IsString()
id: string;
}
class BDto {
@IsNumber()
age: number;
}
class CDto {
@IsString()
name: string;
@IsOptional()
@IsString()
address?: string
}
@Controller('example')
export class ExampleController {
@Get('test')
test(@Body() body: ADto | BDto): string {
// 验证失败会抛出异常终止程序,第三个参数AND,OR来控制处理逻辑,默认是OR逻辑
validateMultipleDto(body, [ADto, BDto]);
return 'Hello world.';
}
/**
* @description 假如入参是 { name: 'test', test: 'test' }
* 实际body会是 { name: 'test' }, test属性会被自动移除
*/
@Get('test2')
test2(@Body() body: CDto) {
return 'Hello world.';
}
}
请确保.env文件配置已经就绪
使用 npx prisma db push
同步数据库架构
同步数据库架构后执行pnpm run seed
初始化数据库, 有问题或需要调整也可通过pnpm run seed:rollback
回滚初始化动作
npx prisma db pull
同步数据库架构到Prisma模型文件中
npx prisma format
格式化schema文件
npx prisma generate
生成Prisma Client文件
根据业务实际情况调整schema.prisma文件
npx prisma format
格式化schema文件
npx prisma generate
生成Prisma Client文件,每次scheme变更后都应执行
npx prisma-docs-generator serve
基于generate的结果生成模型文档
npx prisma studio
通过web浏览数据库数据
npx prisma db push
本地或开发环境可通过此命令直接同步数据库架构 警告: 请不要在测试或生产等正式环境使用此命令
npx prisma migrate dev --name [本次迁移的标题]
schema变更后创建迁移脚本
npx prisma migrate deploy
执行迁移脚本
npx prisma migrate status
查看当前迁移状态
npx prisma migrate resolve --rolled-back [migrate_name]
回滚到指定记录位置
当创建迁移文件后,如果你手动进行了迁移,可通过npx prisma migrate resolve --applied [migrate_name]
将该次迁移手动标记为完成
针对隐私数据入库及查询做二次加密保护,不可通过数据库直接查看隐私数据.
libs/db/src/db.service.ts
内的useUserHook方法默认已对用户密码做不可逆加密入库
不可逆加密隐私数据参考如下:
import { encodeMD5 } from '@shared';
encodeMD5('password'); // 第二个参数为密钥, 默认取.env内的 COMPASS_PRIVACY_DATA_SECRET 值
在shared/interceptors/response.interceptor.ts
定义的默认拦截逻辑,示例如下:
@Controller('example')
export class ExampleController {
@Get('test')
test() {
return 'Hello world.'; // 实际响应: { statusCode: 100200, data: 'Hello world.', message: '请求成功' } HttpStatus = 200
}
@Get('test2')
test2() {
return new HttpResponse('Hello world.', { responseType: 'text' }); // 实际响应: 'Hello world.'
}
@Get('test3')
test3() {
// 尽管是throw,但是客户端收到的返回依旧以HttpResponse配置为准,可以用来快捷中断程序逻辑执行,又控制响应的状态与数据
// 实际响应: { statusCode: 100400, data: 'Hello world.', message: '请求成功' } HttpStatus = 403
throw new HttpResponse('Hello world.', {
statusCode: ResponseCode.BAD_REQUEST,
httpStatus: HttpStatus.FORBIDDEN,
});
}
}
支持JWT授权,并按权限给予访问能力, 示例如下:
@Controller('oauth')
export class OauthController {
constructor(
private jwtService: JwtService,
private oauthService: OauthService,
) {}
@Public() // public装饰器指明该接口完全开放,跳过jwt验证,跳过权限验证
@Post('login')
async login(@Body() body: EMailLoginDto | TelephoneLoginDto) {
validateMultipleDto(body, [EMailLoginDto, TelephoneLoginDto]);
// 验证登录是否有效,通过后签发token
const result = await this.oauthService.validateLogin(body);
const signStr = this.jwtService.sign(result);
return { ...userInfo, token: signStr };
}
/**
* @description 该接口必须通过JWT验证后再通过用户权限验证,用户必须拥有对应权限
* Auth 接受两个参数,第一个参数类型必须是 PERMISSIONS | PERMISSIONS[]
* 第二个参数可选,类型是 'AND' | 'OR',默认是'AND',即所有声明的权限都必须具备,OR则代表声明的权限具备任意一个均可
* @param user @User()装饰器可以快捷的拿到授权通过后的用户信息数据
*/
@Auth(PERMISSIONS.COMMON_USER_QUERY)
@Get('test')
async test(@User() user: unknown) {
return user;
}
/**
* @description 默认访问该接口必须先通过JWT验证
*/
@Get('test2')
async test2(@User() user: any) {
return user;
}
}
shared/utils/jwt.strategy.ts
内会根据用户所具备的角色去聚合用户权限集
shared/guards/jwt-auth.guard.ts
具体处理用户访问权限的守卫
.env 文件内提供正确的 COMPASS_EMAIL_USER 及 COMPASS_EMAIL_PASSWORD 变量, 使用示例如下:
// example.module.ts
import { EmailModule, EmailService } from '@app/email';
import { CompassEnv, getEnv } from '@shared';
const emailUser = getEnv(CompassEnv.EMAIL_USER);
const emailPassword = getEnv(CompassEnv.EMAIL_PASSWORD);
@Module({
imports: [
// 默认使用outlook服务,请按需调整, 详见: https://nodemailer.com/usage/#setting-it-up
EmailModule.forRoot({
service: 'outlook365',
auth: {
user: emailUser,
pass: emailPassword,
},
}),
],
})
export class ExampleModule {}
// example.service.ts
@Injectable()
export class ExampleService {
constructor(private emailService: EmailService) {}
sendEmailMsg(msg: string) {
// 具体参考 https://nodemailer.com/message/#common-fields
// 发出邮件
return this.emailService.sendMail({
from: SYSTEM_EMAIL_FROM, // 声明发送方
to: data.email, // 发送的目标
subject: '邮箱验证', // 主题
// 实际发送内容, replaceVariablesInString用来对模板内的变量做替换
html: replaceVariablesInString(EMAIL_CAPTCHA_TEMPLATE, {
context: 'Compass Service',
code: code.toString(),
}),
});
}
}
按需调整.env文件内 COMPASS_REDIS_HOST,COMPASS_REDIS_PORT,COMPASS_REDIS_PASSWORD 等变量,使用示例如下:
// example.service.ts
import { RedisManagerService, CAPTCHA_REDIS_KEY } from '@app/redis-manager';
@Injectable()
export class ExampleService {
constructor(private redisService: RedisManagerService,) {}
async getCache(msg: string) {
// 具体参考 https://github.com/liaoliaots/nestjs-redis/blob/HEAD/docs/latest/redis.md
await this.redisService.get(CAPTCHA_REDIS_KEY, {
// 通过params替换CAPTCHA_REDIS_KEY内的变量值以定位到具体key
params: {
type: 'email',
account: user.email,
}
});
}
async setCache() {
const code = random(100000, 999999);
// 将code码记入缓存
await this.redisService.set(CAPTCHA_REDIS_KEY, String(code), {
params: { type: 'email', account: data.email },
});
}
}
默认会将所有入站请求打印在控制台,日志级别为log级.逻辑详见shared/middleware/logger.middleware.ts
npm run start:dev
或其他start启动项目后,访问/api/docs路径
在 apps/compass-service/src/middleware/index.ts
路径内启用
在 apps/compass-service/src/middleware/express.middleware.ts
内启用
详见.circleci/config.yml
- compression 移除,压缩支持应该在nginx层处理,而不在服务器
- csurf 已废弃,不再采用
- LoggerService 已移除,改为采用 @nestjs/common 内置的 Logger
- 扩展的HttpException已被移除,改为采用 @nestjs/common 内置的 HttpException
- SessionModule已被移除,这个模块并不适合在生产环境使用
npx prisma-docs-generator serve
数据模型文档
npm run start:dev
启动服务后访问: http://localhost:8080/api/docs