docs: update docs

This commit is contained in:
hantsy 2020-08-07 21:58:06 +08:00
parent 70ef263ea5
commit b785eb843b
5 changed files with 164 additions and 33 deletions

View File

@ -9,8 +9,10 @@ A NestJS RESTful APIs sample project.
## Docs
* [Building RESTful APIs with NestJS](./docs/guide.md)
* [Getting Started](./docs/guide.md)
* [Connecting to MongoDB](./docs/mongo.md)
* [Protect your APIs with JWT Token](./docs/auth.md)
* [Dealing with model relations](./docs/model.md)
## Build

View File

@ -1,13 +1,13 @@
# Protect your API resource with JWT Token
# Protect your APIs with JWT Token
In the last post, we connected to a Mongo server and use a real database to replace the dummy data storage. In this post, we will explore how to protect your APIs when exposing to a client application.
When we come to the security of a web application, technically it will include:
* **Authentication** - The application will ask you to provide your principal and then it will identify who are you.
* **Authorization**- Based on your claims, check if you have permissions to perform some tasks.
* **Authorization**- Based on your claims, check if you have permissions to perform some operations.
[Passportjs](http://www.passportjs.org/) is one of the most popular authentication framework for [expressjs](https://expressjs.com/) platform. Nestjs has great integration with passportjs with `@nestjs/passportjs`. We will follow the [Authentication](https://docs.nestjs.com/techniques/authentication) chapter of the official guide to add *local* and *jwt* strategies to your application.
[Passportjs](http://www.passportjs.org/) is one of the most popular authentication frameworks on the [Expressjs](https://expressjs.com/) platform. Nestjs has great integration with passportjs with its `@nestjs/passportjs` module. We will follow the [Authentication](https://docs.nestjs.com/techniques/authentication) chapter of the official guide to add *local* and *jwt* strategies to the application we have done the previous posts.
## Prerequisites
@ -26,7 +26,9 @@ Firstly generate a `AuthModule` and `AuthService` .
nest g mo auth
nest g s auth
```
The authentication should work with users in the application. Create a standalone `UserModule` to handle user queries.
The authentication should work with users in the application.
Similarly, create a standalone `UserModule` to handle user queries.
```bash
nest g mo user
@ -58,7 +60,7 @@ export class User extends Document {
export const UserSchema = SchemaFactory.createForClass(User);
```
The `User` class is to wrap a document in Mongo, and `UserSchema` is to describe `User` document.
The `User` class is to wrap a document in Mongoose, and `UserSchema` is to describe `User` document.
Register `UserSchema` in `UserModule`, then you can use `Model<User>` to perform some operations on `User` document.
@ -73,7 +75,7 @@ Register `UserSchema` in `UserModule`, then you can use `Model<User>` to perfor
export class UserModule {}
```
The *users* here is the token to identify different `Model` when injecting a `Model`.
The *users* here is used as the *token* to identify different `Model` when injecting a `Model`. When registering a `UserSchema` in mongoose, the name attribute in the above `MongooseModule.forFeature` is also the collection name of `User ` documents.
Add a `findByUsername` method in `UserService`.
@ -88,6 +90,19 @@ export class UserService {
}
```
In the `@Module` declaration of the `UserModule`, register `UserService` in `providers`, and do not forget to add it into `exports`, thus other modules can use this service when importing `UserModule`.
```typescript
//...other imports
import { UserService } from './user.service';
@Module({
providers: [UserService],
exports:[UserService]//exposing users to other modules...
})
export class UserModule {}
```
Create a test case to test the `findByUsername` method.
```typescript
@ -138,7 +153,7 @@ describe('UserService', () => {
});
});
```
In the `@Module` declaration of the `UserModule`, register `UserService` in `providers`, and do not forget to add it into `exports`, thus other can use this service when importing `UserModule`.
`UserService` depends on a `Model<User>`, use a provider to mock it by jest mocking feature. Using `jest.spyOn` method, you can stub the details of a methods, and watch of the calling of this method.
Let's move to `AuthModule`.
@ -174,9 +189,9 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
}
```
In the constructor, use `super` to providing the essential options of the strategy you are using, for a local strategy, it requires username and password fields.
In the constructor, use `super` to provide the essential options of the strategy you are using. For the local strategy, it requires username and password fields.
And the validate method it used to validate the authentication strategy against given info, here it is the *username* and *password* provided from request.
And the validate method is used to validate the authentication info against given info, here it is the *username* and *password* provided from request.
> More details about the configuration options and validation of local strategy, check [passport-local](http://www.passportjs.org/packages/passport-local/) project.
@ -204,7 +219,7 @@ export class AuthService {
}
```
> In the real application, we could use a crypto util to hash password.
> In the real application, we could use a crypto util to hash and compare the input password. We will discuss it in the further post.
It invokes `findByUsername` in `UserService` from `UserModule`. Imports `UserModule` in the declaration of `AuthModule`.
@ -271,11 +286,11 @@ Let's summarize how local strategy works.
1. When a user hits *auth/login* with `username` and `password`, `LocalAuthGuard` will be applied.
2. `LocalAuthGuard` will trigger `LocalStrategy` , and invokes its `validate` method, and store the result back to `request.user`.
3. Back the controller, read user principal from `request`, generate a JWT access token and send back to client.
3. Back the controller, read user principal from `request`, generate a JWT token and send it back to the client.
After logging in, the `access token` can be extracted and put into the HTTP header in the new request to access the protected resources.
Let's have a look at how jwt strategy works.
Let's have a look at how JWT strategy works.
Firstly implement the `JwtStrategy`.
@ -366,7 +381,9 @@ If you want to set a default strategy, change `PassportModule` in the declaratio
export class AuthModule {}
```
Like the data initializer for Post, add a service to insert sample data for users.
There are several application lifecycle hooks provided in Nestjs at runtime. In your codes you can observe these lifecycle events and perform some specific tasks for your application.
For example, create a data initializer for `Post` to insert sample data.
```typescript
@Injectable()
@ -392,9 +409,27 @@ export class UserDataInitializerService
}
```
Now run the application.
> More info about the lifecycle hooks, check the [Lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events) chapter of the official docs.
## Run the application
Open your terminal, run the application by executing the following command.
```bash
npm run start
```
Login using the *username/password* pair.
```bash
>curl http://localhost:3000/auth/login -d "{\"username\":\"hantsy\", \"password\":\"password\"}" -H "Content-Type:application/json"
>{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZjJkMGU0ODZhOTZiZTEyMDBmZWZjZWMiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTk2Nzg4NDg5LCJleHAiOjE1OTY3OTIwODl9.4oYpKTikoTfeeaUBoEFr9d1LPcN1pYqHjWXRuZXOfek"}
```
Try to access the */profile* endpoint using this *access_token*.
```bash
>curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZjJkMGU0ODZhOTZiZTEyMDBmZWZjZWMiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTk2Nzg4NDg5LCJleHAiOjE1OTY3OTIwODl9.4oYpKTikoTfeeaUBoEFr9d1LPcN1pYqHjWXRuZXOfek"
{"username":"hantsy","email":"hantsy@example.com","id":"5f2d0e486a96be1200fefcec","roles":["USER"]}
```

View File

@ -1,6 +1,6 @@
# Building RESTful APIs with NestJS
In this post, I will demonstrate how to kickstart a simple RESTful APIs with NestJS from a newbie's respective.
In this post, I will demonstrate how to kickstart a simple RESTful APIs with NestJS from a newbie's viewpoint.
@ -11,7 +11,7 @@ As described in the [Nestjs](https://nestjs.com) website, Nestjs is *a progressi
Nestjs combines the best programming practice and the cutting-edge techniques from the NodeJS communities.
* A lot of NestJS concepts are heavily inspired by the effort of the popular frameworks in the world, esp. [Angular](https://www.angular.io), [Spring WebMVC](http://www.spring.io).
* Nestjs is flexible, you are free to choose [Expressjs](https://expressjs.com/) or [Fastify](https://www.fastify.io) as the background engine.
* Nestjs hides the complexities of web programming in NodeJS, it provides a common abstraction of the web request handling, you are free to choose [Expressjs](https://expressjs.com/) or [Fastify](https://www.fastify.io) as the background engine.
* Nestjs provides a lot of third party project integration, from database operations, such as Mongoose, TypeORM, etc. to a Message Broker, such as Redis, RabbitMQ, etc.
If you are new to Nestjs like me but has some experience of Angular or Spring WebMVC, bootstrap a Nestjs project is really a piece of cake.
@ -106,7 +106,7 @@ The default structure of this project is very similar with the one generated by
* *src/main.ts* is the entry file of this application.
* *src/app\** is the top level component in a nest application.
* There is an a*pp.module.ts* is a Nestjs `Module` which is similar with Angular `NgModule`, and used to organize codes in the logic view.
* The *app.service.ts* is an `@Injectable` component, similar with the service in Angular or Spring's Service, it is used for handling business logic. A service is annotated with `@Injectable`.
* The *app.service.ts* is an `@Injectable` component, similar with the service in [Angular](https://spring.io) or Spring's Service, it is used for handling business logic. A service is annotated with `@Injectable`.
* The *app.controller.ts* is the controller of MVC, to handle incoming request, and responds the handled result back to client. The annotatoin `@Controller()` is similar with Spring MVC's `@Controller`.
* The *app.controller.spec.ts* is test file for *app.controller.ts*. Nestjs uses [Jest](https://jestjs.io/) as testing framework.
* *test* folder is for storing e2e test files.
@ -123,9 +123,9 @@ I will reuse the concept I've used in the former examples - the blogging posts.
In the next steps, we will create:
* A new module `post` to organize all the features.
* A new module `PostModule` to organize all these features.
* A `Post` interface to present the `Post` entity resource.
* A `PostService` component to serve the data for controller. In this post, we use dummy data storage temporarily, and replace it with a real Mongo in the future post.
* A `PostService` component to serve the data for controller. In this post, we use a dummy data storage temporarily, and we will replace it with a real MongoDB in the future post.
* A `PostController` to expose APIs which is responsible for handling incoming requests.
@ -151,13 +151,13 @@ import { PostModule } from './post/post.module';
export class AppModule {}
```
Then generate the `PostService`, `Post` respectively.
Then generate a `Post` service and interface respectively.
```bash
nest g s post
nest g interface post
```
The `PostService` is added to `PostModule` automatcially when it is generated.
The `PostService` is added to `PostModule` automatically when it is generated.
```typescript
//...other imports
@ -169,7 +169,7 @@ import { PostService } from './post.service';
export class PostModule {}
```
After it is done there are 4 files created in the *src/app/post* folder, including two specs files for testing `PostController` and `PostService`.
After it is done there are 4 files created in the *src/app/post* folder, including a *spec* file for testing `PostService`.
```bash
post.interface.ts
@ -190,7 +190,7 @@ export interface Post {
}
```
In the above interface, add some fields as you see, here we make the `id`, `createdAt`, `updatedAt` are optional now.
In the above interface, add some fields as you see, here we make the `id`, `createdAt`, `updatedAt` *optional* now.
Let's create a method in `PostService` to fetch all posts.
@ -224,7 +224,7 @@ export class PostService {
}
```
In the codes, we used an array `posts` for the background data storage. The `findAll` method returns a `Observerable` which is created from the dummy `posts` we have declared.
In the codes, we used an array `posts` as the background data storage. The `findAll` method returns a `Observerable` which is created from the dummy `posts` we have declared.
Add a test for this method. Open `post.service.spec.ts`, add a new test case.
@ -258,7 +258,7 @@ describe('PostService', () => {
}
```
In the `beforeEach` , it prepares a `TestingModule` to assemble resources in the test.
Similar with Angular, in the `beforeEach` hook , it prepares a `TestingModule` to assemble the required resources in the test.
> Like the annotations (@BeforeEach, etc.) provided JUnit 5 in Java world, there are some similar hooks ready for preparing the test and doing clean work in a Jest test, such as `beforeAll, afterAll, beforeEache, afterEach` , etc.
@ -281,12 +281,12 @@ Time: 4.563 s, estimated 11 s
Ran all test suites.
```
> If you want to track the changes on test codes and rerun the test cases automatically, use `npm run test:watch` instead.
It works.
> If you want to track the changes of test codes and rerun the test cases automatically, use `npm run test:watch` instead.
Let's modify the `findAll` slightly, make it accept a keyword to query the posts.
```typescript

View File

@ -390,7 +390,7 @@ export class PostService {
}
```
As a convention in Nestjs, you have to make `PostController` `REQUEST` scoped.
As a convention in Nestjs, you have to make `PostController` available in the `REQUEST` scoped.
```typescript
@Controller({path:'posts', scope:Scope.REQUEST})
@ -399,6 +399,100 @@ export class PostController {...}
In the test codes, you have to `resolve` to replace `get` to get the instance from Nestjs test harness.
```typescript
describe('Post Controller', () => {
describe('Replace PostService in provider(useClass: PostServiceStub)', () => {
let controller: PostController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PostService,
useClass: PostServiceStub,
},
],
controllers: [PostController],
}).compile();
controller = await module.resolve<PostController>(PostController);// use resovle here....
});
...
```
`PostService` also should be changed to request scoped.
```typescript
@Injectable({ scope: Scope.REQUEST })
export class PostService {...}
```
In the `post.service.spec.ts` , you have to update the mocking progress.
```typescript
describe('PostService', () => {
let service: PostService;
let model: Model<Post>;
let commentModel: Model<Comment>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PostService,
{
provide: POST_MODEL,
useValue: {
new: jest.fn(),
constructor: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
create: jest.fn(),
remove: jest.fn(),
exec: jest.fn(),
deleteMany: jest.fn(),
deleteOne: jest.fn(),
updateOne: jest.fn(),
findOneAndUpdate: jest.fn(),
findOneAndDelete: jest.fn(),
},
},
{
provide: COMMENT_MODEL,
useValue: {
new: jest.fn(),
constructor: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
updateOne: jest.fn(),
deleteOne: jest.fn(),
update: jest.fn(),
create: jest.fn(),
remove: jest.fn(),
exec: jest.fn(),
},
},
{
provide: REQUEST,
useValue: {
user: {
id: 'dummyId',
},
},
},
],
}).compile();
service = await module.resolve<PostService>(PostService);
model = module.get<Model<Post>>(POST_MODEL);
commentModel = module.get<Model<Comment>>(COMMENT_MODEL);
});
//...
```
## Run the application
Now we have done the clean work, run the application to make sure it works as expected.
@ -434,6 +528,6 @@ $ curl http://localhost:3000/posts/5ef0b7fe4d067321141845fc/comments
After cleaning up the codes, we do not need the `@nestjs/mongoose` dependency, let's remove it.
```bash
npm r --save @nestjs/mongoose
npm uninstall --save @nestjs/mongoose
```

View File

@ -2,7 +2,7 @@
In the last post , we created a RESTful API application for simple CRUD functionalities. In this post, we will enrich it:
* Add Mongo support
* Add MongoDB support
* Change dummy data storage to use MongoDB
* Clean the testing codes