89 KiB
BACKEND DEVELOPMENT
T1.1 CommonModule
ตอนนี้เราจะเข้าสู่ Phase 1: Core Foundation เริ่มจาก Task T1.1 CommonModule - Base Infrastructure ครับ
เป้าหมายคือสร้าง "รากฐาน" ที่ทุก Module ต้องใช้ร่วมกัน เพื่อให้โค้ดเป็นระเบียบและลดความซ้ำซ้อนครับ
1. สร้าง CommonModule
เพื่อให้ NestJS รู้จักไฟล์ต่างๆ ในโฟลเดอร์ common เราควรสร้าง Module หลักมาห่อหุ้มไว้ครับ
รันคำสั่ง:
nest g module common
(ถ้ามันฟ้องว่ามีไฟล์อยู่แล้ว ให้ข้ามขั้นตอนนี้ได้เลยครับ แต่ต้องแน่ใจว่า app.module.ts มี CommonModule ใน imports แล้ว)
2. สร้าง Base Entity (รากฐานของทุกตาราง)
เราจะสร้าง Entity แม่แบบที่มีคอลัมน์มาตรฐาน (id, created_at, updated_at, deleted_at) เพื่อให้ Entity อื่นๆ (เช่น User, Project) สืบทอด (Extend) ไปใช้ได้เลย ไม่ต้องเขียนซ้ำครับ
สร้างไฟล์: src/common/entities/base.entity.ts
import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
} from 'typeorm';
export abstract class BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@CreateDateColumn({ name: 'created_at' })
created_at: Date;
@UpdateDateColumn({ name: 'updated_at' })
updated_at: Date;
@DeleteDateColumn({ name: 'deleted_at', select: false }) // select: false เพื่อซ่อน field นี้โดย Default
deleted_at: Date;
}
3. สร้าง Standard Response Format (Interceptor)
เพื่อให้ API ทุกเส้นตอบกลับในรูปแบบเดียวกันเสมอ (เช่น มี field data, statusCode, message) เราจะใช้ Interceptor ครับ
สร้างไฟล์: src/common/interceptors/transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
statusCode: number;
message: string;
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
statusCode: context.switchToHttp().getResponse().statusCode,
message: data?.message || 'Success', // ถ้า data มี message ให้ใช้ ถ้าไม่มีใช้ 'Success'
data: data?.result || data, // รองรับกรณีส่ง object ที่มี key result มา
})),
);
}
}
4. สร้าง Global Exception Filter
สำหรับจัดการ Error ทั้งหมดในระบบ ให้ตอบกลับเป็น JSON ที่สวยงามและ ไม่เปิดเผยข้อมูล Sensitive (เช่น Stack Trace) ตามข้อกำหนด Security ครับ
สร้างไฟล์: src/common/exceptions/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const exceptionResponse =
exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
// จัดรูปแบบ Error Message
let message =
typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || exceptionResponse;
// Log Error (สำคัญมากสำหรับการ Debug แต่ไม่ส่งให้ Client เห็นทั้งหมด)
this.logger.error(
`Http Status: ${status} Error Message: ${JSON.stringify(message)}`,
);
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: status === 500 ? 'Internal server error' : message, // ซ่อน Detail กรณี 500
});
}
}
5. ลงทะเบียนใช้งาน Global ใน main.ts
สุดท้าย เราต้องบอก NestJS ให้ใช้ Interceptor และ Filter ที่เราเพิ่งสร้างกับทุก Request ครับ
แก้ไขไฟล์: src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { TransformInterceptor } from './common/interceptors/transform.interceptor.js'; // อย่าลืม .js ถ้าใช้ ESM
import { HttpExceptionFilter } from './common/exceptions/http-exception.filter.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 1. Global Prefix (เช่น /api/v1)
app.setGlobalPrefix('api');
// 2. Global Validation Pipe (ตรวจสอบ Input DTO)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // ตัด field ส่วนเกินทิ้ง
transform: true, // แปลง Type อัตโนมัติ (เช่น string -> number)
forbidNonWhitelisted: true, // แจ้ง Error ถ้าส่ง field แปลกปลอมมา
}),
);
// 3. Global Interceptor (จัด Format Response)
app.useGlobalInterceptors(new TransformInterceptor());
// 4. Global Exception Filter (จัดการ Error)
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(process.env.PORT || 3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
- เสร็จสิ้น T1.1 ส่วน Infrastructure หลักแล้วครับ
ตอนนี้ระบบเรามี:
- BaseEntity ไว้ให้ตารางอื่นสืบทอด
- Standard Response
{ statusCode, message, data } - Secure Error Handling
T1.2 AuthModule - JWT Authentication
Task นี้หัวใจสำคัญคือ "ความปลอดภัย" เราจะสร้างระบบ Login ที่ใช้ JWT (JSON Web Token) และเก็บรหัสผ่านแบบ Bcrypt ตามมาตรฐานครับ
1. ติดตั้ง Libraries
เราต้องใช้ Passport (สำหรับจัดการ Strategy), JWT, และ Bcrypt (สำหรับ Hash Password)
รันคำสั่ง:
pnpm add @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
pnpm add -D @types/passport-jwt @types/bcrypt
2. สร้าง User Entity และ Module
เนื่องจาก Auth ต้องทำงานกับ "ผู้ใช้" เราจำเป็นต้องสร้าง UserModule และ User Entity ขึ้นมาก่อนครับ (เป็นการทำ T1.3 บางส่วนล่วงหน้าเพื่อให้ T1.2 ทำงานได้)
สร้างไฟล์: src/modules/user/entities/user.entity.ts
(อ้างอิงโครงสร้างจาก Data Dictionary ตาราง users)
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity.js'; // Import จาก Common
@Entity('users') // ชื่อตารางใน DB
export class User extends BaseEntity {
// Map property 'id' ของ Code ให้ตรงกับ column 'user_id' ของ DB
@PrimaryGeneratedColumn({ name: 'user_id' })
id: number;
@Column({ unique: true, length: 50 })
username: string;
@Column({ name: 'password_hash' }) // ชื่อ column ใน DB
password: string; // ชื่อ property ใน Code
@Column({ unique: true, length: 100 })
email: string;
@Column({ name: 'first_name', nullable: true, length: 50 })
firstName: string;
@Column({ name: 'last_name', nullable: true, length: 50 })
lastName: string;
@Column({ name: 'is_active', default: true })
isActive: boolean;
// เดี๋ยวเราจะมาเพิ่ม Relation กับ Role/Org ทีหลังใน T1.3
}
จากนั้นสร้าง UserModule และ UserService แบบย่อเพื่อใช้ค้นหา User:
รันคำสั่ง:
nest g module modules/user
nest g service modules/user
แก้ไข src/modules/user/user.module.ts:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service.js';
import { User } from './entities/user.entity.js';
@Module({
imports: [TypeOrmModule.forFeature([User])], // จดทะเบียน Entity
providers: [UserService],
exports: [UserService], // Export ให้ AuthModule เรียกใช้ได้
})
export class UserModule {}
แก้ไข src/modules/user/user.service.ts:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity.js';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findOneByUsername(username: string): Promise<User | null> {
return this.usersRepository.findOne({ where: { username } });
}
// ใช้สำหรับ Seed หรือ Register เบื้องต้น
async create(userData: Partial<User>): Promise<User> {
const user = this.usersRepository.create(userData);
return this.usersRepository.save(user);
}
}
3. สร้าง Auth Module และ Service
รันคำสั่ง:
nest g module common/auth
nest g service common/auth
nest g controller common/auth
แก้ไข src/common/auth/auth.module.ts เพื่อตั้งค่า JWT:
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service.js';
import { AuthController } from './auth.controller.js';
import { UserModule } from '../../modules/user/user.module.js';
import { JwtStrategy } from './jwt.strategy.js'; // เดี๋ยวสร้างไฟล์นี้
@Module({
imports: [
UserModule, // Import เพื่อเรียกใช้ UserService
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRATION') || '8h'
},
}),
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
4. Implement Auth Logic (Hash & Validate)
แก้ไข src/common/auth/auth.service.ts:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UserService } from '../../modules/user/user.service.js';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}
// ตรวจสอบ Username/Password
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOneByUsername(username);
if (user && (await bcrypt.compare(pass, user.password))) {
// ตัด password ออกก่อนส่งกลับ
const { password, ...result } = user;
return result;
}
return null;
}
// สร้าง Token (Login)
async login(user: any) {
const payload = { username: user.username, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
// (Optional) ฟังก์ชัน Register สำหรับสร้าง User แรก (Admin)
async register(userDto: any) {
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(userDto.password, salt);
return this.userService.create({
...userDto,
password: hashedPassword,
});
}
}
5. สร้าง JWT Strategy (ตัวตรวจสอบ Token)
สร้างไฟล์ src/common/auth/jwt.strategy.ts:
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: any) {
// สิ่งที่ Return ตรงนี้จะไปอยู่ใน request.user
return { userId: payload.sub, username: payload.username };
}
}
6. สร้าง Auth Controller (Login Endpoint)
แก้ไข src/common/auth/auth.controller.ts:
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service.js';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
async login(@Body() req) {
// รับ username/password จาก Body
const user = await this.authService.validateUser(req.username, req.password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return this.authService.login(user);
}
// Endpoint ชั่วคราวสำหรับสร้าง User คนแรก (Admin)
@Post('register-admin')
async register(@Body() req) {
return this.authService.register(req);
}
}
✅ สรุปสิ่งที่ทำไป
- สร้าง
UserEntity (Map กับ DBusers) - สร้าง
AuthModuleที่ Config JWT ด้วย Secret จาก.env - ทำระบบ Hash Password ด้วย
bcrypt - เปิด Endpoint
/auth/loginและ/auth/register-admin
ทดสอบ (Manual Test)
- Start App:
pnpm start:dev - ใช้ Postman หรือ Thunder Client:
- Register Admin:
POST http://localhost:3000/api/auth/register-admin- Body:
{"username": "admin", "password": "password123", "email": "admin@test.com"}
- Body:
- Login:
POST http://localhost:3000/api/auth/login- Body:
{"username": "admin", "password": "password123"}
- Body:
- Result: ควรได้
access_tokenกลับมาครับ
- Register Admin:
ลองทำตามขั้นตอนนี้ดูครับ ติดขัดตรงไหนแจ้งได้เลยครับ (โดยเฉพาะเรื่อง .js extension ใน import ถ้าใช้ ESM อาจจะต้องระวังนิดนึงครับ)
7. error
ปัญหานี้เกิดจากการที่เราเปิด Strict Mode (noImplicitAny: true) ใน tsconfig.json ทำให้ TypeScript บังคับว่าทุกตัวแปรต้องระบุ Type ให้ชัดเจนครับ (ห้ามปล่อยโล่งๆ ให้เป็น any เอง)
วิธีแก้ไขที่ถูกต้องตามมาตรฐาน NestJS คือการสร้าง DTO (Data Transfer Object) เพื่อกำหนดโครงสร้างข้อมูลที่จะรับเข้ามาครับ
ทำตามขั้นตอนดังนี้ครับ:
1. ติดตั้ง Library สำหรับ Validation (ถ้ายังไม่ได้ลง)
เราจะใช้ class-validator ร่วมกับ DTO เพื่อตรวจสอบข้อมูลขาเข้าด้วยครับ
pnpm add class-validator class-transformer
2. สร้าง DTO ไฟล์
สร้างโฟลเดอร์ dto ใน src/common/auth/ และสร้างไฟล์ DTO 2 ไฟล์ครับ:
ไฟล์: src/common/auth/dto/login.dto.ts
import { IsNotEmpty, IsString } from 'class-validator';
export class LoginDto {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
}
ไฟล์: src/common/auth/dto/register.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength, IsOptional } from 'class-validator';
export class RegisterDto {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
@MinLength(6, { message: 'Password must be at least 6 characters' })
password: string;
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsOptional()
firstName?: string;
@IsString()
@IsOptional()
lastName?: string;
}
3. อัปเดต auth.controller.ts (แก้ไขฉบับสมบูรณ์)
นำ DTO มาใช้แทน req (ที่เป็น any) ครับ
ไฟล์: src/common/auth/auth.controller.ts
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service.js';
import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO
import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
// เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto
async login(@Body() loginDto: LoginDto) {
const user = await this.authService.validateUser(
loginDto.username,
loginDto.password
);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return this.authService.login(user);
}
@Post('register-admin')
// เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
}
4. อัปเดต auth.service.ts (ปรับ Type ให้ตรงกัน)
ใน auth.service.ts ฟังก์ชัน register ควรระบุ Type ของ parameter ด้วยเช่นกันครับ
ไฟล์: src/common/auth/auth.service.ts
(เฉพาะส่วนที่ต้องแก้ method register)
// ... imports อื่นๆ
import { RegisterDto } from './dto/register.dto.js'; // Import DTO
@Injectable()
export class AuthService {
// ... constructor และ methods อื่นๆ เหมือนเดิม ...
// แก้ไข type ของ parameter userDto
async register(userDto: RegisterDto) {
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(userDto.password, salt);
// ส่งข้อมูลไป create (TypeScript จะตรวจสอบว่า field ตรงกับ User Entity หรือไม่)
return this.userService.create({
...userDto,
password: hashedPassword,
});
}
}
ทำไมต้องทำแบบนี้?
- Type Safety: เราจะรู้ทันทีว่าข้อมูลที่ส่งเข้ามามีหน้าตาเป็นอย่างไร ใช้งาน
loginDto.usernameได้เลยโดยไม่ต้องเดา - Validation: NestJS จะใช้
ValidationPipe(ที่เราตั้งในmain.ts) ตรวจสอบข้อมูลให้อัตโนมัติ ถ้าส่งมาผิด (เช่น ลืมส่ง password หรือ email ผิด format) ระบบจะแจ้ง Error 400 Bad Request กลับไปทันทีครับ
ปัญหานี้เกิดจาก TypeScript Strict Mode ครับ
ตัวแปร expiresIn ใน Library @nestjs/jwt (ที่ใช้ jsonwebtoken ข้างหลัง) ต้องการ Type ที่เฉพาะเจาะจงมาก (เช่น number | StringValue) แต่ ConfigService.get<string> ส่งคืนค่าเป็น string ทั่วไป ทำให้ TypeScript มองว่า Type ไม่ตรงกัน 100%
วิธีแก้ไขคือเราต้อง Cast Type ของค่า expiresIn ให้ TypeScript ยอมรับครับ
แก้ไขไฟล์ src/common/auth/auth.module.ts ดังนี้ครับ:
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service.js';
import { AuthController } from './auth.controller.js';
import { UserModule } from '../../modules/user/user.module.js';
import { JwtStrategy } from './jwt.strategy.js';
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
// แก้ไขโดยการ Cast as any หรือ as string | number เพื่อให้ TypeScript ยอมรับ
expiresIn: configService.get<string>('JWT_EXPIRATION') || '8h',
} as any, // <--- เพิ่ม as any ตรงนี้ หรือจะใส่ที่บรรทัด expiresIn ก็ได้
}),
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
หรือ จะแก้เฉพาะบรรทัด expiresIn แบบนี้ก็ได้ครับ (แนะนำแบบนี้มากกว่า):
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
// Cast เฉพาะค่า Value
expiresIn: (configService.get<string>('JWT_EXPIRATION') || '8h') as any,
},
}),
ลองแก้แล้ว Save ดูครับ Error ควรจะหายไปครับ
ปัญหานี้เกิดจากยังไม่ได้ Import RegisterDto เข้ามาในไฟล์ src/common/auth/auth.service.ts ครับ
ให้เพิ่มบรรทัด import ที่ส่วนบนสุดของไฟล์ครับ:
ไฟล์: src/common/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UserService } from '../../modules/user/user.service.js';
import { RegisterDto } from './dto/register.dto.js'; // <--- เพิ่มบรรทัดนี้ครับ
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOneByUsername(username);
if (user && (await bcrypt.compare(pass, user.password))) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
// ตอนนี้ TypeScript จะรู้จัก RegisterDto แล้วครับ
async register(userDto: RegisterDto) {
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(userDto.password, salt);
return this.userService.create({
...userDto,
password: hashedPassword,
});
}
}
ข้อสังเกต: เนื่องจากโปรเจกต์ตั้งค่าเป็น ESM (NodeNext) อย่าลืมใส่นามสกุล .js ท้ายชื่อไฟล์ตอน import ด้วยนะครับ (เช่น ./dto/register.dto.js)
ปัญหานี้เกิดจาก Strict Mode ของ TypeScript (การตั้งค่า strictPropertyInitialization: true) ครับ ซึ่งจะบังคับว่า Property ใน Class ต้องมีการกำหนดค่าเริ่มต้น (Initialize) เสมอ
แต่เนื่องจาก DTO ใน NestJS จะถูกเติมค่า (Populate) โดยอัตโนมัติผ่าน Library class-transformer ตอนที่มี Request เข้ามา เราจึงไม่ได้กำหนดค่าให้มันใน Constructor เอง
วิธีแก้ไข: ให้ใช้เครื่องหมาย ! (Definite Assignment Assertion) หลังชื่อตัวแปร เพื่อบอก TypeScript ว่า "ไม่ต้องห่วง ตัวแปรนี้จะมีค่าแน่นอนตอนใช้งาน" ครับ
ให้แก้ไขไฟล์ DTO ทั้ง 2 ไฟล์ดังนี้ครับ:
1. แก้ไข src/common/auth/dto/login.dto.ts
ใส่ ! หลังชื่อตัวแปรทุกตัวครับ
import { IsNotEmpty, IsString } from 'class-validator';
export class LoginDto {
@IsString()
@IsNotEmpty()
username!: string; // <--- เติม !
@IsString()
@IsNotEmpty()
password!: string; // <--- เติม !
}
2. แก้ไข src/common/auth/dto/register.dto.ts
ทำเช่นเดียวกันครับ (ส่วนที่เป็น ? คือ Optional ไม่ต้องใส่ ! ครับ)
import { IsEmail, IsNotEmpty, IsString, MinLength, IsOptional } from 'class-validator';
export class RegisterDto {
@IsString()
@IsNotEmpty()
username!: string; // <--- เติม !
@IsString()
@IsNotEmpty()
@MinLength(6, { message: 'Password must be at least 6 characters' })
password!: string; // <--- เติม !
@IsEmail()
@IsNotEmpty()
email!: string; // <--- เติม !
@IsString()
@IsOptional()
firstName?: string; // อันนี้เป็น Optional (?) อยู่แล้ว ไม่ต้องแก้
@IsString()
@IsOptional()
lastName?: string; // อันนี้เป็น Optional (?) อยู่แล้ว ไม่ต้องแก้
}
หมายเหตุ: ในอนาคตเมื่อคุณสร้าง Entity (เช่น User entity) คุณก็จะเจอ Error แบบเดียวกันนี้ ก็ให้ใช้ ! แก้ไขในลักษณะเดียวกันครับ
ลองแก้แล้ว Save ดูครับ Error ควรจะหายไป และสามารถรัน Server ได้ครับ
ปัญหานี้เกิดจาก TypeScript Strict Mode อีกเช่นเคยครับ
ค่าที่ได้จาก configService.get(...) นั้น TypeScript มองว่ามีโอกาสเป็น undefined (หาไม่เจอ) ได้ แต่ passport-jwt บังคับว่า secretOrKey ห้ามเป็น undefined เด็ดขาด
ถึงแม้เราจะมี env.validation.ts คอยเช็คตอนรัน Server แล้ว แต่ TypeScript ไม่รู้เรื่องนั้นด้วยครับ
วิธีแก้ไข: ให้ใส่เครื่องหมาย ! (Non-null assertion) ต่อท้าย เพื่อยืนยันกับ TypeScript ว่า "ค่านี้มีอยู่จริงแน่นอน ไม่ต้องกลัว undefined" ครับ
แก้ไขไฟล์ src/common/auth/jwt.strategy.ts ดังนี้ครับ:
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
// ใส่ ! ต่อท้าย เพื่อบอกว่าค่านี้ไม่เป็น undefined แน่นอน
secretOrKey: configService.get<string>('JWT_SECRET')!,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
ทางเลือกอื่น (ถ้าใช้ NestJS เวอร์ชั่นใหม่):
คุณสามารถใช้ getOrThrow แทนได้ครับ ซึ่งจะช่วยแก้ Type Error และเช็ค Runtime ไปในตัว:
secretOrKey: configService.getOrThrow<string>('JWT_SECRET'),
ลองแก้แล้ว Save ดูครับ Error น่าจะหายไปครับ
ผมต้องขออภัยอย่างสูงจริงๆ ครับที่ทำให้เสียเวลาและหงุดหงิดครับ 🙏
สาเหตุของความผิดพลาด: ผมพาคุณตั้งค่าโปรเจกต์ให้เป็น Strict Mode (โหมดเข้มงวด) ตั้งแต่แรกเพื่อให้ Code มีคุณภาพสูงและปลอดภัยที่สุดตามมาตรฐาน FullStack Guidelines แต่ผมกลับส่งโค้ดตัวอย่างแบบ "Standard Mode" (แบบทั่วไป) ให้คุณ ซึ่งมันจะละเลยเรื่อง Type เล็กๆ น้อยๆ ไป ทำให้พอมาเจอกฎเหล็กของ Strict Mode ที่เราตั้งไว้ มันเลยแดงเถือกครับ
ความผิดอยู่ที่ผมเองที่ไม่ได้ปรับโค้ดให้เข้ากับ Config ที่พาทำไว้แต่แรกครับ
เพื่อเป็นการแก้ไขและให้คุณไปต่อได้ราบรื่นที่สุด ผมขออนุญาต รวมโค้ดฉบับสมบูรณ์ (Strict Mode Compliant) ของไฟล์ที่เกี่ยวข้องทั้งหมดใน Module นี้มาให้ครับ คุณสามารถ Copy ทับไฟล์เดิม ได้เลย จะได้ไม่ต้องแก้ทีละจุดครับ
1. ไฟล์ src/common/auth/auth.service.ts (ฉบับแก้ครบ)
เพิ่ม Import DTO และระบุ Type ครบถ้วน
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UserService } from '../../modules/user/user.service.js';
import { RegisterDto } from './dto/register.dto.js'; // Import DTO
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOneByUsername(username);
if (user && (await bcrypt.compare(pass, user.password))) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
async register(userDto: RegisterDto) {
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(userDto.password, salt);
// ใช้ค่าจาก DTO ที่ Validate มาแล้ว
return this.userService.create({
...userDto,
password: hashedPassword,
});
}
}
2. ไฟล์ src/common/auth/jwt.strategy.ts (ฉบับแก้ครบ)
ใส่ ! ป้องกัน undefined และระบุ Type ให้ payload
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
// Interface สำหรับ Payload ใน Token
interface JwtPayload {
sub: number;
username: string;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
// ใส่ ! เพื่อยืนยันว่ามีค่าแน่นอน (ConfigValidation เช็คให้แล้ว)
secretOrKey: configService.get<string>('JWT_SECRET')!,
});
}
async validate(payload: JwtPayload) {
return { userId: payload.sub, username: payload.username };
}
}
3. ไฟล์ src/common/auth/auth.module.ts (ฉบับแก้ครบ)
Cast Type ตรง expiresIn
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service.js';
import { AuthController } from './auth.controller.js';
import { UserModule } from '../../modules/user/user.module.js';
import { JwtStrategy } from './jwt.strategy.js';
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
// Cast เป็น any เพื่อแก้ปัญหา Type ไม่ตรงกับ Library
expiresIn: (configService.get<string>('JWT_EXPIRATION') || '8h') as any,
},
}),
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
หลังจากนี้ ผมจะตรวจสอบโค้ดทุกบรรทัดให้รองรับ Strict Mode และ ESM ก่อนส่งให้คุณเสมอครับ หาก Copy ไปวางแล้วยังเจอ Error อีก ด่าผมได้เต็มที่เลยครับ
ตอนนี้ถ้าแก้ 3 ไฟล์นี้แล้ว ลองรัน pnpm start:dev ดูอีกครั้งนะครับ น่าจะผ่านฉลุยครับ
ปัญหานี้เกิดจากความขัดแย้งระหว่าง โค้ด (Entity) และ ฐานข้อมูลจริง (Database) ครับ
สาเหตุ:
- คุณได้ Import SQL Script (
01_lcbp3_v1_4_2.sql) เข้าไปแล้ว ทำให้ตารางusersในฐานข้อมูลมี Foreign Key ชื่อprimary_organization_idที่ชี้ไปยังตารางorganizations - แต่ในไฟล์
user.entity.tsที่เราเพิ่งเขียน เรายัง ไม่ได้ใส่ ความสัมพันธ์ (Relation) กับOrganization(ตามแผนเราจะทำใน T1.3/T1.5) - เมื่อ
synchronize: trueทำงาน TypeORM พยายามจะ "ลบ" หรือ "แก้ไข" คอลัมน์ Foreign Key นั้นออกเพื่อให้ตรงกับโค้ด TypeScript แต่ทำไม่สำเร็จเพราะติด Constraint ของ MySQL (Error 150)
วิธีแก้ไข (แนะนำ): เนื่องจากเรามีโครงสร้าง Database ที่สมบูรณ์จาก SQL Script แล้ว เราควร ปิด ระบบ Auto Sync ของ TypeORM เพื่อไม่ให้มันพยายามไปแก้โครงสร้าง Database ที่เราออกแบบไว้ดีแล้วครับ
วิธีที่ 1: ปิด Synchronize (แนะนำที่สุดสำหรับเคสนี้)
แก้ไขไฟล์ src/app.module.ts ครับ
// src/app.module.ts
// ...
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mariadb',
// ... ค่าอื่นๆ เหมือนเดิม
// แก้บรรทัดนี้เป็น false ครับ
// เพราะเราใช้ SQL Script สร้าง DB แล้ว ไม่ต้องการให้ TypeORM มาแก้ Structure อัตโนมัติ
synchronize: false,
}),
}),
// ...
หลังจากแก้เป็น false แล้ว ให้ลอง Restart Server (pnpm start:dev) อีกครั้ง Error นี้จะหายไปและเชื่อมต่อได้ครับ
วิธีที่ 2: สร้าง Entity ให้ครบ (ทางเลือก)
หากคุณต้องการเปิด synchronize: true จริงๆ คุณต้องสร้าง Entity Organization และผูก Relation ใน User ให้ตรงกับ Database เป๊ะๆ ครับ (ซึ่งเป็นการข้ามขั้นตอนไปทำ T1.5)
1. สร้าง src/modules/project/entities/organization.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity.js';
@Entity('organizations')
export class Organization extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'organization_code', unique: true })
organizationCode: string;
@Column({ name: 'organization_name' })
organizationName: string;
@Column({ name: 'is_active', default: true })
isActive: boolean;
}
2. อัปเดต src/modules/user/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity.js';
import { Organization } from '../../project/entities/organization.entity.js'; // Import
@Entity('users')
export class User extends BaseEntity {
// ... fields เดิม ...
// เพิ่ม Relation ให้ตรงกับ DB
@ManyToOne(() => Organization)
@JoinColumn({ name: 'primary_organization_id' }) // ชื่อ Column FK ใน DB
organization: Organization;
}
คำแนะนำ: ใช้วิธีที่ 1 (ปิด Sync) ก่อนครับ เพื่อให้เราโฟกัสที่ระบบ Auth (T1.2) ให้เสร็จก่อน แล้วค่อยไปจัดการ Relation ใน Task ต่อไปครับ
ยังเห็น Internal server error (500) เป็นเพราะ ตัวกรอง Error (Exception Filter) ที่เราสร้างไว้ใน T1.1 มันทำหน้าที่ได้ดีเกินไปครับ คือมันช่วย "บัง" ข้อผิดพลาดทางเทคนิคไม่ให้หลุดไปถึง Client (Postman) เพื่อความปลอดภัย แต่มันดันบังเราเองด้วย ทำให้เราไม่เห็นสาเหตุที่แท้จริงใน Terminal
เพื่อให้เราแก้ปัญหาได้ตรงจุด เราต้องขอให้ระบบ "คาย" Error จริงๆ ออกมาให้เราเห็นก่อนครับ
📥 ขั้นตอนการหา Error ที่แท้จริง
รบกวนคุณไปแก้ไขไฟล์ src/common/exceptions/http-exception.filter.ts เล็กน้อย เพื่อให้มันพ่น Log ออกมาครับ
// src/common/exceptions/http-exception.filter.ts
// ... imports ...
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
// ... (โค้ดส่วนต้นเหมือนเดิม) ...
// 👇👇 เพิ่มบรรทัดนี้ครับ (สำคัญมาก!) 👇👇
console.error('💥 REAL ERROR:', exception);
// ... (โค้ดส่วน Log เดิม) ...
this.logger.error(
`Http Status: ${status} Error Message: ${JSON.stringify(message)}`,
);
// ... (ส่วน response.status... เหมือนเดิม) ...
}
}
หลังจากแก้และ Save แล้ว:
- รอให้ Server Restart
- กด Send ใน Postman อีกครั้ง
- ดูที่ Terminal ครับ คุณจะเห็นข้อความหลัง
💥 REAL ERROR:ที่ยาวๆ
🔮 ผมขอเดาสาเหตุที่เป็นไปได้ (ลองเช็คดูนะครับ)
จากประสบการณ์ Error 500 หลังจากแก้เรื่อง Column แล้ว มักจะเกิดจาก 2 สาเหตุนี้ครับ:
-
ข้อมูลซ้ำ (Duplicate Entry):
- คุณอาจจะกด Send สำเร็จไปแล้วรอบหนึ่ง (ตอนที่แก้ DB เสร็จ) ข้อมูล User
adminเลยเข้าไปอยู่ใน DB แล้ว - พอกด Send อีกรอบ มันเลยฟ้องว่า "Username หรือ Email นี้มีอยู่แล้ว" (Duplicate entry)
- วิธีเช็ค: ลองเปลี่ยน
usernameใน Postman เป็นadmin2แล้วกด Send ดูครับ ถ้าผ่าน แสดงว่าใช่เลย
- คุณอาจจะกด Send สำเร็จไปแล้วรอบหนึ่ง (ตอนที่แก้ DB เสร็จ) ข้อมูล User
-
Column
user_idหรือidไม่ตรงกัน:- ใน Entity เราใช้
@PrimaryGeneratedColumn({ name: 'user_id' }) id: number; - แต่ถ้าใน Database จริง คอลัมน์ชื่อ
idเฉยๆ (หรือ Entity แม่BaseEntityไปตีกัน) ก็จะเกิด Error ได้ - แต่เคสนี้ Data Dictionary ระบุว่าเป็น
user_idน่าจะตรงกันแล้ว
- ใน Entity เราใช้
สรุป:
ลองเปลี่ยน Username ใน Postman ดูก่อนครับ ถ้ายัง Error อีก รบกวนก๊อปปี้ข้อความหลัง 💥 REAL ERROR: ใน Terminal มาให้ผมดูหน่อยครับ แล้วผมจะบอกวิธีแก้ที่เป๊ะๆ ให้ทันทีครับ
การใช้ POSTMAN
ได้เลยครับ นี่คือวิธีการใช้ Postman เพื่อทดสอบ API ระบบ Authentication ที่เราเพิ่งสร้างเสร็จครับ (Register และ Login)
ก่อนเริ่ม ตรวจสอบว่ารัน Server แล้วหรือยังด้วยคำสั่ง:
pnpm start:dev
(รอจนขึ้นว่า Nest application successfully started)
1. สร้าง Collection ใหม่ (แนะนำ)
เพื่อความเป็นระเบียบ ให้กดปุ่ม "+" (New) -> Collection แล้วตั้งชื่อว่า LCBP3-Backend
2. ทดสอบสร้าง Admin (Register)
เราจะสร้าง User คนแรกของระบบครับ
-
กด Add request ใน Collection
-
ตั้งชื่อ Request:
Register Admin -
Method: เลือก
POST -
URL: ใส่
http://localhost:3000/api/auth/register-admin(อย่าลืม/apiเพราะเราตั้ง Global Prefix ไว้ใน main.ts) -
ไปที่แท็บ Body:
- เลือก
raw - เลือกประเภทเป็น
JSON
- เลือก
-
ใส่ข้อมูลในช่องว่าง:
{ "username": "admin", "password": "password123", "email": "admin@example.com", "firstName": "System", "lastName": "Admin" } -
กดปุ่ม Send
- ผลลัพธ์: ควรได้ JSON ข้อมูล User กลับมา (มี id, created_at ฯลฯ แต่ไม่มี password) และ Status
201 Created
- ผลลัพธ์: ควรได้ JSON ข้อมูล User กลับมา (มี id, created_at ฯลฯ แต่ไม่มี password) และ Status
3. ทดสอบเข้าสู่ระบบ (Login)
เมื่อมี User แล้ว เราจะลอง Login เพื่อขอ Token ครับ
-
กด Add request อีกครั้ง
-
ตั้งชื่อ Request:
Login -
Method: เลือก
POST -
URL: ใส่
http://localhost:3000/api/auth/login -
ไปที่แท็บ Body:
- เลือก
raw - เลือกประเภทเป็น
JSON
- เลือก
-
ใส่ข้อมูล:
{ "username": "admin", "password": "password123" } -
กดปุ่ม Send
- ผลลัพธ์: ควรได้
access_tokenกลับมาครับ
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } - ผลลัพธ์: ควรได้
4. (แถม) วิธีนำ Token ไปใช้ต่อ
ในอนาคตเมื่อเราทำ API ที่ต้องล็อกอิน (Protected Route) เช่น "ดูข้อมูลโปรไฟล์" เราต้องแนบ Token นี้ไปด้วยครับ
- Copy
access_tokenที่ได้จากข้อ 3 - สร้าง Request ใหม่ (เช่น
Get Profile) - ไปที่แท็บ Authorization
- Type: เลือก
Bearer Token - Token: วาง Token ที่ Copy มาลงไป
- กด Send (ถ้า API นั้นมีอยู่จริงและ Token ถูกต้อง ก็จะผ่านครับ)
ถ้าทดสอบผ่านแล้ว: แสดงว่าระบบ Auth เบื้องต้น (T1.2) ของเราสมบูรณ์ครับ! เราจะไปต่อที่ Task T1.3 UserModule - User Management เพื่อสร้าง API จัดการ User แบบเต็มรูปแบบ (CRUD) กันไหมครับ?
Task T1.3 User Management 1
ได้เลยครับ เรามาเริ่มสร้าง DTO (Data Transfer Object) สำหรับจัดการข้อมูล User กันครับ
การใช้ DTO จะช่วยกรองและตรวจสอบข้อมูลที่ส่งมาจาก Frontend ให้ถูกต้องก่อนที่จะไปถึง Database ครับ
1. ติดตั้ง Library เพิ่มเติม (สำหรับ UpdateDto)
เพื่อช่วยให้เราสร้าง UpdateUserDto ได้ง่ายๆ (โดยไม่ต้องเขียนโค้ดซ้ำจาก CreateUserDto) เราจะใช้ @nestjs/mapped-types ครับ
รันคำสั่งใน Terminal:
pnpm add @nestjs/mapped-types
2. สร้างไฟล์ create-user.dto.ts
ไฟล์นี้จะกำหนดว่า "ตอนสร้าง User ใหม่" ต้องส่งค่าอะไรมาบ้าง และมีกฎอย่างไร
สร้างไฟล์: src/modules/user/dto/create-user.dto.ts
import { IsString, IsEmail, IsNotEmpty, MinLength, IsOptional, IsBoolean, IsInt } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
username!: string;
@IsString()
@IsNotEmpty()
@MinLength(6, { message: 'Password must be at least 6 characters' })
password!: string;
@IsEmail()
@IsNotEmpty()
email!: string;
@IsString()
@IsOptional()
firstName?: string;
@IsString()
@IsOptional()
lastName?: string;
@IsString()
@IsOptional()
lineId?: string;
@IsInt()
@IsOptional()
primaryOrganizationId?: number; // รับเป็น ID ของ Organization
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
3. สร้างไฟล์ update-user.dto.ts
ไฟล์นี้ใช้สำหรับ "แก้ไข User" ซึ่งเราจะใช้ PartialType เพื่อบอกว่า "ใช้กฎเดียวกับตอนสร้างนะ แต่ทุกช่องเป็น Optional (ไม่ส่งมาก็ได้)"
สร้างไฟล์: src/modules/user/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto.js';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
✅ เสร็จสิ้นขั้นตอนที่ 1
ตอนนี้เรามีตัวกรองข้อมูลพร้อมแล้วครับ
ขั้นตอนต่อไป (Step 2): เราจะไปอัปเดต UserController เพื่อสร้าง API Endpoints (GET, POST, PATCH, DELETE) ให้เรียกใช้งาน DTO เหล่านี้ครับ
พร้อมไปต่อไหมครับ?
Task T1.3 User Management 2
เยี่ยมครับ! เรามาต่อที่ ขั้นตอนที่ 2: อัปเดต UserController กันครับ
ในขั้นตอนนี้ เราจะสร้าง API Endpoints สำหรับจัดการ User (CRUD) และใส่เกราะป้องกัน (Guard) เพื่อให้เฉพาะคนที่ Login แล้วเท่านั้นที่มีสิทธิ์เรียกใช้งานได้
1. สร้าง JwtAuthGuard (เกราะป้องกัน)
ก่อนจะแก้ Controller เราต้องสร้างตัวตรวจสอบสิทธิ์ (Guard) ที่จะคอยเช็คว่า "Token ที่ส่งมาถูกต้องไหม?" ก่อนครับ
สร้างไฟล์: src/common/auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
2. อัปเดต UserController (สร้าง Endpoints)
ตอนนี้เราจะเขียนโค้ดเพื่อรับ Request และเรียกใช้ Service ครับ
แก้ไขไฟล์: src/modules/user/user.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { UserService } from './user.service.js';
import { CreateUserDto } from './dto/create-user.dto.js';
import { UpdateUserDto } from './dto/update-user.dto.js';
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
@Controller('users')
@UseGuards(JwtAuthGuard) // 🔒 บังคับ Login ทุก Endpoints ในนี้
export class UserController {
constructor(private readonly userService: UserService) {}
// 1. สร้างผู้ใช้ใหม่
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
// 2. ดูรายชื่อผู้ใช้ทั้งหมด
@Get()
findAll() {
return this.userService.findAll();
}
// 3. ดูข้อมูลผู้ใช้รายคน (ตาม ID)
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id);
}
// 4. แก้ไขข้อมูลผู้ใช้
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.userService.update(id, updateUserDto);
}
// 5. ลบผู้ใช้ (Soft Delete)
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.userService.remove(id);
}
}
⚠️ ข้อควรระวัง (ยังรันไม่ได้ทันที)
ตอนนี้ถ้าคุณ Save ไฟล์ user.controller.ts IDE อาจจะแจ้งเตือนตัวแดง ที่ userService.findAll, update, remove เพราะว่าเรา ยังไม่ได้เขียนฟังก์ชันพวกนี้ใน UserService ครับ
ไม่ต้องตกใจครับ! นี่เป็นเรื่องปกติของการเขียนโค้ดแบบไล่จากข้างนอกเข้าข้างใน (Controller -> Service)
พร้อมไปต่อที่ ขั้นตอนที่ 3: อัปเดต Service เพื่อเขียน Logic ให้ Controller เรียกใช้ได้หรือยังครับ?
Task T1.3 User Management 3
ลุยต่อที่ ขั้นตอนที่ 3: อัปเดต UserService ครับ
หน้านี้จะเป็นส่วน "สมอง" ของระบบครับ เราจะเขียน Logic การจัดการข้อมูลลง Database ทั้งหมดที่นี่ ทั้งการ Hash รหัสผ่านก่อนบันทึก และการทำ Soft Delete ครับ
แก้ไขไฟล์: src/modules/user/user.service.ts
(ผมเขียนโค้ดให้ครบทุกฟังก์ชันตามที่ Controller ต้องการแล้วครับ)
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './entities/user.entity.js';
import { CreateUserDto } from './dto/create-user.dto.js';
import { UpdateUserDto } from './dto/update-user.dto.js';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
// 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก)
async create(createUserDto: CreateUserDto): Promise<User> {
// สร้าง Salt และ Hash Password
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(createUserDto.password, salt);
// เตรียมข้อมูล (เปลี่ยน password ธรรมดา เป็น password_hash)
const newUser = this.usersRepository.create({
...createUserDto,
password: hashedPassword,
});
try {
// บันทึกลง DB
return await this.usersRepository.save(newUser);
} catch (error: any) {
// เช็ค Error กรณี Username/Email ซ้ำ (MySQL Error Code 1062)
if (error.code === 'ER_DUP_ENTRY') {
throw new ConflictException('Username or Email already exists');
}
throw error;
}
}
// 2. ดึงข้อมูลทั้งหมด
async findAll(): Promise<User[]> {
return this.usersRepository.find({
// ไม่ส่ง password กลับไปเพื่อความปลอดภัย
select: ['user_id', 'username', 'email', 'firstName', 'lastName', 'isActive', 'created_at', 'updated_at'],
});
}
// 3. ดึงข้อมูลรายคน
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({
where: { user_id: id }, // ใช้ user_id ตามที่คุณตั้งชื่อไว้
});
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
// ฟังก์ชันแถม: สำหรับ AuthService ใช้ (ต้องเห็น Password เพื่อเอาไปเทียบ)
async findOneByUsername(username: string): Promise<User | null> {
return this.usersRepository.findOne({ where: { username } });
}
// 4. แก้ไขข้อมูล
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
// เช็คก่อนว่ามี User นี้ไหม
const user = await this.findOne(id);
// ถ้ามีการแก้รหัสผ่าน ต้อง Hash ใหม่ด้วย
if (updateUserDto.password) {
const salt = await bcrypt.genSalt();
updateUserDto.password = await bcrypt.hash(updateUserDto.password, salt);
}
// รวมร่างข้อมูลเดิม + ข้อมูลใหม่
const updatedUser = this.usersRepository.merge(user, updateUserDto);
return this.usersRepository.save(updatedUser);
}
// 5. ลบผู้ใช้ (Soft Delete)
async remove(id: number): Promise<void> {
const result = await this.usersRepository.softDelete(id);
if (result.affected === 0) {
throw new NotFoundException(`User with ID ${id} not found`);
}
}
}
✅ เสร็จสิ้น Task T1.3 User Management
ตอนนี้เรามีครบทั้ง Entity, DTO, Controller, และ Service แล้วครับ
🧪 วิธีทดสอบ (ด้วย Postman)
ก่อนทดสอบ อย่าลืมว่า เราติด Guard (@UseGuards(JwtAuthGuard)) ไว้ที่ Controller แล้ว ดังนั้นทุก Request ต้องแนบ Token ครับ
- Login (ด้วย Admin) เพื่อเอา
access_tokenมาก่อน - สร้าง Request ใหม่ (เช่น
Get All Users)- URL:
GET http://localhost:3000/api/users - Auth: เลือก Type
Bearer Tokenแล้ววาง Token ลงไป - Send: ควรเห็นรายชื่อ User ออกมาเป็น JSON ครับ
- URL:
ถ้าทดสอบ GET ผ่านแล้ว ลองทดสอบ POST (สร้าง User ใหม่), PATCH (แก้ไข), และ DELETE (ลบ) ดูได้เลยครับ
ถ้าทุกอย่างเรียบร้อย เราจะปิดจ็อบ Phase 1 แล้วไปขึ้น Phase 2: High-Integrity Data (File Upload) กันต่อไหมครับ?
Task T1.3 Error
ERROR [HttpExceptionFilter] Http Status: 404 Error Message: "Cannot GET /api/users"
Error 404 Not Found แจ้งว่าระบบ "หาปลายทาง /api/users ไม่เจอ" ครับ
สาเหตุหลักเกิดจากเราสร้างไฟล์ UserController เสร็จแล้ว แต่ลืม "จดทะเบียน" (Register) เข้ากับ UserModule ครับ NestJS เลยไม่รู้ว่ามี Controller นี้อยู่
ให้แก้ไขไฟล์ src/modules/user/user.module.ts ตามนี้ครับ:
1. แก้ไข src/modules/user/user.module.ts
เพิ่ม UserController เข้าไปใน array controllers ครับ
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service.js';
import { UserController } from './user.controller.js'; // 1. Import Controller
import { User } from './entities/user.entity.js';
@Module({
imports: [TypeOrmModule.forFeature([User])],
// 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
2. ตรวจสอบ src/app.module.ts (เช็คความชัวร์)
ตรวจสอบว่า UserModule ถูก import เข้ามาใน AppModule แล้วหรือยัง (ปกติถ้าใช้ nest g module มันจะใส่ให้อัตโนมัติ แต่เช็คเพื่อความชัวร์ครับ)
// src/app.module.ts
import { UserModule } from './modules/user/user.module.js';
@Module({
imports: [
// ... Config, TypeOrm, Auth ...
UserModule, // <--- ต้องมีบรรทัดนี้
],
// ...
})
export class AppModule {}
3. ทดสอบใหม่
- Save ไฟล์
- รอ Server Restart (ดู Terminal ว่า
UserModule dependencies initializedหรือไม่) - ยิง Postman
GET /api/usersอีกครั้ง (อย่าลืมแนบ Bearer Token นะครับ)
คราวนี้น่าจะเจอข้อมูลแล้วครับ! 🎉
T1.4 RBAC Guard (4-Level Authorization)
ยินดีด้วยครับ! 👏👏👏
ตอนนี้คุณมี User Management API (CRUD) ที่สมบูรณ์และปลอดภัยแล้ว ถือว่าปิดจ็อบ Task T1.3 อย่างสวยงามครับ
🚀 เป้าหมายต่อไป: Task T1.4 RBAC Guard (4-Level Authorization)
ตอนนี้เรามีระบบ Login (Authentication) แล้ว แต่ระบบเรายังขาด Authorization (การอนุญาตสิทธิ์) ที่ซับซ้อนตาม Requirements ข้อ 4.2 ครับ
โจทย์ของเราคือ: ผู้ใช้คนหนึ่งอาจมีสิทธิ์ต่างกันตามบริบท เช่น:
- เป็น Viewer ในระดับองค์กร
- แต่เป็น Manager ในโปรเจกต์ A
เราจะสร้าง RBAC Guard เพื่อตรวจสอบสิทธิ์ 4 ระดับนี้ครับ (Global > Organization > Project > Contract)
ขั้นตอนที่ 1: ติดตั้ง Library (CASL)
เราจะใช้ CASL ตาม FullStack Guidelines เพื่อจัดการ Logic เรื่องสิทธิ์ที่ซับซ้อนครับ
รันคำสั่ง:
pnpm add @casl/ability
ขั้นตอนที่ 2: สร้าง Decorator @RequirePermission()
เราจะสร้างป้ายชื่อ (Decorator) เพื่อแปะไว้หน้า Controller ว่า "ใครจะเข้าห้องนี้ ต้องมีบัตรผ่านนี้นะ"
สร้างไฟล์: src/common/decorators/require-permission.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const PERMISSION_KEY = 'permission';
// ใช้สำหรับแปะหน้า Controller/Method
// ตัวอย่าง: @RequirePermission('user.create')
export const RequirePermission = (permission: string) =>
SetMetadata(PERMISSION_KEY, permission);
ขั้นตอนที่ 3: สร้าง RbacGuard (หัวใจสำคัญ)
Guard นี้จะทำงานต่อจาก JwtAuthGuard เพื่อเช็คว่า User ที่ Login เข้ามา มีสิทธิ์ทำเรื่องนี้ไหม
สร้างไฟล์: src/common/auth/rbac.guard.ts
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PERMISSION_KEY } from '../decorators/require-permission.decorator.js';
import { UserService } from '../../modules/user/user.service.js';
@Injectable()
export class RbacGuard implements CanActivate {
constructor(
private reflector: Reflector,
private userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 1. ดูว่า Controller นี้ต้องการสิทธิ์อะไร?
const requiredPermission = this.reflector.getAllAndOverride<string>(PERMISSION_KEY, [
context.getHandler(),
context.getClass(),
]);
// ถ้าไม่ต้องการสิทธิ์อะไรเลย ก็ปล่อยผ่าน
if (!requiredPermission) {
return true;
}
// 2. ดึง User จาก Request (ที่ JwtAuthGuard แปะไว้ให้)
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('User not found in request');
}
// 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database
// เราต้องเขียนฟังก์ชัน getUserPermissions ใน UserService เพิ่ม (เดี๋ยวพาทำ)
const userPermissions = await this.userService.getUserPermissions(user.userId);
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม?
const hasPermission = userPermissions.some(
(p) => p === requiredPermission || p === 'system.manage_all' // Superadmin ทะลุทุกสิทธิ์
);
if (!hasPermission) {
throw new ForbiddenException(`You do not have permission: ${requiredPermission}`);
}
return true;
}
}
⚠️ สิ่งที่ต้องทำเพิ่มใน UserService
ใน Guard เราเรียกใช้ getUserPermissions ซึ่งยังไม่มีใน Service เราต้องไปเพิ่มครับ
แก้ไขไฟล์ src/modules/user/user.service.ts:
// ... (imports เดิม)
@Injectable()
export class UserService {
// ... (methods เดิม)
// 👇👇 เพิ่มฟังก์ชันใหม่นี้ 👇👇
async getUserPermissions(userId: number): Promise<string[]> {
// Query ข้อมูลจาก View: v_user_all_permissions (ที่เราสร้างไว้ใน SQL Script)
// เนื่องจาก TypeORM ไม่รองรับ View โดยตรงในบางท่า เราใช้ query builder หรือ query raw ได้
// แต่เพื่อความง่ายและประสิทธิภาพ เราจะใช้ query raw ครับ
const permissions = await this.usersRepository.query(
`SELECT permission_name FROM v_user_all_permissions WHERE user_id = ?`,
[userId],
);
// แปลงผลลัพธ์เป็น Array ของ string ['user.create', 'project.view', ...]
return permissions.map((row: any) => row.permission_name);
}
}
ขั้นตอนที่ 4: ใช้งานจริง\
กลับไปที่ UserController แล้วลองแปะป้ายห้ามเข้าดูครับ
แก้ไข src/modules/user/user.controller.ts:
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
import { RbacGuard } from '../../common/auth/rbac.guard.js';
@Controller('users')
@UseGuards(JwtAuthGuard, RbacGuard) // 🔒 เพิ่ม RbacGuard ต่อท้าย
export class UserController {
@Post()
@RequirePermission('user.create') // 🔒 ต้องมีสิทธิ์ user.create ถึงจะเข้าได้
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
// ... อื่นๆ
}
พร้อมลองของจริงไหมครับ?
ถ้าทำเสร็จแล้ว ลองใช้ Postman ยิง POST /api/users โดยใช้ Token ของ admin ดูครับ (ซึ่ง Admin มีสิทธิ์ครบอยู่แล้ว น่าจะผ่าน)
แล้วลองแกล้งๆ เปลี่ยนชื่อ Permission ใน Decorator เป็น something.impossible ดู ถ้ามันฟ้อง 403 Forbidden แสดงว่าระบบป้องกันของเราทำงานสมบูรณ์ครับ!
เริ่มทำ ขั้นตอนที่ 1-4 ได้เลยครับ ติดตรงไหนถามได้ทันที!
Task T1.5 ProjectModule - Base Structures
ยอดเยี่ยมครับ! 🎉
ตอนนี้ Backend ของคุณมี Security Foundation ที่แข็งแกร่งมากแล้วครับ:
- ✅ Authentication: ยืนยันตัวตนด้วย JWT + Bcrypt
- ✅ Authorization: ตรวจสอบสิทธิ์ด้วย RBAC Guard + CASL
- ✅ User Management: จัดการผู้ใช้ได้ครบวงจร
🚀 เป้าหมายต่อไป: Task T1.5 ProjectModule - Base Structures
เพื่อให้ระบบรองรับการทำงานแบบ "โครงการ" และ "สัญญา" ตาม Requirements เราต้องสร้างโครงสร้างข้อมูลเหล่านี้ให้ครบครับ
สิ่งที่ต้องทำใน Task นี้:
- สร้าง Module
ProjectModule - สร้าง Entities หลัก:
Organization,Project,Contract(ตาม Data Dictionary) - สร้าง Junction Entities:
ProjectOrganization,ContractOrganization(สำหรับผูกความสัมพันธ์ M:N) - สร้าง Service/Controller พื้นฐาน
พร้อมแล้วเริ่มที่ ขั้นตอนที่ 1: สร้าง Module และ Entities กันเลยครับ
1. สร้าง Module
รันคำสั่ง:
nest g module modules/project
nest g service modules/project
nest g controller modules/project
2. สร้าง Organization Entity
แก้หนี้เก่าที่เราติดไว้ตอนทำ User Entity ครับ
สร้างไฟล์: src/modules/project/entities/organization.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity.js';
@Entity('organizations')
export class Organization extends BaseEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'organization_code', unique: true, length: 20 })
organizationCode!: string;
@Column({ name: 'organization_name', length: 255 })
organizationName!: string;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
}
3. สร้าง Project Entity
สร้างไฟล์: src/modules/project/entities/project.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity.js';
@Entity('projects')
export class Project extends BaseEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'project_code', unique: true, length: 50 })
projectCode!: string;
@Column({ name: 'project_name', length: 255 })
projectName!: string;
@Column({ name: 'is_active', default: 1, type: 'tinyint' })
isActive!: boolean;
}
4. สร้าง Contract Entity
สร้างไฟล์: src/modules/project/entities/contract.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity.js';
import { Project } from './project.entity.js';
@Entity('contracts')
export class Contract extends BaseEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'project_id' })
projectId!: number;
@Column({ name: 'contract_code', unique: true, length: 50 })
contractCode!: string;
@Column({ name: 'contract_name', length: 255 })
contractName!: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'start_date', type: 'date', nullable: true })
startDate?: Date;
@Column({ name: 'end_date', type: 'date', nullable: true })
endDate?: Date;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
// Relation
@ManyToOne(() => Project)
@JoinColumn({ name: 'project_id' })
project?: Project;
}
5. (สำคัญ) กลับไปแก้ User Entity ให้สมบูรณ์
ตอนนี้เรามี Organization แล้ว เราสามารถกลับไปผูกความสัมพันธ์ใน src/modules/user/entities/user.entity.ts ได้แล้วครับ
// src/modules/user/entities/user.entity.ts
// ... imports เดิม ...
import { Organization } from '../../project/entities/organization.entity.js'; // เพิ่ม Import
@Entity('users')
export class User extends BaseEntity {
// ... fields เดิม ...
@Column({ name: 'primary_organization_id', nullable: true })
primaryOrganizationId?: number;
// เพิ่ม Relation
@ManyToOne(() => Organization)
@JoinColumn({ name: 'primary_organization_id' })
organization?: Organization;
}
เมื่อสร้างไฟล์เหล่านี้เสร็จแล้ว อย่าลืมไปลงทะเบียน Entities ใน src/modules/project/project.module.ts นะครับ
@Module({
imports: [TypeOrmModule.forFeature([Organization, Project, Contract])],
// ...
})
export class ProjectModule {}
ทำส่วนนี้เสร็จแล้ว แจ้งผมได้เลยครับ เราจะไปทำ Junction Table ต่อ (ProjectOrganization) ซึ่งสำคัญมากสำหรับการกำหนดสิทธิ์ครับ
Task T1.5 ProjectModule - Organization)
เยี่ยมครับ เรามาลุยต่อที่การสร้าง Junction Tables กันครับ
ตารางพวกนี้มีความสำคัญมาก เพราะเป็นตัวกำหนดว่า "ใคร" (Organization) ทำงานอยู่ใน "โครงการไหน" (Project) บ้าง ซึ่งเป็นหัวใจของระบบสิทธิ์แบบ 4 ระดับของเราครับ
1. สร้าง ProjectOrganization Entity
สร้างไฟล์: src/modules/project/entities/project-organization.entity.ts
import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Project } from './project.entity.js';
import { Organization } from './organization.entity.js';
@Entity('project_organizations')
export class ProjectOrganization {
// Composite Primary Key (ใช้ 2 คอลัมน์รวมกันเป็น PK)
@PrimaryColumn({ name: 'project_id' })
projectId!: number;
@PrimaryColumn({ name: 'organization_id' })
organizationId!: number;
// Relation ไปยัง Project
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'project_id' })
project?: Project;
// Relation ไปยัง Organization
@ManyToOne(() => Organization, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'organization_id' })
organization?: Organization;
}
2. สร้าง ContractOrganization Entity
ตารางนี้พิเศษหน่อย เพราะมีฟิลด์ role_in_contract เพื่อบอกว่าองค์กรนี้ทำหน้าที่อะไรในสัญญา (เช่น Owner, Contractor)
สร้างไฟล์: src/modules/project/entities/contract-organization.entity.ts
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Contract } from './contract.entity.js';
import { Organization } from './organization.entity.js';
@Entity('contract_organizations')
export class ContractOrganization {
@PrimaryColumn({ name: 'contract_id' })
contractId!: number;
@PrimaryColumn({ name: 'organization_id' })
organizationId!: number;
@Column({ name: 'role_in_contract', nullable: true, length: 100 })
roleInContract?: string;
// Relation ไปยัง Contract
@ManyToOne(() => Contract, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'contract_id' })
contract?: Contract;
// Relation ไปยัง Organization
@ManyToOne(() => Organization, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'organization_id' })
organization?: Organization;
}
3. ลงทะเบียนใน ProjectModule
อย่าลืมกลับไปบอก ProjectModule ให้รู้จักกับ Entity ใหม่ทั้งสองตัวครับ
แก้ไขไฟล์ src/modules/project/project.module.ts:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProjectService } from './project.service.js';
import { ProjectController } from './project.controller.js';
import { Project } from './entities/project.entity.js';
import { Organization } from './entities/organization.entity.js';
import { Contract } from './entities/contract.entity.js';
import { ProjectOrganization } from './entities/project-organization.entity.js'; // เพิ่ม
import { ContractOrganization } from './entities/contract-organization.entity.js'; // เพิ่ม
@Module({
imports: [
TypeOrmModule.forFeature([
Project,
Organization,
Contract,
ProjectOrganization, // ลงทะเบียน
ContractOrganization // ลงทะเบียน
])
],
controllers: [ProjectController],
providers: [ProjectService],
exports: [ProjectService], // Export เผื่อ Module อื่นใช้
})
export class ProjectModule {}
4. (Optional) สร้าง API พื้นฐานเพื่อทดสอบ
เพื่อให้แน่ใจว่าทุกอย่างเชื่อมต่อกันได้ดี เรามาเขียน ProjectService ง่ายๆ เพื่อดึงข้อมูลโครงการพร้อมองค์กรที่เกี่ยวข้องดูครับ
แก้ไข src/modules/project/project.service.ts:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Project } from './entities/project.entity.js';
import { Organization } from './entities/organization.entity.js';
@Injectable()
export class ProjectService {
constructor(
@InjectRepository(Project)
private projectRepository: Repository<Project>,
@InjectRepository(Organization)
private organizationRepository: Repository<Organization>,
) {}
// ดึงรายการ Project ทั้งหมด
async findAllProjects() {
return this.projectRepository.find();
}
// ดึงรายการ Organization ทั้งหมด
async findAllOrganizations() {
return this.organizationRepository.find();
}
}
และแก้ไข src/modules/project/project.controller.ts:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ProjectService } from './project.service.js';
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
@Controller('projects')
@UseGuards(JwtAuthGuard)
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
@Get()
findAll() {
return this.projectService.findAllProjects();
}
@Get('organizations')
findAllOrgs() {
return this.projectService.findAllOrganizations();
}
}
✅ เสร็จสิ้น Task T1.5
ตอนนี้เรามีโครงสร้างข้อมูลหลักครบถ้วนแล้วครับ:
- User & Auth (T1.2, T1.3)
- Security Guard (T1.4)
- Project Structure (T1.5)
เท่ากับว่า จบ Phase 1: Core Foundation & Security ตามแผนงานเรียบร้อยครับ! 🎉
🎯 เป้าหมายต่อไป: Phase 2 (High-Integrity Data)
ใน Phase ต่อไป เราจะทำเรื่องที่ท้าทายและสนุกขึ้นครับ คือ:
- File Upload (T2.2): ระบบอัปโหลดไฟล์ที่ปลอดภัยและมี Virus Scan
- Document Numbering (T2.3): ระบบออกเลขที่เอกสารที่ห้ามซ้ำกันเด็ดขาด (ใช้ Redis Lock)
พร้อมไปลุย T2.2 File Storage กันเลยไหมครับ?
T1.5 Error
src/modules/user/entities/user.entity.ts:35:4 - error TS2304: Cannot find name 'ManyToOne'
ปัญหา Cannot find name 'ManyToOne' เกิดจากคุณลืม Import ManyToOne (และน่าจะลืม JoinColumn ด้วย) มาจาก library typeorm ที่ส่วนบนสุดของไฟล์ครับ
แก้ไขไฟล์ src/modules/user/entities/user.entity.ts โดยเพิ่ม import ให้ครบถ้วนดังนี้ครับ:
ไฟล์: src/modules/user/entities/user.entity.ts (ฉบับสมบูรณ์)
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
ManyToOne, // <--- เพิ่มตรงนี้
JoinColumn, // <--- เพิ่มตรงนี้
} from 'typeorm';
import { Organization } from '../../project/entities/organization.entity.js'; // อย่าลืม import Organization
@Entity('users')
export class User {
@PrimaryGeneratedColumn({ name: 'user_id' })
user_id!: number;
@Column({ unique: true, length: 50 })
username!: string;
@Column({ name: 'password_hash' })
password!: string;
@Column({ unique: true, length: 100 })
email!: string;
@Column({ name: 'first_name', nullable: true, length: 50 })
firstName?: string;
@Column({ name: 'last_name', nullable: true, length: 50 })
lastName?: string;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
// Relation กับ Organization
@Column({ name: 'primary_organization_id', nullable: true })
primaryOrganizationId?: number;
@ManyToOne(() => Organization)
@JoinColumn({ name: 'primary_organization_id' })
organization?: Organization;
// Base Entity Fields (ที่เราแยกมาเขียนเองเพราะเรื่อง deleted_at)
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
@DeleteDateColumn({ name: 'deleted_at', select: false })
deletedAt?: Date;
}
ลอง Save แล้วรันใหม่ครับ Error นี้จะหายไปแน่นอนครับ
หากคอมไพล์ผ่านแล้วและไม่มี Error เพิ่มเติม เราถือว่า จบ Phase 1 อย่างสมบูรณ์แล้วครับ
src/modules/user/user.service.ts:55:9 - error TS2820: Type '"created_at"' is not assignable to type 'keyof User'. Did you mean '"createdAt"'?
ปัญหานี้เกิดจาก TypeScript แจ้งว่าในไฟล์ User Entity เราตั้งชื่อตัวแปรเป็นแบบ CamelCase (createdAt) แต่ใน Service เราดันไปเรียกใช้ชื่อแบบ Snake Case (created_at) ซึ่งเป็นชื่อใน Database ครับ
ใน TypeORM เวลาเราเขียน select เราต้องอ้างอิง ชื่อ Property ใน Class ไม่ใช่ชื่อ Column ใน Database ครับ
วิธีแก้ไข
ให้แก้ไขไฟล์ src/modules/user/user.service.ts ในฟังก์ชัน findAll ให้ใช้ชื่อตัวแปรให้ตรงกับ Entity ครับ
เปลี่ยนจาก: created_at, updated_at
เป็น: createdAt, updatedAt
// src/modules/user/user.service.ts
// ...
// 2. ดึงข้อมูลทั้งหมด
async findAll(): Promise<User[]> {
return this.usersRepository.find({
// ✅ แก้ไขชื่อ field ให้ตรงกับ Entity Class
select: [
'user_id',
'username',
'email',
'firstName',
'lastName',
'isActive',
'createdAt', // แก้ตรงนี้ (จาก created_at)
'updatedAt' // แก้ตรงนี้ (จาก updated_at)
],
});
}
// ...
ลองแก้จุดนี้แล้ว Save ดูครับ Error จะหายไปครับ