简体   繁体   中英

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

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

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM