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
.