Fernando Boza
Posted on March 17, 2021
We're going to build a NestJS server, on top of Fastify framework, pushing out a GraphQL API hosted on a MongoDB server. We'll create a User login and register model route and secure with passport and bcrypt, we'll go with the passport strategy JWT.
For reference here is the video companion and the github
https://github.com/FernandoBoza/auth-nest-graphql
nest new project-name
yarn add
- @nestjs/graphql graphql
- @nestjs/jwt
- @nestjs/mongoose mongoose
- @nestjs/passport passport passport-jwt
- @nestjs/platform-fastify
- bcrypt
yarn add -D
NestJS
Expres => Fastify main.ts
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(3000);
}
bootstrap().then((r) => r);
Change app.controller => app.resolver
import { AppService } from './app.service';
import { Query, Resolver } from '@nestjs/graphql';
@Resolver()
export class AppResolver {
constructor(private readonly appService: AppService) {}
@Query(() => String)
getHello(): string {
return this.appService.getHello();
}
}
App module remove controller from @Module directory
MongooseModule.forRoot('mongodb://localhost:27017/nest-auth'),
GraphQLModule.forRoot({
autoSchemaFile: true,
playground: true,
debug: false,
}),
cli
nest g mo user
nest g s user
nest g r user
cd src/user
nest g gu user
touch user.entity.ts
touch jwt.strategy.ts
touch user-inputs.dto.ts
touch user.guard.ts
USER.TS => USER.ENTITY.TS
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { Document, Types } from 'mongoose';
export enum Roles {
Admin = 'Admin',
Basic = 'Basic',
}
registerEnumType(Roles, {
name: 'Roles',
description: 'Admin create projects & tasks, Basic create tasks',
});
@ObjectType()
@Schema()
export class User {
@Field(() => String)
_id: Types.ObjectId;
@Field()
@Prop()
firstName: string;
@Field()
@Prop()
lastName: string;
@Field()
@Prop()
password: string;
@Field()
@Prop({ unique: true })
email: string;
@Field({ nullable: true })
@Prop()
imageURL?: string;
@Field((type) => Roles, { defaultValue: Roles.Admin, nullable: true })
@Prop()
role?: Roles;
@Field()
@Prop()
createdAt: string = new Date().toISOString();
}
export type UserDocument = User & Document;
export const UserSchema = SchemaFactory.createForClass(User);
JWT.STRATEGY.TS
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'hard!to-guess_secret',
});
}
async validate(payload: any) {
const { _id, email } = payload;
return { _id, email };
}
}
USER MODULE
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './user.entity';
import { JwtStrategy } from './jwt.strategy';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
JwtModule.register({
secret: 'hard!to-guess_secret',
signOptions: { expiresIn: '24h' },
}),
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
providers: [UserResolver, UserService, JwtStrategy],
})
export class UserModule {}
user.guard.ts =>
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
user-inputs.dto.ts
import { InputType, Field, OmitType, PartialType } from '@nestjs/graphql';
@InputType()
export class CreateUserInput {
@Field()
firstName: string;
@Field()
lastName: string;
@Field()
password: string;
@Field()
// @IsEmail()
email: string;
@Field()
createdAt: string = new Date().toISOString();
}
@InputType()
export class UpdateUserInput extends PartialType(
OmitType(CreateUserInput, ['password'] as const),
) {}
User.services
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { User, UserDocument } from './user.entity';
import { CreateUserInput, UpdateUserInput } from './user-inputs.dto';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { GraphQLError } from 'graphql';
@Injectable()
export class UserService {
constructor(
private jwtService: JwtService,
@InjectModel(User.name) private UserModel: Model<UserDocument>,
) {}
async create(createUserInput: CreateUserInput) {
try {
const isUser = await this.UserModel.findOne({
email: createUserInput.email,
});
if (isUser) {
throw new GraphQLError('Nah Bro, you already exist 🤡');
} else {
createUserInput.password = await bcrypt
.hash(createUserInput.password, 10)
.then((r) => r);
return await new this.UserModel(createUserInput).save();
}
} catch (err) {
console.error(err);
}
}
async login({ password, email }) {
try {
const res = await this.UserModel.findOne({ email });
return res && (await bcrypt.compare(password, res.password))
? this.getToken(email, res._id).then((result) => result)
: new GraphQLError('Nah Bro, you already exist 🤡');
} catch (err) {
console.error(err);
}
}
async getToken(email, _id): Promise<string> {
try {
return await this.jwtService.signAsync({ email, _id });
} catch (err) {
console.error(err);
}
}
async findAll() {
try {
return await this.UserModel.find().exec();
} catch (err) {
console.error(err);
}
}
async findOne(_id: Types.ObjectId) {
try {
return await this.UserModel.findById(_id).exec();
} catch (err) {
console.error(err);
}
}
async update(_id, updateUserInput: UpdateUserInput) {
try {
return await this.UserModel.findByIdAndUpdate(_id, updateUserInput, {
new: true,
}).exec();
} catch (err) {
console.error(err);
}
}
async updatePassword(_id, userPass, newPass) {
try {
const User = await this.UserModel.findById({ _id: _id });
if (await bcrypt.compare(userPass, User.password)) {
User.password = await bcrypt.hash(newPass, 10);
return await new this.UserModel(User).save();
}
} catch (err) {
console.error(err);
}
}
async remove(_id: string) {
try {
return await this.UserModel.findByIdAndDelete(_id).exec();
} catch (err) {
console.error(err);
}
}
}
USER.RESOLVER
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.entity';
import { CreateUserInput, UpdateUserInput } from './user-inputs.dto';
import { Types } from 'mongoose';
import { CurrentUser } from './user.decorator';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from './user.guard';
import { GraphQLError } from 'graphql';
@Resolver(() => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Mutation(() => User)
async createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
try {
return await this.userService.create(createUserInput);
} catch (err) {
console.error(err);
}
}
@Mutation(() => String)
async login(
@Args('email') email: string,
@Args('password') password: string,
): Promise<string | GraphQLError> {
try {
return await this.userService.login({ email, password });
} catch (err) {
console.error(err);
}
}
@Query(() => [User])
@UseGuards(GqlAuthGuard)
async findAll() {
try {
return await this.userService.findAll();
} catch (err) {
console.error(err);
}
}
@Query(() => User)
@UseGuards(GqlAuthGuard)
async findOne(@Args('_id', { type: () => String }) _id: Types.ObjectId) {
try {
return await this.userService.findOne(_id);
} catch (err) {
console.error(err);
}
}
@Mutation(() => User)
@UseGuards(GqlAuthGuard)
async updateUser(
@CurrentUser() user: User,
@Args('updateUserInput')
updateUserInput: UpdateUserInput,
) {
try {
return await this.userService.update(user._id, updateUserInput);
} catch (err) {
console.error(err);
}
}
@Mutation(() => User)
@UseGuards(GqlAuthGuard)
async updatePassword(
@CurrentUser() user: User,
@Args('currPass') currPass: string,
@Args('newPass') newPass: string,
) {
try {
return await this.userService.updatePassword(user._id, currPass, newPass);
} catch (err) {
console.error(err);
}
}
@Mutation(() => User)
@UseGuards(GqlAuthGuard)
async removeUser(@Args('_id') _id: string) {
try {
return await this.userService.remove(_id);
} catch (err) {
console.error(err);
}
}
@Query(() => User)
@UseGuards(GqlAuthGuard)
async CurrentUser(@CurrentUser() user: User) {
try {
return await this.userService.findOne(user._id);
} catch (err) {
console.error(err);
}
}
}
USER.DECORATOR
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const { _id, email } = GqlExecutionContext.create(
context,
).getContext().req.user;
return {
_id,
email,
};
},
);
brew services start mongodb-community@4.4
yarn run start:dev
Posted on March 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.