Home > database >  Unexpected NestJs error: Nest can't resolve dependencies of the UserService
Unexpected NestJs error: Nest can't resolve dependencies of the UserService

Time:01-19

I'm trying to build a simple api in NestJs with authentication to save recipes to a MongoDB.

I was trying to add an email service to send a confirmation email for new users and ran into a dependency error I'm not able to figure out myself.

The error in question:

Error: Nest can't resolve dependencies of the UserService (?). Please make sure that the argument UserModel at index [0] is available in the EmailModule context.

Potential solutions:
- Is EmailModule a valid NestJS module?
- If UserModel is a provider, is it part of the current EmailModule?
- If UserModel is exported from a separate @Module, is that module imported within EmailModule?
  @Module({
    imports: [ /* the Module containing UserModel */ ]
  })

    at Injector.lookupComponentInParentModules (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\@nestjs\core\injector\injector.js:241:19)
    at Injector.resolveComponentInstance (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\@nestjs\core\injector\injector.js:194:33)
    at resolveParam (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\@nestjs\core\injector\injector.js:116:38)
    at async Promise.all (index 0)
    at Injector.resolveConstructorParams (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\@nestjs\core\injector\injector.js:131:27)
    at Injector.loadInstance (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\@nestjs\core\injector\injector.js:57:13)
    at Injector.loadProvider (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\@nestjs\core\injector\injector.js:84:9)
    at async Promise.all (index 4)
    at InstanceLoader.createInstancesOfProviders (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\@nestjs\core\injector\instance-loader.js:47:9)
    at C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\@nestjs\core\injector\instance-loader.js:32:13

It states the UserModel is missing in the EmailModule but that doesn't seem to be the case.

EmailModule:

import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { UserModule } from "src/user/user.module";
import { JwtService } from "@nestjs/jwt";
import { UserService } from "src/user/user.service";

@Module({
  imports: [ConfigModule, UserModule],
  controllers: [],
  providers: [JwtService, UserService],
})
export class EmailModule {}

Email Service:

import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { createTransport } from "nodemailer";
import * as Mail from "nodemailer/lib/mailer";

@Injectable()
export default class EmailService {
  private nodemailerTransport: Mail;

  constructor(private readonly configService: ConfigService) {
    this.nodemailerTransport = createTransport({
      service: configService.get("EMAIL_SERVICE"),
      auth: {
        user: configService.get("EMAIL_USER"),
        pass: configService.get("EMAIL_PASSWORD"),
      },
    });
  }

  sendMail(options: Mail.Options) {
    return this.nodemailerTransport.sendMail(options);
  }
}

Email Confirmation Service:

import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
import EmailService from "./email.service";
import { UserService } from "src/user/user.service";
import { AccountStatus } from "src/types";
import { BadRequestException } from "@nestjs/common/exceptions";

interface VerificationTokenPayload {
  email: string;
}

@Injectable()
export class EmailConfirmationService {
  constructor(
    private jwtService: JwtService,
    private configService: ConfigService,
    private emailService: EmailService,
    private userService: UserService,
  ) {}

  sendVerificationLink(email: string) {
    const payload: VerificationTokenPayload = { email };
    const token = this.jwtService.sign(payload, {
      secret: this.configService.get("JWT_VERIFICATION_TOKEN_SECRET"),
      expiresIn: `${this.configService.get("JWT_VERIFICATION_TOKEN_EXPIRATION_TIME")}s`,
    });

    const url = `${this.configService.get("EMAIL_CONFIRMATION_URL")}?token=${token}`;

    const text = `Welcome to Pantry! To confirm the email address, click here: ${url}`;

    return this.emailService.sendMail({
      to: email,
      subject: "Pantry Account Confirmation",
      text,
    });
  }

  async confirmEmail(email: string) {
    const user = await this.userService.findOne(email);

    if (user && user.status !== AccountStatus.Created)
      throw new BadRequestException("Email already confirmed");

    await this.userService.markEmailAsConfirmed(email);
  }

  async decodeConfirmationToken(token: string) {
    try {
      const payload = await this.jwtService.verify(token, {
        secret: this.configService.get("JWT_VERIFICATION_TOKEN_SECRET"),
      });

      if (typeof payload === "object" && "email" in payload) {
        return payload.email;
      }
      throw new BadRequestException();
    } catch (error) {
      if (error?.name === "TokenExpiredError") {
        throw new BadRequestException("Email confirmation token expired");
      }
      throw new BadRequestException("Bad confirmation token");
    }
  }

  public async resendConfirmationLink(email: string) {
    const user = await this.userService.findOne(email)
    if (user.status === AccountStatus.Confirmed) {
      throw new BadRequestException('Email already confirmed');
    }
    await this.sendVerificationLink(user.email);
  }
}

I will add the other services & modules below in case they are of any use.

User Module:

import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { User, UserSchema } from "./schemas/user.schema";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";

@Module({
  imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
  controllers: [UserController],
  providers: [UserService]
})
export class UserModule {}

User Service:

import { BadRequestException, Injectable } from "@nestjs/common";
import { NotFoundException } from "@nestjs/common/exceptions";
import { InjectModel } from "@nestjs/mongoose";
import { Model, isValidObjectId } from "mongoose";
import { AccountStatus } from "src/types";
import { User, UserDocument } from "./schemas/user.schema";
import { MSG_USER_NOT_FOUND } from "./user-messages";

@Injectable()
export class UserService {
  constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}

  private readonly defaultProjection = {
    __v: false,
    password: false,
  };

  async findOne(email: string): Promise<User> {
    const user = this.userModel.findOne({ email }, this.defaultProjection);
    if (user === null) throw new NotFoundException(MSG_USER_NOT_FOUND);
    return user;
  }

  async deleteOne(id: string): Promise<any> {
    if (!isValidObjectId(id)) throw new BadRequestException();

    const result = await this.userModel.deleteOne({ _id: id }).exec();

    if (result.deletedCount !== 1) throw new NotFoundException(MSG_USER_NOT_FOUND);
    return result;
  }

  async updateOne(id: string, userData: User) {
    if (!isValidObjectId(id)) throw new BadRequestException();
    let result;

    try {
      result = await this.userModel.findByIdAndUpdate(id, userData).setOptions({ new: true });
    } catch (e) {
      throw new BadRequestException();
    }

    if (result === null) throw new NotFoundException(MSG_USER_NOT_FOUND);
    return result;
  }

  async markEmailAsConfirmed(email: string) {
    const user = await this.findOne(email);
    return this.updateOne(user.email, {...user, status: AccountStatus.Confirmed})
  }
}

Auth Module:

import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { MongooseModule } from "@nestjs/mongoose";
import { PassportModule } from "@nestjs/passport";
import { EmailModule } from "src/email/email.module";
import { EmailConfirmationService } from "src/email/emailConfirmation.service";
import { User, UserSchema } from "src/user/schemas/user.schema";
import { UserModule } from "src/user/user.module";
import { UserService } from "src/user/user.service";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./jwt.strategy";
import { LocalStrategy } from "./local.auth";

@Module({
  imports: [
    UserModule,
    EmailModule,
    PassportModule,
    JwtModule.register({ secret: "secretKey", signOptions: { expiresIn: "10m" } }),
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
  ],
  providers: [
    AuthService,
    JwtStrategy,
    UserService,
    LocalStrategy,
    EmailConfirmationService,
    ConfigService,
  ],
  controllers: [AuthController],
})
export class AuthModule {}

Auth Service:

import { Injectable, NotAcceptableException, BadRequestException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { InjectModel } from "@nestjs/mongoose";
import * as bcrypt from "bcrypt";
import {
  MSG_USER_EMAIL_TAKEN,
  MSG_USER_NAME_TAKEN,
  MSG_USER_NOT_FOUND,
  MSG_USER_WRONG_CRED,
} from "src/user/user-messages";
import { UserService } from "../user/user.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
import { Model } from "mongoose";
import { User, UserDocument } from "src/user/schemas/user.schema";
import { AccountStatus } from "src/types";

@Injectable()
export class AuthService {
  constructor(
    @InjectModel(User.name) private userModel: Model<UserDocument>,
    private userService: UserService,
    private jwtService: JwtService,
  ) {}

  async validateUser({ email, password }: LoginDto) {
    const user = await this.userService.findOne(email);
    if (!user) throw new NotAcceptableException(MSG_USER_NOT_FOUND);

    const passwordValid = await bcrypt.compare(password, user.password);

    if (user && passwordValid) return user;
    return null;
  }

  async register({ email, username, password }: RegisterDto) {
    const userWithEmail = await this.userModel.findOne({ email });
    if (userWithEmail) throw new BadRequestException(MSG_USER_EMAIL_TAKEN);

    const userWithName = await this.userModel.findOne({ username });
    if (userWithName) throw new BadRequestException(MSG_USER_NAME_TAKEN);

    const createdUser = new this.userModel({email, username, password});
    createdUser.status = AccountStatus.Created;
    const newUser = await createdUser.save();
    return newUser;
  }

  async login(login: LoginDto) {
    const user = await this.validateUser(login);
    if (!user) throw new NotAcceptableException(MSG_USER_WRONG_CRED);

    const payload = { email: user.email, sub: user._id };

    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

I hope this is enough to get some help, it's hard for me to share the entire repository at this point since it's work related

CodePudding user response:

as you're trying to use UserService in another module other than where it was registered (which was UserModule), you need to expose it, like this:

@Module({
  imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
  controllers: [UserController],
  providers: [UserService]
  exports: [UserService], // <<<<
})
export class UserModule {}

then remove UserService from EmailModule.

  • Related