Introduction: Why Ownership Matters in APIs

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.

The vulnerability we’re closing

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 userId as 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 userId equals the authenticated user’s id.
    • Do not rely on the client’s userId blindly; treat it as a hint that you still validate.
  • Controller layer (defensive assignment)
    • For non-admins, override dto.userId = req.user.userId so the body can’t spoof a different user.

This two-step pattern prevents mistakes if new endpoints are added later.

How OwnerOrAdminGuard Solves This

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:

// src/common/guards/owner-or-admin.guard.ts
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';

@Injectable()
export class OwnerOrAdminGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest() as any;
    const user = req.user as { userId: string; role: 'user' | 'admin' } | undefined;
    if (!user) return false; // Global JWT guard should set this

    if (user.role === 'admin') return true;

    const bodyUserId = String(req.body?.userId || '');
    if (user.userId !== bodyUserId) {
      throw new ForbiddenException('You can only modify your own progress');
    }
    return true;
  }
}

Explanation

  • The guard ensures req.user exists (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.userId matches the userId from 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:

// src/reading/reading.controller.ts
  @Patch('progress')
  @UseGuards(AuthGuard('jwt'), OwnerOrAdminGuard)
  // Update reading progress for a user and book (auth required)
  updateProgress(@CurrentUser() user: TokenUser, @Body() updateProgressDto: UpdateProgressDto) {
    // For non-admins, force userId to the token's subject.
    // Admins may update any user's progress (userId comes from body).
    if (user.role !== 'admin') {
      updateProgressDto.userId = user.userId;
    }
    const session = this.readingService.updateProgress(updateProgressDto);
    return { success: true, data: session };
  }

With this guard in place, only the owner or an admin can update a user’s reading progress.

Accessing the Authenticated User with CurrentUser Decorator

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:

// src/common/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export interface TokenUser {
  userId: string;
  role: 'user' | 'admin';
}

export const CurrentUser = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext): TokenUser | undefined => {
    const req = ctx.switchToHttp().getRequest() as any;
    return req.user as TokenUser | undefined;
  },
);

Explanation:

  • The decorator extracts the user object from the request, which was set by the authentication process.
  • In the controller, you can use @CurrentUser() user: TokenUser to get the user’s ID and role.

Example usage in the controller:

// src/reading/reading.controller.ts
  @Patch('progress')
  @UseGuards(AuthGuard('jwt'), OwnerOrAdminGuard)
  // Update reading progress for a user and book (auth required)
  // Now you understand how the @CurrentUser decorator is used
  updateProgress(@CurrentUser() user: TokenUser, @Body() updateProgressDto: UpdateProgressDto) {
    // For non-admins, force userId to the token's subject.
    // Admins may update any user's progress (userId comes from body).
    if (user.role !== 'admin') {
      updateProgressDto.userId = user.userId;
    }
    const session = this.readingService.updateProgress(updateProgressDto);
    return { success: true, data: session };
  }

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.

Test It (curl & SPA)
Common pitfalls (and quick fixes)
  • 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 / currentPage validation in your DTO/class-validator to avoid bad writes.
  • Extending ownership: If you add endpoints like PATCH /reading/:userId/progress, compare req.params.userId to req.user.userId (unless admin).
Review and What’s Next

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.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal