251120:2340 Setting Project แก้ Correspondence

This commit is contained in:
2025-11-20 23:49:19 +07:00
parent 20c0f51e2a
commit 58cee2d007
8 changed files with 2467 additions and 16 deletions

View File

@@ -1,3 +0,0 @@
---
alwaysApply: true
---

View File

@@ -1,6 +0,0 @@
---
description: Documents Management Sytem Version 1.1.0
globs:
- docs/LCBP3-DMS_V1_1_0_*.md
alwaysApply: true
---

View File

@@ -375,7 +375,7 @@ JWT_EXPIRATION=8h
คุณต้องการให้ผมช่วยตั้งค่า `ConfigModule` ใน NestJS เพื่ออ่านค่า `JWT_SECRET` นี้อย่างปลอดภัยพร้อม Validate ว่าห้ามเป็นค่าว่างไหมครับ? คุณต้องการให้ผมช่วยตั้งค่า `ConfigModule` ใน NestJS เพื่ออ่านค่า `JWT_SECRET` นี้อย่างปลอดภัยพร้อม Validate ว่าห้ามเป็นค่าว่างไหมครับ?
## ได้เลยครับ เรามาลุยงาน **T0.1 Secure Configuration Setup** ตามแผน Backend Plan กันต่อครับ ## **T0.1 Secure Configuration Setup** ตามแผน Backend Plan กันต่อครับ
เป้าหมายของ Task นี้คือการทำให้ระบบอ่านค่า Environment Variables ได้อย่างถูกต้อง และ **"ต้องไม่ Start"** ถ้าค่า Config สำคัญๆ (เช่น Database Password, JWT Secret) หายไป หรือไม่ถูกต้อง เพื่อความปลอดภัยครับ เป้าหมายของ Task นี้คือการทำให้ระบบอ่านค่า Environment Variables ได้อย่างถูกต้อง และ **"ต้องไม่ Start"** ถ้าค่า Config สำคัญๆ (เช่น Database Password, JWT Secret) หายไป หรือไม่ถูกต้อง เพื่อความปลอดภัยครับ
@@ -471,7 +471,7 @@ import { envValidationSchema } from './common/config/env.validation.js'; // ส
export class AppModule {} export class AppModule {}
``` ```
### 4\. สร้างไฟล์ Template สำหรับ Docker Override (ตามแผน T0.1) ### 4. สร้างไฟล์ Template สำหรับ Docker Override (ตามแผน T0.1)
ตามแผนงาน เราต้องเตรียมไฟล์ `docker-compose.override.yml.example` เพื่อให้นักพัฒนาคนอื่นรู้ว่าต้อง Inject Secrets อย่างไร โดยไม่เปิดเผยความลับใน `docker-compose.yml` หลัก ตามแผนงาน เราต้องเตรียมไฟล์ `docker-compose.override.yml.example` เพื่อให้นักพัฒนาคนอื่นรู้ว่าต้อง Inject Secrets อย่างไร โดยไม่เปิดเผยความลับใน `docker-compose.yml` หลัก
@@ -496,7 +496,7 @@ services:
- MYSQL_PASSWORD=secret_password_here - MYSQL_PASSWORD=secret_password_here
``` ```
### 5\. ทดสอบความปลอดภัย (Validation Check) ### 5. ทดสอบความปลอดภัย (Validation Check)
ลองทดสอบว่าระบบ Validation ทำงานถูกต้องหรือไม่ โดยการ **ลบ** หรือ **เปลี่ยนชื่อ** ตัวแปรสำคัญในไฟล์ `.env` (เช่น ลบ `JWT_SECRET`) แล้วลองรัน: ลองทดสอบว่าระบบ Validation ทำงานถูกต้องหรือไม่ โดยการ **ลบ** หรือ **เปลี่ยนชื่อ** ตัวแปรสำคัญในไฟล์ `.env` (เช่น ลบ `JWT_SECRET`) แล้วลองรัน:
@@ -508,10 +508,230 @@ pnpm start:dev
App **ต้อง Crash** และแสดง Error Message ชัดเจน เช่น: App **ต้อง Crash** และแสดง Error Message ชัดเจน เช่น:
`Config validation error: "JWT_SECRET" is required` `Config validation error: "JWT_SECRET" is required`
ถ้าขึ้นแบบนี้แสดงว่าระบบ **Secure Configuration** ของเราทำงานถูกต้องตามแผน T0.1 แล้วครับ\! 🎉 ถ้าขึ้นแบบนี้แสดงว่าระบบ **Secure Configuration** ของเราทำงานถูกต้องตามแผน T0.1 แล้วครับ! 🎉
----- -----
**ขั้นตอนถัดไป:** ## **T0.2 Redis & Queue Infrastructure**
เราจะไปต่อที่ **T0.2 Redis & Queue Infrastructure** เพื่อเตรียมระบบ Caching และ Background Job ไหมครับ?
เป้าหมายของ Task นี้คือเตรียม **Redis** สำหรับทำ 2 เรื่องสำคัญ:
1. **Distributed Locking (Redlock):** ป้องกัน Race Condition เวลาออกเลขที่เอกสาร
2. **Message Queue (BullMQ):** สำหรับจัดการงานเบื้องหลัง (Background Jobs) เช่น การรวมอีเมลแจ้งเตือน (Digest Notification)
มาเริ่มกันเลยครับ
-----
### 1. เพิ่ม Redis ใน Docker Compose
แก้ไขไฟล์ `docker-compose.yml` เพื่อเพิ่ม Service Redis ครับ โดยเราจะตั้ง Password เพื่อความปลอดภัย
**ไฟล์: `docker-compose.yml`**
```yaml
version: '3.8'
services:
# ... (mariadb & pma เดิม) ...
# เพิ่ม Redis Service
redis:
image: redis:7-alpine
container_name: lcbp3-redis-local
restart: always
# ใช้ Command นี้เพื่อตั้ง Password
command: redis-server --requirepass "redis_password_secure"
ports:
- '6379:6379'
volumes:
- redis_data:/data
networks:
- lcbp3-net
volumes:
db_data:
redis_data: # เพิ่ม Volume
networks:
lcbp3-net:
driver: bridge
```
จากนั้นรันคำสั่งเพื่อ Start Redis:
```bash
docker-compose up -d
```
### 2. อัปเดต Environment Config
เพิ่มค่า Config ของ Redis ลงในไฟล์ `.env` และไฟล์ Validation ครับ
**ไฟล์: `.env`**
```env
# ... (ค่าเดิม) ...
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=redis_password_secure
```
**ไฟล์: `src/common/config/env.validation.ts`**
```typescript
// ... (import Joi) ...
export const envValidationSchema = Joi.object({
// ... (App & Database Config เดิม) ...
// 4. Redis Configuration (เพิ่มส่วนนี้)
REDIS_HOST: Joi.string().required(),
REDIS_PORT: Joi.number().default(6379),
REDIS_PASSWORD: Joi.string().required(),
});
```
### 3. ติดตั้ง Library สำหรับ BullMQ
เราจะใช้ `@nestjs/bullmq` ซึ่งเป็น Wrapper ของ `bullmq` (Redis Queue ที่ทันสมัยกว่า Bull ธรรมดา)
รันคำสั่ง:
```bash
pnpm add @nestjs/bullmq bullmq
```
### 4. ตั้งค่า BullModule ใน AppModule
แก้ไข `src/app.module.ts` เพื่อเชื่อมต่อ Redis เข้ากับระบบ Queue ครับ
**ไฟล์: `src/app.module.ts`**
```typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BullModule } from '@nestjs/bullmq'; // Import BullModule
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { envValidationSchema } from './common/config/env.validation.js';
@Module({
imports: [
// 1. Config (เดิม)
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
validationSchema: envValidationSchema,
validationOptions: { abortEarly: true },
}),
// 2. TypeORM (เดิม)
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mariadb',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
autoLoadEntities: true,
synchronize: configService.get<string>('NODE_ENV') === 'development',
}),
}),
// 3. BullMQ (Redis) Setup [NEW]
BullModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
connection: {
host: configService.get<string>('REDIS_HOST'),
port: configService.get<number>('REDIS_PORT'),
password: configService.get<string>('REDIS_PASSWORD'),
},
}),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
```
### 5. ทดสอบการเชื่อมต่อ Redis
ลอง Start App ดูอีกครั้งครับ:
```bash
pnpm start:dev
```
ถ้าไม่มี Error ขึ้น และ App Start ได้ปกติ แสดงว่า **T0.2 เสร็จสมบูรณ์** ครับ\! ตอนนี้เรามีทั้ง Database และ Redis พร้อมสำหรับการพัฒนา Business Logic แล้ว
-----
### 6. **แก้ไข Error**
ปัญหานี้เกิดจากยังไม่ได้ **Import** `BullModule` เข้ามาในไฟล์ `src/app.module.ts` ครับ (TypeScript เลยไม่รู้จักว่า `BullModule` คืออะไร)
ให้เพิ่มบรรทัดนี้ที่ส่วนบนสุดของไฟล์ `src/app.module.ts` ครับ:
```typescript
import { BullModule } from '@nestjs/bullmq';
```
หรือลองตรวจสอบไฟล์ `src/app.module.ts` ให้ครบถ้วนตามนี้ครับ:
```typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BullModule } from '@nestjs/bullmq'; // <--- ต้องมีบรรทัดนี้ครับ
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { envValidationSchema } from './common/config/env.validation.js';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
validationSchema: envValidationSchema,
validationOptions: { abortEarly: true },
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mariadb',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
autoLoadEntities: true,
synchronize: configService.get<string>('NODE_ENV') === 'development',
}),
}),
// Queue Configuration
BullModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
connection: {
host: configService.get<string>('REDIS_HOST'),
port: configService.get<number>('REDIS_PORT'),
password: configService.get<string>('REDIS_PASSWORD'),
},
}),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
```

2195
Setting Project T1.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,13 @@ import {
Body, Body,
UseGuards, UseGuards,
Request, Request,
Param, // <--- ✅ 1. เพิ่ม Param
ParseIntPipe, // <--- ✅ 2. เพิ่ม ParseIntPipe
} from '@nestjs/common'; } from '@nestjs/common';
import { CorrespondenceService } from './correspondence.service.js'; import { CorrespondenceService } from './correspondence.service.js';
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js'; import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto.js'; // <--- ✅ 3. เพิ่ม Import DTO นี้
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js'; import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
import { RbacGuard } from '../../common/auth/rbac.guard.js'; import { RbacGuard } from '../../common/auth/rbac.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js'; import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
@@ -28,4 +32,19 @@ export class CorrespondenceController {
findAll() { findAll() {
return this.correspondenceService.findAll(); return this.correspondenceService.findAll();
} }
// ✅ เพิ่ม Endpoint นี้ครับ
@Post(':id/submit')
@RequirePermission('correspondence.create') // หรือจะสร้าง Permission ใหม่ 'workflow.submit' ก็ได้
submit(
@Param('id', ParseIntPipe) id: number,
@Body() submitDto: SubmitCorrespondenceDto,
@Request() req: any,
) {
return this.correspondenceService.submit(
id,
submitDto.templateId,
req.user,
);
}
} }

View File

@@ -0,0 +1,7 @@
import { IsInt, IsNotEmpty } from 'class-validator';
export class SubmitCorrespondenceDto {
@IsInt()
@IsNotEmpty()
templateId!: number;
}

BIN
extensions_list.txt Normal file

Binary file not shown.

19
test.sql Normal file
View File

@@ -0,0 +1,19 @@
-- 1. สร้าง Template ชื่อ "General Approval"
INSERT INTO correspondence_routing_templates (id, template_name, description, is_active)
VALUES (
1,
'General Approval',
'Template สำหรับการอนุมัติทั่วไป',
1
);
-- 2. สร้าง Steps (ส่งไป Org ID 1 ก่อน แล้วส่งไป Org ID 2)
-- (สมมติว่า Org ID 1 = Owner, Org ID 2 = Consultant ตาม Seed Data เดิม)
INSERT INTO correspondence_routing_template_steps (
template_id,
sequence,
to_organization_id,
step_purpose,
expected_days
)
VALUES (1, 1, 22, 'FOR_REVIEW', 3),
(1, 2, 1, 'FOR_APPROVAL', 5);