Reputation: 4708
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
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
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
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
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
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
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
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
Reputation: 1271
@Column({ select: false })
password: string
Can read about hidden columns here
Upvotes: 5
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
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
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
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