NestJS ile Basit Authentication Rest API

Önceki yazılarımızda NestJS’den biraz bahsetmiştik. Kolay ve modüler yapısı sayesinde kolayca web ve backend uygulamaları geliştirebiliriz. Bu yazımızda NestJS ile basit Rest Web servis/API oluşturacağız. Veri tabanı olarak MongoDB kullanacağız.

Eğer önceki yazımızı okumadıysanız sizi şöyle alalım;

Öncelikle nest paketi global olarak kurulu değilse kuralım.

npm i -g @nestjs/cli

Artık NestJS projesi oluşturabiliriz. Projeyi oluşturmak istediğimiz yola giderek projemizi oluşturalım.

nest new project-name

Projemizde kullanacağımız bazı özellikler için gerekli paketleri yükleyelim.

npm i class-validator class-transformer @nestjs/jwt passport-jwt @nestjs/passport passport passport-local
npm i --save-dev @types/passport-jwt @types/passport-local

Veri tabanı olarak Mongodb kullanacağımız için gerekli node paketlerini yükleyelim.

npm i @nestjs/mongoose mongoose

Tüm bağlılıkları yüklediğimize göre projemizi açabiliriz.

Projenin dosya ve klasör dizilimi

NestJS kodları okumaya main.ts‘den başlar. API ayarlarımızı yapılandırmak için src klasörünün içine app.config.ts adında dosya oluşturalım ve ilgili global ayarlarımızı yazalım.

export const Config = {
 apiPort: 5050,
 apiPrefix: 'api',
 jwtSecretKey: 'secretKey',
 jwtExpiresIn: '1 days', 
 mongoDbConnectionString: 'mongodb://localhost/database_name'
}
  • apiPort: Rest servisin çalışacağı port numarası
  • apiPrefix: Rest servisin çalışacağı ön ek adresi.
  • jwtSecretKey: Token oluşturulurken şifreleme keyi.
  • jwtExpiresIn: Token’ın geçerli olacağı zaman.
  • mongoDbConnectionString: Mongodb bağlantısı.

Proje ayarları için main.ts dosyasını açalım. Ardından gerekli ayarlamaları yapalım.

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { Config } from './app.config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: console
  });
  app.setGlobalPrefix(Config.apiPrefix);
  app.useGlobalPipes(new ValidationPipe()); //Dtolarda tanımlanan tüm validasyonları uygulamaya yarar.
  app.enableCors();
  await app.listen(Config.apiPort);
  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

Veritabanı olarak Mongodb kullanacağımız için app.module’de Connection String belirtmemiz gerekiyor;

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Config } from './app.config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    MongooseModule.forRoot(Config.mongoDbConnectionString),
    UserModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Artık modüllerimizi oluşturabiliriz. NestJS’ın CLI(command line interface) ile kolayca controller, module veya service oluşturabiliriz.

Tüm CLI komutlarını görebilmek için şu linki ziyaret edebilirsiniz; https://docs.nestjs.com/cli/usages

Kullanıcı işlemleri için user adında module, controller ve service oluşturuyoruz.

nest g mo user & nest g co user & nest g s user

User modülünü oluşturduk. src dizininde common klasörü içerisine resimdeki gibi dosyalarımızı oluşturalım ve hazır hale getirelim.

password.helper veri tabanında tutulan hashlenmiş şifreyle servise gönderilen şifrenin doğrulunu kontrol eder. Buna ek olarak yeni kullanıcı kaydında şifreyi veri tabanına kaydetmek için hashlenmiş bir biçimde bize döner.

import { randomBytes, scrypt } from 'crypto';
 export class PasswordHelper {
   constructor() { }
   public async passwordHash(password: string): Promise {
     return new Promise((resolve, reject) => {
       const salt = randomBytes(16).toString("hex");
       scrypt(password, salt, 64, (err, derivedKey) => {
         const hash = salt + ":" + derivedKey.toString('hex');
         resolve(hash);
       });
     });
   }
  public async verifyPasswordHash(password: string, hash: string): Promise {
     return new Promise((resolve, reject) => {
       const [salt, key] = hash.split(":");
       scrypt(password, salt, 64, (err, derivedKey) => {
         if (err) reject(err);
         if (key == derivedKey.toString('hex')) {
           resolve(true);
         } else {
           resolve(false);
         }
       });
     })
   }
 }

jwt-auth.guard ve local-auth.guard yetkisiz girişleri kontrol eder. Sonrasında Controller’da NestJS core’da bulunan UseGuards içine bu iki classı ekleyeceğiz.

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

Artık birkaç adım önce oluşturduğum User modülünü yazmaya başlayabiliriz. Öncelikle yine resimdeki gibi User klasörünün içine gerekli klasör ve dosyaları oluşturalım.

RegisterUserDto User servisine gelen Post işleminde hangi alanın hangi tipte olduğu, zorunlu olup olmadığı vb. gibi kontrol etmemizi sağlar. Örneğin userName boş olarak gönderildiğinde servisten otomatik olarak hata mesajı dönecektir.

import { IsNotEmpty, IsString, IsEmail } from 'class-validator';

export class RegisterUserDto {
    @IsNotEmpty()
    @IsString()
    userName: string;

    @IsNotEmpty()
    @IsEmail()
    email: string;

    @IsNotEmpty()
    @IsString()
    password: string;

    @IsNotEmpty()
    @IsString()
    name: string;

    @IsNotEmpty()
    @IsString()
    surname: string;
}

Interfaceleri tanımlıyoruz;

export interface IJwtPayload {
    readonly userId: string;
    readonly userName: string;
}
import { Document } from 'mongoose';

export interface IUser extends Document {
    readonly userName: string;
    readonly email: string;
    readonly password: string;
    readonly name: string;
    readonly surname: string;
}

Mongodb şemasını oluşturuyoruz;

import { Document } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
  
  @Prop({ required: true, unique: true })
  userName: string;

  @Prop({ required: true, unique: true  })
  email: string;

  @Prop({ required: true, select: false })
  password: string;

  @Prop({ required: true })
  name: string;

  @Prop({ required: true })
  surname: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

Json Web Token ve Local Token için user modülü içerisinde Strategy sınıflarını oluşturuyoruz.

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { IJwtPayload } from '../interfaces/jwt-payload.interface';
import { Config } from '../../app.config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: Config.jwtSecretKey,
    });
  }

  async validate(payload: IJwtPayload) {
    return { userName: payload.userName, userId: payload.userId };
  }
}
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { UserService } from '../user.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly service: UserService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.service.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Tüm bu işlemlerden sonra User modulüne controller ve providerları import etmemiz gerekiyor.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { Config } from '../app.config';

import { UserController } from './user.controller';
import { User, UserSchema } from './schemas/user.schema';
import { UserService } from './user.service';

import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { PasswordHelper } from '../common/helpers/password.helper';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
    PassportModule,
    JwtModule.register({
      secret: Config.jwtSecretKey,
      signOptions: { expiresIn: Config.jwtExpiresIn },
    })
  ],
  controllers: [UserController],
  providers: [LocalStrategy, JwtStrategy, PasswordHelper, UserService],
})

export class UserModule {}

User servisinde veri tabanıyla bağlantı kuracağımız gerekli fonksiyonları yazıyoruz. Sonrasında controller ile user servisten dataları alıp API tarafında döneceğiz.

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { JwtService } from '@nestjs/jwt';
import { IUser } from './interfaces/user.interface';
import { IJwtPayload } from './interfaces/jwt-payload.interface';
import { User, UserDocument } from './schemas/user.schema';
import { RegisterUserDto } from './dto/register-user.dto';
import { PasswordHelper } from '../common/helpers/password.helper';

@Injectable()
export class UserService {
    constructor(
        @InjectModel(User.name) private readonly userModel: Model<UserDocument>,
        private jwtService: JwtService,
        private passwordHelper: PasswordHelper
    ) { }

    async login(user: IUser) {
        try {
            const payload: IJwtPayload = { userName: user.userName, userId: user.id };
            return { access_token: this.jwtService.sign(payload) };
        } catch (err) {
            throw new HttpException('Server Error', HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    async register(registerUserDto: RegisterUserDto): Promise<User> {
        try {
            const create = new this.userModel(registerUserDto);
            return create.save();
        } catch (err) {
            throw new HttpException('Server Error', HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    async validateUser(userName: string, pass: string): Promise<IUser | null> {
        try {
            let findUser: IUser | null = null;
            const find = await this.userModel.find({ userName }).select('+password').exec();
            findUser = find.length > 0 ? find[0] : null;

            if (findUser != null) {
                const check = await this.passwordHelper.verifyPasswordHash(pass, findUser.password);
                return check ? findUser : null;
            }
            return findUser;
        } catch (err) {
            throw new HttpException('Server Error', HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    async registerFindUser(userName: string, email: string): Promise<boolean> {
        try {
            const user = await this.userModel.find({ $or: [{ userName }, { email }] }).exec();
            return user.length > 0 ? true : false;
        } catch (err) {
            throw new HttpException('Server Error', HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    async findUserById(id: string): Promise<IUser> {
        try {
            const find = await this.userModel.findById(id).exec();
            return find;
        } catch (err) {
            throw new HttpException('Server Error', HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }


}

Servise gelen istekleri karşılayacağımız Controller’ı hazırlayalım;

import { Body, Controller, HttpStatus, Post, Get, UseGuards, Request, Param, HttpException } from '@nestjs/common';
import { UserService } from './user.service';
import { RegisterUserDto } from './dto/register-user.dto';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { LocalAuthGuard } from '../common/guards/local-auth.guard';
import { PasswordHelper } from '../common/helpers/password.helper';
import { IUser } from './interfaces/user.interface';

@Controller()
export class UserController {
    constructor(
        private readonly service: UserService,
        private passwordHelper: PasswordHelper
    ) { }

    @UseGuards(LocalAuthGuard) //LocalAuthGuard ile gelen auth isteğini erişim izni veriyoruz.
    @Post('user/login')
    async login(@Request() req) {
        return this.service.login(req.user);
    }

    @Post('user/register')
    async create(@Body() registerUserDto: RegisterUserDto) {
        const userCheck = await this.service.registerFindUser(registerUserDto.userName, registerUserDto.email);
        if (userCheck) throw new HttpException('REGISTER_EXISTING_USER', HttpStatus.INTERNAL_SERVER_ERROR);
        registerUserDto.password = await this.passwordHelper.passwordHash(registerUserDto.password);
        await this.service.register(registerUserDto);
    }

    @UseGuards(JwtAuthGuard) //JwtAuthGuard ile yetkisiz istekleri engelliyoruz.
    @Get('user/profile')
    async getProfile(@Request() req) {
        const userId = req.user.userId;
        const user: IUser = await this.service.findUserById(userId);
        return user;
    }

    @UseGuards(JwtAuthGuard) //JwtAuthGuard ile yetkisiz istekleri engelliyoruz.
    @Get('user/:id')
    async getUserById(@Param('id') id: string) {
        const user: IUser = await this.service.findUserById(id);
        if (!user.id) throw new HttpException('BAD_REQUEST', HttpStatus.BAD_REQUEST);
        return user;
    }
}

Artık projemizi debug modda çalıştırabiliriz;

npm run start:dev

Eğer bir hata yapmadıysak terminalde ki görünüm şu şekilde olacaktır;

Artık servise http://localhost:5050 adresinden istek atabilirsiniz.

API Router Listesi

  • /user/login (POST)
  • /user/register (POST)
  • /user/profile (GET)
  • /user/:userId (GET)

Makalenin en altında vereceğim Github adresinden Postman dosyalarını alabilirsiniz ve import edebilirsiniz. Sonrasında servisi local ortamda test edebilirsiniz. Burada uzunca parametreleri ve headerda gönderilecek verileri yazmıyorum.

Servisin production sürümünü almak için şu komutu kullanabilirsiniz;

npm run build

İşlem tamamlandıktan sonra dist/ klasörü dahil tüm dosyaları sunucumuza ilgili yerlere yüklemeliyiz. Projeyi production modunda çalıştırmak için;

npm run start:prod

Sunucumuzda OpenLiteSpeed gibi sistemler kullanıyorsak Virtual Host bölümünden Node Context oluşturarak projenin sadece ilgili domain altında çalışmasını sağlayabiliriz.

Umarım yararlı olmuştur 🙂

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

This site uses Akismet to reduce spam. Learn how your comment data is processed.