Nestjs + TypeORM + PostgreSql 實作登入系統

janlin002
11 min readDec 12, 2023
Photo by Dan Nelson on Unsplash

上一篇文章介紹過Nest後,我們今天要來時做一個登入系統

老話一句: 有任何錯誤都歡迎留言給我

那我們話不多說進入今天的文章

流程

登入系統分為兩個部分: 1. 註冊 2. 登入

註冊

註冊首先會先確認是否有該使用者,確定沒有以後會將新使用者的密碼用 bcrypt 這個套件做基本的加密,然後回傳 JWT 給前端,前端之後有需要打 Api 都要拿這組 JWT 來驗證是否為該使用者

登入

登入步驟稍微多一點點,首先須先確認使用者的密碼正確,然後使用驗證 JWT 是否一致,確定無誤以後,才能登入成功

環境下載

先創立一個 Nest 的專案,project-name 這邊名字自己取就可以了

$ npm i -g @nestjs/cli
$ nest new [project-name]

下載完以後,我們會需要下載一些,等一下要用到的套件

npm i @nestjs/jwt @nestjs/passport @types/passport-jwt passport passport-jwt

以上都下載完以後,我們現在開始實作,如何連線到 PostgreSql

連線 DB PostgreSql

npm install --save @nestjs/typeorm typeorm pg

這邊推薦一篇文章,可以快速的建立本地端的 PostgreSQL server,並透過 pgAdmin 做圖形化的管理

以後會專門做影片跟大家講解

開始實作

nest g res [資料夾名稱]

ex. nest g res user

透過 nest g res [資料夾名稱] 我們可以快速的建立一個 resource ,生成的程式碼中,有兩個比較特別 dto 跟 entities:

dto: dto 我們前一篇介紹過了,這篇就不多做解釋

entities: entities 就是建立 database tables 的地方

設計 table

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
email: string;

@Column()
password: string;
}

PrimaryGeneratedColumn 是這個 table 的主鍵(primary key)

Column 代表 table 裡面的其他欄位

在 service 中調用 db 的資料

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
constructor(
// 將 DB 注入到 service 中
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

// ...CRUD functions
}

註冊

controller.ts

import { Controller, Post, Body } from '@nestjs/common';

import { AuthService } from './auth.service';
import { RegisterDto } from './dto/registerDto';

@Controller('user')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('/register')
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
}

service.ts

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';

import { User } from '@/entity/user.entities';
import { RegisterDto } from './dto/registerDto';

@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private jwtService: JwtService,
) {}

// 註冊
async register(registerDto: RegisterDto) {
const { email, password } = registerDto;

// 透過 bcrypt 幫明碼加密
const salt = await bcrypt.genSalt();
const passwordHash = await bcrypt.hash(password, salt);

try {
// 因為我們的 email 在 entity 有定義為 unique
// 所以如果輸入 db 裡已經有的 email 會 response error
this.usersRepository.save({
email,
password,
});

return {
status: HttpStatus.OK,
message: 'User registered successfully',
};
} catch (error) {
throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

registerDto.ts

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

export class RegisterDto {
@IsEmail()
@IsNotEmpty()
email: string;

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

登入

controller.ts

import { Controller, Post, Body } from '@nestjs/common';

import { AuthService } from './auth.service';
import { LoginDto } from './dto/loginDto';

@Controller('user')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('/login')
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
}

service.ts

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';

import { User } from '@/entity/user.entities';
import { LoginDto } from './dto/loginDto';

@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private jwtService: JwtService,
) {}

// 登入
async login(loginDto: LoginDto) {
const { email, password } = loginDto;

const user = await this.usersRepository.findOne({ where: { email } });

// 如果沒有該使用者就回傳,找不到該使用者
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}

// 這邊需要特別注意,compare 後方的兩個欄位不能放錯
// 第一個欄位是使用者輸入的密碼(明碼)
// 第二個欄位是我們儲存在 db 裡面的密碼(暗碼)
const isSameUser = await bcrypt.compare(password, user.password);

// 密碼錯誤
if (!isSameUser) {
throw new HttpException('Invalid credentials', HttpStatus.BAD_REQUEST);
}

// 如果確定為本人,則把 requests 包裝成 jwt 回傳給前端,作為之後 call api 的鑰匙
const access_token = await this.jwtService.signAsync(loginDto);

return {
status: HttpStatus.OK,
message: 'User logged in successfully',
access_token,
};
}
}

loginDto.ts

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

export class LoginDto {
@IsEmail()
@IsNotEmpty()
email: string;

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

程式碼

參考文章

--

--