Skip to content

Aiden-FE/compass-service

Repository files navigation

compass-service 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 构建项目

特性

Monorepo 结构,易于扩展服务及公共资源沉淀

nest g app [project_name] 创建一个子应用到monorepo

nest g lib [library_name] 创建一个包到monorepo

./shared 文件夹内直接添加可被共享的资源文件

Typescript/Jest/Airbnb Eslint/Prettier

  • 支持Typescript环境
  • npm run format 进行代码格式化
  • npm run lint 进行代码检查,默认基于Airbnb规范
  • npm run test 进行单元测试
  • npm run test:e2e 进行端到端测试

支持环境变量控制

支持.env文件控制环境变量,示例可见: .env.example,复制示例文件进入.env文件后按需配置即可

支持Google reCAPTCHA v3 人机校验

客户端:

<!-- 插入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

  1. 接口/api/v1/recaptcha/validate收到{ token }后会提交google验证
  2. 验证通过后会下发一个五分钟有效的临时token
  3. 用户在登录时可将用户信息与临时token一并提交登录接口
  4. 登录接口验证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.';
  }
}

PrismaORM 数据库管理

请确保.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的结果生成模型文档

web浏览数据库数据

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校验 + 用户权限集验证

支持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 具体处理用户访问权限的守卫

EMail邮件服务支持

.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(),
      }),
    });
  }
}

支持连接redis服务

按需调整.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

支持Swagger API文档

npm run start:dev 或其他start启动项目后,访问/api/docs路径

helmet 安全的响应头设置

apps/compass-service/src/middleware/index.ts 路径内启用

默认启用 express json,urlencoded 中间件

apps/compass-service/src/middleware/express.middleware.ts 内启用

集成circleciCI自动集成部署

详见.circleci/config.yml

业务功能

支持基于Chat gpt的AI对话功能

支持授权管理

支持待办事项管理

支持权限管理(未完善)

支持角色管理(未完善)

支持用户管理(未完善)

2.0 移除的特性

  • 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