260322:1648 Correct Coresspondence / Doing RFA / Correct CI
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s

This commit is contained in:
admin
2026-03-22 16:48:12 +07:00
parent e5deedb42e
commit 11984bfa29
683 changed files with 105251 additions and 29068 deletions
@@ -16,9 +16,9 @@
nest g module common
```
*(ถ้ามันฟ้องว่ามีไฟล์อยู่แล้ว ให้ข้ามขั้นตอนนี้ได้เลยครับ แต่ต้องแน่ใจว่า `app.module.ts` มี `CommonModule` ใน imports แล้ว)*
_(ถ้ามันฟ้องว่ามีไฟล์อยู่แล้ว ให้ข้ามขั้นตอนนี้ได้เลยครับ แต่ต้องแน่ใจว่า `app.module.ts` มี `CommonModule` ใน imports แล้ว)_
-----
---
### 2. สร้าง Base Entity (รากฐานของทุกตาราง)
@@ -27,12 +27,7 @@ nest g module common
สร้างไฟล์: `src/common/entities/base.entity.ts`
```typescript
import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
} from 'typeorm';
import { PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
export abstract class BaseEntity {
@PrimaryGeneratedColumn()
@@ -49,7 +44,7 @@ export abstract class BaseEntity {
}
```
-----
---
### 3. สร้าง Standard Response Format (Interceptor)
@@ -58,12 +53,7 @@ export abstract class BaseEntity {
สร้างไฟล์: `src/common/interceptors/transform.interceptor.ts`
```typescript
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@@ -74,25 +64,20 @@ export interface Response<T> {
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
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
@@ -101,14 +86,7 @@ export class TransformInterceptor<T>
สร้างไฟล์: `src/common/exceptions/http-exception.filter.ts`
```typescript
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
@@ -120,26 +98,18 @@ export class HttpExceptionFilter implements ExceptionFilter {
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
const exceptionResponse =
exception instanceof HttpException
? exception.getResponse()
: '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)}`,
);
this.logger.error(`Http Status: ${status} Error Message: ${JSON.stringify(message)}`);
response.status(status).json({
statusCode: status,
@@ -151,7 +121,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
}
```
-----
---
### 5. ลงทะเบียนใช้งาน Global ใน `main.ts`
@@ -178,7 +148,7 @@ async function bootstrap() {
whitelist: true, // ตัด field ส่วนเกินทิ้ง
transform: true, // แปลง Type อัตโนมัติ (เช่น string -> number)
forbidNonWhitelisted: true, // แจ้ง Error ถ้าส่ง field แปลกปลอมมา
}),
})
);
// 3. Global Interceptor (จัด Format Response)
@@ -193,9 +163,9 @@ async function bootstrap() {
bootstrap();
```
-----
---
* **เสร็จสิ้น T1.1 ส่วน Infrastructure หลักแล้วครับ**
- **เสร็จสิ้น T1.1 ส่วน Infrastructure หลักแล้วครับ**
ตอนนี้ระบบเรามี:
@@ -207,7 +177,7 @@ bootstrap();
Task นี้หัวใจสำคัญคือ "ความปลอดภัย" เราจะสร้างระบบ Login ที่ใช้ **JWT (JSON Web Token)** และเก็บรหัสผ่านแบบ **Bcrypt** ตามมาตรฐานครับ
-----
---
### 1. ติดตั้ง Libraries
@@ -225,7 +195,7 @@ pnpm add -D @types/passport-jwt @types/bcrypt
เนื่องจาก Auth ต้องทำงานกับ "ผู้ใช้" เราจำเป็นต้องสร้าง `UserModule` และ `User` Entity ขึ้นมาก่อนครับ (เป็นการทำ T1.3 บางส่วนล่วงหน้าเพื่อให้ T1.2 ทำงานได้)
สร้างไฟล์: `src/modules/user/entities/user.entity.ts`
*(อ้างอิงโครงสร้างจาก Data Dictionary ตาราง `users`)*
_(อ้างอิงโครงสร้างจาก Data Dictionary ตาราง `users`)_
```typescript
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@@ -254,7 +224,7 @@ export class User extends BaseEntity {
@Column({ name: 'is_active', default: true })
isActive: boolean;
// เดี๋ยวเราจะมาเพิ่ม Relation กับ Role/Org ทีหลังใน T1.3
}
```
@@ -296,13 +266,13 @@ import { User } from './entities/user.entity.js';
export class UserService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<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);
@@ -342,8 +312,8 @@ import { JwtStrategy } from './jwt.strategy.js'; // เดี๋ยวสร้
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRATION') || '8h'
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRATION') || '8h',
},
}),
}),
@@ -369,7 +339,7 @@ import { UserService } from '../../modules/user/user.service.js';
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private jwtService: JwtService
) {}
// ตรวจสอบ Username/Password
@@ -451,16 +421,16 @@ export class AuthController {
}
return this.authService.login(user);
}
// Endpoint ชั่วคราวสำหรับสร้าง User คนแรก (Admin)
@Post('register-admin')
async register(@Body() req) {
return this.authService.register(req);
return this.authService.register(req);
}
}
```
-----
---
### ✅ สรุปสิ่งที่ทำไป
@@ -473,11 +443,11 @@ export class AuthController {
1. Start App: `pnpm start:dev`
2. ใช้ **Postman** หรือ **Thunder Client**:
* **Register Admin:** `POST http://localhost:3000/api/auth/register-admin`
* Body: `{"username": "admin", "password": "password123", "email": "admin@test.com"}`
* **Login:** `POST http://localhost:3000/api/auth/login`
* Body: `{"username": "admin", "password": "password123"}`
* **Result:** ควรได้ `access_token` กลับมาครับ
- **Register Admin:** `POST http://localhost:3000/api/auth/register-admin`
- Body: `{"username": "admin", "password": "password123", "email": "admin@test.com"}`
- **Login:** `POST http://localhost:3000/api/auth/login`
- Body: `{"username": "admin", "password": "password123"}`
- **Result:** ควรได้ `access_token` กลับมาครับ
ลองทำตามขั้นตอนนี้ดูครับ ติดขัดตรงไหนแจ้งได้เลยครับ (โดยเฉพาะเรื่อง `.js` extension ใน import ถ้าใช้ ESM อาจจะต้องระวังนิดนึงครับ)
@@ -555,7 +525,7 @@ export class RegisterDto {
```typescript
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service.js';
import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO
import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO
import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO
@Controller('auth')
@@ -564,23 +534,20 @@ export class AuthController {
@Post('login')
// เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto
async login(@Body() loginDto: LoginDto) {
const user = await this.authService.validateUser(
loginDto.username,
loginDto.password
);
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);
return this.authService.register(registerDto);
}
}
```
@@ -590,7 +557,7 @@ export class AuthController {
ใน `auth.service.ts` ฟังก์ชัน `register` ควรระบุ Type ของ parameter ด้วยเช่นกันครับ
**ไฟล์: `src/common/auth/auth.service.ts`**
*(เฉพาะส่วนที่ต้องแก้ method register)*
_(เฉพาะส่วนที่ต้องแก้ method register)_
```typescript
// ... imports อื่นๆ
@@ -604,7 +571,7 @@ export class AuthService {
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,
@@ -667,7 +634,7 @@ export class AuthModule {}
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
// Cast เฉพาะค่า Value
expiresIn: (configService.get<string>('JWT_EXPIRATION') || '8h') as any,
expiresIn: (configService.get<string>('JWT_EXPIRATION') || '8h') as any,
},
}),
```
@@ -691,7 +658,7 @@ import { RegisterDto } from './dto/register.dto.js'; // <--- เพิ่มบ
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private jwtService: JwtService
) {}
async validateUser(username: string, pass: string): Promise<any> {
@@ -781,7 +748,7 @@ export class RegisterDto {
}
```
-----
---
**หมายเหตุ:** ในอนาคตเมื่อคุณสร้าง **Entity** (เช่น `User` entity) คุณก็จะเจอ Error แบบเดียวกันนี้ ก็ให้ใช้ `!` แก้ไขในลักษณะเดียวกันครับ
@@ -810,7 +777,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
// ใส่ ! ต่อท้าย เพื่อบอกว่าค่านี้ไม่เป็น undefined แน่นอน
secretOrKey: configService.get<string>('JWT_SECRET')!,
secretOrKey: configService.get<string>('JWT_SECRET')!,
});
}
@@ -838,11 +805,11 @@ secretOrKey: configService.getOrThrow<string>('JWT_SECRET'),
เพื่อเป็นการแก้ไขและให้คุณไปต่อได้ราบรื่นที่สุด ผมขออนุญาต **รวมโค้ดฉบับสมบูรณ์ (Strict Mode Compliant)** ของไฟล์ที่เกี่ยวข้องทั้งหมดใน Module นี้มาให้ครับ คุณสามารถ **Copy ทับไฟล์เดิม** ได้เลย จะได้ไม่ต้องแก้ทีละจุดครับ
-----
---
##### 1. ไฟล์ `src/common/auth/auth.service.ts` (ฉบับแก้ครบ)
*เพิ่ม Import DTO และระบุ Type ครบถ้วน*
_เพิ่ม Import DTO และระบุ Type ครบถ้วน_
```typescript
import { Injectable } from '@nestjs/common';
@@ -855,7 +822,7 @@ import { RegisterDto } from './dto/register.dto.js'; // Import DTO
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private jwtService: JwtService
) {}
async validateUser(username: string, pass: string): Promise<any> {
@@ -878,7 +845,7 @@ export class AuthService {
async register(userDto: RegisterDto) {
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(userDto.password, salt);
// ใช้ค่าจาก DTO ที่ Validate มาแล้ว
return this.userService.create({
...userDto,
@@ -890,7 +857,7 @@ export class AuthService {
##### 2. ไฟล์ `src/common/auth/jwt.strategy.ts` (ฉบับแก้ครบ)
*ใส่ `!` ป้องกัน undefined และระบุ Type ให้ payload*
_ใส่ `!` ป้องกัน undefined และระบุ Type ให้ payload_
```typescript
import { ExtractJwt, Strategy } from 'passport-jwt';
@@ -911,7 +878,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
// ใส่ ! เพื่อยืนยันว่ามีค่าแน่นอน (ConfigValidation เช็คให้แล้ว)
secretOrKey: configService.get<string>('JWT_SECRET')!,
secretOrKey: configService.get<string>('JWT_SECRET')!,
});
}
@@ -923,7 +890,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
##### 3. ไฟล์ `src/common/auth/auth.module.ts` (ฉบับแก้ครบ)
*Cast Type ตรง expiresIn*
_Cast Type ตรง expiresIn_
```typescript
import { Module } from '@nestjs/common';
@@ -958,7 +925,7 @@ import { JwtStrategy } from './jwt.strategy.js';
export class AuthModule {}
```
-----
---
หลังจากนี้ ผมจะตรวจสอบโค้ดทุกบรรทัดให้รองรับ **Strict Mode** และ **ESM** ก่อนส่งให้คุณเสมอครับ หาก Copy ไปวางแล้วยังเจอ Error อีก ด่าผมได้เต็มที่เลยครับ
@@ -989,10 +956,10 @@ export class AuthModule {}
useFactory: async (configService: ConfigService) => ({
type: 'mariadb',
// ... ค่าอื่นๆ เหมือนเดิม
// แก้บรรทัดนี้เป็น false ครับ
// เพราะเราใช้ SQL Script สร้าง DB แล้ว ไม่ต้องการให้ TypeORM มาแก้ Structure อัตโนมัติ
synchronize: false,
synchronize: false,
}),
}),
// ...
@@ -1000,7 +967,7 @@ export class AuthModule {}
หลังจากแก้เป็น `false` แล้ว ให้ลอง **Restart Server** (`pnpm start:dev`) อีกครั้ง Error นี้จะหายไปและเชื่อมต่อได้ครับ
-----
---
##### วิธีที่ 2: สร้าง Entity ให้ครบ (ทางเลือก)
@@ -1069,12 +1036,10 @@ export class HttpExceptionFilter implements ExceptionFilter {
// ... (โค้ดส่วนต้นเหมือนเดิม) ...
// 👇👇 เพิ่มบรรทัดนี้ครับ (สำคัญมาก!) 👇👇
console.error('💥 REAL ERROR:', exception);
console.error('💥 REAL ERROR:', exception);
// ... (โค้ดส่วน Log เดิม) ...
this.logger.error(
`Http Status: ${status} Error Message: ${JSON.stringify(message)}`,
);
this.logger.error(`Http Status: ${status} Error Message: ${JSON.stringify(message)}`);
// ... (ส่วน response.status... เหมือนเดิม) ...
}
@@ -1087,23 +1052,21 @@ export class HttpExceptionFilter implements ExceptionFilter {
2. กด **Send** ใน Postman อีกครั้ง
3. ดูที่ **Terminal** ครับ คุณจะเห็นข้อความหลัง `💥 REAL ERROR:` ที่ยาวๆ
-----
---
##### 🔮 ผมขอเดาสาเหตุที่เป็นไปได้ (ลองเช็คดูนะครับ)
จากประสบการณ์ Error 500 หลังจากแก้เรื่อง Column แล้ว มักจะเกิดจาก 2 สาเหตุนี้ครับ:
1. **ข้อมูลซ้ำ (Duplicate Entry):**
* คุณอาจจะกด Send สำเร็จไปแล้วรอบหนึ่ง (ตอนที่แก้ DB เสร็จ) ข้อมูล User `admin` เลยเข้าไปอยู่ใน DB แล้ว
* พอกด Send อีกรอบ มันเลยฟ้องว่า **"Username หรือ Email นี้มีอยู่แล้ว"** (Duplicate entry)
* *วิธีเช็ค:* ลองเปลี่ยน `username` ใน Postman เป็น `admin2` แล้วกด Send ดูครับ ถ้าผ่าน แสดงว่าใช่เลย
- คุณอาจจะกด Send สำเร็จไปแล้วรอบหนึ่ง (ตอนที่แก้ DB เสร็จ) ข้อมูล User `admin` เลยเข้าไปอยู่ใน DB แล้ว
- พอกด Send อีกรอบ มันเลยฟ้องว่า **"Username หรือ Email นี้มีอยู่แล้ว"** (Duplicate entry)
- _วิธีเช็ค:_ ลองเปลี่ยน `username` ใน Postman เป็น `admin2` แล้วกด Send ดูครับ ถ้าผ่าน แสดงว่าใช่เลย
2. **Column `user_id` หรือ `id` ไม่ตรงกัน:**
* ใน Entity เราใช้ `@PrimaryGeneratedColumn({ name: 'user_id' }) id: number;`
* แต่ถ้าใน Database จริง คอลัมน์ชื่อ `id` เฉยๆ (หรือ Entity แม่ `BaseEntity` ไปตีกัน) ก็จะเกิด Error ได้
* *แต่เคสนี้ Data Dictionary ระบุว่าเป็น `user_id` น่าจะตรงกันแล้ว*
- ใน Entity เราใช้ `@PrimaryGeneratedColumn({ name: 'user_id' }) id: number;`
- แต่ถ้าใน Database จริง คอลัมน์ชื่อ `id` เฉยๆ (หรือ Entity แม่ `BaseEntity` ไปตีกัน) ก็จะเกิด Error ได้
- _แต่เคสนี้ Data Dictionary ระบุว่าเป็น `user_id` น่าจะตรงกันแล้ว_
**สรุป:**
ลองเปลี่ยน Username ใน Postman ดูก่อนครับ ถ้ายัง Error อีก รบกวนก๊อปปี้ข้อความหลัง `💥 REAL ERROR:` ใน Terminal มาให้ผมดูหน่อยครับ แล้วผมจะบอกวิธีแก้ที่เป๊ะๆ ให้ทันทีครับ
@@ -1118,9 +1081,9 @@ export class HttpExceptionFilter implements ExceptionFilter {
pnpm start:dev
```
*(รอจนขึ้นว่า Nest application successfully started)*
_(รอจนขึ้นว่า Nest application successfully started)_
-----
---
### 1. สร้าง Collection ใหม่ (แนะนำ)
@@ -1134,24 +1097,24 @@ pnpm start:dev
2. ตั้งชื่อ Request: `Register Admin`
3. **Method:** เลือก `POST`
4. **URL:** ใส่ `http://localhost:3000/api/auth/register-admin`
*(อย่าลืม `/api` เพราะเราตั้ง Global Prefix ไว้ใน main.ts)*
_(อย่าลืม `/api` เพราะเราตั้ง Global Prefix ไว้ใน main.ts)_
5. ไปที่แท็บ **Body**:
* เลือก `raw`
* เลือกประเภทเป็น `JSON`
- เลือก `raw`
- เลือกประเภทเป็น `JSON`
6. ใส่ข้อมูลในช่องว่าง:
```json
{
"username": "admin",
"password": "password123",
"email": "admin@example.com",
"firstName": "System",
"lastName": "Admin"
}
```
```json
{
"username": "admin",
"password": "password123",
"email": "admin@example.com",
"firstName": "System",
"lastName": "Admin"
}
```
7. กดปุ่ม **Send**
* **ผลลัพธ์:** ควรได้ JSON ข้อมูล User กลับมา (มี id, created\_at ฯลฯ แต่ไม่มี password) และ Status `201 Created`
- **ผลลัพธ์:** ควรได้ JSON ข้อมูล User กลับมา (มี id, created_at ฯลฯ แต่ไม่มี password) และ Status `201 Created`
### 3. ทดสอบเข้าสู่ระบบ (Login)
@@ -1162,25 +1125,25 @@ pnpm start:dev
3. **Method:** เลือก `POST`
4. **URL:** ใส่ `http://localhost:3000/api/auth/login`
5. ไปที่แท็บ **Body**:
* เลือก `raw`
* เลือกประเภทเป็น `JSON`
- เลือก `raw`
- เลือกประเภทเป็น `JSON`
6. ใส่ข้อมูล:
```json
{
"username": "admin",
"password": "password123"
}
```
```json
{
"username": "admin",
"password": "password123"
}
```
7. กดปุ่ม **Send**
* **ผลลัพธ์:** ควรได้ `access_token` กลับมาครับ
<!-- end list -->
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
- **ผลลัพธ์:** ควรได้ `access_token` กลับมาครับ
<!-- end list -->
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### 4. (แถม) วิธีนำ Token ไปใช้ต่อ
@@ -1193,7 +1156,7 @@ pnpm start:dev
5. **Token:** วาง Token ที่ Copy มาลงไป
6. กด Send (ถ้า API นั้นมีอยู่จริงและ Token ถูกต้อง ก็จะผ่านครับ)
-----
---
**ถ้าทดสอบผ่านแล้ว:** แสดงว่าระบบ Auth เบื้องต้น (T1.2) ของเราสมบูรณ์ครับ\!
เราจะไปต่อที่ **Task T1.3 UserModule - User Management** เพื่อสร้าง API จัดการ User แบบเต็มรูปแบบ (CRUD) กันไหมครับ?
@@ -1214,7 +1177,7 @@ pnpm start:dev
pnpm add @nestjs/mapped-types
```
-----
---
### 2. สร้างไฟล์ `create-user.dto.ts`
@@ -1261,7 +1224,7 @@ export class CreateUserDto {
}
```
-----
---
### 3. สร้างไฟล์ `update-user.dto.ts`
@@ -1276,7 +1239,7 @@ import { CreateUserDto } from './create-user.dto.js';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
```
-----
---
### ✅ เสร็จสิ้นขั้นตอนที่ 1
@@ -1306,7 +1269,7 @@ import { AuthGuard } from '@nestjs/passport';
export class JwtAuthGuard extends AuthGuard('jwt') {}
```
-----
---
### 2. อัปเดต `UserController` (สร้าง Endpoints)
@@ -1315,17 +1278,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') {}
แก้ไขไฟล์: `src/modules/user/user.controller.ts`
```typescript
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
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';
@@ -1356,10 +1309,7 @@ export class UserController {
// 4. แก้ไขข้อมูลผู้ใช้
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
update(@Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(id, updateUserDto);
}
@@ -1371,7 +1321,7 @@ export class UserController {
}
```
-----
---
### ⚠️ ข้อควรระวัง (ยังรันไม่ได้ทันที)
@@ -1404,7 +1354,7 @@ import { UpdateUserDto } from './dto/update-user.dto.js';
export class UserService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private usersRepository: Repository<User>
) {}
// 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก)
@@ -1470,7 +1420,7 @@ export class UserService {
// รวมร่างข้อมูลเดิม + ข้อมูลใหม่
const updatedUser = this.usersRepository.merge(user, updateUserDto);
return this.usersRepository.save(updatedUser);
}
@@ -1485,7 +1435,7 @@ export class UserService {
}
```
-----
---
### ✅ เสร็จสิ้น Task T1.3 User Management
@@ -1497,15 +1447,16 @@ export class UserService {
1. **Login** (ด้วย Admin) เพื่อเอา `access_token` มาก่อน
2. **สร้าง Request ใหม่** (เช่น `Get All Users`)
* **URL:** `GET http://localhost:3000/api/users`
* **Auth:** เลือก Type `Bearer Token` แล้ววาง Token ลงไป
* **Send:** ควรเห็นรายชื่อ User ออกมาเป็น JSON ครับ
- **URL:** `GET http://localhost:3000/api/users`
- **Auth:** เลือก Type `Bearer Token` แล้ววาง Token ลงไป
- **Send:** ควรเห็นรายชื่อ User ออกมาเป็น JSON ครับ
ถ้าทดสอบ `GET` ผ่านแล้ว ลองทดสอบ `POST` (สร้าง User ใหม่), `PATCH` (แก้ไข), และ `DELETE` (ลบ) ดูได้เลยครับ
ถ้าทุกอย่างเรียบร้อย เราจะปิดจ็อบ Phase 1 แล้วไปขึ้น **Phase 2: High-Integrity Data (File Upload)** กันต่อไหมครับ?
### **"มอบหมายบทบาท (Assign Role)"**
**ถูกต้องที่สุดครับ!** คุณเข้าใจคอนเซปต์ของระบบ RBAC ในระบบนี้ได้อย่างทะลุปรุโปร่งครับ 👍
ถ้าเราสร้าง User ลงในตาราง `users` เพียงอย่างเดียว User คนนั้นจะ:
@@ -1515,11 +1466,11 @@ export class UserService {
ดังนั้น การ **"มอบหมายบทบาท (Assign Role)"** จึงเป็นขั้นตอนที่ขาดไม่ได้ครับ
-----
---
#### 🛠️ สิ่งที่เราต้องทำเพิ่ม (เพื่อให้ Admin มอบสิทธิ์ได้)
ใน Task T1.3 เราทำแค่ `Create User` (ลงตาราง users) ไปแล้ว ตอนนี้เราต้องทำ API สำหรับ **Assign Role** (ลงตาราง user\_assignments) ครับ
ใน Task T1.3 เราทำแค่ `Create User` (ลงตาราง users) ไปแล้ว ตอนนี้เราต้องทำ API สำหรับ **Assign Role** (ลงตาราง user_assignments) ครับ
มาสร้างฟังก์ชันนี้กันครับ:
@@ -1572,12 +1523,12 @@ import { User } from './entities/user.entity.js';
export class UserAssignmentService {
constructor(
@InjectRepository(UserAssignment)
private assignmentRepo: Repository<UserAssignment>,
private assignmentRepo: Repository<UserAssignment>
) {}
async assignRole(dto: AssignRoleDto, assigner: User) {
// Validation: ตรวจสอบกฎเหล็ก (เลือกได้แค่ Scope เดียว)
const scopes = [dto.organizationId, dto.projectId, dto.contractId].filter(v => v != null);
const scopes = [dto.organizationId, dto.projectId, dto.contractId].filter((v) => v != null);
if (scopes.length > 1) {
throw new BadRequestException('Cannot assign multiple scopes at once. Choose one of Org, Project, or Contract.');
}
@@ -1652,7 +1603,7 @@ import { UserAssignmentService } from './user-assignment.service.js'; // อย
export class UserController {
constructor(
private readonly userService: UserService,
private readonly assignmentService: UserAssignmentService, // <--- Inject เพิ่ม
private readonly assignmentService: UserAssignmentService // <--- Inject เพิ่ม
) {}
// ... Endpoints เดิม ...
@@ -1665,13 +1616,13 @@ export class UserController {
}
```
-----
---
#### 📝 สรุปภาพรวม
1. **สร้าง User:** `POST /users` -\> ได้ User เปล่าๆ (Login ได้ แต่ทำไรไม่ได้)
2. **กำหนดสิทธิ์:** `POST /users/assign-role` -\> ยิง API นี้เพื่อบอกว่า "User นี้ เป็น Project Manager ของ Project A นะ"
* Body: `{ "userId": 5, "roleId": 6, "projectId": 1 }`
- Body: `{ "userId": 5, "roleId": 6, "projectId": 1 }`
3. **ใช้งาน:** เมื่อ User ID 5 ล็อกอิน ระบบจะไปเช็คใน `user_assignments` เห็นว่ามีสิทธิ์ Project Manager ของ Project 1 จึงอนุญาตให้ทำงานได้ครับ
ระบบ RBAC ของเรายืดหยุ่นและรองรับ Real-world scenario มากๆ ครับ (เช่น คนหนึ่งคนเป็น Admin ใน Project A แต่เป็นแค่ Viewer ใน Project B ก็ทำได้ เพราะเรามีตาราง `user_assignments` ตัวนี้แหละครับ)
@@ -1700,14 +1651,14 @@ import { User } from './entities/user.entity.js';
@Module({
imports: [TypeOrmModule.forFeature([User])],
// 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
controllers: [UserController],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
```
-----
---
#### 2. ตรวจสอบ `src/app.module.ts` (เช็คความชัวร์)
@@ -1755,16 +1706,16 @@ import { UserAssignment } from './entities/user-assignment.entity.js'; // <--- 2
@Module({
imports: [
// 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment
TypeOrmModule.forFeature([User, UserAssignment]),
TypeOrmModule.forFeature([User, UserAssignment]),
],
controllers: [UserController],
providers: [
UserService,
UserAssignmentService // <--- 4. ลงทะเบียน Service เป็น Provider
UserService,
UserAssignmentService, // <--- 4. ลงทะเบียน Service เป็น Provider
],
exports: [
UserService,
UserAssignmentService // <--- 5. Export เผื่อที่อื่นใช้
UserService,
UserAssignmentService, // <--- 5. Export เผื่อที่อื่นใช้
],
})
export class UserModule {}
@@ -1778,7 +1729,7 @@ export class UserModule {}
ตอนนี้คุณมี **User Management API** (CRUD) ที่สมบูรณ์และปลอดภัยแล้ว ถือว่าปิดจ็อบ **Task T1.3** อย่างสวยงามครับ
-----
---
### 🚀 เป้าหมายต่อไป: Task T1.4 RBAC Guard (4-Level Authorization)
@@ -1786,12 +1737,12 @@ export class UserModule {}
**โจทย์ของเราคือ:** ผู้ใช้คนหนึ่งอาจมีสิทธิ์ต่างกันตามบริบท เช่น:
* เป็น **Viewer** ในระดับองค์กร
* แต่เป็น **Manager** ในโปรเจกต์ A
- เป็น **Viewer** ในระดับองค์กร
- แต่เป็น **Manager** ในโปรเจกต์ A
เราจะสร้าง **RBAC Guard** เพื่อตรวจสอบสิทธิ์ 4 ระดับนี้ครับ (Global \> Organization \> Project \> Contract)
-----
---
### ขั้นตอนที่ 1: ติดตั้ง Library (CASL)
@@ -1816,8 +1767,7 @@ export const PERMISSION_KEY = 'permission';
// ใช้สำหรับแปะหน้า Controller/Method
// ตัวอย่าง: @RequirePermission('user.create')
export const RequirePermission = (permission: string) =>
SetMetadata(PERMISSION_KEY, permission);
export const RequirePermission = (permission: string) => SetMetadata(PERMISSION_KEY, permission);
```
### ขั้นตอนที่ 3: สร้าง `RbacGuard` (หัวใจสำคัญ)
@@ -1836,7 +1786,7 @@ import { UserService } from '../../modules/user/user.service.js';
export class RbacGuard implements CanActivate {
constructor(
private reflector: Reflector,
private userService: UserService,
private userService: UserService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
@@ -1893,10 +1843,10 @@ export class UserService {
// 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],
[userId]
);
// แปลงผลลัพธ์เป็น Array ของ string ['user.create', 'project.view', ...]
@@ -1918,7 +1868,6 @@ 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) {
@@ -1929,7 +1878,7 @@ export class UserController {
}
```
-----
---
**พร้อมลองของจริงไหมครับ?**
ถ้าทำเสร็จแล้ว ลองใช้ Postman ยิง `POST /api/users` โดยใช้ Token ของ `admin` ดูครับ (ซึ่ง Admin มีสิทธิ์ครบอยู่แล้ว น่าจะผ่าน)
@@ -1947,7 +1896,7 @@ export class UserController {
2. ✅ **Authorization:** ตรวจสอบสิทธิ์ด้วย RBAC Guard + CASL
3. ✅ **User Management:** จัดการผู้ใช้ได้ครบวงจร
-----
---
### 🚀 เป้าหมายต่อไป: Task T1.5 ProjectModule - Base Structures
@@ -1974,7 +1923,7 @@ nest g controller modules/project
### 2. สร้าง Organization Entity
*แก้หนี้เก่าที่เราติดไว้ตอนทำ User Entity ครับ*
_แก้หนี้เก่าที่เราติดไว้ตอนทำ User Entity ครับ_
สร้างไฟล์: `src/modules/project/entities/organization.entity.ts`
@@ -2064,7 +2013,7 @@ export class Contract extends BaseEntity {
}
```
-----
---
### 5. (สำคัญ) กลับไปแก้ `User` Entity ให้สมบูรณ์
@@ -2089,7 +2038,7 @@ export class User extends BaseEntity {
}
```
-----
---
เมื่อสร้างไฟล์เหล่านี้เสร็จแล้ว อย่าลืมไปลงทะเบียน Entities ใน `src/modules/project/project.module.ts` นะครับ
@@ -2193,12 +2142,12 @@ import { ContractOrganization } from './entities/contract-organization.entity.js
@Module({
imports: [
TypeOrmModule.forFeature([
Project,
Organization,
Project,
Organization,
Contract,
ProjectOrganization, // ลงทะเบียน
ContractOrganization // ลงทะเบียน
])
ContractOrganization, // ลงทะเบียน
]),
],
controllers: [ProjectController],
providers: [ProjectService],
@@ -2226,7 +2175,7 @@ export class ProjectService {
@InjectRepository(Project)
private projectRepository: Repository<Project>,
@InjectRepository(Organization)
private organizationRepository: Repository<Organization>,
private organizationRepository: Repository<Organization>
) {}
// ดึงรายการ Project ทั้งหมด
@@ -2257,7 +2206,7 @@ export class ProjectController {
findAll() {
return this.projectService.findAllProjects();
}
@Get('organizations')
findAllOrgs() {
return this.projectService.findAllOrganizations();
@@ -2265,7 +2214,7 @@ export class ProjectController {
}
```
-----
---
### ✅ เสร็จสิ้น Task T1.5
@@ -2304,7 +2253,7 @@ import {
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
ManyToOne, // <--- เพิ่มตรงนี้
ManyToOne, // <--- เพิ่มตรงนี้
JoinColumn, // <--- เพิ่มตรงนี้
} from 'typeorm';
import { Organization } from '../../project/entities/organization.entity.js'; // อย่าลืม import Organization
@@ -2354,7 +2303,7 @@ export class User {
ลอง Save แล้วรันใหม่ครับ Error นี้จะหายไปแน่นอนครับ
-----
---
หากคอมไพล์ผ่านแล้วและไม่มี Error เพิ่มเติม เราถือว่า **จบ Phase 1** อย่างสมบูรณ์แล้วครับ
@@ -2381,23 +2330,23 @@ export class User {
return this.usersRepository.find({
// ✅ แก้ไขชื่อ field ให้ตรงกับ Entity Class
select: [
'user_id',
'username',
'email',
'firstName',
'lastName',
'isActive',
'user_id',
'username',
'email',
'firstName',
'lastName',
'isActive',
'createdAt', // แก้ตรงนี้ (จาก created_at)
'updatedAt' // แก้ตรงนี้ (จาก updated_at)
],
});
}
// ...
```
ลองแก้จุดนี้แล้ว Save ดูครับ Error จะหายไปครับ
-----
---
### 🚀