Nest.js/Mongoose: Why is my pre save hook failing to be triggered?

Viewed 3912

I just started working with Nest.js and so far it's been smooth. However I've encountered an issue in which my mongoose pre save hook in the User schema isn't being triggered. This should be straightforward, but for whatever reason the password is being saved in its plain form rather than hashed. What gives?

One more minor issue - how do I define a field that references another schema when working with @Prop decorators? The profile field is supposed to be of mongoose.schema.types.objectid which would simply be profile: { type: mongoose.Schema.Types.ObjectId, ref: 'Profile' } when working without decorators.

Below are the relevant snippets.

User schema

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema({
  timestamps: true,
})
export class User extends Document {
  @Prop({ required: true })
  fullname: string;

  @Prop({ required: true, unique: true })
  username: string;

  @Prop({ required: true, unique: true, lowercase: true })
  email: string;

  @Prop({ required: true })
  password: string;

  @Prop({ required: true, ref: 'Profile' })
  profile: string

  @Prop({ required: true, enum: ['admin', 'basic' ]})
  role: string
}

export const UserSchema = SchemaFactory.createForClass(User);

Users module

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import * as bcrypt from 'bcrypt';

import { User, UserSchema } from './user.model';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: User.name,
        useFactory: () => {
          const schema = UserSchema;

          schema.pre<User>('save', async function (next: Function) {
            const user = this;

            const salt = await bcrypt.genSalt(10);
            const hashedPassword = await bcrypt.hash(user.password, salt);

            user.password = hashedPassword;

            next();
          });

          schema.methods.comparePasswords = async function (submittedPassword) {
            const user = this;

            await bcrypt.compare(submittedPassword, user.password);
          };

          return schema;
        },
      },
    ]),
  ],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Users Service

import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { JwtService } from '@nestjs/jwt';

import { UsersService } from '../users/users.service';
import { ProfilesService } from '../profiles/profiles.service';
import { User } from '../users/user.model';

@Injectable()
export class AuthService {
  constructor(
    @InjectModel(User.name) private readonly userModel: Model<User>,
    private usersService: UsersService,
    private profilesService: ProfilesService,
    private jwtService: JwtService
  ) {}

  async signup(signupData): Promise<any> {
    const foundUser = await this.userModel.findOne({ email: signupData.email });

    if (foundUser) {
      throw new HttpException(
        'Email is already in use',
        HttpStatus.BAD_REQUEST
      );
    }

    const createdProfile = await this.profilesService.createProfile();

    const createdUser = await this.userModel.create({
      ...signupData,
      profile: createdProfile._id,
      role: 'basic',
    });

    const createdUserCopy = { ...createdUser.toObject() };

    delete createdUserCopy.password;
    delete createdUserCopy.__v;

    const payload = {
      username: createdUser.username,
      sub: createdUser._id,
    };

    return {
      user: createdUserCopy,
      token: this.jwtService.sign(payload),
    };
  }
}
2 Answers

I Know this question has been asked long time ago, But for people who face this issue again, here is how how to fix it.

useFactory: () => {
          const schema = UserSchema;

          schema.pre<User>('save', async function () {
            const user = this;

            const salt = await bcrypt.genSalt(10);
            const hashPassword = await bcrypt.hash(user.password, salt);

            user.salt = salt;
            user.password = hashPassword;
          });
          return schema;
        }

instead of calling next() manually, you can use a function that returns a promise. In particular, you can use async/await.

You can not use async in combination with next()

schema.pre<User>('save', function (next) {
  const user = this;
  console.log(user)
  next();
});

Should work

Related