├── .prettierrc ├── .env ├── src ├── logs │ ├── logs.service.ts │ ├── logs.controller.ts │ ├── __tests__ │ │ ├── logs.service.spec.ts │ │ └── logs.controller.spec.ts │ ├── logs.entity.ts │ └── logs.module.ts ├── user │ ├── dto │ │ ├── get-user.dto.ts │ │ └── create-user.dto.ts │ ├── profile.entity.ts │ ├── __tests__ │ │ ├── user.service.spec.ts │ │ └── user.controller.spec.ts │ ├── pipes │ │ └── create-user.pipe.ts │ ├── user.module.ts │ ├── user.entity.ts │ ├── user.controller.ts │ └── user.service.ts ├── roles │ ├── dto │ │ ├── create-role.dto.ts │ │ └── update-role.dto.ts │ ├── roles.entity.ts │ ├── roles.module.ts │ ├── roles.controller.ts │ ├── __tests__ │ │ ├── roles.service.spec.ts │ │ └── roles.controller.spec.ts │ └── roles.service.ts ├── auth │ ├── dto │ │ └── signin-user.dto.ts │ ├── auth.service.spec.ts │ ├── auth.controller.spec.ts │ ├── auth.strategy.ts │ ├── auth.controller.ts │ ├── auth.module.ts │ └── auth.service.ts ├── enum │ └── config.enum.ts ├── configuration.ts ├── filters │ ├── typeorm.filter.ts │ ├── http-exception.filter.ts │ └── all-exception.filter.ts ├── main.ts ├── guards │ ├── admin │ │ └── admin.guard.ts │ └── jwt.guard.ts └── app.module.ts ├── tsconfig.build.json ├── nest-cli.json ├── config ├── config.yml ├── config.development.yml └── config.production.yml ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── .env.production ├── .env.development ├── tsconfig.json ├── docker-compose.yml ├── webpack-hmr.config.js ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DB_TYPE=mysql 2 | DB_HOST=127.0.0.1 3 | DB_PORT=3090 4 | 5 | DB_SYNC=false 6 | -------------------------------------------------------------------------------- /src/logs/logs.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class LogsService {} 5 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/logs/logs.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | @Controller('logs') 4 | export class LogsController {} 5 | -------------------------------------------------------------------------------- /src/user/dto/get-user.dto.ts: -------------------------------------------------------------------------------- 1 | interface UserQeury { 2 | page: number; 3 | limit?: number; 4 | username?: string; 5 | role?: number; 6 | gender?: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/roles/dto/create-role.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty } from 'class-validator'; 2 | 3 | export class CreateRoleDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | name: string; 7 | } 8 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/roles/dto/update-role.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateRoleDto } from './create-role.dto'; 3 | 4 | export class UpdateRoleDto extends PartialType(CreateRoleDto) {} 5 | -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | http: 2 | host: 'localhost' 3 | port: 8080 4 | 5 | dbs: 6 | postgres: 7 | url: 'localhost' 8 | port: 5432 9 | database: 'yaml-db' 10 | 11 | sqlite: 12 | database: 'sqlite.db' 13 | -------------------------------------------------------------------------------- /config/config.development.yml: -------------------------------------------------------------------------------- 1 | http: 2 | host: 'localhost' 3 | port: 8080 4 | 5 | dbs: 6 | postgres: 7 | url: 'localhost' 8 | port: 5432 9 | database: 'yaml-dev' 10 | 11 | sqlite: 12 | database: 'sqlite.db' 13 | -------------------------------------------------------------------------------- /config/config.production.yml: -------------------------------------------------------------------------------- 1 | http: 2 | host: 'localhost' 3 | port: 8080 4 | 5 | dbs: 6 | postgres: 7 | url: 'localhost' 8 | port: 5432 9 | database: 'yaml-pro' 10 | 11 | sqlite: 12 | database: 'sqlite.db' 13 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | DB_DATABASE=proddb 2 | DB_HOST=yourdomain.com 3 | DB_PORT=3306 4 | 5 | DB_USERNAME=root 6 | DB_PASSWORD=long-random-password 7 | 8 | DB_SYNC=true 9 | 10 | # JWT secret 11 | SECRET="mHgGuoJYOuiXILqnXyqKQ1joRQ6YhoPBb6CUfVp6qi1BBK2rtQNjsgaxRxRAsOii" -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | DB_DATABASE=testdb 2 | DB_HOST=127.0.0.1 3 | DB_PORT=3090 4 | 5 | DB_USERNAME=root 6 | DB_PASSWORD=123456 7 | 8 | # 用在开发过程中,作用:同步实体->数据库 9 | DB_SYNC=true 10 | 11 | LOG_ON=false 12 | TIMESTAMP=true 13 | 14 | # JWT secret 15 | SECRET="mHgGuoJYOuiXILqnXyqKQ1joRQ6YhoPBb6CUfVp6qi1BBK2rtQNjsgaxRxRAsOii" -------------------------------------------------------------------------------- /src/roles/roles.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { User } from '../user/user.entity'; 3 | 4 | @Entity() 5 | export class Roles { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | name: string; 11 | 12 | @ManyToMany(() => User, (user) => user.roles) 13 | users: User[]; 14 | } 15 | -------------------------------------------------------------------------------- /src/user/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, Length } from 'class-validator'; 2 | import { Roles } from 'src/roles/roles.entity'; 3 | 4 | export class CreateUserDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | @Length(6, 20) 8 | username: string; 9 | 10 | @IsString() 11 | @IsNotEmpty() 12 | @Length(6, 64) 13 | password: string; 14 | 15 | roles?: Roles[] | number[]; 16 | } 17 | -------------------------------------------------------------------------------- /src/roles/roles.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RolesController } from './roles.controller'; 3 | import { RolesService } from './roles.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Roles } from './roles.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Roles])], 9 | controllers: [RolesController], 10 | providers: [RolesService], 11 | }) 12 | export class RolesModule {} 13 | -------------------------------------------------------------------------------- /src/roles/roles.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, UseGuards } from '@nestjs/common'; 2 | import { RolesService } from './roles.service'; 3 | import { JwtGuard } from 'src/guards/jwt.guard'; 4 | 5 | @Controller('roles') 6 | @UseGuards(JwtGuard) 7 | export class RolesController { 8 | constructor(private readonly rolesService: RolesService) {} 9 | @Get(':id') 10 | findOne(@Param('id') id: string) { 11 | return this.rolesService.findOne(+id); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/auth/dto/signin-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, Length } from 'class-validator'; 2 | 3 | export class SigninUserDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | @Length(6, 20, { 7 | // $value: 当前用户传入的值 8 | // $property: 当前属性名 9 | // $target: 当前类 10 | // $constraint1: 最小长度 ... 11 | message: `用户名长度必须在$constraint1到$constraint2之间,当前传递的值是:$value`, 12 | }) 13 | username: string; 14 | 15 | @IsString() 16 | @IsNotEmpty() 17 | @Length(6, 32) 18 | password: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/user/profile.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | JoinColumn, 5 | OneToOne, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { User } from './user.entity'; 9 | 10 | @Entity() 11 | export class Profile { 12 | @PrimaryGeneratedColumn() 13 | id: number; 14 | 15 | @Column() 16 | gender: number; 17 | 18 | @Column() 19 | photo: string; 20 | 21 | @Column() 22 | address: string; 23 | 24 | @OneToOne(() => User) //链表 25 | @JoinColumn() //增加表的管理 26 | user: User; 27 | } 28 | -------------------------------------------------------------------------------- /src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/logs/__tests__/logs.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { LogsService } from '../logs.service'; 3 | 4 | describe('LogsService', () => { 5 | let service: LogsService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [LogsService], 10 | }).compile(); 11 | 12 | service = module.get(LogsService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/user/__tests__/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService } from '../user.service'; 3 | 4 | describe('UserService', () => { 5 | let service: UserService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserService], 10 | }).compile(); 11 | 12 | service = module.get(UserService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/roles/__tests__/roles.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RolesService } from '../roles.service'; 3 | 4 | describe('RolesService', () => { 5 | let service: RolesService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [RolesService], 10 | }).compile(); 11 | 12 | service = module.get(RolesService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/logs/logs.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | JoinColumn, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { User } from '../user/user.entity'; 9 | 10 | @Entity() 11 | export class Logs { 12 | @PrimaryGeneratedColumn() 13 | id: number; 14 | 15 | @Column() 16 | path: string; 17 | 18 | @Column() 19 | methods: string; 20 | 21 | @Column() 22 | data: string; 23 | 24 | @Column() 25 | result: number; 26 | 27 | @ManyToOne(() => User, (user) => user.logs) 28 | @JoinColumn() 29 | user: User; 30 | } 31 | -------------------------------------------------------------------------------- /src/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthController } from './auth.controller'; 3 | 4 | describe('AuthController', () => { 5 | let controller: AuthController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [AuthController], 10 | }).compile(); 11 | 12 | controller = module.get(AuthController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/enum/config.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ConfigEnum { 2 | DB_TYPE = 'DB_TYPE', 3 | DB_HOST = 'DB_HOST', 4 | DB_PORT = 'DB_PORT', 5 | DB_DATABASE = 'DB_DATABASE', 6 | DB_USERNAME = 'DB_USERNAME', 7 | DB_PASSWORD = 'DB_PASSWORD', 8 | DB_SYNC = 'DB_SYNC', 9 | 10 | SECRET = 'SECRET', 11 | 12 | REDIS_HOST = 'REDIS_HOST', 13 | REDIS_PORT = 'REDIS_PORT', 14 | REDIS_PASSWORD = 'REDIS_PASSWORD', 15 | REDIS_RECONNECT = 'REDIS_RECONNECT', 16 | } 17 | 18 | export enum LogEnum { 19 | LOG_LEVEL = 'LOG_LEVEL', 20 | LOG_ON = 'LOG_ON', 21 | TIMESTAMP = 'TIMESTAMP', 22 | } 23 | -------------------------------------------------------------------------------- /src/logs/__tests__/logs.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { LogsController } from '../logs.controller'; 3 | 4 | describe('LogsController', () => { 5 | let controller: LogsController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [LogsController], 10 | }).compile(); 11 | 12 | controller = module.get(LogsController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/user/__tests__/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserController } from '../user.controller'; 3 | 4 | describe('UserController', () => { 5 | let controller: UserController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [UserController], 10 | }).compile(); 11 | 12 | controller = module.get(UserController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/roles/__tests__/roles.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RolesController } from '../roles.controller'; 3 | 4 | describe('RolesController', () => { 5 | let controller: RolesController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [RolesController], 10 | }).compile(); 11 | 12 | controller = module.get(RolesController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/user/pipes/create-user.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; 2 | import { CreateUserDto } from '../dto/create-user.dto'; 3 | //user的pip管道 4 | @Injectable() 5 | export class CreateUserPipe implements PipeTransform { 6 | transform(value: CreateUserDto, metadata: ArgumentMetadata) { 7 | if (value.roles && value.roles instanceof Array && value.roles.length > 0) { 8 | // Roles[] 9 | if (value.roles[0]['id']) { 10 | value.roles = value.roles.map((role) => role.id); 11 | } 12 | // number[] 13 | } 14 | return value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Use root/example as user/password credentials 2 | version: '3.1' 3 | 4 | services: 5 | db: 6 | image: mysql 7 | # NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password 8 | # (this is just an example, not intended to be a production configuration) 9 | command: --default-authentication-plugin=mysql_native_password 10 | restart: always 11 | environment: 12 | MYSQL_ROOT_PASSWORD: 123456 13 | ports: 14 | - 3090:3306 15 | 16 | # navicat 17 | adminer: 18 | image: adminer 19 | restart: always 20 | ports: 21 | - 8080:8080 22 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import * as yaml from 'js-yaml'; 3 | import { join } from 'path'; 4 | import * as _ from 'lodash'; 5 | 6 | const YAML_COMMON_CONFIG_FILENAME = 'config.yml'; 7 | 8 | const filePath = join(__dirname, '../config', YAML_COMMON_CONFIG_FILENAME); 9 | 10 | const envPath = join( 11 | __dirname, 12 | '../config', 13 | `config.${process.env.NODE_ENV || 'development'}.yml`, 14 | ); 15 | 16 | const commonConfig = yaml.load(readFileSync(filePath, 'utf8')); 17 | 18 | const envConfig = yaml.load(readFileSync(envPath, 'utf8')); 19 | 20 | // 因为ConfigModule有一个load方法->函数 21 | export default () => { 22 | return _.merge(commonConfig, envConfig); 23 | }; 24 | -------------------------------------------------------------------------------- /webpack-hmr.config.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals'); 2 | const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin'); 3 | 4 | module.exports = function (options, webpack) { 5 | return { 6 | ...options, 7 | entry: ['webpack/hot/poll?100', options.entry], 8 | externals: [ 9 | nodeExternals({ 10 | allowlist: ['webpack/hot/poll?100'], 11 | }), 12 | ], 13 | plugins: [ 14 | ...options.plugins, 15 | new webpack.HotModuleReplacementPlugin(), 16 | new webpack.WatchIgnorePlugin({ 17 | paths: [/\.js$/, /\.d\.ts$/], 18 | }), 19 | new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }), 20 | ], 21 | }; 22 | }; -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/logs/logs.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LogsController } from './logs.controller'; 3 | import { LogsService } from './logs.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Logs } from './logs.entity'; 6 | import { LoggerModule } from 'nestjs-pino'; //日志打印工具 7 | @Module({ 8 | imports: [ 9 | TypeOrmModule.forFeature([Logs]), 10 | LoggerModule.forRoot({ 11 | pinoHttp: { 12 | transport: { 13 | target: 'pino-pretty', //打印日志样式优化 14 | options: { 15 | colorize: true, 16 | }, 17 | }, 18 | }, 19 | }), 20 | ], 21 | controllers: [LogsController], 22 | providers: [LogsService], 23 | }) 24 | export class LogsModule {} 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/filters/typeorm.filter.ts: -------------------------------------------------------------------------------- 1 | // 2 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 3 | import { TypeORMError, QueryFailedError } from 'typeorm'; 4 | 5 | @Catch(TypeORMError) 6 | export class TypeormFilter implements ExceptionFilter { 7 | catch(exception: TypeORMError, host: ArgumentsHost) { 8 | const ctx = host.switchToHttp(); 9 | let code = 500; 10 | if (exception instanceof QueryFailedError) { 11 | code = exception.driverError.errno; 12 | } 13 | // 响应 请求对象 14 | const response = ctx.getResponse(); 15 | response.status(500).json({ 16 | code: code, 17 | timestamp: new Date().toISOString(), 18 | // path: request.url, 19 | // method: request.method, 20 | message: exception.message, 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/auth/auth.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { ConfigEnum } from 'src/enum/config.enum'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor(protected configService: ConfigService) { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | secretOrKey: configService.get(ConfigEnum.SECRET), 14 | }); 15 | } 16 | 17 | async validate(payload: any) { 18 | // req.user 19 | // cache中的token 20 | return { userId: payload.sub, username: payload.username }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './user.entity'; 6 | import { LoggerModule } from 'nestjs-pino'; //日志打印工具 7 | import { Roles } from 'src/roles/roles.entity'; 8 | import { Logs } from 'src/logs/logs.entity'; 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forFeature([User, Logs, Roles]), 12 | LoggerModule.forRoot({ 13 | pinoHttp: { 14 | transport: { 15 | target: 'pino-pretty', //打印日志样式优化 16 | options: { 17 | colorize: true, 18 | }, 19 | }, 20 | }, 21 | }), 22 | ], 23 | controllers: [UserController], 24 | providers: [UserService], 25 | exports: [UserService], 26 | }) 27 | export class UserModule {} 28 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post, UseFilters } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { TypeormFilter } from 'src/filters/typeorm.filter'; 4 | import { SigninUserDto } from './dto/signin-user.dto'; 5 | 6 | @Controller('auth') 7 | @UseFilters(new TypeormFilter()) //错误 信息返回封装 8 | export class AuthController { 9 | constructor(private authService: AuthService) {} 10 | 11 | @Post('/signin') 12 | async signin(@Body() dto: SigninUserDto) { 13 | const { username, password } = dto; 14 | const token = await this.authService.signin(username, password); 15 | // 设置token 16 | // await this.redis.set(username, token); 17 | return { 18 | access_token: token, 19 | }; 20 | } 21 | 22 | @Post('/signup') 23 | signup(@Body() dto: SigninUserDto) { 24 | const { username, password } = dto; 25 | return this.authService.signup(username, password); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/filters/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@nestjs/common'; 2 | import { 3 | ArgumentsHost, 4 | Catch, 5 | ExceptionFilter, 6 | HttpException, 7 | } from '@nestjs/common'; 8 | 9 | @Catch(HttpException) 10 | export class HttpExceptionFilter implements ExceptionFilter { 11 | constructor(private logger: LoggerService) {} 12 | async catch(exception: HttpException, host: ArgumentsHost) { 13 | const ctx = host.switchToHttp(); 14 | // 响应 请求对象 15 | const response = ctx.getResponse(); 16 | // const request = Pctx.getRequest(); 17 | // http状态码 18 | const status = exception.getStatus(); 19 | this.logger.error(exception.message, exception.stack); 20 | response.status(status).json({ 21 | code: status, 22 | timestamp: new Date().toISOString(), 23 | // path: request.url, 24 | // method: request.method, 25 | message: exception.message || exception.name, 26 | }); 27 | // throw new Error('Method not implemented.'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 toimc 2 | // 3 | // This software is released under the MIT License. 4 | // https://opensource.org/licenses/MIT 5 | 6 | import { HttpAdapterHost, NestFactory } from '@nestjs/core'; 7 | import { AppModule } from './app.module'; 8 | import { Logger, ValidationPipe } from '@nestjs/common'; 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.create(AppModule); 12 | app.setGlobalPrefix('api'); 13 | 14 | const httpAdapter = app.get(HttpAdapterHost); 15 | // 全局Filter只能有一个 16 | const logger = new Logger(); 17 | // app.useGlobalFilters(new HttpExceptionFilter(logger)); 18 | // app.useGlobalFilters(new AllExceptionFilter(logger, httpAdapter)); 19 | app.useGlobalPipes( 20 | new ValidationPipe({ 21 | // 去除在类上不存在的字段 22 | // whitelist: true, 23 | }), 24 | ); 25 | await app.listen(3000); 26 | if (module.hot) { 27 | module.hot.accept(); 28 | module.hot.dispose(() => app.close()); 29 | } 30 | } 31 | bootstrap(); 32 | -------------------------------------------------------------------------------- /src/guards/admin/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | // import { Observable } from 'rxjs'; 3 | import { UserService } from 'src/user/user.service'; 4 | import { User } from 'src/user/user.entity'; 5 | 6 | @Injectable() 7 | export class AdminGuard implements CanActivate { 8 | // 常见的错误:在使用AdminGuard未导入UserModule 9 | constructor(private userService: UserService) {} 10 | async canActivate(context: ExecutionContext): Promise { 11 | // 1. 获取请求对象 12 | const req = context.switchToHttp().getRequest(); 13 | // 2. 获取请求中的用户信息进行逻辑上的判断 -> 角色判断 14 | // console.log('user', req.user); 15 | const user = (await this.userService.find(req.user.username)) as User; 16 | // console.log( 17 | // '🚀 ~ file: admin.guard.ts ~ line 16 ~ AdminGuard ~ canActivate ~ user', 18 | // user, 19 | // ); 20 | // 普通用户 21 | // 后面加入更多的逻辑 22 | if (user && user.roles.filter((o) => o.id === 1).length > 0) { 23 | return true; 24 | } 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | PrimaryGeneratedColumn, 5 | OneToMany, 6 | ManyToMany, 7 | JoinTable, 8 | OneToOne, 9 | } from 'typeorm'; 10 | import { Logs } from '../logs/logs.entity'; 11 | import { Roles } from 'src/roles/roles.entity'; 12 | import { Profile } from './profile.entity'; 13 | // import { Roles } from '../roles/roles.entity'; 14 | 15 | @Entity() 16 | export class User { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Column({ unique: true }) //设置唯一账号 21 | username: string; 22 | 23 | @Column() 24 | password: string; 25 | 26 | // typescript -> 数据库 关联关系 Mapping, 27 | //一对多 28 | @OneToMany(() => Logs, (logs) => logs.user, { cascade: true }) 29 | logs: Logs[]; 30 | 31 | //多对多 32 | @ManyToMany(() => Roles, (roles) => roles.users, { cascade: ['insert'] }) 33 | @JoinTable({ name: 'users_roles' }) 34 | roles: Roles[]; 35 | 36 | //, { cascade: true }设置连表更新 一对一 37 | @OneToOne(() => Profile, (profile) => profile.user, { cascade: true }) 38 | profile: Profile; 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | build/ 5 | tmp/ 6 | temp/ 7 | dist/ 8 | # General 9 | .DS_Store 10 | .AppleDouble 11 | .LSOverride 12 | 13 | # Icon must end with two \r 14 | Icon 15 | 16 | 17 | # Thumbnails 18 | ._* 19 | 20 | # Files that might appear in the root of a volume 21 | .DocumentRevisions-V100 22 | .fseventsd 23 | .Spotlight-V100 24 | .TemporaryItems 25 | .Trashes 26 | .VolumeIcon.icns 27 | .com.apple.timemachine.donotpresent 28 | 29 | # Directories potentially created on remote AFP share 30 | .AppleDB 31 | .AppleDesktop 32 | Network Trash Folder 33 | Temporary Items 34 | .apdisk 35 | 36 | # General 37 | .DS_Store 38 | .AppleDouble 39 | .LSOverride 40 | 41 | # Icon must end with two \r 42 | Icon 43 | 44 | 45 | # Thumbnails 46 | ._* 47 | 48 | # Files that might appear in the root of a volume 49 | .DocumentRevisions-V100 50 | .fseventsd 51 | .Spotlight-V100 52 | .TemporaryItems 53 | .Trashes 54 | .VolumeIcon.icns 55 | .com.apple.timemachine.donotpresent 56 | 57 | # Directories potentially created on remote AFP share 58 | .AppleDB 59 | .AppleDesktop 60 | Network Trash Folder 61 | Temporary Items 62 | .apdisk 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | import { UserModule } from 'src/user/user.module'; 5 | import { PassportModule } from '@nestjs/passport'; 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { ConfigModule, ConfigService } from '@nestjs/config'; 8 | import { ConfigEnum } from 'src/enum/config.enum'; 9 | import { JwtStrategy } from './auth.strategy'; 10 | 11 | @Module({ 12 | imports: [ 13 | UserModule, 14 | PassportModule, 15 | JwtModule.registerAsync({ 16 | imports: [ConfigModule], 17 | useFactory: async (configService: ConfigService) => { 18 | // console.log({ 19 | // secret: configService.get(ConfigEnum.SECRET), 20 | // }); 21 | return { 22 | secret: configService.get(ConfigEnum.SECRET), 23 | signOptions: { 24 | expiresIn: '1d', //设置token过期时间,1天 25 | }, 26 | }; 27 | }, 28 | inject: [ConfigService], 29 | }), 30 | ], 31 | providers: [AuthService, JwtStrategy], 32 | controllers: [AuthController], 33 | }) 34 | export class AuthModule {} 35 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { UserService } from 'src/user/user.service'; 4 | import * as argon2 from 'argon2'; 5 | @Injectable() 6 | export class AuthService { 7 | constructor( 8 | private userService: UserService, 9 | private jwt: JwtService, 10 | ) {} 11 | async signin(username: string, password: string) { 12 | const user = await this.userService.find(username); 13 | 14 | if (!user) { 15 | throw new ForbiddenException('用户不存在,请注册'); 16 | } 17 | 18 | // 用户密码进行比对 19 | const isPasswordValid = await argon2.verify(user.password, password); 20 | if (!isPasswordValid) { 21 | throw new ForbiddenException('用户名或者密码错误'); 22 | } 23 | 24 | return await this.jwt.signAsync({ 25 | username: user.username, 26 | sub: user.id, 27 | }); 28 | } 29 | 30 | async signup(username: string, password: string) { 31 | const user = await this.userService.find(username); 32 | 33 | if (user) { 34 | throw new ForbiddenException('用户已存在'); 35 | } 36 | 37 | const res = await this.userService.create({ username, password }); 38 | return res; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/roles/roles.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { CreateRoleDto } from './dto/create-role.dto'; 4 | import { UpdateRoleDto } from './dto/update-role.dto'; 5 | import { Roles } from './roles.entity'; 6 | import { Repository } from 'typeorm'; 7 | 8 | @Injectable() 9 | export class RolesService { 10 | constructor( 11 | @InjectRepository(Roles) private roleRepository: Repository, 12 | ) {} 13 | 14 | async create(createRoleDto: CreateRoleDto) { 15 | const role = await this.roleRepository.create(createRoleDto); 16 | return this.roleRepository.save(role); 17 | } 18 | 19 | findAll() { 20 | return this.roleRepository.find(); 21 | } 22 | 23 | findOne(id: number) { 24 | return this.roleRepository.findOne({ 25 | where: { 26 | id, 27 | }, 28 | }); 29 | } 30 | 31 | async update(id: number, updateRoleDto: UpdateRoleDto) { 32 | const role = await this.findOne(id); 33 | const newRole = this.roleRepository.merge(role, updateRoleDto); 34 | return this.roleRepository.save(newRole); 35 | } 36 | 37 | remove(id: number) { 38 | // delete -> AfterRemove 不会触发 39 | return this.roleRepository.delete(id); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/filters/all-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | HttpException, 4 | HttpStatus, 5 | LoggerService, 6 | } from '@nestjs/common'; 7 | import { ArgumentsHost, Catch } from '@nestjs/common'; 8 | import { HttpAdapterHost } from '@nestjs/core'; 9 | 10 | import * as requestIp from 'request-ip'; 11 | 12 | @Catch() 13 | export class AllExceptionFilter implements ExceptionFilter { 14 | constructor( 15 | private readonly logger: LoggerService, 16 | private readonly httpAdapterHost: HttpAdapterHost, 17 | ) {} 18 | catch(exception: unknown, host: ArgumentsHost) { 19 | const { httpAdapter } = this.httpAdapterHost; 20 | const ctx = host.switchToHttp(); 21 | const request = ctx.getRequest(); 22 | const response = ctx.getResponse(); 23 | 24 | const httpStatus = 25 | exception instanceof HttpException 26 | ? exception.getStatus() 27 | : HttpStatus.INTERNAL_SERVER_ERROR; 28 | 29 | const responseBody = { 30 | headers: request.headers, 31 | query: request.query, 32 | body: request.body, 33 | params: request.params, 34 | timestamp: new Date().toISOString(), 35 | // 还可以加入一些用户信息 36 | // IP信息 37 | ip: requestIp.getClientIp(request), 38 | exceptioin: exception['name'], 39 | error: exception['response'] || 'Internal Server Error', 40 | }; 41 | 42 | this.logger.error('[toimc]', responseBody); 43 | httpAdapter.reply(response, responseBody, httpStatus); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/guards/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { ExtractJwt } from 'passport-jwt'; 4 | import { verify } from 'jsonwebtoken'; 5 | import { ConfigService } from '@nestjs/config'; 6 | import { ConfigEnum } from '../enum/config.enum'; 7 | // import { InjectRedis, Redis } from '@nestjs-modules/ioredis'; 8 | 9 | export class JwtGuard extends AuthGuard('jwt') { 10 | constructor( 11 | private configService: ConfigService, // @InjectRedis() private readonly redis: Redis, 12 | ) { 13 | super(); 14 | } 15 | 16 | // async canActivate(context: ExecutionContext): Promise { 17 | // // custom logic can go here 18 | // const request = context.switchToHttp().getRequest(); 19 | // const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); 20 | // // const cacheToken = this.redis.get(token); 21 | // if (!token) { 22 | // throw new UnauthorizedException(); 23 | // } 24 | // const payload = await verify( 25 | // token, 26 | // this.configService.get(ConfigEnum.SECRET), 27 | // ); 28 | // const username = payload['username']; 29 | // const tokenCache = username ? await this.redis.get(username) : null; 30 | // if (!payload || !username || tokenCache !== token) { 31 | // throw new UnauthorizedException(); 32 | // } 33 | 34 | // const parentCanActivate = (await super.canActivate(context)) as boolean; // this is necessary due to possibly returning `boolean | Promise | Observable 35 | // // custom logic goes here too 36 | // return parentCanActivate; 37 | // } 38 | } 39 | 40 | // 装饰器 41 | // @JwtGuard() 42 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | ParseIntPipe, 8 | Patch, 9 | Post, 10 | Query, 11 | UseFilters, 12 | UseGuards, 13 | } from '@nestjs/common'; 14 | import { UserService } from './user.service'; 15 | import { ConfigService } from '@nestjs/config'; 16 | import { User } from './user.entity'; 17 | import { Logger } from 'nestjs-pino'; 18 | import { TypeormFilter } from 'src/filters/typeorm.filter'; 19 | import { CreateUserPipe } from './pipes/create-user.pipe'; 20 | import { CreateUserDto } from './dto/create-user.dto'; 21 | import { AuthGuard } from '@nestjs/passport'; 22 | import { AdminGuard } from '../guards/admin/admin.guard'; 23 | import { JwtGuard } from 'src/guards/jwt.guard'; 24 | 25 | @Controller('user') 26 | @UseFilters(new TypeormFilter()) //错误 信息返回封装 27 | @UseGuards(JwtGuard) //守卫,设置token 28 | export class UserController { 29 | constructor( 30 | private userService: UserService, 31 | private configService: ConfigService, 32 | private logger: Logger, 33 | ) { 34 | this.logger.log('User'); 35 | } 36 | 37 | @Get() 38 | getUsers(@Query() query: UserQeury): any { 39 | //分页查询 40 | return this.userService.findAll(query); 41 | } 42 | // 43 | @Post() 44 | addUser(@Body(CreateUserPipe) dto: CreateUserDto): any { 45 | //@Body data参数 46 | // todo 解析Body参数 47 | const user = dto as User; 48 | return this.userService.create(user); 49 | } 50 | @Patch('/:id') 51 | updateUser(@Body() dto: any, @Param('id') id: number): any { 52 | // todo 传递参数id 53 | // todo 异常处理 54 | const user = dto as User; 55 | return this.userService.update(id, user); 56 | } 57 | 58 | @UseGuards(AdminGuard) //管理员守卫,不是id为2的不行 59 | @Delete('/:id') 60 | deleteUser(@Param('id') id: number): any { 61 | // todo 传递参数id 62 | return this.userService.remove(id); 63 | } 64 | 65 | //ParseIntPipe 管道,将 id 转换成数字形式 66 | @Get('/findAddProfile') 67 | findAddProfile(@Query('id', ParseIntPipe) query: any): any { 68 | return this.userService.findAddProfile(query.id); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserModule } from './user/user.module'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import * as dotenv from 'dotenv'; 5 | import * as Joi from 'joi'; 6 | import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; 7 | import { ConfigEnum } from './enum/config.enum'; 8 | import { User } from './user/user.entity'; 9 | import { Profile } from './user/profile.entity'; 10 | import { Roles } from './roles/roles.entity'; 11 | import { Logs } from './logs/logs.entity'; 12 | import { AuthModule } from './auth/auth.module'; 13 | import { LogsModule } from './logs/logs.module'; 14 | import { RolesModule } from './roles/roles.module'; 15 | 16 | const envFilePath = `.env.${process.env.NODE_ENV || `development`}`; 17 | 18 | @Module({ 19 | imports: [ 20 | ConfigModule.forRoot({ 21 | isGlobal: true, 22 | envFilePath, 23 | load: [() => dotenv.config({ path: '.env' })], 24 | validationSchema: Joi.object({ 25 | NODE_ENV: Joi.string() 26 | .valid('development', 'production') 27 | .default('development'), 28 | DB_PORT: Joi.number().default(3306), 29 | DB_HOST: Joi.string().ip(), 30 | DB_TYPE: Joi.string().valid('mysql', 'postgres'), 31 | DB_DATABASE: Joi.string().required(), 32 | DB_USERNAME: Joi.string().required(), 33 | DB_PASSWORD: Joi.string().required(), 34 | DB_SYNC: Joi.boolean().default(false), 35 | }), 36 | }), 37 | TypeOrmModule.forRootAsync({ 38 | imports: [ConfigModule], 39 | inject: [ConfigService], 40 | useFactory: (configService: ConfigService) => 41 | ({ 42 | type: configService.get(ConfigEnum.DB_TYPE), 43 | host: configService.get(ConfigEnum.DB_HOST), 44 | port: configService.get(ConfigEnum.DB_PORT), 45 | username: configService.get(ConfigEnum.DB_USERNAME), 46 | password: configService.get(ConfigEnum.DB_PASSWORD), 47 | database: configService.get(ConfigEnum.DB_DATABASE), 48 | entities: [User, Profile, Logs, Roles], 49 | // 同步本地的schema与数据库 -> 初始化的时候去使用 50 | synchronize: configService.get(ConfigEnum.DB_SYNC), 51 | logging: process.env.NODE_ENV === 'development', 52 | }) as TypeOrmModuleOptions, 53 | }), 54 | // TypeOrmModule.forRoot({ 55 | // type: 'mysql', 56 | // host: 'localhost', 57 | // port: 3090, 58 | // username: 'root', 59 | // password: '123456', 60 | // database: 'testdb', 61 | // entities: [], 62 | // synchronize: true, 63 | // logging: ['error'], 64 | // }), 65 | UserModule, 66 | LogsModule, 67 | RolesModule, 68 | AuthModule, 69 | ], 70 | controllers: [], 71 | providers: [], 72 | }) 73 | export class AppModule {} 74 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { In, Repository } from 'typeorm'; 4 | import { User } from './user.entity'; 5 | import { Logs } from 'src/logs/logs.entity'; 6 | import { Roles } from 'src/roles/roles.entity'; 7 | import * as argon2 from 'argon2'; 8 | @Injectable() 9 | export class UserService { 10 | constructor( 11 | @InjectRepository(User) private readonly userRepository: Repository, 12 | @InjectRepository(Logs) private readonly logsRepository: Repository, 13 | @InjectRepository(Roles) 14 | private readonly rolesRepository: Repository, 15 | ) {} 16 | findAll(query: UserQeury) { 17 | //动态查询 18 | const { limit, page, username, gender, role } = query; 19 | const take = limit || 10; 20 | return this.userRepository.find({ 21 | select: { 22 | //设置需要返回的数据 23 | id: true, 24 | username: true, 25 | }, 26 | 27 | relations: { 28 | //设置连表查询 29 | profile: true, 30 | roles: true, 31 | }, 32 | where: { 33 | //设置动态查询 34 | username, 35 | profile: { 36 | gender, 37 | }, 38 | roles: { 39 | id: role, 40 | }, 41 | }, 42 | take, //设置分页条件 43 | skip: (page - 1) * take, 44 | }); 45 | } 46 | find(username: string) { 47 | return this.userRepository.findOne({ 48 | where: { username }, 49 | relations: ['roles'], 50 | }); 51 | } 52 | 53 | findOne(id: number) { 54 | return this.userRepository.findOne({ where: { id } }); 55 | } 56 | 57 | async create(user: Partial) { 58 | if (!user.roles) { 59 | const role = await this.rolesRepository.findOne({ where: { id: 1 } }); 60 | user.roles = [role]; 61 | } 62 | if (user.roles instanceof Array && typeof user.roles[0] === 'number') { 63 | // {id, name} -> { id } -> [id] 64 | // 查询所有的用户角色 65 | user.roles = await this.rolesRepository.find({ 66 | where: { 67 | id: In(user.roles), 68 | }, 69 | }); 70 | } 71 | const userTmp = this.userRepository.create(user); 72 | // 对用户密码使用argon2加密 73 | userTmp.password = await argon2.hash(userTmp.password); 74 | const res = await this.userRepository.save(userTmp); 75 | return res; 76 | } 77 | //Partial会拼接没有传的数据,相当于动态sql 78 | async update(id: number, user: Partial) { 79 | const userTemp = await this.findAddProfile(id); 80 | const newUser = this.userRepository.merge(userTemp, user); 81 | // 联合模型更新,需要使用save方法或者queryBuilder 82 | return this.userRepository.save(newUser); 83 | 1; 84 | // 下面的update方法,只适合单模型的更新,不适合有关系的模型更新 85 | // return this.userRepository.update(parseInt(id), newUser); 86 | } 87 | remove(id: number) { 88 | return this.userRepository.delete(id); 89 | } 90 | 91 | findAddProfile(id: number) { 92 | return this.userRepository.findOne({ 93 | where: { id }, 94 | relations: { 95 | profile: true, 96 | }, 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestproject1", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "cross-env NODE_ENV=development nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "cross-env NODE_ENV=production node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^10.0.0", 24 | "@nestjs/config": "^3.0.0", 25 | "@nestjs/core": "^10.0.0", 26 | "@nestjs/jwt": "^10.1.0", 27 | "@nestjs/mapped-types": "^2.0.2", 28 | "@nestjs/passport": "^10.0.0", 29 | "@nestjs/platform-express": "^10.0.0", 30 | "@nestjs/typeorm": "^10.0.0", 31 | "argon2": "^0.31.0", 32 | "class-transformer": "^0.5.1", 33 | "class-validator": "^0.14.0", 34 | "config": "^3.3.9", 35 | "joi": "^17.9.2", 36 | "js-yaml": "^4.1.0", 37 | "lodash": "^4.17.21", 38 | "mapped-types": "^0.0.1", 39 | "mysql2": "^3.6.0", 40 | "nestjs-pino": "^3.3.0", 41 | "passport-jwt": "^4.0.1", 42 | "pino-pretty": "^10.2.0", 43 | "reflect-metadata": "^0.1.13", 44 | "request-ip": "^3.3.0", 45 | "rxjs": "^7.8.1", 46 | "typeorm": "^0.3.17" 47 | }, 48 | "devDependencies": { 49 | "@nestjs/cli": "^10.0.0", 50 | "@nestjs/schematics": "^10.0.0", 51 | "@nestjs/testing": "^10.0.0", 52 | "@types/express": "^4.17.17", 53 | "@types/jest": "^29.5.2", 54 | "@types/js-yaml": "^4.0.5", 55 | "@types/node": "^20.3.1", 56 | "@types/passport-jwt": "^3.0.9", 57 | "@types/supertest": "^2.0.12", 58 | "@types/webpack-env": "^1.18.1", 59 | "@typescript-eslint/eslint-plugin": "^6.0.0", 60 | "@typescript-eslint/parser": "^6.0.0", 61 | "cross-env": "^7.0.3", 62 | "eslint": "^8.42.0", 63 | "eslint-config-prettier": "^9.0.0", 64 | "eslint-plugin-prettier": "^5.0.0", 65 | "jest": "^29.5.0", 66 | "prettier": "^3.0.0", 67 | "run-script-webpack-plugin": "^0.2.0", 68 | "source-map-support": "^0.5.21", 69 | "supertest": "^6.3.3", 70 | "ts-jest": "^29.1.0", 71 | "ts-loader": "^9.4.3", 72 | "ts-node": "^10.9.1", 73 | "tsconfig-paths": "^4.2.0", 74 | "typescript": "^5.1.3", 75 | "webpack": "^5.88.2", 76 | "webpack-node-externals": "^3.0.0" 77 | }, 78 | "jest": { 79 | "moduleFileExtensions": [ 80 | "js", 81 | "json", 82 | "ts" 83 | ], 84 | "rootDir": "src", 85 | "testRegex": ".*\\.spec\\.ts$", 86 | "transform": { 87 | "^.+\\.(t|j)s$": "ts-jest" 88 | }, 89 | "collectCoverageFrom": [ 90 | "**/*.(t|j)s" 91 | ], 92 | "coverageDirectory": "../coverage", 93 | "testEnvironment": "node" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前言: 2 | 3 | 此 .md 博客是我对这两个星期学习nest.js框架的一个结构梳理,旨在讲个大概的项目结构,基本的结构和代码是用来干什么的,不能做到从 0 基础到完全完成Nest.js项目的初始化。 4 | 5 | # 项目功能: 6 | 7 | ## 核心技术栈 8 | 9 | **Nestjs + TypeORM + MySQL + Jwt + Docker** 10 | 11 | ## 功能 12 | 13 | 基本的一对一,多对多,一对多的数据库表连接,数据库链表查询,user类的完整crud,分页查询配置,jwt鉴权,密码加密处理。 14 | 15 | > **旨在对node后端代码的一个相对完整的书写** 16 | 17 | # 项目介绍 18 | 19 | ## 项目目录 20 | 21 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1692106865341-649241d8-7ac6-45d9-ae4f-67c863ef782a.png#averageHue=%23272b2e&clientId=ubcc800f3-e335-4&from=paste&height=535&id=u553a9b30&originHeight=1183&originWidth=439&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=78369&status=done&style=none&taskId=u8190c8f8-e4da-4d5f-bb59-625765d43db&title=&width=198.5) 22 | 23 | ## 文件介绍 24 | 25 | ### 配置文件 26 | 27 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1692107060546-a9010922-e814-4f83-bac8-bb0b0cecdb47.png#averageHue=%2326282b&clientId=ubcc800f3-e335-4&from=paste&height=325&id=u3a21623b&originHeight=650&originWidth=380&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=50591&status=done&style=none&taskId=u90ba709e-53e8-4e9e-a20c-bfc7351b011&title=&width=190) 28 | 这些文件就不做过多介绍了,有框架基础的应该都知道是干啥的,基本都是些项目框架的配置文件 29 | 30 | ### 中枢文件 31 | 32 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1692108695964-8dce07fc-805e-4007-9069-0cf3389b4301.png#averageHue=%23282b2d&clientId=ubcc800f3-e335-4&from=paste&height=253&id=uf3f1a29a&originHeight=495&originWidth=446&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=24882&status=done&style=none&taskId=uca59ffbc-7501-4902-bdc4-37fd435e225&title=&width=228.33334350585938) 33 | 34 | #### main.ts 35 | 36 | 主要负责框架 **代理启动** 服务器 37 | 38 | #### app.module.ts 39 | 40 | 代码的中枢,完成 **数据库连接 **的各种连接配置 41 | 42 | ### 数据库与接口文件 43 | 44 | 结构类似于 springboot框架的结构,代码逻辑有比较明确的分级 45 | user有较为标准的文件结构,咱们拿一个user类为例: 46 | 47 | #### 文件目录 48 | 49 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1692108572378-15e24a63-6d5e-4d20-a39d-f754f1ba4cfa.png#averageHue=%23242b30&clientId=ubcc800f3-e335-4&from=paste&height=285&id=u15029771&originHeight=634&originWidth=413&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=44779&status=done&style=none&taskId=u1b226f45-1898-4aa1-9821-bdc1ec1a7d4&title=&width=185.33334350585938) 50 | 51 | #### xxx.module.ts 52 | 53 | **连接user类接口的中枢** 54 | 55 | ```typescript 56 | @Module({ 57 | imports: [ 58 | TypeOrmModule.forFeature([User, Logs, Roles]), 59 | LoggerModule.forRoot({ 60 | pinoHttp: { 61 | transport: { 62 | target: 'pino-pretty', //打印日志样式优化 63 | options: { 64 | colorize: true, 65 | }, 66 | }, 67 | }, 68 | }), 69 | ], 70 | controllers: [UserController], 71 | providers: [UserService], 72 | exports: [UserService], 73 | }) 74 | export class UserModule {} 75 | 76 | ``` 77 | 78 | #### xxx.entity.ts 79 | 80 | **创建数据库表的文件** 81 | 82 | ```typescript 83 | import { 84 | Column, 85 | Entity, 86 | PrimaryGeneratedColumn, 87 | OneToMany, 88 | ManyToMany, 89 | JoinTable, 90 | OneToOne, 91 | } from 'typeorm'; 92 | import { Logs } from '../logs/logs.entity'; 93 | import { Roles } from 'src/roles/roles.entity'; 94 | import { Profile } from './profile.entity'; 95 | // import { Roles } from '../roles/roles.entity'; 96 | 97 | @Entity() 98 | export class User { 99 | @PrimaryGeneratedColumn() 100 | id: number; 101 | 102 | @Column({ unique: true }) //设置唯一账号 103 | username: string; 104 | 105 | @Column() 106 | password: string; 107 | 108 | // typescript -> 数据库 关联关系 Mapping, 109 | //一对多 110 | @OneToMany(() => Logs, (logs) => logs.user, { cascade: true }) 111 | logs: Logs[]; 112 | 113 | //多对多 114 | @ManyToMany(() => Roles, (roles) => roles.users, { cascade: ['insert'] }) 115 | @JoinTable({ name: 'users_roles' }) 116 | roles: Roles[]; 117 | 118 | //, { cascade: true }设置连表更新 一对一 119 | @OneToOne(() => Profile, (profile) => profile.user, { cascade: true }) 120 | profile: Profile; 121 | } 122 | 123 | ``` 124 | 125 | #### xxx.controller.ts(controller层,控制层) 126 | 127 | **具体的业务模块流程的控制**,**接口请求入口**,controller层主要调用Service层里面的接口控制具体的业务流程,控制的配置也要在配置文件中进行。 128 | 129 | ```typescript 130 | @Controller('user') 131 | @UseFilters(new TypeormFilter()) //错误 信息返回封装 132 | @UseGuards(JwtGuard) //守卫,设置token 133 | export class UserController { 134 | constructor( 135 | private userService: UserService, 136 | private configService: ConfigService, 137 | private logger: Logger, 138 | ) { 139 | this.logger.log('User'); 140 | } 141 | 142 | @Get() 143 | getUsers(@Query() query: UserQeury): any { 144 | //分页查询 145 | return this.userService.findAll(query); 146 | } 147 | // 148 | @Post() 149 | addUser(@Body(CreateUserPipe) dto: CreateUserDto): any { 150 | //@Body data参数 151 | // todo 解析Body参数 152 | const user = dto as User; 153 | return this.userService.create(user); 154 | } 155 | @Patch('/:id') 156 | updateUser(@Body() dto: any, @Param('id') id: number): any { 157 | // todo 传递参数id 158 | // todo 异常处理 159 | const user = dto as User; 160 | return this.userService.update(id, user); 161 | } 162 | 163 | @UseGuards(AdminGuard) //管理员守卫,不是id为2的不行 164 | @Delete('/:id') 165 | deleteUser(@Param('id') id: number): any { 166 | // todo 传递参数id 167 | return this.userService.remove(id); 168 | } 169 | 170 | //ParseIntPipe 管道,将 id 转换成数字形式 171 | @Get('/findAddProfile') 172 | findAddProfile(@Query('id', ParseIntPipe) query: any): any { 173 | return this.userService.findAddProfile(query.id); 174 | } 175 | } 176 | 177 | ``` 178 | 179 | #### xxx.service.ts(service层,业务层) 180 | 181 | **接口业务模块的逻辑应用设计**,和DAO层一样都是先设计接口,再创建要实现的类,然后在配置文件中进行配置其实现的关联。接下来就可以在service层调用接口进行业务逻辑应用的处理 182 | 183 | #### dto(实体类,请求类) 184 | 185 | ### 工具文件 186 | 187 | #### guards(守卫) 188 | 189 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1692109143744-0d224362-b5d1-422f-8241-a9a60b095d01.png#averageHue=%23212f3a&clientId=ubcc800f3-e335-4&from=paste&height=125&id=ucd3b738c&originHeight=188&originWidth=436&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=13205&status=done&style=none&taskId=ube77239d-e734-4f8c-b890-678cd59498c&title=&width=290.6666666666667) 190 | 完成的是全局 **jwt** 工具的封装,和指定**管理员 jwt 鉴权**函数工具的封装 191 | 192 | #### filters(过滤器,接口返回封装) 193 | 194 | 接口**数据返回类型**的封装,配置,**请求日志**函数的封装 195 | 196 | #### test(测试文件) 197 | 198 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/26685644/1692109338450-a1d7c297-856b-48b6-a7ab-768af5a11bdc.png#averageHue=%231d3240&clientId=ubcc800f3-e335-4&from=paste&height=92&id=udd5f0bee&originHeight=138&originWidth=452&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=10130&status=done&style=none&taskId=u5273ad7e-88e9-4e1e-8616-e577363b13e&title=&width=301.3333333333333) 199 | 200 | #### enum(数据枚举) 201 | 202 | # 项目运行 203 | 204 | ## 运行环境 205 | 206 | > 1. node环境 207 | > 2. docker环境 208 | > 3. mysql环境 209 | 210 | 这几个是项目运行的重要环境 211 | 关于安装docker环境遇见的问题,可以看我写过的一个博客: 212 | [https://blog.csdn.net/Azbtt/article/details/132182380](https://blog.csdn.net/Azbtt/article/details/132182380) 213 | 214 | ## 运行配置 215 | 216 | 运行项目之前,请先自行配置本地的数据库环境,并修改项目根目录中的配置文件:`.env`,`.env.development`(开发),`.env.production`(生产)。 217 | 推荐使用`node14 LTS`安装依赖: 218 | 219 | ``` 220 | npm i 221 | ``` 222 | 223 | ## 运行命令 224 | 225 | 运行项目: 226 | 227 | ``` 228 | npm run start:dev 229 | ``` 230 | 231 | # Github地址: 232 | 233 | > [https://github.com/wzz778/NestjsProjectDemo](https://github.com/wzz778/NestjsProjectDemo) 234 | 235 | 236 | 学习视频地址:[慕课网实战课程 - 提升个人技术的真实项目演练](https://coding.imooc.com/learn/list/617.html) 237 | --------------------------------------------------------------------------------