Code cùng Anson (P1): NestJS-MySQL Type ORM
minhthuong031103
Hai modules chính của phần này là auth và user
Các thư viện sẽ sử dụng:
npm i class-validator class-transformer @nestjs/passport passport passport-local express-session @nestjs/typeorm typeorm mysql2
declare namespace NodeJS {
export interface ProcessEnv {
PORT: number;
MYSQL_DB_HOST?: string;
MYSQL_DB_USERNAME?: string;
MYSQL_DB_PASSWORD?: string;
MYSQL_DB_PORT?: number;
MYSQL_DB_DATABASE?: string;
}
}
Cái TypeORMModule for Root không dùng nữa nên là phải theo Docs mới
database.provider.ts;
import entities from 'src/utils/typeorm';
import { DataSource } from 'typeorm';
export const databaseProviders = [
{
provide: 'DATA_SOURCE',
useFactory: async () => {
const dataSource = new DataSource({
host: process.env.MYSQL_DB_HOST, //mặc định là 3306
port: process.env.MYSQL_DB_PORT,
username: process.env.MYSQL_DB_USERNAME, //mặc định là root
password: process.env.MYSQL_DB_PASSWORD, //password mà mình cài khi cài mysql server
database: process.env.MYSQL_DB_DATABASE, //tên của data base
entities: entities, //các entites được định nghĩa và export ra
synchronize: true, //Đồng bộ với các entites, nó sẽ tự cập nhật trong database nếu có sự thay đổi
});
return dataSource.initialize(); //gọi hàm này để provider return về DATA SOURCE trong inject container
},
},
];
//Cái code này có trong document của NestJS ORM, tương tự cái module for Root thôi
database.module.ts;
import { Module } from '@nestjs/common';
import { databaseProviders } from './database.provider';
@Module({
providers: [...databaseProviders],
exports: [...databaseProviders],
})
export class DatabaseModule {}
Mục đích của DatabaseModule là để import vào các module mà cần dùng tới database
import 'reflect-metadata'; //dùng cho các decorators
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DataSource } from 'typeorm';
import * as session from 'express-session';
import * as passport from 'passport';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const dataSource: DataSource = app.get<DataSource>('DATA_SOURCE');
// Ta đã có module Data Source rồi và nó nằm trong inject container => lấy dựa trên provide
//ví dụ muốn lấy sessionRepository
const sessionRepository = dataSource.getRepository(Session);
// với Session là Entity đã được định nghĩa
const { PORT, COOKIE_SECRET } = process.env; //PORT 3001 env
app.setGlobalPrefix('api'); //set global prefix (api)
app.useGlobalPipes(new ValidationPipe());
//Global pipe
//Định nghĩa cho session
app.use(
session({
secret: COOKIE_SECRET,
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 86400000,
},
})
);
app.use(passport.initialize()); //init passport
app.use(passport.session()); //use passprt session
try {
await app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
} catch (error) {
console.log(error);
}
}
bootstrap(); //run app
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { UserModule } from './user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import entities from './utils/typeorm';
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '.env', //config env path bằng Config
}),
PassportModule.register({ session: true }), //Tiến hành đăng ký Passport sử dụng session
AuthModule,
UserModule,
//Thêm User và Auth Module
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
mysql -u root -p //Sau đó tiến hành nhập password
Create database chatapp_db;
Show database; //hiện các database
Use database chatapp_db;
show tables; hiện ra các bảng
describe column_name //miêu tả các thuộc tính (cột) có trong bảng
select * from table_name //select từ table
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude } from 'class-transformer';
@Entity()
export class User {
//cái cless này mình export nó để xíu nữa dùng làm Type cho User được luôn
@PrimaryGeneratedColumn() //Khóa chính và tự generate
id: number;
@Column({ unique: true })
username: string;
@Column({ nullable: true })
email: string;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
@Exclude() //exclude để khi chuyển sang plain object thì nó không return password
password: string;
}
import { User } from './entities/User';
const entities = [User];
export { User };
export default entities;
Khi ta đã có data Source, cứ mỗi 1 Entity ta sẽ tạo 1 provider tương ứng với entity đó dựa trên DataSource, mục đích của Provider là sẽ tạo ra Repository tương ứng với Entity trong inject Container đó để dùng trong service...
User.provider.ts;
import { User } from 'src/utils/typeorm';
import { DataSource } from 'typeorm';
export const userProviders = [
{
provide: 'USER_REPOSITORY',
useFactory: (dataSource: DataSource) => dataSource.getRepository(User),
inject: ['DATA_SOURCE'],
},
];
//Lúc này ta sẽ có được USER_REPOSITORY Trong inject container
/*
và có thể dùng nó bằng cách
constructor(
@Inject('USER_REPOSITORY')
private readonly userRepository: Repository<User> //Định nghĩa instance
) {}
*/
export enum Routes {
AUTH = 'auth',
}
export enum Services {
//Cách dùng constant Service inject này là mình sẽ bỏ nó vào providers
//Còn bản thân nó nếu mà module khác dùng thì cần export
// ví dụ providers: [{ provide: Services.USER, useClass: UserService }],
// sau đó @Inject trong constructor của service hoặc controller
AUTH = 'AUTH_SERVICE',
USER = 'USER_SERVICE',
}
Khi ta đã có userProviders và Databasemodule, để dùng được UserRepository trong service, ta cần imports:[databaseModule] và thêm ...userproviders vào providers
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { Services } from 'src/utils/constants';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/utils/typeorm';
import { userProviders } from 'src/database/providers/User.provider';
import { DatabaseModule } from 'src/database/database.module';
@Module({
imports: [DatabaseModule],
controllers: [UserController],
providers: [
...userProviders,
{ provide: Services.USER, useClass: UserService },
],
exports: [{ provide: Services.USER, useClass: UserService }],
})
export class UserModule {}
import { Controller } from '@nestjs/common';
@Controller('user')
export class UserController {}
chủ yếu hiện tại cái service này để inject vào cho auth nên controller chưa dùng
import {
CreateUserDetail,
FindUserOptions,
FindUserParams,
} from 'src/utils/types';
export interface IUserServices {
createUser(userDetails: CreateUserDetail);
findUser(findUserParams: FindUserParams, findUserOptions: FindUserOptions);
}
import * as bcrypt from 'bcrypt';
export async function hashPassword(rawPassword: string) {
const salt = await bcrypt.genSalt();
return bcrypt.hash(rawPassword, salt);
}
export async function compareHash(rawPassword: string, hash: string) {
return bcrypt.compare(rawPassword, hash);
}
##Định nghĩa type cho User Service
export type CreateUserDetail = {
username: string;
password: string;
firstName: string;
lastName: string;
};
export type ValidateUserDetails = {
username: string;
password: string;
};
export type FindUserParams = Partial<{
id: number;
email: string;
username: string;
}>;
export type FindUserOptions = Partial<{
selectAll: boolean;
}>;
import { Body, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { IUserServices } from './user';
import {
CreateUserDetail,
FindUserOptions,
FindUserParams,
} from 'src/utils/types';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/utils/typeorm';
import { Repository } from 'typeorm';
import { hashPassword } from 'src/utils/typeorm/helper';
@Injectable()
export class UserService implements IUserServices {
//Implement theo interface
constructor(
//Để inject repository ta làm như sau:
@Inject('USER_REPOSITORY')
private readonly userRepository: Repository<User> //Định nghĩa instance
) {}
async createUser(userDetails: CreateUserDetail) {
const user = await this.userRepository.findOneBy({
username: userDetails.username,
});
if (user) {
throw new HttpException('User already exists', HttpStatus.CONFLICT);
}
const password = await hashPassword(userDetails.password);
const newUser = this.userRepository.create({ ...userDetails, password });
return this.userRepository.save(newUser);
}
async findUser(
findUserParams: FindUserParams,
findUserOptions: FindUserOptions
) {
const user = await this.userRepository.findOneBy(findUserParams);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
return user;
}
}
Phần User xem như là xong
#Cài đặt Local Strategy để dùng trong Auth Module
import { Inject, Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { Services } from 'src/utils/constants';
import { IAuthServices } from '../auth';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
//Strategy nằm trong passport-local
//nên xíu nó tự có name là 'local'
constructor(
@Inject(Services.AUTH) private readonly authService: IAuthServices //Providers của AUTH
) {
//super({ usernameField: "email" }) if you want to use email instead of username
super();
}
async validate(username: string, password: string) {
return this.authService.validateUser({ username, password });
//Hàm validate với giá trị return để set trong request.user
// nếu mình return có giá trị => hợp lệ, nếu là null=> không hợp lệ
}
}
import { ExecutionContext } from '@nestjs/common';
import { Injectable } from '@nestjs/common/decorators';
import { AuthGuard } from '@nestjs/passport';
@Injectable() //local mình đã định nghĩa ở trên
export class LocalAuthGuard extends AuthGuard('local') {
async canActivate(context: ExecutionContext): Promise<boolean> {
// CanActivate sẽ nhận vào Execution Context và return 1 boolean value
//boolean value này sẽ xác định xem có vượt qua Guard được không
const result = (await super.canActivate(context)) as boolean;
//CanActivate trong super của AuthGuard sẽ check xem trong context có chứa
// request.user không => return boolean
const request = context.switchToHttp().getRequest();
//convert execution Context sang HTTP Request
await super.logIn(request);
//Gọi hàm login của super, dù cho thằng result true hay false gì cũng kệ
// Mục đích là nếu đăng nhập thành công thì cần lưu session id vào cookies
return result;
}
}
Thì khi mà Guard chạy local thành công thì để lưu vào session Mình cần serialize nó và deserialize cho các requests sau
import { Inject, Injectable } from '@nestjs/common';
import { PassportSerializer } from '@nestjs/passport';
import { UserService } from 'src/user/user.service';
import { Services } from 'src/utils/constants';
import { User } from 'src/utils/typeorm';
@Injectable()
export class SessionSerializer extends PassportSerializer {
// kế thừa PassportSerializer
constructor(
@Inject(Services.USER) private readonly userService: UserService
) {
super();
}
serializeUser(user: User, done: Function) {
done(null, user); //null là không có lỗi
}
//Serialize sẽ chạy lần đầu tiên khi gửi request
// sau đó nếu thành công sẽ done() và lưu user vào cookies
// ở đây user khi đăng nhập thì tùy vào logic return của validate mà có các values nào
//nên khi đó mình cần deserialize để các request sau dựa trên thông tin đó
// mà tìm trong database thì sẽ có nhiều dữ liệu về user hơn
async deserializeUser(user: User, done: Function) {
const userDB = this.userService.findUser(
{ id: user.id }, //ở đây mình dùng id để find
{ selectAll: true }
);
return userDB ? done(null, userDB) : done(null, null);
//nếu tìm thấy user trong database thì sẽ done user, còn không thì done null
}
}
import { Module } from '@nestjs/common';
import { AuthController } from '../auth/auth.controller';
import { AuthService } from './auth.service';
import { Services } from 'src/utils/constants';
import { UserModule } from 'src/user/user.module';
import { LocalStrategy } from './utils/localStrategy';
import { SessionSerializer } from './utils/SessionSerializer';
@Module({
imports: [UserModule],
controllers: [AuthController],
providers: [
//cung cấp localStrategy và session Serializer đã định nghĩa phía trên
LocalStrategy,
SessionSerializer,
{
provide: Services.AUTH,
useClass: AuthService,
},
],
})
export class AuthModule {}
import {
Body,
Controller,
Get,
Inject,
Post,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { Routes, Services } from 'src/utils/constants';
import { IAuthServices } from './auth';
import { CreateUserDto } from './dtos/createUser.dto';
import { UserService } from 'src/user/user.service';
import { instanceToPlain } from 'class-transformer';
import { UserLoginDto } from './dtos/userLogin.dto';
import { LocalAuthGuard } from './utils/Guard';
@Controller(Routes.AUTH) //import auth route
export class AuthController {
constructor(
//Inject cac service
@Inject(Services.AUTH)
private authService: AuthService,
@Inject(Services.USER)
private userService: UserService
) {}
@Post('/register')
@UsePipes(ValidationPipe)
registerUser(@Body() createUserDto: CreateUserDto) {
return instanceToPlain(this.userService.createUser(createUserDto));
//instanceToPlain để apply cái Exlude trong User Entity lúc nãy
}
@Post('/login')
@UseGuards(LocalAuthGuard)
//Apply Guard Local đã định nghĩa
login() {
console.log('ok');
}
@Get('/status')
status() {}
@Post('/logout')
logout() {}
}
Auth service này lát dùng trong local strategy khi login
import { User } from 'src/utils/typeorm';
import { ValidateUserDetails } from 'src/utils/types';
export interface IAuthServices {
validateUser(userCredentials: ValidateUserDetails): Promise<User | null>;
// return lại user hoặc null ( dùng trong validate của local strategy)
}
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { IAuthServices } from './auth';
import { Services } from 'src/utils/constants';
import { UserService } from 'src/user/user.service';
import { ValidateUserDetails } from 'src/utils/types';
import { compareHash } from 'src/utils/typeorm/helper';
@Injectable() //Injectable decorator
export class AuthService implements IAuthServices {
constructor(
@Inject(Services.USER) //inject user service
private userService: UserService
) {}
async validateUser(userDetails: ValidateUserDetails) {
{
const user = await this.userService.findUser(
{ username: userDetails.username },
{ selectAll: true }
);
console.log(user);
if (!user)
throw new HttpException('Invalid Credentials', HttpStatus.UNAUTHORIZED);
const isPasswordValid = await compareHash(
userDetails.password,
user.password
);
console.log(isPasswordValid);
return isPasswordValid ? user : null;
}
}
}
Done