first commit
commit
696ec918e8
@ -0,0 +1,120 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
- name: cache
|
||||||
|
host:
|
||||||
|
path: /tmp/cache/nanjing
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: restore-cache
|
||||||
|
pull: if-not-exists
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
restore: true
|
||||||
|
mount:
|
||||||
|
- ./.npm-cache
|
||||||
|
- ./node_modules
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
|
||||||
|
- name: install
|
||||||
|
image: node:14.16.1
|
||||||
|
pull: if-not-exists
|
||||||
|
commands:
|
||||||
|
# 删除文件 npm install
|
||||||
|
# - rm -rf package-lock.json
|
||||||
|
- npm --registry https://registry.npm.taobao.org install
|
||||||
|
when:
|
||||||
|
event: [push, pull_request]
|
||||||
|
branch: [development, test, master, sso]
|
||||||
|
|
||||||
|
- name: rebuild-cache
|
||||||
|
pull: if-not-exists
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
rebuild: true
|
||||||
|
mount:
|
||||||
|
- ./.npm-cache
|
||||||
|
- ./node_modules
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
|
||||||
|
# build
|
||||||
|
- name: build
|
||||||
|
pull: if-not-exists
|
||||||
|
image: node:14.16.1
|
||||||
|
commands:
|
||||||
|
- npm run build
|
||||||
|
when:
|
||||||
|
event: [push, pull_request]
|
||||||
|
branch: [development, test, master, sso]
|
||||||
|
|
||||||
|
# docker-build
|
||||||
|
- name: docker-build
|
||||||
|
pull: if-not-exists
|
||||||
|
image: plugins/docker
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
settings:
|
||||||
|
repo: hub.kaiguawang.cn/qunsense/api
|
||||||
|
registry: hub.kaiguawang.cn
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- '0.0.1'
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
when:
|
||||||
|
event: [push, pull_request]
|
||||||
|
branch: [development, test, master]
|
||||||
|
|
||||||
|
# run
|
||||||
|
- name: dev-run
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
pull: if-not-exists
|
||||||
|
settings:
|
||||||
|
host: 172.17.0.1
|
||||||
|
username: root
|
||||||
|
password:
|
||||||
|
from_secret: ssh_password
|
||||||
|
port: 22
|
||||||
|
command_timeout: 300s
|
||||||
|
script:
|
||||||
|
- cd /home/runtime/apps/qunsense
|
||||||
|
- docker compose up -d api
|
||||||
|
when:
|
||||||
|
event: [push, pull_request]
|
||||||
|
branch: [development]
|
||||||
|
|
||||||
|
- name: notify
|
||||||
|
image: fifsky/drone-wechat-work
|
||||||
|
pull: if-not-exists
|
||||||
|
# pull: always
|
||||||
|
settings:
|
||||||
|
url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4166884e-9d7b-4e7a-a1c8-80d71d4755ba
|
||||||
|
msgtype: markdown
|
||||||
|
content: |
|
||||||
|
{{if eq .Status "success" }}
|
||||||
|
#### 🎉 ${DRONE_REPO} 构建成功
|
||||||
|
> Commit: ${DRONE_COMMIT_MESSAGE}
|
||||||
|
> Author: ${DRONE_COMMIT_AUTHOR}
|
||||||
|
{{else}}
|
||||||
|
#### ❌ ${DRONE_REPO} 构建失败
|
||||||
|
> Commit: ${DRONE_COMMIT_MESSAGE}
|
||||||
|
> Author: ${DRONE_COMMIT_AUTHOR}
|
||||||
|
> 请立即修复!!!
|
||||||
|
{{end}}
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- failure
|
||||||
|
- success
|
||||||
|
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# project-tree
|
||||||
|
.git
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.upload
|
||||||
|
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
.history
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
upload/**/*
|
||||||
|
./swagger-spec.json
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
FROM node:14.16.1
|
||||||
|
|
||||||
|
ARG NODE_ENV=development
|
||||||
|
ENV NODE_ENV=$NODE_ENV
|
||||||
|
|
||||||
|
# Set a working directory
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY ./cert cert
|
||||||
|
# Install Node.js dependencies
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN npm config set registry https://registry.npm.taobao.org
|
||||||
|
#RUN npm config set unsafe-perm true
|
||||||
|
COPY ./patches patches
|
||||||
|
RUN npm install --production;
|
||||||
|
RUN npm run postinstall;
|
||||||
|
|
||||||
|
RUN mkdir upload && mkdir upload/file
|
||||||
|
RUN touch swagger-spec.json && chmod 777 swagger-spec.json
|
||||||
|
RUN chmod 777 -R upload
|
||||||
|
RUN touch error.log && chmod 777 error.log
|
||||||
|
|
||||||
|
COPY dist* ./
|
||||||
|
RUN ls
|
||||||
|
COPY production.env production.env
|
||||||
|
COPY production.env development.env
|
||||||
|
# Run the container under "node" user by default
|
||||||
|
USER node
|
||||||
|
|
||||||
|
CMD [ "node", "src/main.js" ]
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_FAMILY=4
|
||||||
|
REDIS_DB=0
|
||||||
|
# REDIS_PASSWORD=
|
||||||
|
|
||||||
|
DOC=true
|
||||||
|
|
||||||
|
CLIENT_URI=https://liulinlin.test.qunsense.com/
|
||||||
|
|
||||||
|
MYSQL_HOST=127.0.0.1
|
||||||
|
MYSQL_PORT=3306
|
||||||
|
MYSQL_USERNAME=api-visitor
|
||||||
|
MYSQL_PASSWORD=api-visitor
|
||||||
|
MYSQL_DATABASE=api-visitor
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"postinstall": "patch-package",
|
||||||
|
"migration:create": "typeorm migration:create",
|
||||||
|
"migration:run": "npx typeorm-ts-node-esm migration:run -d src/config/config.mysql.ts",
|
||||||
|
"migration:revert": "npx typeorm-ts-node-esm migration:revert -d src/config/config.mysql.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/bull": "^0.6.3",
|
||||||
|
"@nestjs/common": "^9.0.0",
|
||||||
|
"@nestjs/core": "^9.0.0",
|
||||||
|
"@nestjs/jwt": "^10.0.3",
|
||||||
|
"@nestjs/passport": "^9.0.3",
|
||||||
|
"@nestjs/platform-express": "^9.0.0",
|
||||||
|
"@nestjs/schedule": "^3.0.0",
|
||||||
|
"@nestjs/swagger": "^6.3.0",
|
||||||
|
"@nestjs/typeorm": "^9.0.1",
|
||||||
|
"@types/cron": "^2.0.1",
|
||||||
|
"ali-oss": "^6.17.1",
|
||||||
|
"alipay-sdk": "^3.4.0",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"bluebird": "^3.7.2",
|
||||||
|
"bull": "^4.10.4",
|
||||||
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"co-wechat-api": "^3.11.0",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"global": "^4.4.0",
|
||||||
|
"ioredis": "^5.3.2",
|
||||||
|
"joi": "^17.9.2",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"nest-wechatpay-node-v3": "^1.0.1",
|
||||||
|
"nest-winston": "^1.9.3",
|
||||||
|
"node-xlsx": "^0.23.0",
|
||||||
|
"passport": "^0.6.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"patch-package": "^7.0.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rxjs": "^7.2.0",
|
||||||
|
"simple-flakeid": "^0.0.5",
|
||||||
|
"superagent": "^8.0.9",
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.0.607",
|
||||||
|
"tencentcloud-sdk-nodejs-faceid": "^4.0.609",
|
||||||
|
"tree": "^0.1.3",
|
||||||
|
"typeorm": "0.3.15",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"wechatpay-node-v3": "^2.1.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^9.0.0",
|
||||||
|
"@nestjs/schematics": "^9.0.0",
|
||||||
|
"@nestjs/testing": "^9.0.0",
|
||||||
|
"@types/bull": "^4.10.0",
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/jest": "29.5.0",
|
||||||
|
"@types/node": "18.15.11",
|
||||||
|
"@types/supertest": "^2.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
"eslint": "^8.0.1",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"jest": "29.5.0",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"source-map-support": "^0.5.20",
|
||||||
|
"supertest": "^6.1.3",
|
||||||
|
"ts-jest": "29.0.5",
|
||||||
|
"ts-loader": "^9.2.3",
|
||||||
|
"ts-node": "^10.0.0",
|
||||||
|
"tsconfig-paths": "4.2.0",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/doc/json")
|
||||||
|
getDoc(): string {
|
||||||
|
const json = fs.readFileSync("swagger-spec.json", "utf-8");
|
||||||
|
|
||||||
|
return JSON.parse(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Get("/app.config")
|
||||||
|
appConfig() {
|
||||||
|
return {
|
||||||
|
app_id: process.env.QUNSENSE_SUITE_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Get("/WW_verify_n9zX2u8E9ShFgYmx.txt")
|
||||||
|
// config(): string {
|
||||||
|
// return "n9zX2u8E9ShFgYmx";
|
||||||
|
// }
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import {mysql_config} from './config/config.mysql';
|
||||||
|
import { ConfigModule } from './config/config.module';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { UserModule } from './user/user.module';
|
||||||
|
import { RoleModule } from './role/role.module';
|
||||||
|
import { FileModule } from './file/file.module';
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule,
|
||||||
|
AuthModule,
|
||||||
|
UserModule,
|
||||||
|
TypeOrmModule.forRoot(mysql_config),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
RoleModule,
|
||||||
|
FileModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config({ path: `${process.env.NODE_ENV || 'development'}.env` });
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
UseGuards,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
UsePipes,
|
||||||
|
Body,
|
||||||
|
Query,
|
||||||
|
HttpCode,
|
||||||
|
Res,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import { ValidationPipe, User, Roles, RolesGuard } from '../common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { Login, PollingDto, LoginMobile, ResponseType, UpdateUserDto, IsLogin, QrResponse, SureLogin, IsLoginResult, LoginStatus, } from './auth.params';
|
||||||
|
import { UserEntity } from '../user/user.entity';
|
||||||
|
import redis from 'src/redis';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import Axios from 'axios';
|
||||||
|
|
||||||
|
@ApiTags('用户登录')
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
@InjectRepository(UserEntity)
|
||||||
|
private readonly userRepository: Repository<UserEntity>,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
@Get('login/status/is_login')
|
||||||
|
@ApiOperation({ summary: '获取 token 是否失效' })
|
||||||
|
@UsePipes(new ValidationPipe())
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '返回参数说明',
|
||||||
|
type: LoginStatus,
|
||||||
|
})
|
||||||
|
async loginGetStatus(@Query() loginData: IsLogin): Promise<any> {
|
||||||
|
const result = await redis.get(`qunsense_code:${loginData.qunsense_code}`)
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
status: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { AuthService, expires_in } from './auth.service';
|
||||||
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { UserEntity } from '../user/user.entity';
|
||||||
|
// import AuthResolver from './auth.resolver';
|
||||||
|
// import { RoleEntity } from '../role/role.entity';
|
||||||
|
// import { RoleItemEntity } from '../role-item/role-item.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
UserEntity,
|
||||||
|
// RoleEntity,
|
||||||
|
// RoleItemEntity,
|
||||||
|
]),
|
||||||
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
|
JwtModule.register({
|
||||||
|
secret: 'secretKey',
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: expires_in,
|
||||||
|
// expiresIn: 3600,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, JwtStrategy],//, AuthResolver],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
export class AccessToken {
|
||||||
|
public user_id?: string;
|
||||||
|
|
||||||
|
public expires_in?: number;
|
||||||
|
|
||||||
|
public access_token?: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,169 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsUUID,
|
||||||
|
Length,
|
||||||
|
isNumber,
|
||||||
|
IsNumber,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class Login {
|
||||||
|
@ApiProperty({ description: '从小程序端获取的code' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public code: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '小程序码' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
public qunsense_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SureLogin {
|
||||||
|
@ApiProperty({ description: '小程序码' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
public qunsense_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IsLogin {
|
||||||
|
@ApiProperty({ description: '小程序码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public qunsense_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginMobile {
|
||||||
|
@ApiProperty({ description: '从小程序端获取的code' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public code: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '昵称' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
public nickName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '头像url' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
public avatarUrl?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '小程序码' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
public qunsense_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateUserDto {
|
||||||
|
@ApiProperty({ description: '登录时返回的用户id' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '昵称' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
public nickName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '头像url' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
public avatarUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '扫码的唯一随机字符串' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public randomString: string;
|
||||||
|
}
|
||||||
|
export class PollingDto {
|
||||||
|
@ApiProperty({ description: '扫码的唯一随机字符串' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public randomString: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResponseType {
|
||||||
|
/**
|
||||||
|
* access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRmZjZjNWU3LTAxMDUtNDgxYS1hYjkwLWUxODM1M2I5OTZjMyIsImlhdCI6MTY4MzIwNTY4MywiZXhwIjoxNjgzNTY1NjgzfQ.HxaFrRLdZr5U08EHeMQXuZjAFIrNSgXCtFjGIK0y3ko"
|
||||||
|
created_date: "2023-05-04T13:08:04.000Z"
|
||||||
|
expires_in: 2592000
|
||||||
|
user_id: "4ff6c5e7-0105-481a-ab90-e18353b996c3"
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
'接口调用凭证,如果返回该字段说明用户已经更新了头像和昵称,可以直接登录,否则需要更新头像',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
public access_token: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '创建时间' })
|
||||||
|
@IsString()
|
||||||
|
public created_date?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '有效时间' })
|
||||||
|
@IsNumber()
|
||||||
|
public expires_in: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户id' })
|
||||||
|
@IsString()
|
||||||
|
public user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginStatus {
|
||||||
|
@ApiPropertyOptional({ description: '登录状态返回' })
|
||||||
|
public status: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IsLoginResult {
|
||||||
|
@ApiPropertyOptional({ description: '错误信息' })
|
||||||
|
@IsString()
|
||||||
|
public msg: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '状态码 200 登录成功 400 已失效 500 系统错误 401 未登录',
|
||||||
|
})
|
||||||
|
@IsNumber()
|
||||||
|
public code?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '是否登录' })
|
||||||
|
@IsNumber()
|
||||||
|
public success: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
'接口调用凭证,如果返回该字段说明用户已经更新了头像和昵称,可以直接登录,否则需要更新头像',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
public access_token?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '创建时间', required: false })
|
||||||
|
@IsString()
|
||||||
|
public created_date?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '有效时间', required: false })
|
||||||
|
@IsNumber()
|
||||||
|
public expires_in?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户id', required: false })
|
||||||
|
@IsString()
|
||||||
|
public user_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QrResponse {
|
||||||
|
@ApiPropertyOptional({ description: '图片 base64' })
|
||||||
|
@IsString()
|
||||||
|
public image: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '图片类型' })
|
||||||
|
@IsString()
|
||||||
|
public extension?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '登录码' })
|
||||||
|
@IsString()
|
||||||
|
public code?: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
BadRequestException,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, In } from 'typeorm';
|
||||||
|
|
||||||
|
import { JwtPayload, UserJwtPayload } from './interfaces/jwt-payload.interface';
|
||||||
|
import { UserEntity } from '../user/user.entity';
|
||||||
|
import { Login, PollingDto, LoginMobile, UpdateUserDto, IsLogin } from './auth.params';
|
||||||
|
import redis from 'src/redis';
|
||||||
|
// import { RoleItemEntity } from '../role-item/role-item.entity';
|
||||||
|
// import { RoleEntity } from '../role/role.entity';
|
||||||
|
|
||||||
|
export const expires_in = 3600 * 24 * 30;
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
@InjectRepository(UserEntity)
|
||||||
|
private readonly userRepository: Repository<UserEntity>,
|
||||||
|
// @InjectRepository(RoleItemEntity)
|
||||||
|
// private readonly roleItemRepository: Repository<RoleItemEntity>,
|
||||||
|
// @InjectRepository(RoleEntity)
|
||||||
|
// private readonly roleRepository: Repository<RoleEntity>,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async verify(token: string) {
|
||||||
|
const result = this.jwtService.verify(token.replace('Bearer ', ''));
|
||||||
|
const user = await this.validateUser(result);
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
...user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUserToken(user: UserJwtPayload) {
|
||||||
|
const access_token = this.jwtService.sign({ id: user.id });
|
||||||
|
return {
|
||||||
|
expires_in,
|
||||||
|
access_token,
|
||||||
|
user_id: user.id,
|
||||||
|
created_date: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateUser(payload: UserJwtPayload): Promise<any> {
|
||||||
|
console.log(payload)
|
||||||
|
const user = await this.userRepository.findOne({ where: { id: payload.id } })
|
||||||
|
if (!user) throw new BadRequestException('用户不存在')
|
||||||
|
return { id: payload.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
async signInUser(user: UserEntity): Promise<string> {
|
||||||
|
return this.jwtService.sign({
|
||||||
|
id: user.id,
|
||||||
|
roles: ['user'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async polling(query: PollingDto) {
|
||||||
|
const id = await redis.get(query.randomString)
|
||||||
|
if (id) {
|
||||||
|
redis.del(query.randomString)
|
||||||
|
const user = await this.userRepository.findOne({ where: { id: id } })
|
||||||
|
const access_token = await this.createUserToken(user);
|
||||||
|
return {
|
||||||
|
...access_token,
|
||||||
|
user_id: user.id,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
// console.log('content',context)
|
||||||
|
// add your custom authentication logic here
|
||||||
|
// for example, call super.logIn(request) to establish a session.
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequest(err, user, info) {
|
||||||
|
if (err || !user) {
|
||||||
|
throw err || new UnauthorizedException();
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
export interface JwtPayload {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserJwtPayload {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
ExecutionContext,
|
||||||
|
CanActivate,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { PassportStrategy, AuthGuard } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(private readonly authService: AuthService) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
secretOrKey: 'secretKey',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
const user = await this.authService.validateUser(payload);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
@Catch(HttpException)
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
|
catch(exception: HttpException, host: ArgumentsHost) {
|
||||||
|
// console.log(exception)
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
if (request) {
|
||||||
|
// console.log('exception', exception);
|
||||||
|
const status = exception.getStatus();
|
||||||
|
if (!request.url.startsWith('/graphql')) {
|
||||||
|
return response.status(status).json(exception);
|
||||||
|
// return response.status(status).json({
|
||||||
|
// statusCode: status,
|
||||||
|
// timestamp: new Date().toISOString(),
|
||||||
|
// path: request.url,
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// const gqlHost = GqlArgumentsHost.create(host);
|
||||||
|
// console.log(11111, { ...exception, message: exception.message.message });
|
||||||
|
// FIXME
|
||||||
|
/* tslint:disable-next-line */
|
||||||
|
// @ts-ignore
|
||||||
|
exception.message = exception.message || exception.message;
|
||||||
|
// return { ...exception, message: exception.message.message };
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
PrimaryColumn,
|
||||||
|
Column,
|
||||||
|
BeforeInsert,
|
||||||
|
} from 'typeorm';
|
||||||
|
// import { ObjectType, Field } from '@nestjs/graphql';
|
||||||
|
const snowId = require('simple-flakeid')
|
||||||
|
let gen1 = new snowId.SnowflakeIdv1({ workerId: 1, seqBitLength: 2, workerIdBitLength: 1 })
|
||||||
|
// @ObjectType()
|
||||||
|
@Entity()
|
||||||
|
export class Base extends BaseEntity {
|
||||||
|
@BeforeInsert()
|
||||||
|
generateId() {
|
||||||
|
this.id = String(gen1.NextId())
|
||||||
|
}
|
||||||
|
// @Field()
|
||||||
|
@PrimaryColumn({ length: 12 })
|
||||||
|
// @Column({ type: 'varchar', length: '12', nullable: false })
|
||||||
|
@ApiProperty({ description: "数据唯一id" })
|
||||||
|
id: string;
|
||||||
|
// id: number;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
@ApiProperty({ description: "创建时间" })
|
||||||
|
created_date: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
@ApiProperty({ description: "更新时间" })
|
||||||
|
updated_date: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
// import xlsx from 'node-xlsx';
|
||||||
|
// import { fileClient } from '../lib/oss';
|
||||||
|
// import redis from '../redis';
|
||||||
|
import { getRepository } from 'typeorm';
|
||||||
|
// import { TeamEntity } from '../team/team.entity';
|
||||||
|
// import { MemberEntity } from '../member/member.entity';
|
||||||
|
import { eight_hour } from './const';
|
||||||
|
|
||||||
|
export function formatArgs(args: any) {
|
||||||
|
const newArgs = {
|
||||||
|
...args,
|
||||||
|
before: args.before && new Buffer(args.before, 'base64').toString(),
|
||||||
|
after: args.after && new Buffer(args.after, 'base64').toString(),
|
||||||
|
};
|
||||||
|
return newArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeName = (fileName: string) => {
|
||||||
|
// var fileName = fileName.replace(/\s+/g, '-')
|
||||||
|
// const fileName = fileName;
|
||||||
|
const dotPosition = fileName.lastIndexOf('.');
|
||||||
|
let name = fileName;
|
||||||
|
if (dotPosition > 0) {
|
||||||
|
name = fileName.substring(0, dotPosition);
|
||||||
|
}
|
||||||
|
let extension = '';
|
||||||
|
if (dotPosition !== -1) {
|
||||||
|
extension = fileName.substring(dotPosition);
|
||||||
|
}
|
||||||
|
// return Date.now() + extension;
|
||||||
|
return name + '-' + Date.now() + extension;
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const createXlsx = (base, data, token, name, columns) => {
|
||||||
|
// return setTimeout(async () => {
|
||||||
|
// try {
|
||||||
|
// columns = columns.filter(column => column !== '背景信息');
|
||||||
|
// base = base.map(base1 => {
|
||||||
|
// if (
|
||||||
|
// base1 === '是否愿意参加区域决赛前培训' &&
|
||||||
|
// columns.includes('是否愿意参加总决赛前培训')
|
||||||
|
// ) {
|
||||||
|
// return '是否愿意参加总决赛前培训';
|
||||||
|
// }
|
||||||
|
// if (base1 === '比赛结果' && columns.includes('区域决赛结果')) {
|
||||||
|
// return '区域决赛结果';
|
||||||
|
// }
|
||||||
|
// if (base1 === '比赛结果' && columns.includes('总决赛结果')) {
|
||||||
|
// return '总决赛结果';
|
||||||
|
// }
|
||||||
|
// return base1;
|
||||||
|
// });
|
||||||
|
// const new_data = data.map(item => {
|
||||||
|
// const new_item = columns.map(column => {
|
||||||
|
// let column_index = 0;
|
||||||
|
// base.forEach((rr, index) => {
|
||||||
|
// if (rr === column) {
|
||||||
|
// column_index = index;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// return item[column_index];
|
||||||
|
// });
|
||||||
|
// return new_item;
|
||||||
|
// });
|
||||||
|
// const file_name = '/xlsx/' + token + uuid() + '.xlsx';
|
||||||
|
// const buffer = xlsx.build([
|
||||||
|
// { name: name || 'Sheet1', data: [columns, ...new_data] },
|
||||||
|
// ]);
|
||||||
|
// const file_path = path.resolve(__dirname, '../puppeteer');
|
||||||
|
// const local_file_name = `${file_path}/${token}.xlsx`;
|
||||||
|
// fs.writeFileSync(local_file_name, buffer);
|
||||||
|
// await fileClient.put(file_name, local_file_name);
|
||||||
|
// fs.unlinkSync(local_file_name);
|
||||||
|
// await redis.set(token, file_name, 'EX', 3600);
|
||||||
|
// } catch (e) {
|
||||||
|
// console.log(`导出${name}错误`, e);
|
||||||
|
// }
|
||||||
|
// }, 100);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export const getUserTeamId = async (user_id: string) => {
|
||||||
|
// const user_team = await redis.get(`team:${user_id}`);
|
||||||
|
// if (!user_team) {
|
||||||
|
// const user_member = await getRepository(MemberEntity).findOne({
|
||||||
|
// owner_id: user_id,
|
||||||
|
// });
|
||||||
|
// if (!user_member) {
|
||||||
|
// throw new Error('用户不存在 team');
|
||||||
|
// }
|
||||||
|
// await redis.set(`team:${user_id}`, user_member.team_id);
|
||||||
|
// return user_member.team_id;
|
||||||
|
// }
|
||||||
|
// return user_team;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export const getUserTeamIdNoThrow = async (user_id: string) => {
|
||||||
|
// const user_team = await redis.get(`team:${user_id}`);
|
||||||
|
// if (!user_team) {
|
||||||
|
// const user_member = await getRepository(MemberEntity).findOne({
|
||||||
|
// owner_id: user_id,
|
||||||
|
// });
|
||||||
|
// if (!user_member) {
|
||||||
|
// return undefined;
|
||||||
|
// }
|
||||||
|
// await redis.set(`team:${user_id}`, user_member.team_id);
|
||||||
|
// return user_member.team_id;
|
||||||
|
// }
|
||||||
|
// return user_team;
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const getEightTime = () => {
|
||||||
|
return new Date(Date.now() + eight_hour);
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export const one_day = 24 * 60 * 60 * 1000;
|
||||||
|
export const time = 1000 * 60 * 60 * 8;
|
||||||
|
export const eight_hour = 1000 * 60 * 60 * 8;
|
||||||
|
export const hour = 1000 * 60 * 60;
|
||||||
|
export const BCRYPT_HASH_ROUNDS = 10;
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
export const Roles = (...roles: string[]) => {
|
||||||
|
console.log(roles)
|
||||||
|
return SetMetadata('roles', roles)
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const Test = (...args: any[]) => {
|
||||||
|
// console.log(...args);
|
||||||
|
// return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||||
|
// descriptor.enumerable = false;
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
|
||||||
|
// import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
// import { WsException } from '@nestjs/websockets';
|
||||||
|
|
||||||
|
export const User = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContextHost) => {
|
||||||
|
const type = ctx.getType();
|
||||||
|
if (type === 'http') {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user;
|
||||||
|
} else if (type === 'ws') {
|
||||||
|
const request = ctx.switchToWs().getClient();
|
||||||
|
return request.user;
|
||||||
|
} else {
|
||||||
|
// return GqlExecutionContext.create(ctx).getContext().req.user;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private readonly reflector: Reflector) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const roles = this.reflector.get<string[]>('roles', context.getHandler());
|
||||||
|
if (!roles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
let user;
|
||||||
|
if (request) {
|
||||||
|
user = request.user;
|
||||||
|
} else {
|
||||||
|
// fix graphql 需要特殊处理
|
||||||
|
user = context.getArgs()[2].req.user;
|
||||||
|
}
|
||||||
|
const hasRole = () =>
|
||||||
|
user.roles.some(role => !!roles.find(item => item === role));
|
||||||
|
console.log(roles,user.roles)
|
||||||
|
return user && user.roles && hasRole();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
export * from './decorators/roles.decorator';
|
||||||
|
export * from './decorators/user.decorator';
|
||||||
|
export * from './guards/roles.guard';
|
||||||
|
export * from './validation.pipe';
|
||||||
|
export * from './params/page.params';
|
||||||
|
export * from './params/id.params';
|
||||||
|
export * from './base.entity';
|
||||||
|
export * from './base.func';
|
||||||
|
export * from './base.type';
|
||||||
|
export * from './const';
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CacheInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const isCached = true;
|
||||||
|
if (isCached) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
BadGatewayException,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable, throwError } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ErrorsInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
return next
|
||||||
|
.handle()
|
||||||
|
.pipe(catchError(err => throwError(new BadGatewayException())));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { getMongoRepository } from 'typeorm';
|
||||||
|
// import { LogMongo } from '../../log/log.mongo';
|
||||||
|
|
||||||
|
function createGraphqlLog(request: Request, info, resolve_time: number) {
|
||||||
|
setImmediate(async () => {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const user = request.user;
|
||||||
|
// const new_log = new LogMongo();
|
||||||
|
// new_log.path_name = info.fieldName;
|
||||||
|
// new_log.parent_type_name = info.parentType.name;
|
||||||
|
// new_log.field_name = info.fieldName;
|
||||||
|
// new_log.type = 'graphql';
|
||||||
|
// new_log.ip = request.ip;
|
||||||
|
// new_log.resolve_time = resolve_time;
|
||||||
|
|
||||||
|
// new_log.content_length = request.headers['content-length'];
|
||||||
|
// new_log.user_agent = request.headers['user-agent'];
|
||||||
|
// new_log.sec_fetch_site = String(request.headers['sec-fetch-site']);
|
||||||
|
// new_log.referer = request.headers.referer;
|
||||||
|
// new_log.method = request.method;
|
||||||
|
// if (user) {
|
||||||
|
// new_log.user_id = user.id;
|
||||||
|
// new_log.roles = user.roles;
|
||||||
|
// new_log.permissions = user.permissions;
|
||||||
|
// }
|
||||||
|
// if (request.body.query) {
|
||||||
|
// new_log.query = request.body.query;
|
||||||
|
// }
|
||||||
|
// await getMongoRepository(LogMongo, 'mongo').save(new_log);
|
||||||
|
Logger.debug(`graphql ${info.fieldName} [${resolve_time} ms]`);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(`http graphql 中间件错误 ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createApiLog(request: Request, resolve_time: number) {
|
||||||
|
setImmediate(async () => {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const user = request.user;
|
||||||
|
// const new_log = new LogMongo();
|
||||||
|
// new_log.path_name = request.originalUrl;
|
||||||
|
// // new_log.parent_type_name = info.parentType.name;
|
||||||
|
// // new_log.field_name = info.fieldName;
|
||||||
|
// new_log.type = 'api';
|
||||||
|
// new_log.ip = request.ip;
|
||||||
|
// new_log.resolve_time = resolve_time;
|
||||||
|
|
||||||
|
// new_log.content_length = request.headers['content-length'];
|
||||||
|
// new_log.user_agent = request.headers['user-agent'];
|
||||||
|
// new_log.sec_fetch_site = String(request.headers['sec-fetch-site']);
|
||||||
|
// new_log.referer = request.headers.referer;
|
||||||
|
// new_log.method = request.method;
|
||||||
|
// if (user) {
|
||||||
|
// new_log.user_id = user.id;
|
||||||
|
// new_log.roles = user.roles;
|
||||||
|
// new_log.permissions = user.permissions;
|
||||||
|
// }
|
||||||
|
// if (request.body) {
|
||||||
|
// new_log.body = JSON.stringify(request.body);
|
||||||
|
// }
|
||||||
|
// if (request.query) {
|
||||||
|
// new_log.query = JSON.stringify(request.query);
|
||||||
|
// }
|
||||||
|
// if (request.params) {
|
||||||
|
// new_log.params = JSON.stringify(request.params);
|
||||||
|
// }
|
||||||
|
// await getMongoRepository(LogMongo, 'mongo').save(new_log);
|
||||||
|
Logger.debug(`api ${request.url} [${resolve_time} ms]`);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(`http api 中间件错误 ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拦截所有的请求 并记录所有请求的时间
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoggingInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const start = Date.now();
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap(() => {
|
||||||
|
const ctx = context.switchToHttp();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
const resolve_time = Date.now() - start;
|
||||||
|
if (!request || request.url.startsWith('/graphql')) {
|
||||||
|
// @ts-ignore
|
||||||
|
const info = context.args[3];
|
||||||
|
// @ts-ignore
|
||||||
|
const old_request = context.args[2].request;
|
||||||
|
createGraphqlLog(old_request, info, resolve_time);
|
||||||
|
} else if (request) {
|
||||||
|
createApiLog(request, resolve_time);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { timeout } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TimeoutInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
return next.handle().pipe(timeout(5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsInt, IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
export class Id {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
// @IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
|
readonly id: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsInt } from 'class-validator';
|
||||||
|
|
||||||
|
export class Page {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
readonly skip: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
readonly limit: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
// import { CustomScalar, Scalar } from '@nestjs/graphql';
|
||||||
|
// import { Kind } from 'graphql';
|
||||||
|
|
||||||
|
// @Scalar('Date', type => Date)
|
||||||
|
// export class DateScalar implements CustomScalar<number, Date> {
|
||||||
|
// description = 'Date custom scalar type';
|
||||||
|
|
||||||
|
// parseValue(value: number): Date {
|
||||||
|
// return new Date(value); // value from the client
|
||||||
|
// }
|
||||||
|
|
||||||
|
// serialize(value: Date): number {
|
||||||
|
// return value.getTime(); // value sent to the client
|
||||||
|
// }
|
||||||
|
|
||||||
|
// parseLiteral(ast: any): Date {
|
||||||
|
// if (ast.kind === Kind.INT) {
|
||||||
|
// return new Date(ast.value);
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
PipeTransform,
|
||||||
|
Injectable,
|
||||||
|
ArgumentMetadata,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { validate } from 'class-validator';
|
||||||
|
import { plainToClass } from 'class-transformer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidationPipe implements PipeTransform<any> {
|
||||||
|
async transform(value, { type, metatype }: ArgumentMetadata) {
|
||||||
|
console.log(value)
|
||||||
|
if (type === 'query' && value.skip && value.take) {
|
||||||
|
value = {
|
||||||
|
...value,
|
||||||
|
skip: ((parseInt(value.skip, 10) || 1) - 1) * (parseInt(value.take, 10) || 10),
|
||||||
|
take: parseInt(value.take, 10) || 10,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!metatype || !this.toValidate(metatype)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
console.log(value)
|
||||||
|
const object = plainToClass(metatype, value);
|
||||||
|
const errors = await validate(object);
|
||||||
|
console.log(errors)
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new BadRequestException(errors);
|
||||||
|
// throw new BadRequestException('Validation failed');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toValidate(metatype): boolean {
|
||||||
|
const types = [String, Boolean, Number, Array, Object];
|
||||||
|
return !types.find(type => metatype === type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
export const configCheck = {
|
||||||
|
// NODE_ENV: Joi.string()
|
||||||
|
// .valid(['development', 'production', 'test', 'provision'])
|
||||||
|
// .default('development'),
|
||||||
|
// PORT: Joi.number().default(9999),
|
||||||
|
// SERVER_URL: Joi.string().required(),
|
||||||
|
// CLIENT_URL: Joi.string().required(),
|
||||||
|
// UI_URL: Joi.string().required(),
|
||||||
|
// // API_RUL: Joi.string().required(),
|
||||||
|
// PC_URL: Joi.string().required(),
|
||||||
|
// MOBILE_URL: Joi.string().required(),
|
||||||
|
// API_AUTH_ENABLED: Joi.boolean().required(),
|
||||||
|
// SUPER_CODE: Joi.string().required(),
|
||||||
|
|
||||||
|
// SMS_ACCESS_KEY_ID: Joi.string().required(),
|
||||||
|
// SMS_SECRET_ACCESS_KEY: Joi.string().required(),
|
||||||
|
// SMS_TEMPLATE_CODE: Joi.string().required(),
|
||||||
|
// SMS_TEMPLATE_FOREIGN_CODE: Joi.string().required(),
|
||||||
|
// SMS_TEMPLATE_SIGN_NAME: Joi.string().required(),
|
||||||
|
// SMS_TEMPLATE_CHANGE_DEMAND_STATUS: Joi.string().required(),
|
||||||
|
// SMS_TEMPLATE_CHANGE_DEMAND_END_DATE: Joi.string().required(),
|
||||||
|
// SMS_TEMPLATE_SUBSCRIPTION: Joi.string().required(),
|
||||||
|
|
||||||
|
// OSS_REGION: Joi.string().required(),
|
||||||
|
// OSS_ACCESS_KEY_ID: Joi.string().required(),
|
||||||
|
// OSS_ACCESS_KEY_SECRET: Joi.string().required(),
|
||||||
|
// OSS_BUCKET: Joi.string().required(),
|
||||||
|
// OSS_FILE_HOST: Joi.string().required(),
|
||||||
|
|
||||||
|
REDIS_PORT: Joi.number().required(),
|
||||||
|
REDIS_HOST: Joi.string().required(),
|
||||||
|
REDIS_FAMILY: Joi.number().required(),
|
||||||
|
REDIS_DB: Joi.number().required(),
|
||||||
|
|
||||||
|
// WE_CHAT_APP_ID: Joi.string().required(),
|
||||||
|
// WE_CHAT_APP_SECRET: Joi.string().required(),
|
||||||
|
// WE_CHAT_PC_APP_ID: Joi.string().required(),
|
||||||
|
// WE_CHAT_PC_APP_SECRET: Joi.string().required(),
|
||||||
|
// WE_CHAT_TOKEN: Joi.string().required(),
|
||||||
|
// WE_CHAT_PAY_PASSWORD: Joi.string().required(),
|
||||||
|
// WE_CHAT_PAY_CODE: Joi.string().required(),
|
||||||
|
// WE_CHAT_PAY_SECRET: Joi.string().required(),
|
||||||
|
// WE_CHAT_PAY_NOTIFY_URL: Joi.string().required(),
|
||||||
|
// WE_CHAT_PAY_PLAN_NOTIFY_URL: Joi.string().required(),
|
||||||
|
// WE_CHAT_UPDATE_PAY_PLAN_NOTIFY_URL: Joi.string().required(),
|
||||||
|
|
||||||
|
// ALI_PAY_APP_ID: Joi.string().required(),
|
||||||
|
// ALI_PAY_NOTIFY_URL: Joi.string().required(),
|
||||||
|
// ALI_PAY_PLAN_NOTIFY_URL: Joi.string().required(),
|
||||||
|
// ALI_PAY_UPDATE_PLAN_NOTIFY_URL: Joi.string().required(),
|
||||||
|
|
||||||
|
DOC: Joi.boolean().required(),
|
||||||
|
|
||||||
|
MYSQL_HOST: Joi.string().required(),
|
||||||
|
MYSQL_USERNAME: Joi.string().required(),
|
||||||
|
MYSQL_PASSWORD: Joi.string().required(),
|
||||||
|
MYSQL_DATABASE: Joi.string().required(),
|
||||||
|
MYSQL_PORT: Joi.number().required(),
|
||||||
|
|
||||||
|
// ENTERPRISE_TOKEN: Joi.string().required(),
|
||||||
|
// ENTERPRISE_ENCODING_AES_KEY: Joi.string().required(),
|
||||||
|
// ENTERPRISE_SUITE_ID: Joi.string().required(),
|
||||||
|
// ENTERPRISE_AGENT_ID: Joi.string().required(),
|
||||||
|
// ENTERPRISE_SECRET: Joi.string().required(),
|
||||||
|
|
||||||
|
// REB_ENVELOPE_TOKEN: Joi.string(),
|
||||||
|
// REB_ENVELOPE_SUITE_ID: Joi.string(),
|
||||||
|
// REB_ENVELOPE_SECRET: Joi.string(),
|
||||||
|
// REB_ENVELOPE_ENCODING_AES_KEY: Joi.string(),
|
||||||
|
// REB_ENVELOPE_AGENT_ID: Joi.string(),
|
||||||
|
};
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ConfigService,
|
||||||
|
useValue: new ConfigService(
|
||||||
|
`${process.env.NODE_ENV || 'development'}.env`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [ConfigService],
|
||||||
|
})
|
||||||
|
export class ConfigModule {}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
|
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config({ path: `${process.env.NODE_ENV || 'development'}.env` });
|
||||||
|
// import { mongo_entities } from './config.entities'
|
||||||
|
|
||||||
|
// const API_STATUS = process.env.API_STATUS;
|
||||||
|
|
||||||
|
export const mysql_config: TypeOrmModuleOptions = {
|
||||||
|
name: 'default',
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.MYSQL_HOST,
|
||||||
|
port: Number(process.env.MYSQL_PORT),
|
||||||
|
username: process.env.MYSQL_USERNAME,
|
||||||
|
password: process.env.MYSQL_PASSWORD,
|
||||||
|
database: process.env.MYSQL_DATABASE,
|
||||||
|
entities: [join(__dirname, '../**/**.entity{.ts,.js}')],
|
||||||
|
// entities: [join(__dirname, '**/**.entity{.ts,.js}')],
|
||||||
|
synchronize: true,
|
||||||
|
// migrations: ["/migrations/"],
|
||||||
|
// logger: "advanced-console"
|
||||||
|
// logging: "all",
|
||||||
|
cache: {
|
||||||
|
duration: 1000,
|
||||||
|
// alwaysEnabled: true,
|
||||||
|
type: 'ioredis',
|
||||||
|
options: {
|
||||||
|
port: Number(process.env.REDIS_PORT) || 6370, // Redis port
|
||||||
|
host: process.env.REDIS_HOST, // '127.0.0.1' Redis host
|
||||||
|
},
|
||||||
|
}, // 暂时缓存 10 秒 加入 全局缓存后暂时报错 需要做特殊处理
|
||||||
|
// cache: { duration: 1000, alwaysEnabled: true }, // 暂时缓存 60 秒 因为内存溢出暂时放弃
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mysql_migration_config: DataSourceOptions = {
|
||||||
|
name: 'default',
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.MYSQL_HOST,
|
||||||
|
port: Number(process.env.MYSQL_PORT),
|
||||||
|
username: process.env.MYSQL_USERNAME,
|
||||||
|
password: process.env.MYSQL_PASSWORD,
|
||||||
|
database: process.env.MYSQL_DATABASE,
|
||||||
|
entities: [join(__dirname, '../**/**.entity{.ts,.js}')],
|
||||||
|
migrations: [join(__dirname, '../../migrations/*.ts')],
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export const db_config = mysql_config;
|
||||||
|
// 迁移使用的连接
|
||||||
|
const migrationDataSource = new DataSource(mysql_migration_config)
|
||||||
|
export default migrationDataSource
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
import { configCheck } from './config.check';
|
||||||
|
|
||||||
|
export interface EnvConfig {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConfigService {
|
||||||
|
private readonly envConfig: { [key: string]: string };
|
||||||
|
|
||||||
|
constructor(filePath: string) {
|
||||||
|
const config = dotenv.parse(fs.readFileSync(filePath));
|
||||||
|
this.envConfig = this.validateInput(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string): string {
|
||||||
|
return this.envConfig[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
get isApiAuthEnabled(): boolean {
|
||||||
|
return Boolean(this.envConfig.API_AUTH_ENABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures all needed variables are set, and returns the validated JavaScript object
|
||||||
|
* including the applied default values.
|
||||||
|
*/
|
||||||
|
private validateInput(envConfig: EnvConfig): EnvConfig {
|
||||||
|
const envVarsSchema: Joi.ObjectSchema = Joi.object(configCheck);
|
||||||
|
|
||||||
|
const { error, value: validatedEnvConfig } =envVarsSchema.validate(
|
||||||
|
this.envConfig
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Config validation error: ${error.message}`);
|
||||||
|
}
|
||||||
|
return validatedEnvConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { WeChatPayModuleAsyncOptions } from 'nest-wechatpay-node-v3'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
// import { mongo_entities } from './config.entities'
|
||||||
|
|
||||||
|
// const API_STATUS = process.env.API_STATUS;
|
||||||
|
|
||||||
|
export const wechatpay_config: WeChatPayModuleAsyncOptions = {
|
||||||
|
useFactory: async () => {
|
||||||
|
return {
|
||||||
|
appid: process.env.WE_CHAT_PAY_APP_ID,
|
||||||
|
mchid: process.env.WE_CHAT_PAY_MCHID,
|
||||||
|
publicKey: fs.readFileSync('/Users/mac/foreign/群声/api/README.md'), // 公钥
|
||||||
|
privateKey: fs.readFileSync('/Users/mac/foreign/群声/api/README.md'), // 秘钥
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const db_config = wechatpay_config;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { Column, Entity } from 'typeorm';
|
||||||
|
import { Base } from '../common';
|
||||||
|
|
||||||
|
@Entity({ name: 'files' })
|
||||||
|
export class FileEntity extends Base {
|
||||||
|
@Column({ length: 50, nullable: false })
|
||||||
|
public name: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { FileController } from './file.controller';
|
||||||
|
import { FileEntity } from './file.entity';
|
||||||
|
import { FileService } from './file.service';
|
||||||
|
import { ConfigModule } from '../config/config.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([FileEntity]), ConfigModule],
|
||||||
|
controllers: [FileController],
|
||||||
|
providers: [FileService],
|
||||||
|
exports: [FileService],
|
||||||
|
})
|
||||||
|
export class FileModule {}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateFile {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Base64File {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public base64: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileType {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public type: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { FileEntity } from './file.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FileService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(FileEntity)
|
||||||
|
private readonly fileRepository: Repository<FileEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(id: string): Promise<FileEntity> {
|
||||||
|
const new_file = Object.assign(new FileEntity(), { id });
|
||||||
|
const file = await this.fileRepository.save(new_file);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { List } from '../common';
|
||||||
|
import { FileEntity } from './file.entity';
|
||||||
|
|
||||||
|
export class FileList extends List {
|
||||||
|
list: FileEntity[];
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
|
const config: any = {
|
||||||
|
port: Number(process.env.REDIS_PORT), // Redis port
|
||||||
|
host: process.env.REDIS_HOST, // '127.0.0.1' Redis host
|
||||||
|
// family: Number(process.env.REDIS_FAMILY) || 4, // 4 (IPv4) or 6 (IPv6)
|
||||||
|
// password: 'auth',
|
||||||
|
// db: Number(process.env.REDIS_DB) || 0,
|
||||||
|
// enableReadyCheck: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.REDIS_PASSWORD) {
|
||||||
|
config.password = process.env.REDIS_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redis = new Redis(config);
|
||||||
|
|
||||||
|
export default redis;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export class CreateRoleDto {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateRoleDto } from './create-role.dto';
|
||||||
|
|
||||||
|
export class UpdateRoleDto extends PartialType(CreateRoleDto) {}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export class Role {}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { RoleController } from './role.controller';
|
||||||
|
import { RoleService } from './role.service';
|
||||||
|
|
||||||
|
describe('RoleController', () => {
|
||||||
|
let controller: RoleController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [RoleController],
|
||||||
|
providers: [RoleService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<RoleController>(RoleController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||||
|
import { RoleService } from './role.service';
|
||||||
|
import { CreateRoleDto } from './dto/create-role.dto';
|
||||||
|
import { UpdateRoleDto } from './dto/update-role.dto';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiTags('用户角色')
|
||||||
|
@Controller('roles')
|
||||||
|
export class RoleController {
|
||||||
|
constructor(private readonly roleService: RoleService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() createRoleDto: CreateRoleDto) {
|
||||||
|
return this.roleService.create(createRoleDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll() {
|
||||||
|
return this.roleService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.roleService.findOne(+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
update(@Param('id') id: string, @Body() updateRoleDto: UpdateRoleDto) {
|
||||||
|
return this.roleService.update(+id, updateRoleDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.roleService.remove(+id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RoleService } from './role.service';
|
||||||
|
import { RoleController } from './role.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [RoleController],
|
||||||
|
providers: [RoleService]
|
||||||
|
})
|
||||||
|
export class RoleModule {}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { RoleService } from './role.service';
|
||||||
|
|
||||||
|
describe('RoleService', () => {
|
||||||
|
let service: RoleService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [RoleService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<RoleService>(RoleService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CreateRoleDto } from './dto/create-role.dto';
|
||||||
|
import { UpdateRoleDto } from './dto/update-role.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleService {
|
||||||
|
create(createRoleDto: CreateRoleDto) {
|
||||||
|
return 'This action adds a new role';
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return `This action returns all role`;
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(id: number) {
|
||||||
|
return `This action returns a #${id} role`;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: number, updateRoleDto: UpdateRoleDto) {
|
||||||
|
return `This action updates a #${id} role`;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(id: number) {
|
||||||
|
return `This action removes a #${id} role`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
|
||||||
|
import { IsEnum, IsOptional, IsString } from "class-validator"
|
||||||
|
import { CommonPageArgs } from "src/common"
|
||||||
|
|
||||||
|
export class QueryUserDto extends CommonPageArgs {
|
||||||
|
@ApiPropertyOptional({ description: '真实姓名' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
true_name: string
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "根据名称模糊搜索", required: false })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
|
||||||
|
import { IsEnum, IsOptional, IsString } from "class-validator"
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
UsePipes,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import {
|
||||||
|
Repository,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ValidationPipe,
|
||||||
|
User,
|
||||||
|
Roles,
|
||||||
|
RolesGuard,
|
||||||
|
} from '../common';
|
||||||
|
import { UserEntity } from './user.entity';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
|
@ApiTags('系统用户')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('users')
|
||||||
|
export class UserController {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(UserEntity)
|
||||||
|
private readonly userRepository: Repository<UserEntity>,
|
||||||
|
private readonly userService: UserService
|
||||||
|
// @Inject(forwardRef(() => AuthService))
|
||||||
|
// private readonly authService: AuthService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
@Get('/viewer')
|
||||||
|
@ApiOperation({ summary: '获取个人信息' })
|
||||||
|
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
||||||
|
@UsePipes(new ValidationPipe())
|
||||||
|
@Roles('company', 'tester', 'admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '返回参数说明',
|
||||||
|
})
|
||||||
|
async viewer(@User() viewer: UserEntity) {
|
||||||
|
const result = await this.userRepository.findOne({ where: { id: viewer.id } })
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { BaseEntity, Column, Entity, Index, JoinColumn, OneToMany, OneToOne } from 'typeorm';
|
||||||
|
import { createHmac } from 'crypto';
|
||||||
|
import { Base } from '../common';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
// 用户表
|
||||||
|
@Entity({ name: 'users' })
|
||||||
|
export class UserEntity extends Base {
|
||||||
|
@ApiProperty({ description: '真实姓名' })
|
||||||
|
@Column({ length: 50, nullable: true, type: 'char' })
|
||||||
|
public true_name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '年龄' })
|
||||||
|
@Column({ nullable: true, type: 'int' })
|
||||||
|
public age: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '手机号' })
|
||||||
|
@Column({ length: 50, nullable: true, type: 'char', select: false })
|
||||||
|
public mobile?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户头像' })
|
||||||
|
@Column({ nullable: true, length: 255, type: 'char' })
|
||||||
|
public avatar?: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { UserEntity } from './user.entity';
|
||||||
|
import { UserController } from './user.controller';
|
||||||
|
// import { AuthService } from '../auth/auth.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
UserEntity,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// TesterModule
|
||||||
|
// AuthModule,
|
||||||
|
// forwardRef(() => AuthModule),
|
||||||
|
],
|
||||||
|
providers: [UserService],
|
||||||
|
controllers: [UserController],
|
||||||
|
exports: [UserService],
|
||||||
|
})
|
||||||
|
export class UserModule { }
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { Base } from '../common';
|
||||||
|
import { UserEntity } from './user.entity';
|
||||||
|
|
||||||
|
export class OtherUser extends Base {
|
||||||
|
public nickname?: string;
|
||||||
|
|
||||||
|
// mobile(@Root() { mobile }: UserEntity) {
|
||||||
|
// if (!mobile) {
|
||||||
|
// return mobile;
|
||||||
|
// }
|
||||||
|
// return mobile.substr(0, 3) + '****' + mobile.substr(7);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Field(type => String, { nullable: true })
|
||||||
|
// true_name(@Root() { true_name }: UserEntity) {
|
||||||
|
// if (!true_name) {
|
||||||
|
// return true_name;
|
||||||
|
// }
|
||||||
|
// return true_name.replace(true_name.substr(0, 1), '*');
|
||||||
|
// }
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsBoolean } from 'class-validator';
|
||||||
|
import { CommonFilter } from '../common';
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { BadRequestException, HttpException, HttpStatus, Inject, Injectable, Logger, forwardRef } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { In, Repository, Like, DataSource } from 'typeorm';
|
||||||
|
import { UserEntity } from './user.entity';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import * as bluebird from 'bluebird';
|
||||||
|
import { BCRYPT_HASH_ROUNDS } from '../common';
|
||||||
|
import { QueryUserDto } from './query-user.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(UserEntity)
|
||||||
|
private readonly userRepository: Repository<UserEntity>,
|
||||||
|
private readonly dataSource: DataSource
|
||||||
|
) { }
|
||||||
|
findOneByToken(token: string) {
|
||||||
|
return false;
|
||||||
|
// return true;
|
||||||
|
}
|
||||||
|
findOneByName(username: string) {
|
||||||
|
// return this.userRepository.findOne({ where: { role } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByIdNoRelation(id: string) {
|
||||||
|
return this.userRepository.createQueryBuilder('user')
|
||||||
|
.where(`user.id='${id}'`)
|
||||||
|
.getOne()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理后台的成员查询
|
||||||
|
async findAllAndCount() {
|
||||||
|
const [list, count] = await this.userRepository.createQueryBuilder('user')
|
||||||
|
// .select(['id', 'nickName','team_id'])
|
||||||
|
// .addSelect('user.avatar')
|
||||||
|
.getManyAndCount()
|
||||||
|
return { list, count }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
import { Connection, List } from '../common';
|
||||||
|
import { UserEntity } from './user.entity';
|
||||||
|
|
||||||
|
export class UserList extends List {
|
||||||
|
list: UserEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChartItem {
|
||||||
|
key: Date;
|
||||||
|
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as iosredis from 'ioredis'
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
import { AppService } from 'src/app.service';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
providers:[AppService]
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
// iosredis.disconnect()
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^src/(.*)$": "<rootDir>/../src/$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue