npm install -D prisma
npx prisma init
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String
}
# Configuration
JWT_SECRET=""
# Database
DATABASE_URL=""
# Configuration
JWT_SECRET="mysecret"
# Database
DATABASE_URL="file:./sqlite.db"
npx prisma migrate dev --name init
nest g module prisma
nest g service prisma
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}
@nestjs/passport
@nestjs/jwt
bcrypt
class-validator
class-transformer
passport
passport-jwt
passport-local
Installing all by one command:
npm i @nestjs/passport @nestjs/jwt bcrypt class-validator class-transformer passport passport-jwt passport-local
@types/passport-jwt
@types/passport-local
@types/bcrypt
npm i -D @types/passport-jwt @types/passport-local @types/bcrypt
nest g resource user
export class User {
id?: number;
email: string;
password: string;
name: string;
}
import { User } from '../entities/user.entity';
import {
IsEmail,
IsString,
Matches,
MaxLength,
MinLength,
} from 'class-validator';
export class CreateUserDto extends User {
@IsEmail()
email: string;
@IsString()
@MinLength(4)
@MaxLength(20)
@Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, {
message: 'Password is too weak',
})
password: string;
@IsString()
name: string;
}
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
}
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const data: Prisma.UserCreateInput = {
...createUserDto,
password: await bcrypt.hash(createUserDto.password, 10),
};
const createdUser = await this.prisma.user.create({ data });
return {
...createdUser,
password: undefined,
};
}
findByEmail(email: string) {
return this.prisma.user.findUnique({ where: { email } });
}
}
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
nest g module auth
nest g controller auth
nest g service auth
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { LoginValidationMiddleware } from './middlewares/login-validation.middleware';
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '30d' },
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
})
export class AuthModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoginValidationMiddleware).forRoutes('login');
}
}
import {
Controller,
HttpCode,
HttpStatus,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { AuthRequest } from './models/AuthRequest';
import { IsPublic } from './decorators/is-public.decorator';
@Controller()
export class AuthController {
constructor(private readonly authService: AuthService) {}
@IsPublic()
@UseGuards(LocalAuthGuard)
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Request() req: AuthRequest) {
return this.authService.login(req.user);
}
}
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UnauthorizedError } from './errors/unauthorized.error';
import { User } from '../user/entities/user.entity';
import { UserService } from '../user/user.service';
import { UserPayload } from './models/UserPayload';
import { UserToken } from './models/UserToken';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly userService: UserService,
) {}
async login(user: User): Promise<UserToken> {
const payload: UserPayload = {
sub: user.id,
email: user.email,
name: user.name,
};
return {
access_token: this.jwtService.sign(payload),
};
}
async validateUser(email: string, password: string): Promise<User> {
const user = await this.userService.findByEmail(email);
if (user) {
const isPasswordValid = await bcrypt.compare(password, user.password);
if (isPasswordValid) {
return {
...user,
password: undefined,
};
}
}
throw new UnauthorizedError('Invalid credentials.');
}
}
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserFromJwt } from '../models/UserFromJwt';
import { UserPayload } from '../models/UserPayload';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: UserPayload): Promise<UserFromJwt> {
return {
id: payload.sub,
email: payload.email,
name: payload.name,
};
}
}
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/is-public.decorator';
import { UnauthorizedError } from '../errors/unauthorized.error';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext): Promise<boolean> | boolean {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const canActivate = super.canActivate(context);
if (typeof canActivate === 'boolean') {
return canActivate;
}
const canActivatePromise = canActivate as Promise<boolean>;
return canActivatePromise.catch((error) => {
if (error instanceof UnauthorizedError) {
throw new UnauthorizedException(error.message);
}
throw new UnauthorizedException();
});
}
}
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email' });
}
validate(email: string, password: string) {
return this.authService.validateUser(email, password);
}
}
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err, user) {
if (err || !user) {
throw new UnauthorizedException(err?.message);
}
return user;
}
}
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
import { PrismaModule } from './prisma/prisma.module';
import { UserModule } from './user/user.module';
@Module({
imports: [PrismaModule, UserModule, AuthModule],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
import { Request } from 'express';
import { User } from '../../user/entities/user.entity';
export interface AuthRequest extends Request {
user: User;
}
import { IsEmail, IsString } from 'class-validator';
export class LoginRequestBody {
@IsEmail()
email: string;
@IsString()
password: string;
}
export class UserFromJwt {
id: number;
email: string;
name: string;
}
export interface UserPayload {
sub: number;
email: string;
name: string;
iat?: number;
exp?: number;
}
export interface UserToken {
access_token: string;
}
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from 'src/user/entities/user.entity';
import { AuthRequest } from '../models/AuthRequest';
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext): User => {
const request = context.switchToHttp().getRequest<AuthRequest>();
return request.user;
},
);
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const IsPublic = () => SetMetadata(IS_PUBLIC_KEY, true);
export class UnauthorizedError extends Error {}
import {
BadRequestException,
Injectable,
NestMiddleware,
} from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { LoginRequestBody } from '../models/LoginRequestBody';
import { validate } from 'class-validator';
@Injectable()
export class LoginValidationMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
const body = req.body;
const loginRequestBody = new LoginRequestBody();
loginRequestBody.email = body.email;
loginRequestBody.password = body.password;
const validations = await validate(loginRequestBody);
if (validations.length) {
throw new BadRequestException(
validations.reduce((acc, curr) => {
return [...acc, ...Object.values(curr.constraints)];
}, []),
);
}
next();
}
}
Endpoint: /user
Method: POST
Request Body:
{
"email": "[email protected]",
"password": "Abc@123",
"name": "Felipe Rodrigues"
}
Response Body (Status 201):
{
"id": 1,
"email": "[email protected]",
"name": "Felipe Rodrigues"
}
Endpoint: /login
Method: POST
Request Body:
{
"email": "[email protected]",
"password": "Abc@123"
}
Response Body (Status 200):
{
"access_token": "Generated JWT"
}