diff --git a/package.json b/package.json index 4d0eb94..9961e94 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "dependencies": { "@nestjs/common": "^6.0.0", "@nestjs/core": "^6.0.0", - "@nestjs/platform-express": "^6.0.0", + "@nestjs/platform-express": "^6.0.0", + "class-transformer": "^0.2.3", + "class-validator": "^0.9.1", "reflect-metadata": "^0.1.12", "rimraf": "^2.6.2", "rxjs": "^6.3.3" @@ -46,7 +48,11 @@ "wait-on": "^3.2.0" }, "jest": { - "moduleFileExtensions": ["js", "json", "ts"], + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], "rootDir": "src", "testRegex": ".spec.ts$", "transform": { diff --git a/src/app.module.ts b/src/app.module.ts index 563c676..2bdf766 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,11 +1,32 @@ -import { Module } from '@nestjs/common'; +import { Module, NestModule, MiddlewareConsumer} from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { PostsController } from './posts/posts.controller'; +import { PostsModule } from './modules/posts/posts.module'; +import { DemoMiddleware } from './core/middleware/demo.middleware' +import { APP_GUARD } from '@nestjs/core'; +import { DemoRolesGuard } from './core/guards/demo-roles.guard' +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { LoggingInterceptor } from './core/interceptors/logging.interceptor' + + @Module({ - imports: [], - controllers: [AppController, PostsController], - providers: [AppService], + imports: [PostsModule], + controllers: [AppController], + providers: [ + AppService, + { + provide: APP_GUARD, + useClass: DemoRolesGuard, + }, + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + } + ], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(DemoMiddleware).forRoutes('posts') + } +} diff --git a/src/core/decorators/roles.decorator.ts b/src/core/decorators/roles.decorator.ts new file mode 100644 index 0000000..e73a826 --- /dev/null +++ b/src/core/decorators/roles.decorator.ts @@ -0,0 +1,3 @@ +import { ReflectMetadata, SetMetadata } from '@nestjs/common'; + +export const Roles = (...args: string[]) => SetMetadata('roles', args); diff --git a/src/core/decorators/user.decorator.ts b/src/core/decorators/user.decorator.ts new file mode 100644 index 0000000..5dd8faf --- /dev/null +++ b/src/core/decorators/user.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator } from '@nestjs/common' + +export const User = createParamDecorator((data, req) => { + console.log('data:', data) + + return req.user[data] +}) \ No newline at end of file diff --git a/src/core/filters/demo.filter.spec.ts b/src/core/filters/demo.filter.spec.ts new file mode 100644 index 0000000..2341e9f --- /dev/null +++ b/src/core/filters/demo.filter.spec.ts @@ -0,0 +1,7 @@ +import { DemoFilter } from './demo.filter'; + +describe('DemoFilter', () => { + it('should be defined', () => { + expect(new DemoFilter()).toBeDefined(); + }); +}); diff --git a/src/core/filters/demo.filter.ts b/src/core/filters/demo.filter.ts new file mode 100644 index 0000000..d6c8195 --- /dev/null +++ b/src/core/filters/demo.filter.ts @@ -0,0 +1,16 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'; + +@Catch(HttpException) +export class DemoFilter implements ExceptionFilter { + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus(); + + response.status(status).json({ + statusCode: status, + path: request.url, + }); + } +} diff --git a/src/core/guards/demo-auth.guard.spec.ts b/src/core/guards/demo-auth.guard.spec.ts new file mode 100644 index 0000000..92f909f --- /dev/null +++ b/src/core/guards/demo-auth.guard.spec.ts @@ -0,0 +1,7 @@ +import { DemoAuthGuard } from './demo-auth.guard'; + +describe('DemoAuthGuard', () => { + it('should be defined', () => { + expect(new DemoAuthGuard()).toBeDefined(); + }); +}); diff --git a/src/core/guards/demo-auth.guard.ts b/src/core/guards/demo-auth.guard.ts new file mode 100644 index 0000000..87d47af --- /dev/null +++ b/src/core/guards/demo-auth.guard.ts @@ -0,0 +1,13 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class DemoAuthGuard implements CanActivate { + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + + return request.header('x-demo') === 'secret'; + } +} diff --git a/src/core/guards/demo-roles.guard.spec.ts b/src/core/guards/demo-roles.guard.spec.ts new file mode 100644 index 0000000..ba3635d --- /dev/null +++ b/src/core/guards/demo-roles.guard.spec.ts @@ -0,0 +1,7 @@ +import { DemoRolesGuard } from './demo-roles.guard'; + +describe('DemoRolesGuard', () => { + it('should be defined', () => { + expect(new DemoRolesGuard()).toBeDefined(); + }); +}); diff --git a/src/core/guards/demo-roles.guard.ts b/src/core/guards/demo-roles.guard.ts new file mode 100644 index 0000000..2c75c26 --- /dev/null +++ b/src/core/guards/demo-roles.guard.ts @@ -0,0 +1,28 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class DemoRolesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + console.log('handler:', context.getHandler()); + console.log('class:', context.getClass()); + + const roles = this.reflector.get('roles', context.getHandler()) + console.log(roles) + + if (!roles) { + return true + } + + const request = context.switchToHttp().getRequest(); + const { user } = request; + const hasRole = () => user.roles.some(item => roles.includes(item)) + + return user && user.roles && hasRole(); + } +} diff --git a/src/core/interceptors/errors.interceptor.spec.ts b/src/core/interceptors/errors.interceptor.spec.ts new file mode 100644 index 0000000..ca053f7 --- /dev/null +++ b/src/core/interceptors/errors.interceptor.spec.ts @@ -0,0 +1,7 @@ +import { ErrorsInterceptor } from './errors.interceptor'; + +describe('ErrorsInterceptor', () => { + it('should be defined', () => { + expect(new ErrorsInterceptor()).toBeDefined(); + }); +}); diff --git a/src/core/interceptors/errors.interceptor.ts b/src/core/interceptors/errors.interceptor.ts new file mode 100644 index 0000000..a374e5b --- /dev/null +++ b/src/core/interceptors/errors.interceptor.ts @@ -0,0 +1,14 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor, BadGatewayException } 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 { + return next + .handle() + .pipe( + catchError(error => throwError(new BadGatewayException())) + ); + } +} diff --git a/src/core/interceptors/logging.interceptor.spec.ts b/src/core/interceptors/logging.interceptor.spec.ts new file mode 100644 index 0000000..978e0bb --- /dev/null +++ b/src/core/interceptors/logging.interceptor.spec.ts @@ -0,0 +1,7 @@ +import { LoggingInterceptor } from './logging.interceptor'; + +describe('LoggingInterceptor', () => { + it('should be defined', () => { + expect(new LoggingInterceptor()).toBeDefined(); + }); +}); diff --git a/src/core/interceptors/logging.interceptor.ts b/src/core/interceptors/logging.interceptor.ts new file mode 100644 index 0000000..069d4eb --- /dev/null +++ b/src/core/interceptors/logging.interceptor.ts @@ -0,0 +1,21 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators' + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + console.log('I am a interceptor!') + + const now = Date.now(); + console.log('before...'); + + + + return next + .handle() + .pipe( + tap(() => console.log(`after... ${Date.now() - now}ms`)) + ); + } +} diff --git a/src/core/interceptors/transform.interceptor.spec.ts b/src/core/interceptors/transform.interceptor.spec.ts new file mode 100644 index 0000000..b1cd38a --- /dev/null +++ b/src/core/interceptors/transform.interceptor.spec.ts @@ -0,0 +1,7 @@ +import { TransformInterceptor } from './transform.interceptor'; + +describe('TransformInterceptor', () => { + it('should be defined', () => { + expect(new TransformInterceptor()).toBeDefined(); + }); +}); diff --git a/src/core/interceptors/transform.interceptor.ts b/src/core/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..a5a8184 --- /dev/null +++ b/src/core/interceptors/transform.interceptor.ts @@ -0,0 +1,18 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export interface Response{ + data: T; +} + +@Injectable() +export class TransformInterceptor implements NestInterceptor> { + intercept(context: ExecutionContext, next: CallHandler): Observable> { + return next + .handle() + .pipe( + map(item => ({data: item})) // 这个map和js那个map貌似还是有点不一样,那边返回的是一个数组呀? + ); + } +} diff --git a/src/core/middleware/demo.middleware.spec.ts b/src/core/middleware/demo.middleware.spec.ts new file mode 100644 index 0000000..b2672f0 --- /dev/null +++ b/src/core/middleware/demo.middleware.spec.ts @@ -0,0 +1,7 @@ +import { DemoMiddleware } from './demo.middleware'; + +describe('DemoMiddleware', () => { + it('should be defined', () => { + expect(new DemoMiddleware()).toBeDefined(); + }); +}); diff --git a/src/core/middleware/demo.middleware.ts b/src/core/middleware/demo.middleware.ts new file mode 100644 index 0000000..942e278 --- /dev/null +++ b/src/core/middleware/demo.middleware.ts @@ -0,0 +1,24 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; + +@Injectable() +export class DemoMiddleware implements NestMiddleware { + use(req: any, res: any, next: () => void) { + console.log('hello ~') + req.user = { + roles: [ + 'guest' + ] + } + + if (req.header('x-demo') === 'secret') { + req.user = { + roles: [ + 'member' + ] + } + } + + + next(); + } +} diff --git a/src/core/pipes/demo.pipe.spec.ts b/src/core/pipes/demo.pipe.spec.ts new file mode 100644 index 0000000..e2a7102 --- /dev/null +++ b/src/core/pipes/demo.pipe.spec.ts @@ -0,0 +1,7 @@ +import { DemoPipe } from './demo.pipe'; + +describe('DemoPipe', () => { + it('should be defined', () => { + expect(new DemoPipe()).toBeDefined(); + }); +}); diff --git a/src/core/pipes/demo.pipe.ts b/src/core/pipes/demo.pipe.ts new file mode 100644 index 0000000..214b763 --- /dev/null +++ b/src/core/pipes/demo.pipe.ts @@ -0,0 +1,8 @@ +import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; + +@Injectable() +export class DemoPipe implements PipeTransform { + transform(value: any, metadata: ArgumentMetadata) { + return value; + } +} diff --git a/src/main.ts b/src/main.ts index 13cad38..89a5e14 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { DemoFilter } from './core/filters/demo.filter' async function bootstrap() { const app = await NestFactory.create(AppModule); + // app.useGlobalFilters(new DemoFilter()); await app.listen(3000); } bootstrap(); diff --git a/src/modules/posts/interfaces/post.interface.ts b/src/modules/posts/interfaces/post.interface.ts new file mode 100644 index 0000000..991c559 --- /dev/null +++ b/src/modules/posts/interfaces/post.interface.ts @@ -0,0 +1,3 @@ +export interface Post { + title: string +} diff --git a/src/modules/posts/post.dto.ts b/src/modules/posts/post.dto.ts new file mode 100644 index 0000000..632f55a --- /dev/null +++ b/src/modules/posts/post.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator' + +export class CreatePostDto { + @IsString() + readonly title: string +} \ No newline at end of file diff --git a/src/posts/posts.controller.spec.ts b/src/modules/posts/posts.controller.spec.ts similarity index 100% rename from src/posts/posts.controller.spec.ts rename to src/modules/posts/posts.controller.spec.ts diff --git a/src/modules/posts/posts.controller.ts b/src/modules/posts/posts.controller.ts new file mode 100644 index 0000000..47674d5 --- /dev/null +++ b/src/modules/posts/posts.controller.ts @@ -0,0 +1,51 @@ +import { Controller, Get, Post, Req, Query, Headers, Param, Body, HttpException, HttpStatus, ForbiddenException, UseFilters, UsePipes, ValidationPipe, ParseIntPipe, UseGuards, SetMetadata, UseInterceptors } from '@nestjs/common'; +import { CreatePostDto } from './post.dto'; +import { DemoService } from './providers/demo/demo.service' +import { DemoFilter } from '../../core/filters/demo.filter' +import { DemoAuthGuard } from '../../core/guards/demo-auth.guard' +import { Roles } from '../../core/decorators/roles.decorator' +import { User } from '../../core/decorators/user.decorator' +import { LoggingInterceptor } from '../../core/interceptors/logging.interceptor' +import { TransformInterceptor } from '../../core/interceptors/transform.interceptor' +import { ErrorsInterceptor } from '../../core/interceptors/errors.interceptor' + +@Controller('posts') +// @UseGuards(DemoAuthGuard) +// @UseFilters(DemoFilter) +// @ UseInterceptors(LoggingInterceptor) +export class PostsController { + + constructor(private readonly demoService: DemoService) {} + + @Get() + @UseInterceptors(TransformInterceptor) + @UseInterceptors(ErrorsInterceptor) + index() { + throw new ForbiddenException(); + // return this.demoService.findAll(); + } + + @Get(':id') + show(@Param('id', ParseIntPipe) id) { + console.log('typeof id:', typeof id); + + return { + title: `Post ${id}` + } + } + + @Post() + // @UseFilters(DemoFilter) + @UsePipes(ValidationPipe) + // @SetMetadata('roles', ['member', 89]) + // @Roles('member', 'tester') + @Roles(...['member', 'tester']) + store(@Body() post: CreatePostDto, @User('roles') roles) { + console.log('roles:', roles) + + + // throw new HttpException('没有权限!', HttpStatus.FORBIDDEN) + // throw new ForbiddenException('没有权限!') + this.demoService.create(post); + } +} diff --git a/src/modules/posts/posts.module.ts b/src/modules/posts/posts.module.ts new file mode 100644 index 0000000..b329793 --- /dev/null +++ b/src/modules/posts/posts.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { PostsController } from './posts.controller'; +import { DemoService } from "./providers/demo/demo.service"; + +@Module({ + controllers: [PostsController], + providers: [DemoService] +}) +export class PostsModule {} diff --git a/src/modules/posts/providers/demo/demo.service.spec.ts b/src/modules/posts/providers/demo/demo.service.spec.ts new file mode 100644 index 0000000..552dbbd --- /dev/null +++ b/src/modules/posts/providers/demo/demo.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DemoService } from './demo.service'; + +describe('DemoService', () => { + let service: DemoService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DemoService], + }).compile(); + + service = module.get(DemoService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/posts/providers/demo/demo.service.ts b/src/modules/posts/providers/demo/demo.service.ts new file mode 100644 index 0000000..0168854 --- /dev/null +++ b/src/modules/posts/providers/demo/demo.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { Post } from 'src/modules/posts/interfaces/post.interface'; + +@Injectable() +export class DemoService { + private readonly posts: Post[] = []; + + findAll(): Post[] { + return this.posts; + } + + create(post: Post) { + this.posts.push(post); + } +} diff --git a/src/posts/post.dto.ts b/src/posts/post.dto.ts deleted file mode 100644 index 4783ca3..0000000 --- a/src/posts/post.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class CreatePostDto { - readonly title: string -} \ No newline at end of file diff --git a/src/posts/posts.controller.ts b/src/posts/posts.controller.ts deleted file mode 100644 index e51b874..0000000 --- a/src/posts/posts.controller.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Controller, Get, Post, Req, Query, Headers, Param, Body } from '@nestjs/common'; -import { CreatePostDto } from './post.dto'; - -@Controller('posts') -export class PostsController { - @Get() - index(@Headers('authorization') headers, @Query() query) { - console.log(headers) - console.log(query) - return [ - { - title: 'hello ~' - } - ] - } - - @Get(':id') - show(@Param() params) { - return { - title: `Post ${params.id}` - } - } - - @Post() - store(@Body() post: CreatePostDto) { - console.log(post) - return post - } -} diff --git a/yarn.lock b/yarn.lock index b79d114..a5d4dfd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -797,6 +797,11 @@ ci-info@^1.5.0: resolved "https://registry.npm.taobao.org/ci-info/download/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" integrity sha1-LKINu5zrMtRSSmgzAzE/AwSx5Jc= +class-transformer@^0.2.3: + version "0.2.3" + resolved "https://registry.npm.taobao.org/class-transformer/download/class-transformer-0.2.3.tgz#598c92ca71dcca73f91ccb875d74a3847ccfa32d" + integrity sha1-WYySynHcynP5HMuHXXSjhHzPoy0= + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.npm.taobao.org/class-utils/download/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -807,6 +812,14 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +class-validator@^0.9.1: + version "0.9.1" + resolved "https://registry.npm.taobao.org/class-validator/download/class-validator-0.9.1.tgz#d60e58c5d14abca0a41bce38cf792ad4c46d1531" + integrity sha1-1g5YxdFKvKCkG844z3kq1MRtFTE= + dependencies: + google-libphonenumber "^3.1.6" + validator "10.4.0" + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.npm.taobao.org/cli-boxes/download/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -1787,6 +1800,11 @@ globals@^9.18.0: resolved "https://registry.npm.taobao.org/globals/download/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" integrity sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo= +google-libphonenumber@^3.1.6: + version "3.2.2" + resolved "https://registry.npm.taobao.org/google-libphonenumber/download/google-libphonenumber-3.2.2.tgz#3d9d7ba727e99a50812f21b0ed313723b76c5c54" + integrity sha1-PZ17pyfpmlCBLyGw7TE3I7dsXFQ= + got@^6.7.1: version "6.7.1" resolved "https://registry.npm.taobao.org/got/download/got-6.7.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fgot%2Fdownload%2Fgot-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" @@ -4925,6 +4943,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@10.4.0: + version "10.4.0" + resolved "https://registry.npm.taobao.org/validator/download/validator-10.4.0.tgz#ee99a44afb3bb5ed350a159f056ca72a204cfc3c" + integrity sha1-7pmkSvs7te01ChWfBWynKiBM/Dw= + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"