0
votes

I'm doing a server-side application with NestJS and TypeScript in combination with the implementation of Passport JWT.

A little bit of context first:

My JwtStrategy (no issues here):

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private userService: UserService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'hi',
    });
  }

  async validate(payload: IJwtClaims): Promise<UserEntity> {
    const { sub: id } = payload;

    // Find the user's database record by its "id" and return it.
    const user = await this.userService.findById(id);

    if (!user) {
      throw new UnauthorizedException();
    }

    return user;
  }
}

According to the documentation about the validate() method:

Passport will build a user object based on the return value of our validate() method, and attach it as a property on the Request object.

Thanks to this behavior, I can access the user object in my handler like this:

@Get('hi')
  example(@Req() request: Request) {
    const userId = (request.user as UserEntity).id;
  }

Did you notice that I have used a Type Assertion (tells the compiler to consider the user object as UserEntity) ? Without it, I won't have auto-completion about my entity's properties.

As a quick solution, I have created a class that extends the Request interface and include my own property of type UserEntity.

import { Request } from 'express';
import { UserEntity } from 'entities/user.entity';

export class WithUserEntityRequestDto extends Request {
  user: UserEntity;
}

Now, my handler will be:

@Get('hi')
  example(@Req() request: WithUserEntityRequestDto) {
    const userId = request.user.id; // Nicer
  }

The real issue now:

I have (and will have more) a handler that will receive a payload, let's call it for this example PasswordResetRequestDto.

export class PasswordResetRequestDto {
  currentPassword: string;
  newPassword: string;
}

The handler will be:

@Get('password-reset')
  resetPassword(@Body() request: PasswordResetRequestDto) {
  }

Now, I don't have access to the user's object. I would like to access it to know who is the user that is making this request.

What I have tried:

Use TypeScript Generics and add a new property to my previous WithUserEntityRequestDto class like this:

export class WithUserEntityRequestDto<T> extends Request {
  user: UserEntity;
  newProp: T;
}

And the handler will be:

@Get('password-reset')
  resetPassword(@Req() request: WithUserEntityRequestDto<PasswordResetRequestDto>) {
  }

But now the PasswordResetRequestDto will be under newProp, making it not a scalable solution. Any type that I pass as the generic will be under newProp. Also, I cannot extends T because a class cannot extends two classes. I don't see myself doing classes like this all the time.

What I expect to accomplish:

Pass a type to my WithUserEntityRequestDto class to include the passed type properties and also the user object by default. A way that I can do for example:

request: WithUserEntityRequestDto<AwesomeRequestDto>
request: WithUserEntityRequestDto<BankRequestDto>

And the value will be something like:

{
   user: UserEntity, // As default, always present
   // all the properties of the passed type (T),
   // all the properties of the Request interface
}

My goal is to find an easy and scalable way to extends the Request interface and include any type/class on it, while having the user object (UserEntity) always present.

Thanks for the time and any help/advice/approach will be appreciated.

1

1 Answers

0
votes

Nestjs provides an elegant solution for your problem, which is Custom decoration

it's common practice to attach properties to the request object. Then you manually extract them in each route handler,

What you have to do is create a user decorator:

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

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

then you can simply use it in your controller like this:

    @Get('hi')
  example(@Req() request: Request,@User() user: UserEntity) {
    const userId = user.id; 
  }