VMois
VMois

Reputation: 4708

How to exclude entity field from returned by controller JSON. NestJS + Typeorm

I want to exclude password field from returned JSON. I am using NestJS and Typeorm.

The solution provided on this question doesn't work for me or in NestJS. I can post my code if needed. Any other ideas or solutions? Thanks.

Upvotes: 48

Views: 113460

Answers (12)

dfsg76
dfsg76

Reputation: 524

Whoever comes across this question in the future might also consider using the approach I suggested here: https://stackoverflow.com/a/75436936/4397899

Upvotes: 0

Divyaraj
Divyaraj

Reputation: 11

@Injectable()
export default class TransformInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<AnyObject> {
    return next.handle().pipe(map((args) => instanceToPlain(args)));
  }
}

Upvotes: 1

passport4j
passport4j

Reputation: 273

In addition to this answer for Nestjs.

How to use class-transformer groups in a dynamic way since the groups are fixed in the function decorator @SerializeOptions. Put the dynamic groups in classToClass or plainToClass.

@Post()
@SerializeOptions({
  groups: ['admin','user'],
})
async create(
  @Body() createProjectDto: CreateProjectDto,
) {
  const data = await this.projectsService.create(createProjectDto);
  return classToClass(data, { groups: ['admin','DYNAMIC_GROUP'] });
  //Or you can return
  //return plainToClass(Project, plainObject, {groups: ['admin','DYNAMIC_GROUP']});
}

The classToClass and plainToClass inside the function body do the actual filtering while @SerializeOptions to make sure the filtered data will be returned correctly.

Upvotes: 0

Aminu Zack
Aminu Zack

Reputation: 21

what simply worked for me was just

annotating the field with @Exclude and overiding toJSON model method like so

toJSON() { return classToPlain(this); }

Upvotes: 2

Sebastien H.
Sebastien H.

Reputation: 7196

To avoid any back-pain and headaches, I would suggest using the plainToClass to have a full mongoose/class-transform compatibility and avoid having to make custom overrides to overcome this isse.

Example, add this in your service :

async validateUser(email: string, password: string): Promise<UserWithoutPassword | null> {
    const user = await this.usersService.findOne({ email });

    if (user && await compare(password, user.password))
    {
        return plainToClass(UserWithoutPassword, user.toObject());
    }

    return null;
}

Source : Stackoverflow answer

Upvotes: 0

b.niki
b.niki

Reputation: 31

This is already an old topic, but I still would like to share my solution, maybe it will help somebody. I use Express, but my example is probably suitable for this case as well.

So in your entity class you just define an additional static removePassword() method, which receives an instance of entity itself, and you send an object created by this method instead of original entity object you got from DB:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ name: 'users' })
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string | undefined;

  @Column({ type: 'varchar', length: 100, unique: true })
  email: string | undefined;

  @Column({ type: 'text' })
  password: string | undefined;

  static removePassword(userObj: User) {
    return Object.fromEntries(
      Object.entries(userObj).filter(([key, val]) => key !== 'password')
    );
  }
}

This is basically like calling a filter() method on an array, but slightly more complicated: you create a new object (the one you'll send eventually) from entries array, produced by filtering out a password entry from original entries array (with this exact filter() method).

In your route handlers though you'll just always do something like this:

import { Router, Request, Response, NextFunction } from 'express';
import { User } from '../../entity/User';
import { getRepository } from 'typeorm';

const router = Router();

router.post(
  '/api/users/signin',
  (req: Request, res: Response, next: NextFunction) => {
    const { email } = req.body;

    getRepository(User)
      .findOne({ email })
      .then(user =>
        user ? res.send(User.removePassword(user)) : res.send('No such user:(')
      )
      .catch(err => next(new Error(err.message)));
  }
);

export { router as signinRouter };

You may also use regular method:

withoutPassword() {
  return Object.fromEntries(
    Object.entries(this).filter(([key, val]) => key !== 'password')
  );
}

and in your route handler:

res.send(user.withoutPassword());

Upvotes: 2

firxworx
firxworx

Reputation: 1584

Lots of good answers in this thread. To build on apun's answer above, I think the following approach is the least likely to accidentally leak a password field:

@Column({ select: false })
password: string

If the entity doesn't select that field by default, and it can only be explicitly queried (e.g. via addSelect() if using the query builder), I think it is a lot less likely that there's a slip somewhere, and there's less reliance on the "magic" of a framework (which is ultimately the class-transformer library) to ensure security. Realistically in many projects the only place you'd explicitly select it is where you check credentials.

This approach can also help keep the password hash from accidentally leaking into log entries, etc, which is a consideration that hasn't been mentioned yet. It feels much safer to toss around the user object knowing that it doesn't include sensitive information, especially if it could end up serialized in a log entry somewhere.

All said, the documented approach for NestJS is to use the @Exclude() decorator and the accepted answer is from the project's founder.

I definitely make frequent use of the Exclude() decorator, but not necessarily for password or salt fields.

Upvotes: 53

a p
a p

Reputation: 1271

@Column({ select: false })
password: string

Can read about hidden columns here

Upvotes: 5

Kim Kern
Kim Kern

Reputation: 60567

As an addition to Kamil's answer:

Instead of creating your own interceptor, you can now use the built-in ClassSerializerInterceptor, see the serialization docs.

@UseInterceptors(ClassSerializerInterceptor)

You can use it on a controller class or its individual methods. Each entity returned by such a method will be transformed with class-transformer and hence take the @Exclude annotations into account:

import { Exclude } from 'class-transformer';

export class User {
    /** other properties */    

    @Exclude()
    password: string;
}

You can customize its behavior by defining @SerializeOptions() on your controller or its methods:

@SerializeOptions({
  excludePrefixes: ['_'],
  groups: ['admin']
})

to, for example, expose certain fields only to certain users:

@Expose({ groups: ["admin"] })
adminInfo: string;

Upvotes: 44

Francisco J Sucre G
Francisco J Sucre G

Reputation: 641

You can overwrite the toJSON method of the model like this.

@Entity()
export class User extends BaseAbstractEntity implements IUser {
  static passwordMinLength: number = 7;

  @ApiModelProperty({ example: faker.internet.email() })
  @IsEmail()
  @Column({ unique: true })
  email: string;

  @IsOptional()
  @IsString()
  @MinLength(User.passwordMinLength)
  @Exclude({ toPlainOnly: true })
  @Column({ select: false })
  password: string;

  @IsOptional()
  @IsString()
  @Exclude({ toPlainOnly: true })
  @Column({ select: false })
  passwordSalt: string;

  toJSON() {
    return classToPlain(this);
  }

  validatePassword(password: string) {
    if (!this.password || !this.passwordSalt) {
      return false;
    }
    return comparedToHashed(password, this.password, this.passwordSalt);
  }
}

By using the class-transformer method of plainToClass along with the @Exclude({ toPlainOnly: true }), the password will be excluded from the JSON response, but will be available in the model instance. I like this solution because it keeps all the model configuration in the entity.

Upvotes: 51

Kamil Myśliwiec
Kamil Myśliwiec

Reputation: 9178

I'd suggest creating an interceptor that takes advantage of the class-transformer library:

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    call$: Observable<any>,
  ): Observable<any> {
    return call$.pipe(map(data => classToPlain(data)));
  }
}

Then, simply exclude properties using @Exclude() decorator, for example:

import { Exclude } from 'class-transformer';

export class User {
    id: number;
    email: string;

    @Exclude()
    password: string;
}

Upvotes: 61

Jo&#227;o Silva
Jo&#227;o Silva

Reputation: 167

You can use the package https://github.com/typestack/class-transformer

You can exclude a properties using decorators and also, you can exlude properties using groups.

Upvotes: 1

Related Questions