I am trying to add actual value to the database using nestjs with jest test cases.
The main purpose of test cases is to actually check the database values written and retrived from the database. So I'm not mocking any save or find method as I want to make the actual connection to the database and retrive the records from there. So without mocking the save() and find() methods I'm not able get the test cases running. The compilation fails without it.
I am stuck with users.service.spec.ts file in which I want to use UserDocument and it's methods create(),findAll().
//user.entity.ts
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
export type UserDocument = User & Document;
#Schema()
export class User {
#Prop({ unique: true })
username: string;
#Prop()
age: number;
#Prop()
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
// User.module.ts
import { Module } from '#nestjs/common';
import { UsersService } from './users.service';
import { MongooseModule } from '#nestjs/mongoose'
import { User, UserSchema } from './schemas/user.schema'
import { UsersController } from './users.controller';
#Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService]
})
export class UsersModule {}
// users.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete,UseGuards } from '#nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
#Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
#Post()
create(#Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
#Get()
findAll() {
return this.usersService.findAll();
}
}
//users.service.ts
import { Injectable } from '#nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User, UserDocument } from './schemas/user.schema';
import { Model } from 'mongoose';
import { InjectModel } from '#nestjs/mongoose';
#Injectable()
export class UsersService {
constructor(#InjectModel(User.name) private userModel: Model<UserDocument>) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const createdUser = new this.userModel(createUserDto);
return await createdUser.save();
}
async findAll(): Promise<User[]> {
return await this.userModel.find().exec();
}
}
//users.service.spec.ts
import { Test, TestingModule } from '#nestjs/testing'
import { UsersService } from './users.service'
import { AppConfigModule } from '../config/app-config.module'
import { AppConfigService } from '../config/app-config.service'
import { ConfigModule, ConfigService } from '#nestjs/config'
import { MongooseModule, getModelToken } from '#nestjs/mongoose'
import { PaginationQuery } from 'src/helpers/pagination/pagination-query.dto'
import { User, UserSchema, UserDocument } from './entities/user.entity'
describe('UsersService', () => {
let service: UsersService
let userDocument: UserDocument
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
cache: true
}),
MongooseModule.forRootAsync({
imports: [AppConfigModule],
inject: [AppConfigService],
useFactory: (config: AppConfigService) => ({
uri: `mongodb://${config.TESTDATABASE_HOST}:${config.TESTDATABSE_PORT}`,
dbName: config.TESTDATABASE_NAME
})
})
],
providers: [
UsersService,
{
provide: getModelToken(User.name),
useValue: UserDocument // <----------- Not able to understand what will go here
}
]
}).compile()
service = module.get<UsersService>(UsersService)
userDocument = module.get<UserDocument>(UserDocument) // <---- As this is type here not accepting value
})
it('should be defined', () => {
expect(service).toBeDefined()
})
it('create test data', async () => {
expect(
await service.create({
name: 'demouser',
email: 'demo#gmail.com',
password: 'demo#123',
role: 'user'
})
).toEqual({
statusCode: 201,
message: 'User created successfully',
data: {
isActive: true,
role: 'user',
password:
'$2b$10$VjwR0Wjf6vTaRjQlsizB5OLY04NJOcRyC/pPLbTPTQnWTDIrBU.Sq',
email: 'demo#gmail.com',
name: 'demouser',
_id: expect.any(Number)
}
})
})
})
As I've highlighted in the spec file, UserDocument is the type defined in the users.entity.ts file, the value UserDocument is not accepted in the userDocument = module.get<UserDocument>(UserDocument) line.
I resolved my Problem Error.
Adding this import, it resolved my error.
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])
I can add the Actual value to the database
//users.service.spec.ts
import { Test, TestingModule } from '#nestjs/testing'
import { UsersService } from './users.service'
import { AppConfigModule } from '../config/app-config.module'
import { AppConfigService } from '../config/app-config.service'
import { ConfigModule } from '#nestjs/config'
import { MongooseModule, getModelToken } from '#nestjs/mongoose'
import { User, UserSchema } from './entities/user.entity'
import { isValidObjectId } from 'mongoose'
import { HttpException } from '#nestjs/common'
const testUser = {
name: 'demouser',
email: 'demo#gmail.com',
password: 'demo#123',
role: 'user'
}
describe('UsersService', () => {
let service: UsersService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
cache: true
}),
MongooseModule.forRootAsync({
imports: [AppConfigModule],
inject: [AppConfigService],
useFactory: (config: AppConfigService) => ({
uri: `mongodb://${config.TESTDATABASE_HOST}:${config.TESTDATABSE_PORT}`,
dbName: config.TESTDATABASE_NAME
})
}),
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])//-------import model here
],
providers: [UsersService]
}).compile()
service = module.get<UsersService>(UsersService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})
userDocument = module.get<UserDocument>(UserDocument) must be
userDocument = module.get<UserDocument>(getModelToken(User.name))
since you're trying to get the stub that you have defined for getModelToken(User.name) provider
In useValue: UserDocument you must use some stub/mock object like this one: https://github.com/jmcdo29/testing-nestjs/blob/6c77b2a5001a48673c77c9659d985a083354579f/apps/sequelize-sample/src/cats/cats.service.spec.ts#L17-L20 since you're trying to not use the real model.
Related
I have created auth for application by google (passport-google-oauth20) and apple (passport-appleid). When i try to invoke GET google-sign-in, I have the following error
[Nest] 987 - 07/22/2022, 9:49:18 AM ERROR [ExceptionsHandler] Cannot read properties of undefined (reading 'callbackURL')
TypeError: Cannot read properties of undefined (reading 'callbackURL')
at GoogleStrategy.OAuth2Strategy.authenticate (/home/node/app/node_modules/passport-appleid/lib/strategy.js:76:59)
at attempt (/home/node/app/node_modules/passport/lib/middleware/authenticate.js:369:16)
at authenticate (/home/node/app/node_modules/passport/lib/middleware/authenticate.js:370:7)
at /home/node/app/node_modules/#nestjs/passport/dist/auth.guard.js:96:3
at new Promise (<anonymous>)
at /home/node/app/node_modules/#nestjs/passport/dist/auth.guard.js:88:83
at MixinAuthGuard.<anonymous> (/home/node/app/node_modules/#nestjs/passport/dist/auth.guard.js:49:36)
at Generator.next (<anonymous>)
at fulfilled (/home/node/app/node_modules/#nestjs/passport/dist/auth.guard.js:17:58)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
If i commented Apple Strategy in user module, everything work fine.
I can't understand why GoogleStrategy.OAuth2Strategy.authenticate go to the appleid packages
And If I use only Google Strategy or only Apple Strategy everything works.
#UseGuards(AuthGuard('google'))
#ApiOperation({ summary: 'google sign in' })
#Get('google-sign-in')
public async signInWithGoogle(#Req() req: any) {
return req.user;
}
#UseGuards(AuthGuard('google'))
#ApiOperation({ summary: 'google redirect' })
#Get('auth/google/redirect')
public async signInWithGoogleRedirect(#Req() req: any) {
return this.userService.socialSignIn(req.user);
}
#UseGuards(AuthGuard('apple'))
#ApiOperation({ summary: 'apple sign in' })
#Get('apple-sign-in')
public async signInWithApple(#Req() req: any) {
return req.user;
}
#UseGuards(AuthGuard('apple'))
#ApiOperation({ summary: 'apple redirect' })
#Get('apple/redirect')
public async signInWithAppleRedirect(#Req() req: any) {
return this.userService.socialSignIn(req.user);
}
import { Injectable } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { Profile } from 'passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { RegisteredPlatformsEnum } from 'src/_enum/registered-platforms.enum';
import { ConfigService } from '../config.services';
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(configService: ConfigService) {
super({
clientID: configService.get('GOOGLE_AUTH_CLIENT_ID'),
clientSecret: configService.get('GOOGLE_AUTH_CLIENT_SECRET'),
callbackURL: configService.get('GOOGLE_AUTH_CALLBACK_URL'),
scope: ['email', 'profile'],
});
}
public async validate(
accessToken: string,
refreshToken: string,
profile: Profile,
done: VerifyCallback,
): Promise<any> {
const { name, emails } = profile;
const [userEmail] = emails;
const user = {
email: userEmail.value,
name: name.givenName,
registeredPlatform: RegisteredPlatformsEnum.GOOGLE,
accessToken,
};
done(null, user);
}
}
import path from 'path';
import { Injectable } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { Profile } from 'passport';
import { Strategy, VerifyCallback } from 'passport-appleid';
import { RegisteredPlatformsEnum } from 'src/_enum/registered-platforms.enum';
import { ConfigService } from '../config.services';
#Injectable()
export class AppleStrategy extends PassportStrategy(Strategy, 'apple') {
constructor(configService: ConfigService) {
super({
clientID: configService.get('APPLE_AUTH_CLIENT_ID'),
callbackURL: configService.get('APPLE_AUTH_CALLBACK_URL'),
teamId: configService.get('APPLE_AUTH_TEAM_ID'),
keyIdentifier: configService.get('APPLE_AUTH_KEY_IDENTIFIER'),
privateKeyPath: path.join(
__dirname,
`./AuthKey_${configService.get('APPLE_AUTH_KEY_IDENTIFIER')}.p8`,
),
});
}
public async validate(
accessToken: string,
refreshToken: string,
profile: Profile,
done: VerifyCallback,
): Promise<any> {
const { name, emails } = profile;
const [userEmail] = emails;
const user = {
email: userEmail.value,
name: name.givenName,
registeredPlatform: RegisteredPlatformsEnum.APPLE,
accessToken,
};
done(null, user);
}
}
import { Module, forwardRef, Logger } from '#nestjs/common';
import { TypeOrmModule } from '#nestjs/typeorm';
import { AppModule } from '../../app.module';
import { ConfigService } from '../../services/config.services';
import { UserService } from './services/users.service';
import { UserController } from './user.controller';
import { UserEntity } from '../../model/user.entity';
import { EmailService } from '../../services/email.service';
import { UserRepository } from 'src/repositories/user.repository';
import { GoogleStrategy } from 'src/services/strategy/google-strategy';
import { AppleStrategy } from 'src/services/strategy/apple-strategy';
#Module({
imports: [
forwardRef(() => AppModule),
TypeOrmModule.forFeature([UserEntity, UserRepository]),
],
controllers: [UserController],
providers: [
Logger,
UserService,
ConfigService,
EmailService,
// passport strategies
AppleStrategy,
GoogleStrategy,
],
exports: [UserService],
})
export class UserModule {}
I'm developing a backend using nestjs and passport-local strategy. I use local strategy only for owners login in my projects. but at end of validation it returns the owner in req.user.
how can I change it so it returns the owner in req.owner?
import { Injectable } from '#nestjs/common';
import { OwnerService } from '../owner/owner.service';
#Injectable()
export class AuthService {
constructor(private ownerService: OwnerService) {}
async validateOwner(username: string, pass: string): Promise<any> {
const owner = await this.ownerService.findOneByUsername(username);
// later check with hashed pass
if (owner && owner.owner && owner.owner.password === pass) {
const { password, ...result } = owner.owner;
return result;
}
return null;
}
}
and
import { Strategy } from 'passport-local';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, HttpException } from '#nestjs/common';
import { AuthService } from './auth.service';
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
usernameField: 'username',
passwordField: 'password',
});
}
async validate(username: string, password: string): Promise<any> {
const owner = await this.authService.validateOwner(username, password);
if (!owner) {
throw new HttpException('No Owner found', 404);
}
return owner;
}
}
how I use it:
#UseGuards(AuthGuard('local'))
#Post('login')
async login(#Request() req) {
console.log(req.owner, req.user);
return req.owner;
}
req.owner is empty but req.user has value
UPDATE:
my Authmodule.ts:
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { OwnerModule } from '../owner/owner.module';
import { LocalStrategy } from './local.strategy';
import { PassportModule } from '#nestjs/passport';
import { JwtModule } from '#nestjs/jwt';
#Module({
providers: [AuthService, LocalStrategy],
imports: [
OwnerModule,
PassportModule.register({
property: 'owner',
})
],
exports: [AuthService],
})
export class AuthModule {}
PassportModule.register({ property: 'owner' })
where PassportModule is imported from #nestjs/passport
I created a LocalAuthGuard class that extends my local strategy:
import { Injectable } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
constructor() {
super({
property: 'owner',
});
}
}
then wherever I use this guard, it adds my target field to req.owner instead of req.user.
#UseGuards(LocalAuthGuard)
#Post('login')
async login(#Request() req): Promise<LoginResponse> {
return this.authService.login(req.owner);
}
I have a problem, I'm trying to refactor my nodeJs server with nestJs.
The idea here is to check when starting the server if the 'Roles' collection already exists and contains entries, if not, we generate the roles in the mongoDB collection.
I have this folder stucture : mongoose schemas in a 'models' folder and an index.js that imports them:
./models/role.model.js
const mongoose = require("mongoose");
const Role = mongoose.model(
"Role",
new mongoose.Schema({
name: String
})
);
module.exports = Role;
./models/index.js
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
const db = {};
db.mongoose = mongoose;
db.user = require("./user.model");
db.role = require("./role.model");
db.ROLES = ["user", "admin", "moderator"];
module.exports = db;
and then, in server.js, i import index.js as db then i define Role as db.role and i use an init function with 'Role.estimatedDocumentCount' to count the number of documents and do some logic:
./server.js
const db = require("./models");
const Role = db.role;
[...]
function init() {
Role.estimatedDocumentCount((err, count) => {
if (!err && count === 0) {
new Role({
name: "user"
}).save(err => {
if (err) {
console.log("error", err);
}
console.log("added 'user' to roles collection");
});
}
});
}
It's working super fine on my nodeJs server, but problems comes with the new nestJs server, basically, i think it's a type issue, and i'm not sure if my method is legit in a nestjs environement, assuming i'm an absolute begginer in it... This is what i tried:
./schemas/role.schema.ts
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
export type RoleDocument = Role & Document;
#Schema()
export class Role {
#Prop()
name: string;
}
export const RoleSchema = SchemaFactory.createForClass(Role);
./schemas/index.ts
import mongoose from 'mongoose';
import { User } from './user.schema'
import { Role } from './role.schema'
mongoose.Promise = global.Promise;
export const db = {
mongoose: mongoose,
user: User,
role: Role,
ROLES: ["user", "admin", "moderator"]
};
./main.ts
[...]
import { db } from './auth/schemas';
const Role = db.role;
[...]
function init() {
Role.estimatedDocumentCount((err, count) => {
if (!err && count === 0) {
new Role({
name: "user"
}).save(err => {
if (err) {
console.log("error", err);
}
console.log("added 'user' to roles collection");
});
}
});
}
But it's not working, i get this error:
And when i check the type on the nodeJs server i have this:
But on nestJs server i get another type, this is why i think my issue comes from type, or maybe that method can't work the same way than on nodeJs for some reason...
EDIT: i also tried getting directly the roleSchema in my main.ts file, i'm getting a more interesting type, but it's not working either:
import { RoleSchema } from './auth/schemas/role.schema';
function init() {
RoleSchema.estimatedDocumentCount((err, count) => {
if (!err && count === 0) {
new Role({
name: "user"
}).save(err => {
if (err) {
console.log("error", err);
}
console.log("added 'user' to roles collection");
});
}
});
}
EDIT2: reply to gianfranco
auth.module.ts
import { Module } from '#nestjs/common';
import { ConfigModule } from '#nestjs/config';
import { JwtModule } from '#nestjs/jwt';
import { MongooseModule } from '#nestjs/mongoose';
import { PassportModule } from '#nestjs/passport';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserSchema } from './schemas/user.schema';
import { RoleSchema } from './schemas/role.schema';
#Module({
imports: [
ConfigModule.forRoot(),
MongooseModule.forFeature([{ name: 'User', schema: UserSchema },{ name: 'Role', schema: RoleSchema}]),
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
auth.controller.ts
import {
Body,
Controller,
Post,
Get,
Res,
Request,
UseGuards,
ValidationPipe,
} from '#nestjs/common';
import express, { Response } from 'express';
import { AuthService } from './auth.service';
import { AuthCredentialsDto } from './dto/auth-credentials.dto';
import { LocalAuthGuard } from './guards/auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
#Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
#Post('signup')
async signUp(
#Body(ValidationPipe) authCredentialsDto: AuthCredentialsDto,
): Promise<void> {
return await this.authService.signUp(authCredentialsDto);
}
#UseGuards(LocalAuthGuard)
#Post('signin')
async signIn(#Request() req) {
return this.authService.signIn(req.user);
}
#UseGuards(JwtAuthGuard)
#Get('me')
getMe(#Request() req) {
return req.user;
}
}
Thank you very much for your help!
The key was to implement the logic in auth.service.ts instead of setting it in main.ts.Thanks for your help!
I have this code in NestJs with the following files below, but now it says "Unknown authentication strategy "local"",
I've already looked for solutions and they all point to an import error, but I have the localstrategy imported into auth.module and app.module (I've already tested taking it out of app.module but it doesn't change anything.)
app.module.ts
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ThrottlerModule } from '#nestjs/throttler';
import { MongooseModule } from '#nestjs/mongoose';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { LocalStrategy } from './auth/local.strategy';
#Module({
imports: [
MongooseModule.forRoot(
'mongodb+srv://user:password#db.db.mongodb.net/db?retryWrites=true&w=majority',
),
ThrottlerModule.forRoot({
ttl: 60,
limit: 10,
}),
AuthModule,
UsersModule,
],
controllers: [AppController],
providers: [AppService, LocalStrategy],
})
export class AppModule {}
auth.module.ts
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '#nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { JwtModule } from '#nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';
import 'dotenv/config';
#Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: process.env.JWTSECRET,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { AuthService } from './auth.service';
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email' });
}
async validate(email: string, password: string): Promise<any> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
app.controller.ts
import {
Controller,
Request,
Post,
UseGuards,
Res,
Get,
Body,
} from '#nestjs/common';
import { AuthService } from './auth/auth.service';
import { MakeAuthDto } from './auth/dto/make-auth.dto';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { Roles } from './utils/decorators/roles.decorator';
import { Role } from './utils/enums/role.enum';
import { RolesGuard } from './utils/guards/roles.guard';
#Controller()
export class AppController {
constructor(private authService: AuthService) {}
#UseGuards(LocalAuthGuard)
#Post('auth/login')
async login(
#Body() _: MakeAuthDto,
#Request() req,
#Res({ passthrough: true }) res,
) {
console.log(req.user);
const access_token = await this.authService.login(req.user);
res.cookie('jwt', access_token);
return req.user;
}
#UseGuards(JwtAuthGuard, RolesGuard)
#Roles(Role.Admin)
#Get('tests')
getProfile(#Request() req) {
return req.user;
}
}
local-auth.guard.ts
import { Injectable } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
auth.service.ts
import { Injectable } from '#nestjs/common';
import { UsersService } from 'src/users/users.service';
import { JwtService } from '#nestjs/jwt';
import { UserDocument } from 'src/users/entities/user.entity';
#Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(email: string, pass: string): Promise<UserDocument | any> {
const user = await this.usersService.findOne(email);
if (user && (await user.compareHash(pass))) {
const { password, ...result } = user.toObject();
await this.usersService.updateLastLogin(user._id);
return result;
}
return null;
}
async login(user: UserDocument): Promise<string> {
const payload = { email: user.email, sub: user._id, roles: user.roles };
return this.jwtService.sign(payload);
}
}
jwt-auth.guard.ts
import { Injectable } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable } from '#nestjs/common';
import 'dotenv/config';
const cookieExtractor = function (req) {
let token = null;
if (req && req.cookies) {
token = req.cookies['jwt'];
}
return token;
};
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor]),
ignoreExpiration: false,
secretOrKey: process.env.JWTSECRET,
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email, roles: payload.roles };
}
}
No similar question on stackoverflow solved my problem, does anyone know what it can be?
I am using nestjs 8.0 with typeorm, passport-jwt, and passport-local. Everything seems to be working fine other than the LocalAuthGuard. I am able to successfully create a new user and even use the routes that have JwtAuthGuard but LocalAuthGuard seems to have some issues as I keep getting 401 Unauthorized error
Also, is there a way to console log some output from within the LocalAuthGuard or LocalStrategy?
auth.controller.ts
#Controller(['admin', 'user'])
export class AuthController {
constructor(
private authService: AuthService,
) {}
#UseGuards(LocalAuthGuard)
#Post('login')
login(#Request() req) {
console.log('object');
if (req.path.includes('admin') && !req.user.isAdmin) {
throw new UnauthorizedException();
}
return this.authService.login(req.user);
}
...
}
local.guard.ts
import { Injectable } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
local.strategy.ts
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(usernameOrEmail: string, password: string): Promise<any> {
const user = await this.authService.validateUser({
usernameOrEmail,
password,
});
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
auth.service.ts
#Injectable()
export class AuthService {
constructor(
#InjectRepository(User)
private userRepository: Repository<User>,
private jwtService: JwtService,
) {}
async validateUser({ usernameOrEmail, password }: LoginDto) {
const user = (await this.userRepository.findOne({ username: usernameOrEmail })) ||
(await this.userRepository.findOne({ email: usernameOrEmail }));
if (user && (await bcrypt.compare(password, user.password))) {
return user;
}
return null;
}
...
}
auth.module.ts
#Module({
imports: [
TypeOrmModule.forFeature([User]),
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get('JWT_KEY'),
signOptions: {
expiresIn: '6000s',
},
};
},
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
Any help or suggestion is greatly appreciated.
EDIT
It seems for LocalAuthGuard, username, and password are a must and other properties are optional.
You can create multiple local strategies with different parameters. For example,
Username and password
Phone and OTP
Email and password
Email and OTP
For using multiple local strategies, refer to this answer
Then, you can also pass an options object to specify different property names, for example: super({ usernameField: 'email', passwordField: 'otp' })
implement local strategy as follows
import { Strategy } from 'passport-local';
import { PassportStrategy } from '#nestjs/passport';
import { Inject, Injectable, UnauthorizedException } from '#nestjs/common';
import { IAuthService } from '../services';
import { LoginDto, OtpLoginDto } from '../dto';
import { UserDto } from 'src/modules/user/dto';
import { plainToClass } from 'class-transformer';
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(
#Inject(IAuthService)
private readonly authService: IAuthService,
) {
super({
usernameField: 'user_name',
passwordField: 'otp',
});
}
async validate(user_name: string, otp: string): Promise<any> {
const loginDto = plainToClass(OtpLoginDto, {
username: user_name,
otp: otp,
});
const user: UserDto = await this.authService.verifyOtp(loginDto);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
ref: customize passport