Back to README
Swagger UI
| Versioning
| Validation
| Caching
| Authentication
| Authorization
Authorization (Authentication required)
Ability predicates will allow you to condition the actions of your users for each protected route of your APIs.
Authentication is required, you need to enable it or implement your own strategy that adds the User object in the request.
MongoDB dynamic API uses the User
object in the requests to apply the ability predicates defined in the DynamicApiModule.forFeature
.
You can define them either in the controller options,
or in each route object declared in the routes property.
If the ability predicates are specified in 2, those defined in the route will have priority.
By default, the User
object is added to the request by the useAuth
configuration.
It contains the id
and email
fields of the authenticated user.
We will add the isAdmin
field to the User
object by adding it in the requestAdditionalFields
property.
We also need to add the isAdmin
field in the additionalFields
property of the register
object
to allow the creation of admin users.
Ok, let's see how to protect the /auth/register
and let only the admin users create new users.
Configuration
// src/app.module.ts
import { Module } from '@nestjs/common';
import { DynamicApiModule } from 'mongodb-dynamic-api';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { User } from './users/user';
import { ArticlesModule } from './articles/articles.module';
@Module({
imports: [
DynamicApiModule.forRoot(
'your-mongodb-uri',
{
useAuth: {
user: {
entity: User,
requestAdditionalFields: ['isAdmin'], // <- add the isAdmin field to the request User object
},
register: {
additionalFields: [{ name: 'isAdmin', required: true }], // <- add the isAdmin field to the register body
},
},
},
),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Usage
First, let's create an admin user with the POST
method on the /auth/register
public route.
# POST /auth/register
curl -X 'POST' \
'<your-host>/auth/register' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"email": "admin@test.co",
"password": "admin",
"isAdmin": true
}'
# Server response
{"accessToken":"<admin-jwt-token>"}
Then, in the register configuration, we are going to protect the /auth/register
route by setting
the abilityPredicate
property with a special register ability predicate that takes
only the user as argument and returns true
if the user is an admin.
// src/app.module.ts
@Module({
imports: [
DynamicApiModule.forRoot(
'your-mongodb-uri',
{
useAuth: {
// ...,
register: {
additionalFields: [{ name: 'isAdmin', required: true }],
abilityPredicate: (user: User) => user.isAdmin, // <- only admin users can create new users
},
},
},
),
],
// ...
})
export class AppModule {}
Ok, now let's create a non admin user with the POST
method on the /auth/register
route.
# POST /auth/register
curl -X 'POST' \
'<your-host>/auth/register' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"email": "toto@test.co",
"password": "toto",
"isAdmin": false
}'
# Server response
{"accessToken":"<toto-jwt-token>"}
Next, under toto's account (not admin), we will try to register a new user with the POST
method on the /auth/register
route.
The register ability predicate will return false
and we will receive a 403 Forbidden
error.
# POST /auth/register
curl -X 'POST' \
'<your-host>/auth/register' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <toto-jwt-token>' \
-H 'Content-Type: application/json' \
-d '{
"email": "user@test.co",
"password": "user",
"isAdmin": false
}'
# Server response
{
"message": "Access denied",
"error": "Forbidden",
"statusCode": 403
}
The register route is now well protected and only an admin user can create new users.
We are going to add a new content Article
and protect the get one, update and delete routes with
ability predicates.
We will also add custom DTOs:
- to the
CreateOne
route to define the fields required to create an article. - to the
UpdateOne
route to remove the possibility to update theauthorId
field when editing an article.
Configuration
// src/articles/article.ts
import { Prop } from '@nestjs/mongoose';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from 'mongodb-dynamic-api';
@Schema()
export class Article extends BaseEntity {
@ApiProperty()
@IsString()
@IsNotEmpty()
@Prop({ type: String, required: true })
title: string;
@ApiProperty({ type: Boolean, default: false })
@Prop({ type: Boolean, default: false })
isPublished: boolean;
@ApiProperty()
@Prop({ type: String, required: true })
authorId: string;
}
// src/articles/create-one-article.dto.ts
import { PickType } from '@nestjs/swagger';
import { Article } from './article';
export class CreateOneArticleDto extends PickType(Article, [
'title',
'authorId',
]) {}
// src/articles/update-one-article.dto.ts
import { PartialType, PickType } from '@nestjs/swagger';
import { Article } from './article';
export class UpdateOneArticleDto extends PartialType(
PickType(Article, ['title', 'isPublished']),
) {}
PartialType
and PickType
are decorators from the @nestjs/swagger
package.
They allow you to create a new DTO
by picking (PickType
) only the fields you want from the
original DTO and make them optional (PartialType
).
See nestjs documentation for more details.
We will allow only the author to get, update or delete the article if not published yet and only the admins to delete articles even if they are published.
Let's add our ability predicates to the GetOne
, UpdateOne
, DeleteOne
and DeleteMany
routes.
The ability predicate is an arrow function that takes the Content (the entity) and the User request object (optional) as arguments and returns a boolean.
(entity: Entity, user?: User) => boolean;
// src/articles/articles.module.ts
import { Module } from '@nestjs/common';
import { DynamicApiModule } from 'mongodb-dynamic-api';
import { User } from '../users/user';
import { Article } from './article';
@Module({
imports: [
DynamicApiModule.forFeature({
entity: Article,
controllerOptions: {
path: 'articles',
abilityPredicates: [ // <- declare the ability predicates in the controller options
{
targets: ['DeleteMany', 'DeleteOne'], // <- declare the targeted routes
predicate: (article: Article, user: User) =>
user.isAdmin ||
(article.authorId === user.id && !article.isPublished), // <- add the condition
},
],
},
routes: [
{ type: 'GetMany', isPublic: true }, // <- declare the non protected route by setting isPublic to true
{
type: 'GetOne',
abilityPredicate: (article: Article, user: User) =>
article.authorId === user.id || article.isPublished,
},
{
type: 'CreateOne', // <- protected by default, needs the user to be authenticated to access it
dTOs: { body: CreateOneArticleDto }, // <- add yours dto here
},
{
type: 'UpdateOne',
abilityPredicate: (article: Article, user: User) => // <- declare the ability predicate in the route object
article.authorId === user.id && !article.isPublished,
dTOs: { body: UpdateOneArticleDto }, // <- add yours dto here
},
{ type: 'DeleteMany' }, // <- protected by default and by the ability predicate set in the controller options
{ type: 'DeleteOne' },
],
}),
],
})
export class ArticlesModule {}
Last, don't forget to add Article to the DynamicApiModule.forFeature
in the imports
property of the AppModule
.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { DynamicApiModule } from 'mongodb-dynamic-api';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { User } from './users/user';
import { ArticlesModule } from './articles/articles.module';
@Module({
imports: [
DynamicApiModule.forRoot(
'your-mongodb-uri',
{
useAuth: {
user: {
entity: User,
requestAdditionalFields: ['isAdmin'],
},
register: {
additionalFields: [{ name: 'isAdmin', required: true }],
abilityPredicate: (user: User) => user.isAdmin,
},
},
},
),
ArticlesModule, // <- add the new module here
],
})
export class AppModule {}
Ok now we have 2 APIs, Auth and Articles, with routes strongly protected by ability predicates.
- Auth: Account, Login and Register
- Articles: GetMany, GetOne, CreateOne, UpdateOne, DeleteOne and DeleteMany
CreateOne test
First of all, let's try to create an article with the POST
method on the /articles
route without being authenticated.
# POST /articles
curl -X 'POST' \
'<your-host>/articles' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "My first article",
"authorId": "<author-id>"
}'
# Server response
{ "message": "Unauthorized", "statusCode": 401 }
Ok, now logged in as toto, we will retry to create the article.
# POST /articles
curl -X 'POST' \
'<your-host>/articles' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <toto-jwt-token>' \ # <- replace by the toto jwt token
-H 'Content-Type: application/json' \
-d '{
"title": "My first article",
"authorId": "<toto-id>" # <- replace by the toto id
}'
# Server response
{
"title": "My first article",
"isPublished": false,
"authorId": "<toto-id>",
"createdAt": "article-created-at",
"updatedAt": "article-updated-at",
"id": "<article-id>"
}
GetMany test
Next, we will try to get all the articles with the GET
method on the /articles
route without being authenticated.
# GET /articles
curl -X 'GET' \
'<your-host>/articles' \
-H 'accept: application/json'
# Server response
[
{
"id": "<article-id>",
"title": "My first article",
"isPublished": false,
"authorId": "<toto-id>",
"createdAt": "article-created-at",
"updatedAt": "article-updated-at"
}
]
GetOne test
Now, we will try to get the article with the GET
method on the /articles/:id
route without being authenticated.
# GET /articles/:id
curl -X 'GET' \
'<your-host>/articles/<article-id>' \ # <- replace by the article id
-H 'accept: application/json'
# Server response
{ "message": "Unauthorized", "statusCode": 401 }
Then, logged in as toto, we will retry to get the article.
# GET /articles/:id
curl -X 'GET' \
'<your-host>/articles/<article-id>' \ # <- replace by the article id
-H 'accept: application/json' \
-H 'Authorization: Bearer <toto-jwt-token>' # <- replace by the toto jwt token
# Server response
{
"id": "<article-id>",
"title": "My first article",
"isPublished": false,
"authorId": "<toto-id>",
"createdAt": "article-created-at",
"updatedAt": "article-updated-at"
}
Finally, logged in as admin, we will retry to get the article.
# GET /articles/:id
curl -X 'GET' \
'<your-host>/articles/<article-id>' \ # <- replace by the article id
-H 'accept: application
-H 'Authorization: Bearer <admin-jwt-token>' # <- replace by the admin jwt token
# Server response
{
"message": "Forbidden resource",
"error": "Forbidden",
"statusCode": 403
}
Ok, the GetOne
route is well protected and only the author can access the article if not published yet.
UpdateOne test
Next, logged in as admin, we will try to update the article with the PUT
method on the /articles/:id
route.
# PUT /articles/:id
curl -X 'PATCH' \
'<your-host>/articles/<article-id>' \ # <- replace by the article id
-H 'accept: application/json' \
-H 'Authorization: Bearer <admin-jwt-token>' \ # <- replace by the admin jwt token
-H 'Content-Type: application/json' \
-d '{
"isPublished": true
}'
# Server response
{
"message": "Forbidden resource",
"error": "Forbidden",
"statusCode": 403
}
Let's retry, logged in as toto, to update the article.
# PUT /articles/:id
curl -X 'PATCH' \
'<your-host>/articles/<article-id>' \ # <- replace by the article id
-H 'accept: application/json' \
-H 'Authorization: Bearer <toto-jwt-token>' \ # <- replace by the toto jwt token
-H 'Content-Type: application/json' \
-d '{
"isPublished": true
}''
# Server response
{
"id": "<article-id>",
"title": "My first article",
"isPublished": true,
"authorId": "<toto-id>",
"createdAt": "article-created-at",
"updatedAt": "new-article-updated-at"
}
GetOne (admin) test
Finally, logged in as admin, we will retry to get the published article.
# GET /articles/:id
curl -X 'GET' \
'<your-host>/articles/<article-id>' \ # <- replace by the article id
-H 'accept: application
-H 'Authorization
-H 'Authorization: Bearer <admin-jwt-token>' # <- replace by the admin jwt token
# Server response
{
"id": "<article-id>",
"title": "My first article",
"isPublished": true,
"authorId": "<toto-id>",
"createdAt": "article-created-at",
"updatedAt": "new-article-updated-at"
}
Great, the published article is now accessible to the admin.
DeleteOne test
Here, logged in as toto, we will try to delete the article with the DELETE
method on the /articles/:id
route.
# DELETE /articles/:id
curl -X 'DELETE' \
'<your-host>/articles/<article-id>' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <toto-jwt-token>' # <- replace by the toto jwt token
# Server response
{
"message": "Forbidden resource",
"error": "Forbidden",
"statusCode": 403
}
Finally, logged in as admin, we will retry to delete the article.
# DELETE /articles/:id
curl -X 'DELETE' \
'<your-host>/articles/<article-id>' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <admin-jwt-token>' # <- replace by the admin jwt token
# Server response
{ "deletedCount": 1 }
As expected, the article is well deleted.
DeleteMany test
Same behavior as the DeleteOne
route, but for multiple articles.
Back to README
Swagger UI
| Versioning
| Validation
| Caching
| Authentication
| Authorization