Welcome to the final lesson of our course on implementing Authentication in our ToDo app with NestJS. In the previous lessons, we built a strong foundation by setting up user authentication using bcrypt
for password encryption, integrating JWT
for secure access, and protecting our endpoints with Guards
. In this lesson, we will focus on associating ToDo items with their respective users, which is crucial for maintaining data integrity and ensuring that users interact only with their own tasks.
The core of our task is to ensure each ToDo item is associated with a user. We'll achieve this by updating the Todo
schema to include an ownerId
property to signify which user it belongs to.
Here’s a breakdown of how we update the Todo
schema:
TypeScript1import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2import { HydratedDocument } from 'mongoose'; 3 4export type TodoDocument = HydratedDocument<Todo>; 5 6@Schema() 7export class Todo { 8 // Add this property 9 @Prop({ required: true }) 10 ownerId: string; // Associates the ToDo with a specific user 11 12 // These properties were already there. 13 @Prop({ required: true }) 14 title: string; 15 16 @Prop() 17 description: string; 18 19 @Prop({ default: false }) 20 completed: boolean; 21} 22 23export const TodoSchema = SchemaFactory.createForClass(Todo);
ownerId
: This field is critical as it links each ToDo item to a user, ensuring that users can only access their own ToDo items.title
,description
,completed
: These fields store the ToDo details, enabling users to manage their tasks' status.
The TodoService
contains the logic for CRUD operations on ToDo items, making sure these operations consider the user-specific association. We need to update all of them to require a userId
and relate ToDos with that user. This means that you can only list or update ToDos that you own. When you create a ToDo, it gets associated wit you.
Here’s a part of the TodoService
implementation:
TypeScript1@Injectable() 2export class TodoService { 3 constructor(@InjectModel(Todo.name) private readonly todoModel: Model<TodoDocument>) {} 4 5 async findAll(userId: string, showIncomplete?: boolean): Promise<Todo[]> { 6 const filter: any = { 7 ownerId: userId, 8 }; 9 10 if (showIncomplete) { 11 filter.completed = false; 12 } 13 14 return await this.todoModel.find(filter).exec(); 15 } 16 17 async findOne(userId: string, id: string): Promise<TodoDocument | null> { 18 const todo = await this.todoModel.findById(id).exec(); 19 if (todo?.ownerId !== userId) { 20 return null; 21 } 22 return todo; 23 } 24 25 async createTodo(userId: string, todo: CreateTodoDto): Promise<TodoDocument> { 26 const createdTodo = new this.todoModel({ 27 ownerId: userId, 28 ...todo, 29 }); 30 return createdTodo.save(); 31 } 32 33 // Other CRUD operations like update and delete have similar ownerId concerns... 34}
findAll
andfindOne
: Fetches ToDos for the givenuserId
. This method uses theownerId
to ensure only the user's ToDos are returned.createTodo
: This funciton ensures theownerId
is added to the record in the database- The service contains other methods (
createTodo
,updateTodo
, etc.) that maintain this user association, ensuring that operations obey user-specific constraints.
The TodoController
handles incoming HTTP requests related to ToDo operations and communicates with the TodoService
.
An example endpoint within our TodoController
:
TypeScript1function userIdFromRequest(req: any): string { 2 // The user should be set on the request object by the AuthGuard. 3 // This information was embedded within the JWT token 4 return req.user?.sub ?? ""; 5} 6 7@Controller('todos') 8export class TodoController { 9 constructor(private readonly todosService: TodoService) {} 10 11 @Get() 12 async findAll(@Request() req, @Query('showIncomplete') showIncomplete: boolean): Promise<TodoDto[]> { 13 // Find ToDos that are owned by the user that was received from the token. 14 const todos = await this.todosService.findAll(userIdFromRequest(req), showIncomplete); 15 return todos.map(transformTodoDto); 16 } 17 18 @Get(':id') 19 async findOne(@Request() req, @Param('id') id: string): Promise<TodoDto> { 20 const todo = await this.todosService.findOne(userIdFromRequest(req), id); 21 22 if (!todo) { 23 throw new NotFoundException('Todo not found'); 24 } 25 26 return transformTodoDto(todo); 27 } 28 29 @Post() 30 async create(@Request() req, @Body(new ValidationPipe()) todo: CreateTodoDto): Promise<TodoDto> { 31 const newTodo = await this.todosService.createTodo(userIdFromRequest(req), todo); 32 return transformTodoDto(newTodo); 33 } 34 35 // Other endpoints 36}
@Get()
: This endpoint fetches all ToDos for the current user. It uses a helper function,userIdFromRequest
, to extract the user identity from the request (which was set in theAuthGuard
), ensuring tasks are fetched only for the corresponding user.@Get(':id')
and@Post()
: These endpoints use the same mechanism to pass the user ID to theToDoService
.- The controller has similar logic for other HTTP operations (
POST
,PUT
,DELETE
), consistently maintaining the association with users.
We can see this association in action through an example using send_request.ts
:
TypeScript1// Create a new todo item 2async function createTodo(token: string, username: string) { 3 try { 4 const response = await axios.post('http://localhost:3000/todos', { 5 title: 'Learn NestJS', 6 description: `${username} Explore the basics of NestJS`, 7 }, { 8 headers: { Authorization: `Bearer ${token}` } 9 }); 10 return response.data; 11 } catch (error: any) { 12 console.error('Error:', error.message); 13 } 14} 15 16// Get all todo items 17async function getTodos(token: string) { 18 try { 19 const response = await axios.get('http://localhost:3000/todos', { 20 headers: { Authorization: `Bearer ${token}` } 21 }); 22 console.log('Todos:', response.data); 23 } catch (error: any) { 24 console.error('Error:', error.message); 25 } 26} 27 28async function run() { 29 console.log('Registering a new User 1'); 30 await registerUser("testuser1"); 31 32 console.log('\nLogging in the User 1'); 33 const { access_token: testUser1Token } = await loginUser("testuser1"); 34 35 console.log('\nCreating a new todo for User 1'); 36 await createTodo(testUser1Token, "User 1"); 37 38 console.log('\nGetting all todos for User 1'); 39 await getTodos(testUser1Token); 40}
createTodo
: Adds a new task for "User 1", associating the task with the user's identity through the token.getTodos
: Retrieves tasks, demonstrating user-specific filtering.
In this lesson, we explored how to associate ToDo items with users in a NestJS application, utilizing schemas, services, and controllers to maintain this link. By integrating Mongoose and leveraging its powerful schema capabilities, we ensured that each ToDo was managed securely and accurately.
As you move to the practice exercises, apply what you've learned by creating and managing your ToDos while maintaining these user associations. Congratulations on reaching the end of the course! Your understanding of secure and scalable application development with NestJS and MongoDB provides a solid foundation for future projects. Happy coding!