Welcome to the first lesson of this course on enhancing your API with guards and interceptors. In this lesson, we will focus on a very important topic: enforcing ownership when users update their reading progress.
In the previous course, we promised to close a serious gap: any logged-in user could modify someone else’s reading progress. That’s not okay. In this unit you’ll enforce ownership so users can only update their own progress, while admins keep the ability to update anyone’s (for support/moderation).
You already have:
- JWT authentication (via
JwtAuthGuard) — verifies the request has a valid token. - Role-based authorization (via
RolesGuard) — enforces admin vs user.
Now we add ownership checks so the API matches the updated UI: regular learners can’t edit each other’s progress; admins can. The SPA included with this course respects the same rules (controls are disabled when you’re not allowed to edit).
In this lesson, you will learn how to use NestJS features to make sure only the right users can update their own reading progress or, in some cases, allow an admin to update progress for anyone.
Before: JwtAuthGuard let any authenticated user call write endpoints like PATCH /reading/progress. That meant “Alice” could submit {"userId": 3, ...} and update “Bob’s” progress.
Now: Only:
- the owner (the same
userIdas the token) or - an admin may update a user’s reading progress.
We’ll implement this with a dedicated guard and a tiny controller change to avoid trusting client-provided userId.
Here is what we’ll do:
- Guard layer (
OwnerOrAdminGuard)- If admin → allow.
- If user → only allow when the target
userIdequals the authenticated user’s id. - Do not rely on the client’s
userIdblindly; treat it as a hint that you still validate.
- Controller layer (defensive assignment)
- For non-admins, override
dto.userId = req.user.userIdso the body can’t spoof a different user.
- For non-admins, override
This two-step pattern prevents mistakes if new endpoints are added later.
To solve this problem, we use a guard in NestJS called OwnerOrAdminGuard. A guard is a special class that runs before your controller logic. It can allow or block the request based on custom rules.
Here is the code for the guard:
Explanation
- The guard ensures
req.userexists (set by the JWT strategy). - If the user is an admin → request is allowed.
- If the user is not an admin → checks if
req.body.userIdmatches theuserIdfrom the JWT. - If they don’t match, it throws a ForbiddenException.
This guarantees that only the owner (or an admin) can make changes.
How it is used in the controller:
With this guard in place, only the owner or an admin can update a user’s reading progress.
To make ownership checks, we need to know who is making the request. The CurrentUser decorator helps us get the authenticated user’s information easily in our controller methods.
Here is the code for the decorator:
Explanation:
- The decorator extracts the
userobject from the request, which was set by the authentication process. - In the controller, you can use
@CurrentUser() user: TokenUserto get the user’s ID and role.
Example usage in the controller:
Why both guard & overwrite?
- The guard stops obvious spoofing (
userId≠ token). - The overwrite ensures future changes can’t accidentally re-open spoofing via DTOs.
This ensures that regular users can only update their own progress, while admins can update anyone’s.
Replace placeholders with actual values from your app:
${ADMIN_TOKEN}: JWT token for the seeded admin user.${USER_TOKEN}: JWT token for a normal user.${USER_ID}: ID of the normal user (UUID).${BOOK_ID}: ID of a book (UUID).
- Admin updating someone else (✅ allowed)
Regular user updating their own progress (✅ allowed):
(If you omit userId, the controller injects it from the token for non-admins.)
Regular user trying to update someone else (❌ 403 Forbidden):
Response:
UI note: The updated SPA mirrors these rules—regular users see editing controls enabled only for their entries; admins keep full control. If you switch accounts in the SPA, you’ll see the buttons/inputs enable/disable accordingly.
- Guard order:
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard). We know from the previous courses that reversing them breaks req.user. - 401 vs 403: Missing/invalid token ⇒ 401 (JwtAuthGuard). Valid token but wrong target ⇒ 403 (OwnerOrAdminGuard).
- Don’t trust client userId: Always validate against
req.user.userId, and for non-admins overwrite the DTO. - DTO validation: Keep
bookId/currentPagevalidation in your DTO/class-validator to avoid bad writes. - Extending ownership: If you add endpoints like
PATCH /reading/:userId/progress, comparereq.params.userIdtoreq.user.userId(unless admin).
In this lesson, you learned why it is important to enforce ownership when users update their reading progress. You saw how the OwnerOrAdminGuard checks if the user is allowed to make changes, and how the CurrentUser decorator makes it easy to access the authenticated user’s information in your controller.
You’ve enforced ownership: only the owner (or an admin) can modify reading progress. The guard stops cross-user edits, the controller prevents spoofing, and the SPA reflects the same rules. With authentication, roles, and ownership in place, your API now follows the least-privilege principle.In the next practice exercises, you will get hands-on experience applying these concepts to real code.
