how to serialize body with user role in dto in nestjs

41 views Asked by At

In NestJS, is it possible to conditionally modify the properties of a request body within controller methods based on the user’s role or access level?

For instance, if a user lacks the necessary permissions, the specific property they are not authorized to alter should be omitted from the request body.

Currently, I employ class-validator and class-transformer for body validation using Data Transfer Objects (DTOs), but I’m uncertain about how to access the request object to perform such conditional logic.

To elaborate, accessing the request object is essential for validating the body, as it may contain critical information like the user’s group or role.

Below is my user model for reference:

enum Role {
  USER,
  ADMIN
}

model User {
  id             String   @id @default(uuid()) @db.Uuid
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt
  mobileNumber   String   @unique
  role           Role     @default(USER)
  otpCode        String?
  otpExpiredAt   DateTime?
  firstName      String?
  lastName       String?
}

And here is my DTO:

export class UpdateUserDto implements Partial<User> {
  @IsOptional()
  @IsString()
  firstName?: string;

  @IsOptional()
  @IsString()
  lastName?: string;

  @IsOptional()
  @IsMobilePhone()
  mobileNumber?: string;

  @IsOptional()
  @IsEnum($Enums.Role)
  role?: $Enums.Role;
}

The requirement is such that only users with an admin role should be permitted to update the role property. If a non-admin user attempts to send the role property, it should be excluded from the request body.

Additionally, while I have the user’s information in the request, I need guidance on accessing this within the DTO, similar to how one might use a group property.

I considered using applyDecorators for this purpose, but I was unable to find a way to access the request within its scope.

export const SerializerDtoByRule = ({ groups }: {groups: $Enums.Role[] }) => {
    return applyDecorators(
        Expose(({ value }) => {
            if (groups.length === 0) return value;

            const user = request; // ?? how to accsess request

            if (user && groups.includes(user.role)) {
                return value;
            }

            return undefinded;
        }),
    );
};
1

There are 1 answers

0
Marek Kapusta-Ognicki On

Simple question. Are you sure you need to have those fields omitted or passed through at the dto level? How about?

@Injectable()
export class UserService {
  constructor(
    @Inject(REQUEST) private request: Record<string, unknown>,
    private readonly userRepository: RepositoryForUser,
  ) {}

  async updateUser(dto: UpdateUserDto) {
    let updatable = await this.userRepository.findOneOrThrow(dto.id);
    
    if (!!dto.someImportantField && dto.someImportantField !== updatable.someImportantField) {
       this.allowOnly(Role.Admin);
       updatable.someImportantField = dto.someImportantField;
    }
  }

  private allowOnly(...roles: Role[]) {
     // check from request if matches any role, if not:
     throw new UnauthorizedException();
  }
}

Dto layer is, more or less, a data-transfer object layer. It's good to put there basic validation logic, like is the country code really country code, or check if all data that need to be provided are provided, because it's just easy to do. But there's no need to struggle especially when the goal is to put there some "heavier", business logic, that's rather on services, domain services, cqrs handlers, etc. Another argument that it's not always best to overload DTO is a simple case:

user can't update e-mail address
unless they are admins
or they are updating their own e-mail
but only if they hadn't updated their e-mails for at least one month before

You can write that in dtos, but then somebody will come up and tell: "hey, I want to know who's shady, and I want the logs stored in-app of every e-mail change attempt".


Speaking of accessing Request by class-validator - see https://github.com/typestack/class-validator?tab=readme-ov-file#custom-validation-classes. You can always create a class. And if you can that, you can make it injectable too. And then - you know :)