251121:1700 Backend T3 wait testt

This commit is contained in:
admin
2025-11-21 17:16:40 +07:00
parent 58cee2d007
commit bf0308e350
27 changed files with 6651 additions and 196 deletions

View File

@@ -1,11 +1,10 @@
{ "recommendations": [ { "recommendations": [
"aaron-bond.better-comments", "aaron-bond.better-comments",
"anbuselvanrocky.bootstrap5-vscode", "anbuselvanrocky.bootstrap5-vscode",
"bmewburn.vscode-intelephense-client", "bmewburn.vscode-intelephense-client",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"christian-kohler.path-intellisense", "christian-kohler.path-intellisense",
"codezombiech.gitignore", "codezombiech.gitignore",
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"dsznajder.es7-react-js-snippets", "dsznajder.es7-react-js-snippets",
"dunstontc.vscode-docker-syntax", "dunstontc.vscode-docker-syntax",
@@ -18,7 +17,6 @@
"formulahendry.auto-rename-tag", "formulahendry.auto-rename-tag",
"github.copilot", "github.copilot",
"github.copilot-chat", "github.copilot-chat",
"google.geminicodeassist",
"hansuxdev.bootstrap5-snippets", "hansuxdev.bootstrap5-snippets",
"heybourn.headwind", "heybourn.headwind",
"humao.rest-client", "humao.rest-client",
@@ -34,7 +32,6 @@
"mikestead.dotenv", "mikestead.dotenv",
"ms-azuretools.vscode-containers", "ms-azuretools.vscode-containers",
"ms-azuretools.vscode-docker", "ms-azuretools.vscode-docker",
"ms-edgedevtools.vscode-edge-devtools",
"ms-python.debugpy", "ms-python.debugpy",
"ms-python.python", "ms-python.python",
"ms-vscode-remote.remote-containers", "ms-vscode-remote.remote-containers",

25
.vscode/nap-dms.lcbp3.code-workspace vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"folders": [
{
"path": ".."
}
],
"settings": {
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default",
"enableSsl": "Disabled"
},
"ssh": "Disabled",
"previewLimit": 50,
"server": "localhost",
"port": 3306,
"driver": "MySQL",
"name": "lcbp3_dev",
"database": "lcbp3_dev",
"username": "root"
}
]
}
}

16
.vscode/settings.json vendored
View File

@@ -8,5 +8,21 @@
"javascriptreact", "javascriptreact",
"typescript", "typescript",
"typescriptreact" "typescriptreact"
],
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default",
"enableSsl": "Disabled"
},
"ssh": "Disabled",
"previewLimit": 50,
"server": "localhost",
"port": 3306,
"driver": "MySQL",
"name": "lcbp3_dev",
"database": "lcbp3_dev",
"username": "root"
}
] ]
} }

81
.vscode/tasks.json vendored
View File

@@ -0,0 +1,81 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Backend: Dev",
"type": "shell",
"command": "pnpm run start:dev",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "new"
}
},
{
"label": "Backend: Build",
"type": "shell",
"command": "pnpm run build",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"group": "build",
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "new"
}
},
{
"label": "Backend: Lint",
"type": "shell",
"command": "pnpm run lint",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "new"
}
},
{
"label": "Backend: Test",
"type": "shell",
"command": "pnpm run test",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"group": "test",
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "new"
}
},
{
"label": "Backend: Test E2E",
"type": "shell",
"command": "pnpm run test:e2e",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"group": "test",
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "new"
}
}
]
}

View File

@@ -339,32 +339,55 @@ CREATE TABLE users (
SET NULL SET NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)'; ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)';
-- Initial SUPER_ADMIN user -- Initial SUPER_ADMIN user
INSERT INTO users (username, password_hash, email, is_active) INSERT INTO `users` (
`user_id`,
`username`,
`password_hash`,
`first_name`,
`last_name`,
`email`,
`line_id`,
`primary_organization_id`
)
VALUES ( VALUES (
1,
'superadmin', 'superadmin',
'$2y$10$0kjBMxWq7E4G7P.dc8r5i.cjiPBiup553AsFpDfxUt31gKg9h', '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
'Super',
'Admin',
'superadmin @example.com', 'superadmin @example.com',
NULL,
NULL
),
(
2,
'admin',
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
'Admin',
'คคง.',
'admin@example.com',
NULL,
1 1
) ON DUPLICATE KEY ),
UPDATE email = (
VALUES(email), 3,
is_active =
VALUES(is_active);
-- Create editor01 user
INSERT IGNORE INTO users (username, password_hash, email, is_active)
VALUES (
'editor01', 'editor01',
'$2y$10$0kjBMxWq7E4G7P.dc8r5i.cjiPBiup553AsFpDfxUt31gKg9h', '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
'DC',
'C1',
'editor01 @example.com', 'editor01 @example.com',
1 NULL,
); 41
-- Create viewer01 user (password hash placeholder, must change later) ),
INSERT IGNORE INTO users (username, password_hash, email, is_active) (
VALUES ( 4,
'viewer01', 'viewer01',
'$2y$10$0kjBMxWq7E4G7P.dc8r5i.cjiPBiup553AsFpDfxUt31gKg9h', '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
'Viewer',
'สคฉ.03',
'viewer01 @example.com', 'viewer01 @example.com',
1 NULL,
10
); );
-- ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ -- ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ
CREATE TABLE roles ( CREATE TABLE roles (
@@ -903,6 +926,33 @@ CREATE TABLE user_assignments (
) -- สำหรับ Global scope ) -- สำหรับ Global scope
) )
); );
INSERT INTO `user_assignments` (
`id`,
`user_id`,
`role_id`,
`organization_id`,
`project_id`,
`contract_id`,
`assigned_by_user_id`
)
VALUES (
1,
1,
1,
NULL,
NULL,
NULL,
NULL
),
(
2,
2,
2,
1,
NULL,
NULL,
NULL
);
CREATE TABLE project_organizations ( CREATE TABLE project_organizations (
project_id INT NOT NULL, project_id INT NOT NULL,
organization_id INT NOT NULL, organization_id INT NOT NULL,
@@ -1127,12 +1177,7 @@ VALUES ('RFA', 'Request for Approval', 1, 1),
('MEMO', 'Memorandum', 7, 1), ('MEMO', 'Memorandum', 7, 1),
('MOM', 'Minutes of Meeting', 8, 1), ('MOM', 'Minutes of Meeting', 8, 1),
('NOTICE', 'Notice', 9, 1), ('NOTICE', 'Notice', 9, 1),
( ('OTHER', 'Other', 10, 1);
'OTHER',
'Other',
10,
1
);
-- ตาราง Master เก็บสถานะของเอกสาร -- ตาราง Master เก็บสถานะของเอกสาร
CREATE TABLE correspondence_status ( CREATE TABLE correspondence_status (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',

View File

@@ -0,0 +1,772 @@
# 📝 **Documents Management System Version 1.4.2: Application Requirements Specification**
**สถานะ:** FINAL
**วันที่:** 2025-11-19
**อ้างอิงพื้นฐาน:** v1.4.2
**Classification:** Internal Technical Documentation
## 📌 **1. วัตถุประสงค์**
สร้างเว็บแอปพลิเคชันสำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System - DMS) แบบครบวงจร ที่เน้นความปลอดภัยสูงสุด ความถูกต้องของข้อมูล (Data Integrity) และรองรับการขยายตัวในอนาคต (Scalability) โดยแก้ไขปัญหา Race Condition และเพิ่มความเสถียรในการจัดการไฟล์และ Workflow
- มีฟังก์ชันหลักในการอัปโหลด จัดเก็บ ค้นหา แชร์ และควบคุมสิทธิ์การเข้าถึงเอกสาร
- ช่วยลดการใช้เอกสารกระดาษ เพิ่มความปลอดภัยในการจัดเก็บข้อมูล
- เพิ่มความสะดวกในการทำงานร่วมกันระหว่างองกรณ์
- **เสริม:** ปรับปรุงความปลอดภัยของระบบด้วยมาตรการป้องกันที่ทันสมัย
- **เสริม:** เพิ่มความทนทานของระบบด้วยกลไก resilience patterns
- **เสริม:** สร้างระบบ monitoring และ observability ที่ครอบคลุม
## 🛠️ **2. สถาปัตยกรรมและเทคโนโลยี (System Architecture & Technology Stack)**
ใช้สถาปัตยกรรมแบบ Headless/API-First ที่ทันสมัย ทำงานทั้งหมดบน QNAP Server ผ่าน Container Station เพื่อความสะดวกในการจัดการและบำรุงรักษา
**Domain:** `np-dms.work`, `www.np-dms.work`
**IP:** 159.192.126.103
**Docker Network:** ทุก Service จะเชื่อมต่อผ่านเครือข่ายกลางชื่อ `lcbp3` เพื่อให้สามารถสื่อสารกันได้
### **2.1 Infrastructure & Environment:**
- **Server:** QNAP (Model: TS-473A, RAM: 32GB, CPU: AMD Ryzen V1500B)
- **Containerization:** Container Station (Docker & Docker Compose) ใช้ UI ของ Container Station เป็นหลัก ในการ configuration และการรัน docker command
- **Development Environment:** VS Code/Cursor on Windows 11
- **Data Storage:** `/share/dms-data` บน QNAP
- **ข้อจำกัด:** ไม่สามารถใช้ .env ในการกำหนดตัวแปรภายนอกได้ ต้องกำหนดใน docker-compose.yml เท่านั้น
### **2.2 การจัดการ Configuration (ปรับปรุง):**
- ใช้ `docker-compose.yml` สำหรับ environment variables ตามข้อจำกัดของ QNAP
- **Secrets Management (ใหม่):**
- ห้ามระบุ Sensitive Secrets (Password, Keys) ใน `docker-compose.yml` หลัก
- ต้องใช้ไฟล์ `docker-compose.override.yml` (ที่ถูก gitignore) สำหรับ Inject Environment Variables ที่เป็นความลับในแต่ละ Environment (Dev/Prod)
- ไฟล์ `docker-compose.yml` หลักให้ใส่ค่า Dummy หรือว่างไว้
- **แต่ต้องมี mechanism สำหรับจัดการ sensitive secrets อย่างปลอดภัย** โดยใช้:
- Docker secrets (ถ้ารองรับ)
- External secret management (Hashicorp Vault) หรือ
- Encrypted environment variables
- Development environment ยังใช้ .env ได้ แต่ต้องไม่ commit เข้า version control
- ต้องมี configuration validation during application startup
- ต้องแยก configuration ตาม environment (development, staging, production)
### **2.3 Core Services:**
- **Code Hosting:** Gitea (Self-hosted on QNAP)
- Application name: git
- Service name: gitea
- Domain: `git.np-dms.work`
- หน้าที่: เป็นศูนย์กลางในการเก็บและจัดการเวอร์ชันของโค้ด (Source Code) สำหรับทุกส่วน
- **Backend / Data Platform:** NestJS
- Application name: lcbp3-backend
- Service name: backend
- Domain: `backend.np-dms.work`
- Framework: NestJS (Node.js, TypeScript, ESM)
- หน้าที่: จัดการโครงสร้างข้อมูล (Data Models), สร้าง API, จัดการสิทธิ์ผู้ใช้ (Roles & Permissions), และสร้าง Workflow ทั้งหมดของระบบ
- **Database:** MariaDB 10.11
- Application name: lcbp3-db
- Service name: mariadb
- Domain: `db.np-dms.work`
- หน้าที่: ฐานข้อมูลหลักสำหรับเก็บข้อมูลทั้งหมด
- Tooling: DBeaver (Community Edition), phpmyadmin สำหรับการออกแบบและจัดการฐานข้อมูล
- **Database Management:** phpMyAdmin
- Application name: lcbp3-db
- Service: phpmyadmin:5-apache
- Service name: pma
- Domain: `pma.np-dms.work`
- หน้าที่: จัดการฐานข้อมูล mariadb ผ่าน Web UI
- **Frontend:** Next.js
- Application name: lcbp3-frontend
- Service name: frontend
- Domain: `lcbp3.np-dms.work`
- Framework: Next.js (App Router, React, TypeScript, ESM)
- Styling: Tailwind CSS + PostCSS
- Component Library: shadcn/ui
- หน้าที่: สร้างหน้าตาเว็บแอปพลิเคชันสำหรับให้ผู้ใช้งานเข้ามาดู Dashboard, จัดการเอกสาร, และติดตามงาน โดยจะสื่อสารกับ Backend ผ่าน API
- **Workflow Automation:** n8n
- Application name: lcbp3-n8n
- Service: n8nio/n8n:latest
- Service name: n8n
- Domain: `n8n.np-dms.work`
- หน้าที่: จัดการ workflow ระหว่าง Backend และ Line
- **Reverse Proxy:** Nginx Proxy Manager
- Application name: lcbp3-npm
- Service: Nginx Proxy Manager (nginx-proxy-manage: latest)
- Service name: npm
- Domain: `npm.np-dms.work`
- หน้าที่: เป็นด่านหน้าในการรับ-ส่งข้อมูล จัดการโดเมนทั้งหมด, ทำหน้าที่เป็น Proxy ชี้ไปยัง Service ที่ถูกต้อง, และจัดการ SSL Certificate (HTTPS) ให้อัตโนมัติ
- **Search Engine:** Elasticsearch
- **Cache:** Redis
### **2.4 Business Logic & Consistency (ปรับปรุง):**
- **2.4.1 ตรรกะทางธุรกิจที่ซับซ้อนทั้งหมด** (เช่น การเปลี่ยนสถานะ Workflow [cite: 3.5.4, 3.6.5], การบังคับใช้สิทธิ์ [cite: 4.4], การตรวจสอบ Deadline [cite: 3.2.5]) **จะถูกจัดการในฝั่ง Backend (NestJS)** [cite: 2.3] เพื่อให้สามารถบำรุงรักษาและทดสอบได้ง่าย (Testability)
- **2.4.2 Unified Workflow Engine (ใหม่):** รวม Logic การเดินเอกสารของ `CorrespondenceRouting` และ `RfaWorkflow` ให้ใช้ Core Engine เดียวกันเพื่อลดความซ้ำซ้อนและง่ายต่อการบำรุงรักษา
- **2.4.3 Idempotency Keys (ใหม่):** API ที่สำคัญ (เช่น Submit Document, Approve) ต้องบังคับส่ง Header `Idempotency-Key` เพื่อป้องกันการทำรายการซ้ำจากการกดปุ่มรัวๆ หรือ Network Retry
- **2.4.4 Optimistic Locking (ใหม่):** ใช้ Version Column ใน Database ควบคู่กับ Redis Lock สำหรับการสร้างเลขที่เอกสาร เพื่อเป็น Safety Net ชั้นสุดท้าย
- **2.4.5** **จะไม่มีการใช้ SQL Triggers** เพื่อป้องกันตรรกะซ่อนเร้น (Hidden Logic) และความซับซ้อนในการดีบัก
### **2.5 Data Migration และ Schema Versioning:**
- ต้องมี database migration scripts สำหรับทุก schema change โดยใช้ TypeORM migrations
- ต้องรองรับ rollback ของ migration ได้
- ต้องมี data seeding strategy สำหรับ environment ต่างๆ (development, staging, production)
- ต้องมี version compatibility between schema versions
- Migration scripts ต้องผ่านการทดสอบใน staging environment ก่อน production
- ต้องมี database backup ก่อนทำ migration ใน production
### **2.6 กลยุทธ์ความทนทานและการจัดการข้อผิดพลาด (Resilience & Error Handling Strategy)**
- 2.6.1 Circuit Breaker Pattern: ใช้สำหรับ external service calls (Email, LINE, Elasticsearch)
- 2.6.2 Retry Mechanism: ด้วย exponential backoff สำหรับ transient failures
- 2.6.3 Fallback Strategies: Graceful degradation เมื่อบริการภายนอกล้มเหลว
- 2.6.4 Error Handling: Error messages ต้องไม่เปิดเผยข้อมูล sensitive
- 2.6.5 Monitoring: Centralized error monitoring และ alerting system
## **📦 3. ข้อกำหนดด้านฟังก์ชันการทำงาน (Functional Requirements)**
### **3.1. การจัดการโครงสร้างโครงการและองค์กร**
- 3.1.1. โครงการ (Projects): ระบบต้องสามารถจัดการเอกสารภายในหลายโครงการได้ (ปัจจุบันมี 4 โครงการ และจะเพิ่มขึ้นในอนาคต)
- 3.1.2. สัญญา (Contracts): ระบบต้องสามารถจัดการเอกสารภายในแต่ละสัญญาได้ ในแต่ละโครงการ มีได้หลายสัญญา หรืออย่างน้อย 1 สัญญา
- 3.1.3. องค์กร (Organizations):
- มีหลายองค์กรในโครงการ องค์กรณ์ที่เป็น Owner, Designer และ Consultant สามารถอยู่ในหลายโครงการและหลายสัญญาได้
- Contractor จะถือ 1 สัญญา และอยู่ใน 1 โครงการเท่านั้น
### **3.2. การจัดการเอกสารโต้ตอบ (Correspondence Management)**
- 3.2.1. วัตถุประสงค์: เอกสารโต้ตอบ (correspondences) ระหว่างองกรณื-องกรณ์ ภายใน โครงการ (Projects) และระหว่าง องค์กร-องค์กร ภายนอก โครงการ (Projects), รองรับ To (ผู้รับหลัก) และ CC (ผู้รับสำเนา) หลายองค์กร
- 3.2.2. ประเภทเอกสาร: ระบบต้องรองรับเอกสารรูปแบบ ไฟล์ PDF หลายประเภท (Types) เช่น จดหมาย (Letter), อีเมล์ (Email), Request for Information (RFI), และสามารถเพิ่มประเภทใหม่ได้ในภายหลัง
- 3.2.3. การสร้างเอกสาร (Correspondence):
- ผู้ใช้ที่มีสิทธิ์ (เช่น Document Control) สามารถสร้างเอกสารรอไว้ในสถานะ ฉบับร่าง" (Draft) ได้ ซึ่งผู้ใช้งานต่างองค์กรจะมองไม่เห็น
- เมื่อกด "Submitted" แล้ว การแก้ไข, ถอนเอกสารกลับไปสถานะ Draft, หรือยกเลิก (Cancel) จะต้องทำโดยผู้ใช้ระดับ Admin ขึ้นไป พร้อมระบุเหตุผล
- 3.2.4. การอ้างอิงและจัดกลุ่ม:
- เอกสารสามารถอ้างถึง (Reference) เอกสารฉบับก่อนหน้าได้หลายฉบับ
- สามารถกำหนด Tag ได้หลาย Tag เพื่อจัดกลุ่มและใช้ในการค้นหาขั้นสูง
- 3.2.5. Correspondence Routing & Workflow
- 3.2.5.1 Routing Templates (แม่แบบการส่งต่อ)
- ผู้ดูแลระบบต้องสามารถสร้างแม่แบบการส่งต่อได้
- แม่แบบสามารถเป็นแบบทั่วไป (ใช้ได้ทุกโครงการ) หรือเฉพาะโครงการ
- แต่ละแม่แบบประกอบด้วยลำดับขั้นตอนการส่งต่อ
- การส่งจาก Originator -> Organization 1 -> Organization 2 -> Organization 3 แล้วส่งผลกลับตามลำดับเดิม (โดยถ้า องกรณ์ใดใน Wouting ให้ส่งกลับ ก็สามารถส่งผลกลับตามลำดับเดิมโดยไม่ต้องรอให้ถึง องกรณืในลำดับถัดไป)
- 3.2.5.2 Routing Steps (ขั้นตอนการส่งต่อ) แต่ละขั้นตอนในแม่แบบต้องกำหนด:
- **ลำดับขั้นตอน** (Sequence)
- **องค์กรผู้รับ** (To Organization)
- **วัตถุประสงค์** (Purpose): เพื่ออนุมัติ (FOR_APPROVAL), เพื่อตรวจสอบ (FOR_REVIEW), เพื่อทราบ (FOR_INFORMATION), เพื่อดำเนินการ (FOR_ACTION)
- **ระยะเวลาที่คาดหวัง** (Expected Duration)
- 3.2.5.3 Actual Routing Execution (การส่งต่อจริง) เมื่อสร้างเอกสารและเลือกใช้แม่แบบ ระบบต้อง:
- สร้างลำดับการส่งต่อตามแม่แบบ
- ติดตามสถานะของแต่ละขั้นตอน: ส่งแล้ว (SENT), กำลังดำเนินการ (IN_PROGRESS), ดำเนินการแล้ว (ACTIONED), ส่งต่อแล้ว (FORWARDED), ตอบกลับแล้ว (REPLIED)
- ระบุวันครบกำหนด (Due Date) สำหรับแต่ละขั้นตอน
- บันทึกผู้ดำเนินการและเวลาที่ดำเนินการ
- 3.2.5.4 Routing Flexibility (ความยืดหยุ่น)
- สามารถข้ามขั้นตอนได้ในกรณีพิเศษ (โดยผู้มีสิทธิ์)
- สามารถส่งกลับขั้นตอนก่อนหน้าได้
- สามารถเพิ่มความคิดเห็นในแต่ละขั้นตอน
- แจ้งเตือนอัตโนมัติเมื่อถึงขั้นตอนใหม่หรือใกล้ครบกำหนด
- 3.2.6. การจัดการ: มีการจัดการอย่างน้อยดังนี้
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบของ องกรณ์ ที่เป็นผู้รับได้
- มีระบบแจ้งเตือน ให้ผู้รับผิดชอบขององกรณ์ที่เป็น ผู้รับ/ผู้ส่ง ทราบ เมื่อมีเอกสารใหม่ หรือมีการเปลี่ยนสถานะ
### **3.3. การจัดกาแบบคู่สัญญา (Contract Drawing)**
- 3.3.1. วัตถุประสงค์: แบบคู่สัญญา (Contract Drawing) ใช้เพื่ออ้างอิงและใช้ในการตรวจสอบ
- 3.3.2. ประเภทเอกสาร: ไฟล์ PDF
- 3.3.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
- 3.3.4. การอ้างอิงและจัดกลุ่ม: ใช้สำหรับอ้างอิง ใน Shop Drawings, มีการจัดหมวดหมู่ของ Contract Drawing
### **3.4. การจัดกาแบบก่อสร้าง (Shop Drawing)**
- 3.4.1. วัตถุประสงค์: แบบก่อสร้าง (Shop Drawing) ใช้เในการตรวจสอบ โดยจัดส่งด้วย Request for Approval (RFA)
- 3.4.2. ประเภทเอกสาร: ไฟล์ PDF
- 3.4.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
- 3.4.4. การอ้างอิงและจัดกลุ่ม: ช้สำหรับอ้างอิง ใน Shop Drawings, มีการจัดหมวดหมู่ของ Shop Drawings
### **3.5. การจัดการเอกสารขออนุมัติ (Request for Approval & Workflow)**
- 3.5.1. วัตถุประสงค์: เอกสารขออนุมัติ (Request for Approval) ใช้ในการส่งเอกสารเพิอขออนุมัติ
- 3.5.2. ประเภทเอกสาร: Request for Approval (RFA) เป็นชนิดหนึ่งของ Correspondence ที่มีลักษณะเฉพาะที่ต้องได้รับการอนุมัติ มีประเภทดังนี้:
- Request for Drawing Approval (RFA_DWG)
- Request for Document Approval (RFA_DOC)
- Request for Method statement Approval (RFA_MES)
- Request for Material Approval (RFA_MAT)
- 3.5.2. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
- 3.5.4. การอ้างอิงและจัดกลุ่ม: การจัดการ Drawing (RFA_DWG):
- เอกสาร RFA_DWG จะประกอบไปด้วย Shop Drawing (shop_drawings) หลายแผ่น ซึ่งแต่ละแผ่นมี Revision ของตัวเอง
- Shop Drawing แต่ละ Revision สามารถอ้างอิงถึง Contract Drawing (Ccontract_drawings) หลายแผ่น หรือไม่อ้างถึงก็ได้
- ระบบต้องมีส่วนสำหรับจัดการข้อมูล Master Data ของทั้ง Shop Drawing และ Contract Drawing แยกจากกัน
- 3.5.5. Workflow การอนุมัติ: ต้องรองรับกระบวนการอนุมัติที่ซับซ้อนและเป็นลำดับ เช่น
- ส่งจาก Originator -> Organization 1 -> Organization 2 -> Organization 3 แล้วส่งผลกลับตามลำดับเดิม (โดยถ้า องกรณ์ใดใน Workflow ให้ส่งกลับ ก็สามารถส่งผลกลับตามลำดับเดิมโดยไม่ต้องรอให้ถึง องกรณืในลำดับถัดไป)
- 3.5.6. การจัดการ: มีการจัดการอย่างน้อยดังนี้
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบของ องกรณ์ ที่อยู่ใน Workflow ได้
- มีระบบแจ้งเตือน ให้ผู้รับผิดชอบของ องกรณ์ ที่อยู่ใน Workflow ทราบ เมื่อมี RFA ใหม่ หรือมีการเปลี่ยนสถานะ
### **3.6.การจัดการเอกสารนำส่ง (Transmittals)**
- 3.6.1. วัตถุประสงค์: เอกสารนำส่ง ใช้สำหรับ นำส่ง Request for Approval (RFAS) หลายฉบับ ไปยังองค์กรอื่น
- 3.6.2. ประเภทเอกสาร: ไฟล์ PDF
- 3.6.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
- 3.6.4. การอ้างอิงและจัดกลุ่ม: เอกสารนำส่ง เป็นส่วนหนึ่งใน Correspondence
### **3.7. ใบเวียนเอกสาร (Circulation Sheet)**
- 3.7.1. วัตถุประสงค์: การสื่อสาร เอกสาร (Correspondence) ทุกฉบับ จะมีใบเวียนเอกสารเพื่อควบคุมและมอบหมายงานภายในองค์กร (สามารถดูและแก้ไขได้เฉพาะคนในองค์กร)
- 3.7.2. ประเภทเอกสาร: ไฟล์ PDF
- 3.7.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ในองค์กรนั้น สามารถสร้างและแก้ไขได้
- 3.7.4. การอ้างอิงและจัดกลุ่ม: การระบุผู้รับผิดชอบ:
- ผู้รับผิดชอบหลัก (Main): มีได้หลายคน
- ผู้ร่วมปฏิบัติงาน (Action): มีได้หลายคน
- ผู้ที่ต้องรับทราบ (Information): มีได้หลายคน
- 3.7.5. การติดตามงาน:
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบประเภท Main และ Action ได้
- มีระบบแจ้งเตือนเมื่อมี Circulation ใหม่ และแจ้งเตือนล่วงหน้าก่อนถึงวันแล้วเสร็จ
- สามารถปิด Circulation ได้เมื่อดำเนินการตอบกลับไปยังองค์กรผู้ส่ง (Originator) แล้ว หรือ รับทราบแล้ว (For Information)
### **3.8. ประวัติการแก้ไข (Revisions):** ระบบจะเก็บประวัติการสร้างและแก้ไข เอกสารทั้งหมด
### **3.9. การจัดเก็บไฟล์ (File Handling - ปรับปรุงใหญ่)**
- **3.9.1 Two-Phase Storage Strategy:**
1. **Phase 1 (Upload):** ไฟล์ถูกอัปโหลดเข้าโฟลเดอร์ `temp/` และได้รับ `temp_id`
2. **Phase 2 (Commit):** เมื่อ User กด Submit ฟอร์มสำเร็จ ระบบจะย้ายไฟล์จาก `temp/` ไปยัง `permanent/{YYYY}/{MM}/` และบันทึกลง Database ภายใน Transaction เดียวกัน
3. **Cleanup:** มี Cron Job ลบไฟล์ใน `temp/` ที่ค้างเกิน 24 ชม. (Orphan Files)
- **3.9.2 Security:**
- Virus Scan (ClamAV) ก่อนย้ายเข้า Permanent
- Whitelist File Types: PDF, DWG, DOCX, XLSX, ZIP
- Max Size: 50MB
- Access Control: ตรวจสอบสิทธิ์ผ่าน Junction Table ก่อนให้ Download Link
- **3.9.3 ความปลอดภัยของการจัดเก็บไฟล์:**
- ต้องมีการ scan virus สำหรับไฟล์ที่อัปโหลดทั้งหมด โดยใช้ ClamAV หรือบริการ third-party
- จำกัดประเภทไฟล์ที่อนุญาต: PDF, DWG, DOCX, XLSX, ZIP (ต้องระบุรายการที่ชัดเจน)
- ขนาดไฟล์สูงสุด: 50MB ต่อไฟล์
- ไฟล์ต้องถูกเก็บนอก web root และเข้าถึงได้ผ่าน authenticated endpoint เท่านั้น
- ต้องมี file integrity check (checksum) เพื่อป้องกันการแก้ไขไฟล์
- Download links ต้องมี expiration time (default: 24 ชั่วโมง)
- ต้องบันทึก audit log ทุกครั้งที่มีการดาวน์โหลดไฟล์สำคัญ
### **3.10. การจัดการเลขที่เอกสาร (Document Numbering - ปรับปรุง)**
- 3.10.1. ระบบต้องสามารถสร้างเลขที่เอกสาร (เช่น correspondence_number) ได้โดยอัตโนมัติ
- 3.10.2. การนับเลข Running Number (SEQ) จะต้องนับแยกตาม Key ดังนี้: **โครงการ (Project)**, **องค์กรผู้ส่ง (Originator Organization)**, **ประเภทเอกสาร (Document Type)** และ **ปีปัจจุบัน (Year)**
- 3.10.3. ผู้ดูแลระบบ (Admin) ต้องสามารถกำหนด "รูปแบบ" (Format Template) ของเลขที่เอกสารได้ (เช่น {ORG_CODE}-{TYPE_CODE}-{YEAR_SHORT}-{SEQ:4}) โดยกำหนดแยกตามโครงการและประเภทเอกสาร
- 3.10.4. **กลไก:** ใช้ **Redis Distributed Lock** เป็นด่านแรก
- 3.10.5. **Safety Net:** เพิ่ม **Optimistic Locking** (ตรวจสอบ Version/Last Number ใน DB ขณะ Update) เพื่อป้องกันกรณี Redis ล่ม หรือ Race Condition หลุดรอด
- 3.10.6. ต้องมี retry mechanism และ fallback strategy เมื่อการ generate เลขที่เอกสารล้มเหลว
### **3.11 การจัดการ JSON Details (JSON & Performance - ปรับปรุง)**
- **3.11.1 วัตถุประสงค์**
- จัดเก็บข้อมูลแบบไดนามิกที่เฉพาะเจาะจงกับแต่ละประเภทของเอกสาร
- รองรับการขยายตัวของระบบโดยไม่ต้องเปลี่ยนแปลง database schema
- จัดการ metadata และข้อมูลประกอบสำหรับ correspondence, routing, และ workflows
- **3.11.2 โครงสร้าง JSON Schema**
ระบบต้องมี predefined JSON schemas สำหรับประเภทเอกสารต่างๆ:
- **3.11.2.1 Correspondence Types**
- **GENERIC**: ข้อมูลพื้นฐานสำหรับเอกสารทั่วไป
- **RFI**: รายละเอียดคำถามและข้อมูลทางเทคนิค
- **RFA**: ข้อมูลการขออนุมัติแบบและวัสดุ
- **TRANSMITTAL**: รายการเอกสารที่ส่งต่อ
- **LETTER**: ข้อมูลจดหมายทางการ
- **EMAIL**: ข้อมูลอีเมล
- **3.11.2.2 Routing Types**
- **ROUTING_TEMPLATE**: กฎและเงื่อนไขการส่งต่อ
- **ROUTING_INSTANCE**: สถานะและประวัติการส่งต่อ
- **ROUTING_ACTION**: การดำเนินการในแต่ละขั้นตอน
- **3.11.2.3 Audit Types**
- **AUDIT_LOG**: ข้อมูลการตรวจสอบ
- **SECURITY_SCAN**: ผลการตรวจสอบความปลอดภัย
- **3.11.3 Virtual Columns (ใหม่):** สำหรับ Field ใน JSON ที่ต้องใช้ในการค้นหา (Search) หรือจัดเรียง (Sort) บ่อยๆ **ต้องสร้าง Generated Column (Virtual Column)** ใน Database และทำ Index ไว้ เพื่อประสิทธิภาพสูงสุด
- **3.11.4 Validation Rules**
- ต้องมี JSON schema validation สำหรับแต่ละประเภท
- ต้องรองรับ versioning ของ schema
- ต้องมี default values สำหรับ field ที่ไม่บังคับ
- ต้องตรวจสอบ data types และ format ให้ถูกต้อง
- **3.11.5 Performance Requirements**
- JSON field ต้องมีขนาดไม่เกิน 50KB
- ต้องรองรับ indexing สำหรับ field ที่ใช้ค้นหาบ่อย
- ต้องมี compression สำหรับ JSON ขนาดใหญ่
- **3.11.6 Security Requirements**
- ต้อง sanitize JSON input เพื่อป้องกัน injection attacks
- ต้อง validate JSON structure ก่อนบันทึก
- ต้อง encrypt sensitive data ใน JSON fields
### **3.12 ข้อกำหนดพิเศษ**
- **3.12.1. การจัดการเอกสารโต้ตอบ (Correspondence Management)**
- ผู้ใช้งานที่มีสิทธิ์ระดับสูง (Superadmin) หรือผู้ได้รับอนุญาตเป็นกรณีพิเศษ สามารถเลือก 'สร้างในนามองค์กร (Create on behalf of)' ได้ เพื่อให้สามารถออกเลขที่เอกสาร (Running Number) ขององค์กรอื่นได้โดยไม่ต้องล็อกอินใหม่
- **3.12.2 การสร้างเอกสารขออนุมัติ**
- ผู้ใช้งานที่มีสิทธิ์ระดับสูง (Superadmin) หรือผู้ได้รับอนุญาตเป็นกรณีพิเศษ สามารถเลือก 'สร้างในนามองค์กร (Create on behalf of)' ได้ เพื่อให้สามารถออกเลขที่เอกสาร (Running Number) ขององค์กรอื่นได้โดยไม่ต้องล็อกอินใหม่
- **3.12.3 การจัดการเอกสารนำส่ง (Transmittals)**
- Workflow การอนุมัติ
- **3.12.4 ใบเวียนเอกสาร (Circulation Sheet)**
- Routing & Workflow
## **🔐 4. ข้อกำหนดด้านสิทธิ์และการเข้าถึง (Access Control Requirements)**
### **4.1. ภาพรวม:** ผู้ใช้และองค์กรสามารถดูและแก้ไขเอกสารได้ตามสิทธิ์ที่ได้รับ โดยระบบสิทธิ์จะเป็นแบบ Role-Based Access Control (RBAC)
### **4.2. ลำดับชั้นของสิทธิ์ (Permission Hierarchy)**
- Global: สิทธิ์สูงสุดของระบบ
- Organization: สิทธิ์ภายในองค์กร เป็นสิทธิ์พื้นฐานของผู้ใช้
- Project: สิทธิ์เฉพาะในโครงการ จะถูกพิจารณาเมื่อผู้ใช้อยู่ในโครงการนั้น
- Contract: สิทธิ์เฉพาะในสัญญา จะถูกพิจารณาเมื่อผู้ใช้อยู่ในสัญญานั้น (สัญญาเป็นส่วนหนึ่งของโครงการ)
กฎการบังคับใช้: เมื่อตรวจสอบสิทธิ์ ระบบจะพิจารณาสิทธิ์จากทุกระดับที่ผู้ใช้มี และใช้ สิทธิ์ที่มากที่สุด (Most Permissive) เป็นตัวตัดสิน
ตัวอย่าง: ผู้ใช้ A เป็น Viewer ในองค์กร แต่ถูกมอบหมายเป็น Editor ในโครงการ X เมื่ออยู่ในโครงการ X ผู้ใช้ A จะมีสิทธิ์แก้ไขได้
### **4.3. การกำหนดบทบาท (Roles) และขอบเขต (Scope)**
| บทบาท (Role) | ขอบเขต (Scope) | คำอธิบาย | สิทธิ์หลัก (Key Permissions) |
| :------------------- | :------------- | :------------------ | :----------------------------------------------------------------------------- |
| **Superadmin** | Global | ผู้ดูแลระบบสูงสุด | ทำทุกอย่างในระบบ, จัดการองค์กร, จัดการข้อมูลหลักระดับ Global |
| **Org Admin** | Organization | ผู้ดูแลองค์กร | จัดการผู้ใช้ในองค์กร, จัดการบทบาท/สิทธิ์ภายในองค์กร, ดูรายงานขององค์กร |
| **Document Control** | Organization | ควบคุมเอกสารขององค์กร | เพิ่ม/แก้ไข/ลบเอกสาร, กำหนดสิทธิ์เอกสารภายในองค์กร |
| **Editor** | Organization | ผู้แก้ไขเอกสารขององค์กร | เพิ่ม/แก้ไขเอกสารที่ได้รับมอบหมาย |
| **Viewer** | Organization | ผู้ดูเอกสารขององค์กร | ดูเอกสารที่มีสิทธิ์เข้าถึง |
| **Project Manager** | Project | ผู้จัดการโครงการ | จัดการสมาชิกในโครงการ (เพิ่ม/ลบ/มอบบทบาท), สร้าง/จัดการสัญญาในโครงการ, ดูรายงานโครงการ |
| **Contract Admin** | Contract | ผู้ดูแลสัญญา | จัดการสมาชิกในสัญญา, สร้าง/จัดการข้อมูลหลักเฉพาะสัญญา (ถ้ามี), อนุมัติเอกสารในสัญญา |
### **4.4. Token Management (ปรับปรุง)**
- **Payload Optimization:** ใน JWT Access Token ให้เก็บเฉพาะ `userId` และ `scope` ปัจจุบันเท่านั้น
- **Permission Caching:** สิทธิ์ละเอียด (Permissions List) ให้เก็บใน **Redis** และดึงมาตรวจสอบเมื่อ Request เข้ามา เพื่อลดขนาด Token และเพิ่มความเร็ว
### **4.5. กระบวนการเริ่มต้นใช้งาน (Onboarding Workflow) ที่สมบูรณ์**
- **4.5.1. สร้างองค์กร (Organization)**
- **Superadmin** สร้างองค์กรใหม่ (เช่น บริษัท A)
- **Superadmin** แต่งตั้งผู้ใช้อย่างน้อย 1 คนให้เป็น **Org Admin** หรือ **Document Control** ของบริษัท A
- **4.5.2. เพิ่มผู้ใช้ในองค์กร**
- **Org Admin** ของบริษัท A เพิ่มผู้ใช้อื่นๆ (Editor, Viewer) เข้ามาในองค์กรของตน
- **4.5.3. มอบหมายผู้ใช้ให้กับโครงการ (Project)**
- **Project Manager** ของโครงการ X (ซึ่งอาจมาจากบริษัท A หรือบริษัทอื่น) ทำการ "เชิญ" หรือ "มอบหมาย" ผู้ใช้จากองค์กรต่างๆ ที่เกี่ยวข้องเข้ามาในโครงการ X
- ในขั้นตอนนี้ **Project Manager** จะกำหนด **บทบาทระดับโครงการ** (เช่น Project Member, หรืออาจไม่มีบทบาทพิเศษ ให้ใช้สิทธิ์จากระดับองค์กรไปก่อน)
- **4.5.4. เมอบหมายผู้ใช้ให้กับสัญญา (Contract)**
- **Contract Admin** ของสัญญา Y (ซึ่งเป็นส่วนหนึ่งของโครงการ X) ทำการเลือกผู้ใช้ที่อยู่ในโครงการ X แล้ว มอบหมายให้เข้ามาในสัญญา Y
- ในขั้นตอนนี้ **Contract Admin** จะกำหนด **บทบาทระดับสัญญา** (เช่น Contract Member) และสิทธิ์เฉพาะที่จำเป็น
- **4.5.5 Security Onboarding:**
- ต้องบังคับเปลี่ยน password ครั้งแรกสำหรับผู้ใช้ใหม่
- ต้องมี security awareness training สำหรับผู้ใช้ที่มีสิทธิ์สูง
- ต้องมี process สำหรับการรีเซ็ต password ที่ปลอดภัย
- ต้องบันทึก audit log ทุกครั้งที่มีการเปลี่ยนแปลง permissions
### **4.6. การจัดการข้อมูลหลัก (Master Data Management) ที่แบ่งตามระดับ**
| ข้อมูลหลัก | ผู้มีสิทธิ์จัดการ | ระดับ |
| :---------------------------------- | :------------------------------ | :------------------------------ |
| ประเภทเอกสาร (Correspondence, RFA) | **Superadmin** | Global |
| สถานะเอกสาร (Draft, Approved, etc.) | **Superadmin** | Global |
| หมวดหมู่แบบ (Shop Drawing) | **Project Manager** | Project (สร้างใหม่ได้ภายในโครงการ) |
| Tags | **Org Admin / Project Manager** | Organization / Project |
| บทบาทและสิทธิ์ (Custom Roles) | **Superadmin / Org Admin** | Global / Organization |
| Document Numbering Formats | **Superadmin / Admin** | Global / Organization |
## **👥 5. ข้อกำหนดด้านผู้ใช้งาน (User Interface & Experience)**
### **5.1. Layout หลัก:** หน้าเว็บใช้รูปแบบ App Shell ที่ประกอบด้วย
- Navbar (ส่วนบน): แสดงชื่อระบบ, เมนูผู้ใช้ (Profile), เมนูสำหรับ Document Control/เมนูสำหรับ Admin/Superadmin (จัดการผู้ใช้, จัดการสิทธิ์), และปุ่ม Login/Logout
- Sidebar (ด้านข้าง): เป็นเมนูหลักสำหรับเข้าถึงส่วนที่เกี่ยวข้องกับเอกสารทั้งหมด เช่น Dashboard, Correspondences, RFA, Drawings
- Main Content Area: พื้นที่สำหรับแสดงเนื้อหาหลักของหน้าที่เลือก
### **5.2. หน้า Landing Page:** เป็นหน้าแรกที่แสดงข้อมูลบางส่วนของโครงการสำหรับผู้ใช้ที่ยังไม่ได้ล็อกอิน
### **5.3. หน้า Dashboard:** เป็นหน้าแรกหลังจากล็อกอิน ประกอบด้วย
- การ์ดสรุปภาพรวม (KPI Cards): แสดงข้อมูลสรุปที่สำคัญขององค์กร เช่น จำนวนเอกสาร, งานที่เกินกำหนด
- ตาราง "งานของฉัน" (My Tasks Table): แสดงรายการงานทั้งหมดจาก Circulation ที่ผู้ใช้ต้องดำเนินการ
- Security Metrics: แสดงจำนวน files scanned, security incidents, failed login attempts
### **5.4. การติดตามสถานะ:** องค์กรสามารถติดตามสถานะเอกสารทั้งของตนเอง (Originator) และสถานะเอกสารที่ส่งมาถึงตนเอง (Recipient)
### **5.5. การจัดการข้อมูลส่วนตัว (Profile Page):** ผู้ใช้สามารถจัดการข้อมูลส่วนตัวและเปลี่ยนรหัสผ่านของตนเองได้
### **5.6. การจัดการเอกสารทางเทคนิค (RFA & Workflow):** ผู้ใช้สามารถดู RFA ในรูปแบบ Workflow ทั้งหมดได้ในหน้าเดียว, ขั้นตอนที่ยังไม่ถึงหรือผ่านไปแล้วจะเป็นรูปแบบ diable, สามารถดำเนินการได้เฉพาะในขั้นตอนที่ได้รับมอบหมายงาน (active) เช่น ตรวจสอบแล้ว เพื่อไปยังขั้นตอนต่อไป, สิทธิ์ Document Control ขึ้นไป สามรถกด ไปยังขั้นตอนต่อไป ได้ทุกขั้นตอน, การย้อนกลับ ไปขั้นตอนก่อนหน้า สามารถทำได้โดย สิทธิ์ Document Control ขึ้นไป
### **5.7. การจัดการใบเวียนเอกสาร (Circulation):** ผู้ใช้สามารถดู Circulation ในรูปแบบ Workflow ทั้งหมดได้ในหน้าเดียว,ขั้นตอนที่ยังไม่ถึงหรือผ่านไปแล้วจะเป็นรูปแบบ diable, สามารถดำเนินการได้เฉพาะในขั้นตอนที่ได้รับมอบหมายงาน (active) เช่น ตรวจสอบแล้ว เพื่อไปยังขั้นตอนต่อไป, สิทธิ์ Document Control ขึ้นไป สามรถกด ไปยังขั้นตอนต่อไป ได้ทุกขั้นตอน, การย้อนกลับ ไปขั้นตอนก่อนหน้า สามารถทำได้โดย สิทธิ์ Document Control ขึ้นไป
### **5.8. การจัดการเอกสารนำส่ง (Transmittals):** ผู้ใช้สามารถดู Transmittals ในรูปแบบรายการทั้งหมดได้ในหน้าเดียว
### **5.9. ข้อกำหนด UI/UX การแนบไฟล์ (File Attachment UX):**
- ระบบต้องรองรับการอัปโหลดไฟล์หลายไฟล์พร้อมกัน (Multi-file upload) เช่น การลากและวาง (Drag-and-Drop)
- ในหน้าอัปโหลด (เช่น สร้าง RFA หรือ Correspondence) ผู้ใช้ต้องสามารถกำหนดได้ว่าไฟล์ใดเป็น "เอกสารหลัก" (Main Document เช่น PDF) และไฟล์ใดเป็น "เอกสารแนบประกอบ" (Supporting Attachments เช่น .dwg, .docx, .zip)
- **Security Feedback:** แสดง security warnings สำหรับ file types ที่เสี่ยงหรือ files ที่ fail virus scan
- **File Type Indicators:** แสดง file type icons และ security status
### **5.10 Form & Interaction (ใหม่)**
- **Dynamic Form Generator:** ใช้ Component กลางที่รับ JSON Schema แล้ว Render Form ออกมาอัตโนมัติ เพื่อลดความซ้ำซ้อนของโค้ดหน้าบ้าน และรองรับเอกสารประเภทใหม่ๆ ได้ทันที
- **Optimistic Updates:** การเปลี่ยนสถานะ (เช่น กด Approve, กด Read) ให้ UI เปลี่ยนสถานะทันทีให้ผู้ใช้เห็นก่อนรอ API Response (Rollback ถ้า Failed)
### **5.11 Mobile Responsiveness (ใหม่)**
- **Table Visualization:** บนหน้าจอมือถือ ตารางข้อมูลที่มีหลาย Column (เช่น Correspondence List) ต้องเปลี่ยนการแสดงผลเป็นแบบ **Card View** อัตโนมัติ
- **Navigation:** Sidebar ต้องเป็นแบบ Collapsible Drawer
### **5.12 Resilience & Offline Support (ใหม่)**
- **Auto-Save Draft:** ระบบต้องบันทึกข้อมูลฟอร์มที่กำลังกรอกลง **LocalStorage** อัตโนมัติ เพื่อป้องกันข้อมูลหายกรณีเน็ตหลุดหรือปิด Browser โดยไม่ได้ตั้งใจ
- **Graceful Degradation:** หาก Service รอง (เช่น Search, Notification) ล่ม ระบบหลัก (CRUD) ต้องยังทำงานต่อได้
## **🛡️ 6. ข้อกำหนดที่ไม่ใช่ฟังก์ชันการทำงาน (Non-Functional Requirements)**
### **6.1. การบันทึกการกระทำ (Audit Log):** ทุกการกระทำที่สำคัญของผู้ใช้ (สร้าง, แก้ไข, ลบ, ส่ง) จะถูกบันทึกไว้ใน audit_logs เพื่อการตรวจสอบย้อนหลัง
- **6.1.1 ขอบเขตการบันทึก Audit Log:**
- ทุกการสร้าง/แก้ไข/ลบ ข้อมูลสำคัญ (correspondences, RFAs, drawings, users, permissions)
- ทุกการเข้าถึงข้อมูล sensitive (user data, financial information)
- ทุกการเปลี่ยนสถานะ workflow (status transitions)
- ทุกการดาวน์โหลดไฟล์สำคัญ (contract documents, financial reports)
- ทุกการเปลี่ยนแปลง permission และ role assignment
- ทุกการล็อกอินที่สำเร็จและล้มเหลว
- ทุกการส่งคำขอ API ที่สำคัญ
- **6.1.2 ข้อมูลที่ต้องบันทึกใน Audit Log:**
- ผู้ใช้งาน (user_id)
- การกระทำ (action)
- ชนิดของ entity (entity_type)
- ID ของ entity (entity_id)
- ข้อมูลก่อนการเปลี่ยนแปลง (old_values) - สำหรับ update operations
- ข้อมูลหลังการเปลี่ยนแปลง (new_values) - สำหรับ update operations
- IP address
- User agent
- Timestamp
- Request ID สำหรับ tracing
### **6.2. Data Archiving & Partitioning (ใหม่)**
- สำหรับตารางที่มีขนาดใหญ่และโตเร็ว (เช่น `audit_logs`, `notifications`, `correspondence_revisions`) ต้องออกแบบโดยรองรับ **Table Partitioning** (แบ่งตาม Range วันที่ หรือ List) เพื่อประสิทธิภาพในระยะยาว
### **6.3. การค้นหา (Search):** ระบบต้องมีฟังก์ชันการค้นหาขั้นสูง ที่สามารถค้นหาเอกสาร **correspondence**, **rfa**, **shop_drawing**, **contract-drawing**, **transmittal** และ **ใบเวียน (Circulations)** จากหลายเงื่อนไขพร้อมกันได้ เช่น ค้นหาจากชื่อเรื่อง, ประเภท, วันที่, และ Tag
### **6.4. การทำรายงาน (Reporting):** สามารถจัดทำรายงานสรุปแยกประเภทของ Correspondence ประจำวัน, สัปดาห์, เดือน, และปีได้
### **6.5. ประสิทธิภาพ (Performance):** มีการใช้ Caching กับข้อมูลที่เรียกใช้บ่อย และใช้ Pagination ในตารางข้อมูลเพื่อจัดการข้อมูลจำนวนมาก
- **6.5.1 ตัวชี้วัดประสิทธิภาพ:**
- **API Response Time:** < 200ms (90th percentile) สำหรับ operation ทั่วไป
- **Search Query Performance:** < 500ms สำหรับการค้นหาขั้นสูง
- **File Upload Performance:** < 30 seconds สำหรับไฟล์ขนาด 50MB
- **Concurrent Users:** รองรับผู้ใช้พร้อมกันอย่างน้อย 100 คน
- **Database Connection Pool:** ขนาดเหมาะสมกับ workload (default: min 5, max 20 connections)
- **Cache Hit Ratio:** > 80% สำหรับ cached data
- **Application Startup Time:** < 30 seconds
- **6.5.2 Caching Strategy:**
- **Master Data Cache:** Roles, Permissions, Organizations, Project metadata (TTL: 1 hour)
- **User Session Cache:** User permissions และ profile data (TTL: 30 minutes)
- **Search Result Cache:** Frequently searched queries (TTL: 15 minutes)
- **File Metadata Cache:** Attachment metadata (TTL: 1 hour)
- **Document Cache:** Frequently accessed document metadata (TTL: 30 minutes)
- **ต้องมี cache invalidation strategy ที่ชัดเจน:**
- Invalidate on update/delete operations
- Time-based expiration
- Manual cache clearance สำหรับ admin operations
- ใช้ Redis เป็น distributed cache backend
- ต้องมี cache monitoring (hit/miss ratios)
### **6.6. ความปลอดภัย (Security):**
- มีระบบ Rate Limiting เพื่อป้องกันการโจมตีแบบ Brute-force
- การจัดการ Secret (เช่น รหัสผ่าน DB, JWT Secret) จะต้องทำผ่าน Environment Variable ของ Docker เพื่อความปลอดภัยสูงสุด
- **6.6.1 Rate Limiting Strategy:**
- **Anonymous Endpoints:** 100 requests/hour ต่อ IP address
- **Authenticated Endpoints:**
- Viewer: 500 requests/hour
- Editor: 1000 requests/hour
- Document Control: 2000 requests/hour
- Admin/Superadmin: 5000 requests/hour
- **File Upload Endpoints:** 50 requests/hour ต่อ user
- **Search Endpoints:** 500 requests/hour ต่อ user
- **Authentication Endpoints:** 10 requests/minute ต่อ IP address
- **ต้องมี mechanism สำหรับยกเว้น rate limiting สำหรับ trusted services**
- ต้องบันทึก log เมื่อมีการ trigger rate limiting
- **6.6.2 Error Handling และ Resilience:**
- ต้องมี circuit breaker pattern สำหรับ external service calls
- ต้องมี retry mechanism ด้วย exponential backoff
- ต้องมี graceful degradation เมื่อบริการภายนอกล้มเหลว
- Error messages ต้องไม่เปิดเผยข้อมูล sensitive
- **6.6.3 Input Validation:**
- ต้องมี input validation ทั้งฝั่ง client และ server (defense in depth)
- ต้องป้องกัน OWASP Top 10 vulnerabilities:
- SQL Injection (ใช้ parameterized queries ผ่าน ORM)
- XSS (input sanitization และ output encoding)
- CSRF (CSRF tokens สำหรับ state-changing operations)
- ต้อง validate file uploads:
- File type (white-list approach)
- File size
- File content (magic number verification)
- ต้อง sanitize user inputs ก่อนแสดงผลใน UI
- ต้องใช้ content security policy (CSP) headers
- ต้องมี request size limits เพื่อป้องกัน DoS attacks
- **6.6.4 Session และ Token Management:**
- **JWT token expiration:** 8 hours สำหรับ access token
- **Refresh token expiration:** 7 days
- **Refresh token mechanism:** ต้องรองรับ token rotation และ revocation
- **Token revocation on logout:** ต้องบันทึก revoked tokens จนกว่าจะ expire
- **Concurrent session management:**
- จำกัดจำนวน session พร้อมกันได้ (default: 5 devices)
- ต้องแจ้งเตือนเมื่อมี login จาก device/location ใหม่
- **Device fingerprinting:** สำหรับ security และ audit purposes
- **Password policy:**
- ความยาวขั้นต่ำ: 8 characters
- ต้องมี uppercase, lowercase, number, special character
- ต้องเปลี่ยน password ทุก 90 วัน
- ต้องป้องกันการใช้ password ที่เคยใช้มาแล้ว 5 ครั้งล่าสุด
### **6.7. การสำรองข้อมูลและการกู้คืน (Backup & Recovery):**
- ระบบจะต้องมีกลไกการสำรองข้อมูลอัตโนมัติสำหรับฐานข้อมูล MariaDB [cite: 2.4] และไฟล์เอกสารทั้งหมดใน /share/dms-data [cite: 2.1] (เช่น ใช้ HBS 3 ของ QNAP หรือสคริปต์สำรองข้อมูล) อย่างน้อยวันละ 1 ครั้ง
- ต้องมีแผนการกู้คืนระบบ (Disaster Recovery Plan) ในกรณีที่ Server หลัก (QNAP) ใช้งานไม่ได้
- **6.7.1 ขั้นตอนการกู้คืน:**
- **Database Restoration Procedure:**
- สร้างจาก full backup ล่าสุด
- Apply transaction logs ถึง point-in-time ที่ต้องการ
- Verify data integrity post-restoration
- **File Storage Restoration Procedure:**
- Restore จาก QNAP snapshot หรือ backup
- Verify file integrity และ permissions
- **Application Redeployment Procedure:**
- Deploy จาก version ล่าสุดที่รู้ว่าทำงานได้
- Verify application health
- **Data Integrity Verification Post-Recovery:**
- Run data consistency checks
- Verify critical business data
- **Recovery Time Objective (RTO):** < 4 ชั่วโมง
- **Recovery Point Objective (RPO):** < 1 ชั่วโมง
### **6.8. กลยุทธ์การแจ้งเตือน (Notification Strategy - ปรับปรุง):**
- **6.8.1 ระบบจะส่งการแจ้งเตือน (ผ่าน Email หรือ Line [cite: 2.7]) เมื่อมีการกระทำที่สำคัญ** ดังนี้:
1. เมื่อมีเอกสารใหม่ (Correspondence, RFA) ถูกส่งมาถึงองค์กรณ์ของเรา
2. เมื่อมีใบเวียน (Circulation) ใหม่ มอบหมายงานมาที่เรา
3. (ทางเลือก) เมื่อเอกสารที่เราส่งไป ถูกดำเนินการ (เช่น อนุมัติ/ปฏิเสธ)
4. (ทางเลือก) เมื่อใกล้ถึงวันครบกำหนด (Deadline) [cite: 3.2.5, 3.6.6, 3.7.5]
- **6.8.2 Grouping/Digest (ใหม่):** กรณีมีการแจ้งเตือนประเภทเดียวกันจำนวนมากในช่วงเวลาสั้นๆ (เช่น Approve เอกสาร 10 ฉบับรวด) ระบบต้อง **รวม (Batch)** เป็น 1 Email/Line Notification เพื่อไม่ให้รบกวนผู้ใช้ (Spamming)
- **6.8.3 Notification Delivery Guarantees:**
- **At-least-once delivery:** สำหรับ important notifications
- **Retry mechanism:** ด้วย exponential backoff (max 3 reties)
- **Dead letter queue:** สำหรับ notifications ที่ส่งไม่สำเร็จหลังจาก retries
- **Delivery status tracking:** ต้องบันทึกสถานะการส่ง notifications
- **Fallback channels:** ถ้า Email ล้มเหลว ให้ส่งผ่าน SYSTEM notification
- **Notification preferences:** ผู้ใช้ต้องสามารถกำหนด channel preferences ได้
### **6.9. Maintenance Mode (ใหม่)**
- ระบบต้องมีกลไก **Maintenance Mode** ที่ Admin สามารถเปิดใช้งานได้
- เมื่อเปิด: ผู้ใช้ทั่วไปจะเห็นหน้า "ปิดปรับปรุง" และไม่สามารถเรียก API ได้ (ยกเว้น Admin)
- ใช้สำหรับช่วง Deploy Version ใหม่ หรือ Database Migration
### **6.10. Monitoring และ Observability**
- **6.10.1 Application Monitoring:**
- **Health checks:** /health endpoint สำหรับ load balancer
- **Metrics collection:** Response times, error rates, throughput
- **Distributed tracing:** สำหรับ request tracing across services
- **Log aggregation:** Structured logging ด้วย JSON format
- **Alerting:** สำหรับ critical errors และ performance degradation
- **6.10.2 Business Metrics:**
- จำนวน documents created ต่อวัน
- Workflow completion rates
- User activity metrics
- System utilization rates
- Search query performance
- **6.10.3 Security Monitoring:**
- Failed login attempts
- Rate limiting triggers
- Virus scan results
- File download activities
- Permission changes
### **6.11 JSON Processing & Validation**
- **6.11.1 JSON Schema Management**
- ต้องมี centralized JSON schema registry
- ต้องรองรับ schema versioning และ migration
- ต้องมี schema validation during runtime
- **6.11.2 Performance Optimization**
- **Caching:** Cache parsed JSON structures
- **Compression:** ใช้ compression สำหรับ JSON ขนาดใหญ่
- **Indexing:** Support JSON path indexing สำหรับ query
- **6.11.3 Error Handling**
- ต้องมี graceful degradation เมื่อ JSON validation ล้มเหลว
- ต้องมี default fallback values
- ต้องบันทึก error logs สำหรับ validation failures
## **🧪 7. ข้อกำหนดด้านการทดสอบ (Testing Requirements)**
### **7.1. Unit Testing:**
- ต้องมี unit tests สำหรับ business logic ทั้งหมด
- Code coverage อย่างน้อย 70% สำหรับ backend services
- ต้องทดสอบ RBAC permission logic ทุกระดับ
### **7.2. Integration Testing:**
- ทดสอบการทำงานร่วมกันของ modules
- ทดสอบ database migrations และ data integrity
- ทดสอบ API endpoints ด้วย realistic data
### **7.3. End-to-End Testing:**
- ทดสอบ complete user workflows
- ทดสอบ document lifecycle จาก creation ถึง archival
- ทดสอบ cross-module integrations
### **7.4. Security Testing:**
- **Penetration Testing:** ทดสอบ OWASP Top 10 vulnerabilities
- **Security Audit:** Review code สำหรับ security flaws
- **Virus Scanning Test:** ทดสอบ file upload security
- **Rate Limiting Test:** ทดสอบ rate limiting functionality
### **7.5. Performance Testing:**
- **Load Testing:** ทดสอบด้วย realistic workloads
- **Stress Testing:** หา breaking points ของระบบ
- **Endurance Testing:** ทดสอบการทำงานต่อเนื่องเป็นเวลานาน
### **7.6. Disaster Recovery Testing:**
- ทดสอบ backup และ restoration procedures
- ทดสอบ failover mechanisms
- ทดสอบ data integrity หลังการ recovery
### **7.7 Specific Scenario Testing (เพิ่ม)**
- **Race Condition Test:** ทดสอบยิง Request ขอเลขที่เอกสารพร้อมกัน 100 Request
- **Transaction Test:** ทดสอบปิดเน็ตระหว่าง Upload ไฟล์ (ตรวจสอบว่าไม่มี Orphan File หรือ Broken Link)
- **Permission Test:** ทดสอบ CASL Integration ทั้งฝั่ง Backend และ Frontend ให้ตรงกัน
## **8. ข้อกำหนดด้านการบำรุงรักษา (Maintenance Requirements)**
### **8.1. Log Retention:**
- Audit logs: 7 ปี
- Application logs: 1 ปี
- Performance metrics: 2 ปี
### **8.2. Monitoring และ Alerting:**
- ต้องมี proactive monitoring สำหรับ critical systems
- ต้องมี alerting สำหรับ security incidents
- ต้องมี performance degradation alerts
### **8.3. Patch Management:**
- ต้องมี process สำหรับ security patches
- ต้องทดสอบ patches ใน staging environment
- ต้องมี rollback plan สำหรับ failed updates
### **8.4. Capacity Planning:**
- ต้อง monitor resource utilization
- ต้องมี scaling strategy สำหรับ growth
- ต้องมี performance baselines และ trending
## **9. ข้อกำหนดด้านการปฏิบัติตามกฎระเบียบ (Compliance Requirements)**
### **9.1. Data Privacy:**
- ต้องปฏิบัติตามกฎหมายคุ้มครองข้อมูลส่วนบุคคล
- ต้องมี data retention policies
- ต้องมี data deletion procedures
### **9.2. Audit Compliance:**
- ต้องรองรับ internal และ external audits
- ต้องมี comprehensive audit trails
- ต้องมี reporting capabilities สำหรับ compliance
### **9.3. Security Standards:**
- ต้องปฏิบัติตาม organizational security policies
- ต้องมี security incident response plan
- ต้องมี regular security assessments
## **📋 สรุปการปรับปรุงจากเวอร์ชันก่อนหน้า**
### **Security Enhancements:**
1. **File Upload Security** - Virus scanning, file type validation, access controls
2. **Input Validation** - OWASP Top 10 protection, XSS/CSRF prevention
3. **Rate Limiting** - Comprehensive rate limiting strategy
4. **Secrets Management** - Secure handling of sensitive configuration
### **Architecture Improvements:**
1. **Document Numbering** - Changed from Stored Procedure to Application-level Locking with Optimistic Locking safety net
2. **Resilience Patterns** - Circuit breaker, retry mechanisms, fallback strategies
3. **Monitoring & Observability** - Health checks, metrics, distributed tracing
4. **Caching Strategy** - Comprehensive caching with proper invalidation
5. **Two-Phase File Storage** - Temp -> Permanent storage with transaction safety
6. **Unified Workflow Engine** - Consolidated routing logic for better maintainability
### **Performance Targets:**
1. **API Response Time** - < 200ms (90th percentile)
2. **Search Performance** - < 500ms
3. **File Upload** - < 30 seconds for 50MB files
4. **Cache Hit Ratio** - > 80%
### **Operational Excellence:**
1. **Disaster Recovery** - RTO < 4 hours, RPO < 1 hour
2. **Backup Procedures** - Comprehensive backup and restoration
3. **Security Testing** - Penetration testing and security audits
4. **Performance Testing** - Load testing with realistic workloads
5. **Maintenance Mode** - Graceful system maintenance capabilities
### **User Experience Improvements:**
1. **Dynamic Form Generator** - Reduced code duplication and better schema support
2. **Mobile Responsiveness** - Card view for tables on mobile devices
3. **Auto-Save Draft** - LocalStorage integration for form resilience
4. **Notification Digest** - Reduced notification spam
### **Data Management:**
1. **Virtual Columns** - Improved JSON field search performance
2. **Table Partitioning** - Support for large-scale data growth
3. **Idempotency Keys** - Prevention of duplicate transactions
เอกสารนี้สะท้อนถึงความมุ่งมั่นในการสร้างระบบที่มีความปลอดภัย, มีความทนทาน, และมีประสิทธิภาพสูง พร้อมรองรับการเติบโตในอนาคตและความต้องการทางธุรกิจที่เปลี่ยนแปลงไป
**หมายเหตุ:** Requirements นี้จะถูกทบทวนและปรับปรุงเป็นระยะตาม feedback จากทีมพัฒนาและความต้องการทางธุรกิจที่เปลี่ยนแปลงไป
## **Document Control:**
- **Document:** Application Requirements Specification v1.4.2
- **Version:** 1.4
- **Date:** 2025-11-19
- **Author:** NAP LCBP3-DMS & Gemini
- **Status:** FINAL
- **Classification:** Internal Technical Documentation
- **Approved By:** Nattanin
---
`End of Requirements Specification v1.4.2`

View File

@@ -151,7 +151,7 @@ pnpm --version
(ควรจะขึ้นเป็นตัวเลขเวอร์ชัน เช่น `9.x.x`) (ควรจะขึ้นเป็นตัวเลขเวอร์ชัน เช่น `9.x.x`)
----- ---
### ทางเลือก: ติดตั้งผ่าน Corepack (สำหรับ Node.js เวอร์ชันใหม่) ### ทางเลือก: ติดตั้งผ่าน Corepack (สำหรับ Node.js เวอร์ชันใหม่)
@@ -162,16 +162,16 @@ corepack enable
corepack prepare pnpm@latest --activate corepack prepare pnpm@latest --activate
``` ```
----- ---
### เมื่อติดตั้งเสร็จแล้ว ### เมื่อติดตั้งเสร็จแล้ว
คุณสามารถใช้ `pnpm` แทน `npm` ได้เลยครับ เช่น: คุณสามารถใช้ `pnpm` แทน `npm` ได้เลยครับ เช่น:
* **ติดตั้ง Nest CLI (ถ้ายังไม่ได้ลง):** `pnpm add -g @nestjs/cli` - **ติดตั้ง Nest CLI (ถ้ายังไม่ได้ลง):** `pnpm add -g @nestjs/cli`
* **สร้างโปรเจกต์ NestJS:** `nest new lcbp3-backend` (แล้วเลือก pnpm ตอนระบบถาม) - **สร้างโปรเจกต์ NestJS:** `nest new lcbp3-backend` (แล้วเลือก pnpm ตอนระบบถาม)
* **รันโปรเจกต์:** `pnpm start:dev` - **รันโปรเจกต์:** `pnpm start:dev`
* **ติดตั้ง Library เพิ่ม:** `pnpm add [package-name]` (เช่น `pnpm add typeorm`) - **ติดตั้ง Library เพิ่ม:** `pnpm add [package-name]` (เช่น `pnpm add typeorm`)
ติดตั้งเสร็จแล้ว แจ้งผมได้เลยนะครับ จะได้ไปต่อที่ขั้นตอน **Config Database** กันครับ ติดตั้งเสร็จแล้ว แจ้งผมได้เลยนะครับ จะได้ไปต่อที่ขั้นตอน **Config Database** กันครับ
@@ -196,7 +196,7 @@ pnpm add @nestjs/typeorm typeorm mysql2 @nestjs/config
สร้างไฟล์ `docker-compose.yml` ที่ root ของโปรเจกต์ (ถ้ายังไม่มี): สร้างไฟล์ `docker-compose.yml` ที่ root ของโปรเจกต์ (ถ้ายังไม่มี):
```yaml ```yaml
version: '3.8' version: "3.8"
services: services:
mariadb: mariadb:
@@ -209,7 +209,7 @@ services:
MYSQL_USER: admin MYSQL_USER: admin
MYSQL_PASSWORD: password123 MYSQL_PASSWORD: password123
ports: ports:
- '3306:3306' - "3306:3306"
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
networks: networks:
@@ -222,7 +222,7 @@ services:
environment: environment:
PMA_HOST: mariadb PMA_HOST: mariadb
ports: ports:
- '8080:80' - "8080:80"
depends_on: depends_on:
- mariadb - mariadb
networks: networks:
@@ -248,36 +248,36 @@ docker-compose up -d
```typescript ```typescript
// src/app.module.ts // src/app.module.ts
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { AppController } from './app.controller'; import { AppController } from "./app.controller";
import { AppService } from './app.service'; import { AppService } from "./app.service";
@Module({ @Module({
imports: [ imports: [
// 1. Load Config Module // 1. Load Config Module
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, // ให้เรียกใช้ได้ทุกที่โดยไม่ต้อง import ใหม่ isGlobal: true, // ให้เรียกใช้ได้ทุกที่โดยไม่ต้อง import ใหม่
envFilePath: '.env', // อ่านค่าจากไฟล์ .env envFilePath: ".env", // อ่านค่าจากไฟล์ .env
}), }),
// 2. Setup TypeORM Connection (Async เพื่อรออ่าน Config ก่อน) // 2. Setup TypeORM Connection (Async เพื่อรออ่าน Config ก่อน)
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
type: 'mariadb', // หรือ 'mysql' ก็ได้เพราะใช้ driver เดียวกัน type: "mariadb", // หรือ 'mysql' ก็ได้เพราะใช้ driver เดียวกัน
host: configService.get<string>('DB_HOST'), host: configService.get<string>("DB_HOST"),
port: configService.get<number>('DB_PORT'), port: configService.get<number>("DB_PORT"),
username: configService.get<string>('DB_USERNAME'), username: configService.get<string>("DB_USERNAME"),
password: configService.get<string>('DB_PASSWORD'), password: configService.get<string>("DB_PASSWORD"),
database: configService.get<string>('DB_DATABASE'), database: configService.get<string>("DB_DATABASE"),
// Auto Load Entities: โหลด Entity ทั้งหมดที่อยู่ในโปรเจกต์อัตโนมัติ // Auto Load Entities: โหลด Entity ทั้งหมดที่อยู่ในโปรเจกต์อัตโนมัติ
autoLoadEntities: true, autoLoadEntities: true,
// Synchronize: true เฉพาะ Dev environment (ห้ามใช้ใน Prod) // Synchronize: true เฉพาะ Dev environment (ห้ามใช้ใน Prod)
synchronize: configService.get<string>('NODE_ENV') === 'development', synchronize: configService.get<string>("NODE_ENV") === "development",
// Logging: เปิดดู Query SQL ตอน Dev // Logging: เปิดดู Query SQL ตอน Dev
logging: configService.get<string>('NODE_ENV') === 'development', logging: configService.get<string>("NODE_ENV") === "development",
}), }),
inject: [ConfigService], inject: [ConfigService],
}), }),
@@ -317,7 +317,7 @@ pnpm start:dev
ถ้า Terminal ขึ้นข้อความสีเขียวๆ ว่า `[NestApplication] Nest application successfully started` และไม่มี Error เกี่ยวกับ TypeORM แสดงว่าเชื่อมต่อ MariaDB บน Localhost สำเร็จแล้วครับ\! 🎉 ถ้า Terminal ขึ้นข้อความสีเขียวๆ ว่า `[NestApplication] Nest application successfully started` และไม่มี Error เกี่ยวกับ TypeORM แสดงว่าเชื่อมต่อ MariaDB บน Localhost สำเร็จแล้วครับ\! 🎉
----- ---
**ขั้นตอนถัดไป:** **ขั้นตอนถัดไป:**
เราควรสร้าง **Base Entity** (T1.1) และ **Users Entity** (T1.3) เพื่อเริ่มทำระบบ Login ไหมครับ? เราควรสร้าง **Base Entity** (T1.1) และ **Users Entity** (T1.3) เพื่อเริ่มทำระบบ Login ไหมครับ?
@@ -334,7 +334,7 @@ pnpm start:dev
openssl rand -base64 64 openssl rand -base64 64
``` ```
*ผลลัพธ์จะได้ String ยาวๆ ที่อ่านไม่รู้เรื่อง เช่น:* _ผลลัพธ์จะได้ String ยาวๆ ที่อ่านไม่รู้เรื่อง เช่น:_
`h78/sT5...xYz==` `h78/sT5...xYz==`
### วิธีที่ 2: ใช้ Node.js Crypto (ง่ายสำหรับ Developer) ### วิธีที่ 2: ใช้ Node.js Crypto (ง่ายสำหรับ Developer)
@@ -347,13 +347,13 @@ openssl rand -base64 64
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
``` ```
*ผลลัพธ์จะได้ Hex String ยาว 64 ตัวอักษร* _ผลลัพธ์จะได้ Hex String ยาว 64 ตัวอักษร_
### วิธีที่ 3: ใช้ Password Manager Generator ### วิธีที่ 3: ใช้ Password Manager Generator
หากใช้ 1Password, LastPass หรือ Bitwarden สามารถใช้ฟีเจอร์ "Generate Password" โดยตั้งค่าความยาว 64 ตัวอักษร (รวมตัวเลขและสัญลักษณ์) ก็ใช้ได้เช่นกันครับ หากใช้ 1Password, LastPass หรือ Bitwarden สามารถใช้ฟีเจอร์ "Generate Password" โดยตั้งค่าความยาว 64 ตัวอักษร (รวมตัวเลขและสัญลักษณ์) ก็ใช้ได้เช่นกันครับ
----- ---
### ⚙️ การนำไปใช้งานในโปรเจกต์ ### ⚙️ การนำไปใช้งานในโปรเจกต์
@@ -397,14 +397,14 @@ pnpm add @nestjs/config joi
```typescript ```typescript
// File: src/common/config/env.validation.ts // File: src/common/config/env.validation.ts
import Joi from 'joi'; import Joi from "joi";
// สร้าง Schema สำหรับตรวจสอบค่า Environment Variables // สร้าง Schema สำหรับตรวจสอบค่า Environment Variables
export const envValidationSchema = Joi.object({ export const envValidationSchema = Joi.object({
// 1. Application Environment // 1. Application Environment
NODE_ENV: Joi.string() NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'provision') .valid("development", "production", "test", "provision")
.default('development'), .default("development"),
PORT: Joi.number().default(3000), PORT: Joi.number().default(3000),
// 2. Database Configuration (MariaDB) // 2. Database Configuration (MariaDB)
@@ -417,8 +417,11 @@ export const envValidationSchema = Joi.object({
// 3. Security (JWT) // 3. Security (JWT)
// ต้องมีค่า และควรยาวพอ (ตรวจสอบความยาวได้ถ้าระบุ min) // ต้องมีค่า และควรยาวพอ (ตรวจสอบความยาวได้ถ้าระบุ min)
JWT_SECRET: Joi.string().required().min(32).message('JWT_SECRET must be at least 32 characters long for security.'), JWT_SECRET: Joi.string()
JWT_EXPIRATION: Joi.string().default('8h'), .required()
.min(32)
.message("JWT_SECRET must be at least 32 characters long for security."),
JWT_EXPIRATION: Joi.string().default("8h"),
}); });
``` ```
@@ -428,19 +431,19 @@ export const envValidationSchema = Joi.object({
```typescript ```typescript
// File: src/app.module.ts // File: src/app.module.ts
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { AppController } from './app.controller'; import { AppController } from "./app.controller";
import { AppService } from './app.service'; import { AppService } from "./app.service";
import { envValidationSchema } from './common/config/env.validation.js'; // สังเกต .js สำหรับ ESM import { envValidationSchema } from "./common/config/env.validation.js"; // สังเกต .js สำหรับ ESM
@Module({ @Module({
imports: [ imports: [
// 1. Setup Config Module พร้อม Validation // 1. Setup Config Module พร้อม Validation
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, // เรียกใช้ได้ทั่วทั้ง App ไม่ต้อง import ซ้ำ isGlobal: true, // เรียกใช้ได้ทั่วทั้ง App ไม่ต้อง import ซ้ำ
envFilePath: '.env', // อ่านไฟล์ .env (สำหรับ Dev) envFilePath: ".env", // อ่านไฟล์ .env (สำหรับ Dev)
validationSchema: envValidationSchema, // ใช้ Schema ที่เราสร้างเพื่อตรวจสอบ validationSchema: envValidationSchema, // ใช้ Schema ที่เราสร้างเพื่อตรวจสอบ
validationOptions: { validationOptions: {
// ถ้ามีค่าไหนไม่ผ่าน Validation ให้ Error และหยุดทำงานทันที // ถ้ามีค่าไหนไม่ผ่าน Validation ให้ Error และหยุดทำงานทันที
@@ -453,15 +456,15 @@ import { envValidationSchema } from './common/config/env.validation.js'; // ส
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
type: 'mariadb', type: "mariadb",
host: configService.get<string>('DB_HOST'), host: configService.get<string>("DB_HOST"),
port: configService.get<number>('DB_PORT'), port: configService.get<number>("DB_PORT"),
username: configService.get<string>('DB_USERNAME'), username: configService.get<string>("DB_USERNAME"),
password: configService.get<string>('DB_PASSWORD'), password: configService.get<string>("DB_PASSWORD"),
database: configService.get<string>('DB_DATABASE'), database: configService.get<string>("DB_DATABASE"),
autoLoadEntities: true, autoLoadEntities: true,
// synchronize: true เฉพาะตอน Dev เท่านั้น ห้ามใช้บน Prod // synchronize: true เฉพาะตอน Dev เท่านั้น ห้ามใช้บน Prod
synchronize: configService.get<string>('NODE_ENV') === 'development', synchronize: configService.get<string>("NODE_ENV") === "development",
}), }),
}), }),
], ],
@@ -478,7 +481,7 @@ export class AppModule {}
สร้างไฟล์: `docker-compose.override.yml.example` ที่ root project: สร้างไฟล์: `docker-compose.override.yml.example` ที่ root project:
```yaml ```yaml
version: '3.8' version: "3.8"
services: services:
# Override ค่า Config ของ Service Backend (เมื่อเราสร้าง Container Backend ในอนาคต) # Override ค่า Config ของ Service Backend (เมื่อเราสร้าง Container Backend ในอนาคต)
@@ -510,7 +513,7 @@ App **ต้อง Crash** และแสดง Error Message ชัดเจ
ถ้าขึ้นแบบนี้แสดงว่าระบบ **Secure Configuration** ของเราทำงานถูกต้องตามแผน T0.1 แล้วครับ! 🎉 ถ้าขึ้นแบบนี้แสดงว่าระบบ **Secure Configuration** ของเราทำงานถูกต้องตามแผน T0.1 แล้วครับ! 🎉
----- ---
## **T0.2 Redis & Queue Infrastructure** ## **T0.2 Redis & Queue Infrastructure**
@@ -521,7 +524,7 @@ App **ต้อง Crash** และแสดง Error Message ชัดเจ
มาเริ่มกันเลยครับ มาเริ่มกันเลยครับ
----- ---
### 1. เพิ่ม Redis ใน Docker Compose ### 1. เพิ่ม Redis ใน Docker Compose
@@ -530,7 +533,7 @@ App **ต้อง Crash** และแสดง Error Message ชัดเจ
**ไฟล์: `docker-compose.yml`** **ไฟล์: `docker-compose.yml`**
```yaml ```yaml
version: '3.8' version: "3.8"
services: services:
# ... (mariadb & pma เดิม) ... # ... (mariadb & pma เดิม) ...
@@ -543,7 +546,7 @@ services:
# ใช้ Command นี้เพื่อตั้ง Password # ใช้ Command นี้เพื่อตั้ง Password
command: redis-server --requirepass "redis_password_secure" command: redis-server --requirepass "redis_password_secure"
ports: ports:
- '6379:6379' - "6379:6379"
volumes: volumes:
- redis_data:/data - redis_data:/data
networks: networks:
@@ -611,20 +614,20 @@ pnpm add @nestjs/bullmq bullmq
**ไฟล์: `src/app.module.ts`** **ไฟล์: `src/app.module.ts`**
```typescript ```typescript
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { BullModule } from '@nestjs/bullmq'; // Import BullModule import { BullModule } from "@nestjs/bullmq"; // Import BullModule
import { AppController } from './app.controller'; import { AppController } from "./app.controller";
import { AppService } from './app.service'; import { AppService } from "./app.service";
import { envValidationSchema } from './common/config/env.validation.js'; import { envValidationSchema } from "./common/config/env.validation.js";
@Module({ @Module({
imports: [ imports: [
// 1. Config (เดิม) // 1. Config (เดิม)
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
envFilePath: '.env', envFilePath: ".env",
validationSchema: envValidationSchema, validationSchema: envValidationSchema,
validationOptions: { abortEarly: true }, validationOptions: { abortEarly: true },
}), }),
@@ -634,14 +637,14 @@ import { envValidationSchema } from './common/config/env.validation.js';
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
type: 'mariadb', type: "mariadb",
host: configService.get<string>('DB_HOST'), host: configService.get<string>("DB_HOST"),
port: configService.get<number>('DB_PORT'), port: configService.get<number>("DB_PORT"),
username: configService.get<string>('DB_USERNAME'), username: configService.get<string>("DB_USERNAME"),
password: configService.get<string>('DB_PASSWORD'), password: configService.get<string>("DB_PASSWORD"),
database: configService.get<string>('DB_DATABASE'), database: configService.get<string>("DB_DATABASE"),
autoLoadEntities: true, autoLoadEntities: true,
synchronize: configService.get<string>('NODE_ENV') === 'development', synchronize: configService.get<string>("NODE_ENV") === "development",
}), }),
}), }),
@@ -651,9 +654,9 @@ import { envValidationSchema } from './common/config/env.validation.js';
inject: [ConfigService], inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
connection: { connection: {
host: configService.get<string>('REDIS_HOST'), host: configService.get<string>("REDIS_HOST"),
port: configService.get<number>('REDIS_PORT'), port: configService.get<number>("REDIS_PORT"),
password: configService.get<string>('REDIS_PASSWORD'), password: configService.get<string>("REDIS_PASSWORD"),
}, },
}), }),
}), }),
@@ -674,32 +677,34 @@ pnpm start:dev
ถ้าไม่มี Error ขึ้น และ App Start ได้ปกติ แสดงว่า **T0.2 เสร็จสมบูรณ์** ครับ\! ตอนนี้เรามีทั้ง Database และ Redis พร้อมสำหรับการพัฒนา Business Logic แล้ว ถ้าไม่มี Error ขึ้น และ App Start ได้ปกติ แสดงว่า **T0.2 เสร็จสมบูรณ์** ครับ\! ตอนนี้เรามีทั้ง Database และ Redis พร้อมสำหรับการพัฒนา Business Logic แล้ว
----- ---
### 6. **แก้ไข Error** ### 6. **แก้ไข Error**
ปัญหานี้เกิดจากยังไม่ได้ **Import** `BullModule` เข้ามาในไฟล์ `src/app.module.ts` ครับ (TypeScript เลยไม่รู้จักว่า `BullModule` คืออะไร) ปัญหานี้เกิดจากยังไม่ได้ **Import** `BullModule` เข้ามาในไฟล์ `src/app.module.ts` ครับ (TypeScript เลยไม่รู้จักว่า `BullModule` คืออะไร)
ให้เพิ่มบรรทัดนี้ที่ส่วนบนสุดของไฟล์ `src/app.module.ts` ครับ: ให้เพิ่มบรรทัดนี้ที่ส่วนบนสุดของไฟล์ `src/app.module.ts` ครับ:
```typescript ```typescript
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from "@nestjs/bullmq";
``` ```
หรือลองตรวจสอบไฟล์ `src/app.module.ts` ให้ครบถ้วนตามนี้ครับ: หรือลองตรวจสอบไฟล์ `src/app.module.ts` ให้ครบถ้วนตามนี้ครับ:
```typescript ```typescript
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { BullModule } from '@nestjs/bullmq'; // <--- ต้องมีบรรทัดนี้ครับ import { BullModule } from "@nestjs/bullmq"; // <--- ต้องมีบรรทัดนี้ครับ
import { AppController } from './app.controller'; import { AppController } from "./app.controller";
import { AppService } from './app.service'; import { AppService } from "./app.service";
import { envValidationSchema } from './common/config/env.validation.js'; import { envValidationSchema } from "./common/config/env.validation.js";
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
envFilePath: '.env', envFilePath: ".env",
validationSchema: envValidationSchema, validationSchema: envValidationSchema,
validationOptions: { abortEarly: true }, validationOptions: { abortEarly: true },
}), }),
@@ -707,14 +712,14 @@ import { envValidationSchema } from './common/config/env.validation.js';
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
type: 'mariadb', type: "mariadb",
host: configService.get<string>('DB_HOST'), host: configService.get<string>("DB_HOST"),
port: configService.get<number>('DB_PORT'), port: configService.get<number>("DB_PORT"),
username: configService.get<string>('DB_USERNAME'), username: configService.get<string>("DB_USERNAME"),
password: configService.get<string>('DB_PASSWORD'), password: configService.get<string>("DB_PASSWORD"),
database: configService.get<string>('DB_DATABASE'), database: configService.get<string>("DB_DATABASE"),
autoLoadEntities: true, autoLoadEntities: true,
synchronize: configService.get<string>('NODE_ENV') === 'development', synchronize: configService.get<string>("NODE_ENV") === "development",
}), }),
}), }),
// Queue Configuration // Queue Configuration
@@ -723,9 +728,9 @@ import { envValidationSchema } from './common/config/env.validation.js';
inject: [ConfigService], inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
connection: { connection: {
host: configService.get<string>('REDIS_HOST'), host: configService.get<string>("REDIS_HOST"),
port: configService.get<number>('REDIS_PORT'), port: configService.get<number>("REDIS_PORT"),
password: configService.get<string>('REDIS_PASSWORD'), password: configService.get<string>("REDIS_PASSWORD"),
}, },
}), }),
}), }),

View File

@@ -1505,6 +1505,177 @@ export class UserService {
ถ้าทุกอย่างเรียบร้อย เราจะปิดจ็อบ Phase 1 แล้วไปขึ้น **Phase 2: High-Integrity Data (File Upload)** กันต่อไหมครับ? ถ้าทุกอย่างเรียบร้อย เราจะปิดจ็อบ Phase 1 แล้วไปขึ้น **Phase 2: High-Integrity Data (File Upload)** กันต่อไหมครับ?
### **"มอบหมายบทบาท (Assign Role)"**
**ถูกต้องที่สุดครับ!** คุณเข้าใจคอนเซปต์ของระบบ RBAC ในระบบนี้ได้อย่างทะลุปรุโปร่งครับ 👍
ถ้าเราสร้าง User ลงในตาราง `users` เพียงอย่างเดียว User คนนั้นจะ:
1. ✅ **Login ได้** (เพราะมี Username/Password)
2. ❌ **ทำอะไรไม่ได้เลย** (เพราะไม่มี Row ใน `user_assignments` ระบบสิทธิ์ `v_user_all_permissions` จึงว่างเปล่า)
ดังนั้น การ **"มอบหมายบทบาท (Assign Role)"** จึงเป็นขั้นตอนที่ขาดไม่ได้ครับ
-----
#### 🛠️ สิ่งที่เราต้องทำเพิ่ม (เพื่อให้ Admin มอบสิทธิ์ได้)
ใน Task T1.3 เราทำแค่ `Create User` (ลงตาราง users) ไปแล้ว ตอนนี้เราต้องทำ API สำหรับ **Assign Role** (ลงตาราง user\_assignments) ครับ
มาสร้างฟังก์ชันนี้กันครับ:
##### 1. สร้าง DTO สำหรับการมอบสิทธิ์
สร้างไฟล์: `src/modules/user/dto/assign-role.dto.ts`
```typescript
import { IsInt, IsNotEmpty, IsOptional, ValidateIf } from 'class-validator';
export class AssignRoleDto {
@IsInt()
@IsNotEmpty()
userId!: number;
@IsInt()
@IsNotEmpty()
roleId!: number;
// Scope (ต้องส่งมาอย่างน้อย 1 อัน หรือไม่ส่งเลยถ้าเป็น Global)
@IsInt()
@IsOptional()
organizationId?: number;
@IsInt()
@IsOptional()
projectId?: number;
@IsInt()
@IsOptional()
contractId?: number;
}
```
##### 2. สร้าง `UserAssignmentService` (Logic การบันทึก)
เราควรแยก Service นี้ออกมาเพื่อความเป็นระเบียบครับ (หรือจะใส่ใน UserService ก็ได้ แต่แยกดีกว่า)
สร้างไฟล์: `src/modules/user/user-assignment.service.ts`
```typescript
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserAssignment } from './entities/user-assignment.entity.js'; // ต้องไปสร้าง Entity นี้ก่อน (ดูข้อ 3)
import { AssignRoleDto } from './dto/assign-role.dto.js';
import { User } from './entities/user.entity.js';
@Injectable()
export class UserAssignmentService {
constructor(
@InjectRepository(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);
if (scopes.length > 1) {
throw new BadRequestException('Cannot assign multiple scopes at once. Choose one of Org, Project, or Contract.');
}
// สร้าง Assignment
const assignment = this.assignmentRepo.create({
userId: dto.userId,
roleId: dto.roleId,
organizationId: dto.organizationId,
projectId: dto.projectId,
contractId: dto.contractId,
assignedByUserId: assigner.user_id, // เก็บ Log ว่าใครเป็นคนให้สิทธิ์
});
return this.assignmentRepo.save(assignment);
}
}
```
##### 3. สร้าง Entity `UserAssignment` (ถ้ายังไม่ได้สร้าง)
สร้างไฟล์: `src/modules/user/entities/user-assignment.entity.ts`
```typescript
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
import { User } from './user.entity.js';
// Import Role, Org, Project, Contract entities...
@Entity('user_assignments')
export class UserAssignment {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'user_id' })
userId!: number;
@Column({ name: 'role_id' })
roleId!: number;
@Column({ name: 'organization_id', nullable: true })
organizationId?: number;
@Column({ name: 'project_id', nullable: true })
projectId?: number;
@Column({ name: 'contract_id', nullable: true })
contractId?: number;
@Column({ name: 'assigned_by_user_id', nullable: true })
assignedByUserId?: number;
@CreateDateColumn({ name: 'assigned_at' })
assignedAt!: Date;
// Relation กลับไปหา User (เจ้าของสิทธิ์)
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user?: User;
}
```
##### 4. เพิ่ม Endpoint ใน `UserController`
```typescript
// src/modules/user/user.controller.ts
// ... imports ...
import { AssignRoleDto } from './dto/assign-role.dto.js';
import { UserAssignmentService } from './user-assignment.service.js'; // อย่าลืม Inject เข้ามาใน Constructor
@Controller('users')
@UseGuards(JwtAuthGuard, RbacGuard)
export class UserController {
constructor(
private readonly userService: UserService,
private readonly assignmentService: UserAssignmentService, // <--- Inject เพิ่ม
) {}
// ... Endpoints เดิม ...
@Post('assign-role')
@RequirePermission('permission.assign') // เฉพาะคนมีสิทธิ์ Assign (เช่น Admin)
assignRole(@Body() dto: AssignRoleDto, @Request() req: any) {
return this.assignmentService.assignRole(dto, req.user);
}
}
```
-----
#### 📝 สรุปภาพรวม
1. **สร้าง User:** `POST /users` -\> ได้ User เปล่าๆ (Login ได้ แต่ทำไรไม่ได้)
2. **กำหนดสิทธิ์:** `POST /users/assign-role` -\> ยิง API นี้เพื่อบอกว่า "User นี้ เป็น Project Manager ของ Project A นะ"
* 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` ตัวนี้แหละครับ)
## Task T1.3 Error ## Task T1.3 Error
### ERROR [HttpExceptionFilter] Http Status: 404 Error Message: "Cannot GET /api/users" ### ERROR [HttpExceptionFilter] Http Status: 404 Error Message: "Cannot GET /api/users"
@@ -1564,6 +1735,43 @@ export class AppModule {}
คราวนี้น่าจะเจอข้อมูลแล้วครับ\! 🎉 คราวนี้น่าจะเจอข้อมูลแล้วครับ\! 🎉
#### ปัญหานี้เกิดจากเราได้สร้าง `UserAssignmentService` และเรียกใช้ใน Controller แล้ว แต่เรา **ลืมลงทะเบียน (Register)** Service นี้และ Entity ของมันใน `UserModule` ครับ NestJS เลยไม่รู้ว่าจะเอา Service นี้มาจากไหน
##### 🛠️ วิธีแก้ไข
ให้แก้ไขไฟล์ `src/modules/user/user.module.ts` โดยเพิ่ม `UserAssignment` (Entity) และ `UserAssignmentService` (Provider) เข้าไปครับ
**ไฟล์: `src/modules/user/user.module.ts`**
```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service.js';
import { UserAssignmentService } from './user-assignment.service.js'; // <--- 1. Import Service
import { UserController } from './user.controller.js';
import { User } from './entities/user.entity.js';
import { UserAssignment } from './entities/user-assignment.entity.js'; // <--- 2. Import Entity
@Module({
imports: [
// 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment
TypeOrmModule.forFeature([User, UserAssignment]),
],
controllers: [UserController],
providers: [
UserService,
UserAssignmentService // <--- 4. ลงทะเบียน Service เป็น Provider
],
exports: [
UserService,
UserAssignmentService // <--- 5. Export เผื่อที่อื่นใช้
],
})
export class UserModule {}
```
ลอง Save แล้วรัน `pnpm start:dev` อีกครั้งครับ Error นี้จะหายไป และคุณจะสามารถใช้ฟังก์ชัน Assign Role ได้แล้วครับ\!
## T1.4 RBAC Guard (4-Level Authorization) ## T1.4 RBAC Guard (4-Level Authorization)
ยินดีด้วยครับ\! 👏👏👏 ยินดีด้วยครับ\! 👏👏👏

1386
T2-0 Setting Project.md Normal file

File diff suppressed because it is too large Load Diff

72
T2-Postman.md Normal file
View File

@@ -0,0 +1,72 @@
# **Manual Integration Testing (Postman Checklist)** สำหรับ **Phase 2: High-Integrity Data & Security** โดยเฉพาะครับ
Phase นี้เน้นเรื่อง **การจัดการไฟล์ (File Storage)**, **ความปลอดภัย (Security)** และ **ระบบตรวจสอบข้อมูล (JSON Schema)** ครับ
-----
## 📋 Phase 2 Integration Test Plan
**Pre-requisites (เตรียมข้อมูลก่อนเริ่ม):**
1. **Server:** รัน `pnpm start:dev`
2. **Auth:** Login ด้วย `admin` เพื่อขอ Access Token (ใช้แนบใน Header: `Authorization: Bearer <token>`)
-----
### 🧪 Scenario 1: File Storage (T2.2)
**เป้าหมาย:** ทดสอบว่าระบบอัปโหลดไฟล์ทำงานถูกต้อง (Two-Phase Storage)
| Step | Action (API Endpoint) | Method | Body (Form-Data) | Expected Result |
| :------ | :-------------------------------------------------- | :----- | :---------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| **1.1** | **Upload Valid File**<br>`/api/files/upload` | POST | Key: `file` (Type: File)<br>Value: (เลือกไฟล์ PDF/IMG ขนาด \< 50MB) | - **Status: 201 Created**<br>- Response มี `id`, `originalFilename`<br>- `isTemporary`: **true**<br>- `tempId`: (มีค่า UUID) |
| **1.2** | **Upload Invalid File Type**<br>`/api/files/upload` | POST | Key: `file` (Type: File)<br>Value: (เลือกไฟล์ .exe หรือ .bat) | - **Status: 400 Bad Request**<br>- Message: "Validation failed... expected type is..." |
| **1.3** | **Upload Too Large File**<br>`/api/files/upload` | POST | Key: `file` (Type: File)<br>Value: (ไฟล์ขนาด \> 50MB) | - **Status: 413 Payload Too Large** หรือ **400 Bad Request** |
*หมายเหตุ: การ Commit ไฟล์ (ย้ายจาก Temp -\> Permanent) จะเกิดขึ้นอัตโนมัติเมื่อเรานำไฟล์ไปผูกกับเอกสารใน Phase 3*
-----
### 🧪 Scenario 2: JSON Schema Validation (T2.5)
**เป้าหมาย:** ทดสอบระบบตรวจสอบโครงสร้างข้อมูล JSON
| Step | Action (API Endpoint) | Method | Body (JSON) | Expected Result |
| :------ | :-------------------------------------------------------------------- | :----- | :------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------- |
| **2.1** | **Register Schema**<br>`/api/json-schemas/TEST_SCHEMA` | POST | `{ "type": "object", "properties": { "age": { "type": "integer" } }, "required": ["age"] }` | - **Status: 201 Created**<br>- Response มี `id`, `schemaCode`: "TEST\_SCHEMA" |
| **2.2** | **Validate Valid Data**<br>`/api/json-schemas/TEST_SCHEMA/validate` | POST | `{ "age": 25 }` | - **Status: 201 Created**<br>- Response: `{ "valid": true }` |
| **2.3** | **Validate Invalid Data**<br>`/api/json-schemas/TEST_SCHEMA/validate` | POST | `{ "age": "twenty-five" }` | - **Status: 400 Bad Request**<br>- Message: "JSON Validation Failed..." |
-----
### 🧪 Scenario 3: Security & Rate Limiting (T2.4)
**เป้าหมาย:** ทดสอบระบบป้องกันการโจมตี
| Step | Action (API Endpoint) | Method | Details | Expected Result |
| :------ | :------------------------------------------------- | :----- | :-------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| **3.1** | **Brute Force Login**<br>`/api/auth/login` | POST | กด Send รัวๆ เกิน 5 ครั้ง ภายใน 1 นาที | - **ครั้งที่ 1-5:** Status 201/401 (ปกติ)<br>- **ครั้งที่ 6+:** **Status 429 Too Many Requests**<br>- Message: "ThrottlerException: Too Many Requests" |
| **3.2** | **Security Headers**<br>(ตรวจสอบ Response Headers) | ANY | ยิง Request อะไรก็ได้ | - Header `X-Powered-By` **ต้องไม่มี** (ถูก Helmet ซ่อน)<br>- Header `Content-Security-Policy` **ต้องมี** |
-----
### 🧪 Scenario 4: Document Numbering (T2.3)
**เป้าหมาย:** ทดสอบการรันเลขที่เอกสาร (ทดสอบผ่านการสร้างเอกสารใน Phase 3)
*เนื่องจาก Service นี้เป็น Internal เราจะทดสอบผ่านการสร้าง Correspondence*
| Step | Action (API Endpoint) | Method | Body (JSON) | Expected Result |
| :------ | :------------------------------------------------- | :----- | :---------------------------------------------------------- | :----------------------------------------------------------------- |
| **4.1** | **Generate Number**<br>`/api/correspondences` | POST | `{ "projectId": 1, "typeId": 1, "title": "Test Number 1" }` | - `correspondenceNumber` ลงท้ายด้วย **0001** (หรือเลขล่าสุด +1) |
| **4.2** | **Generate Next Number**<br>`/api/correspondences` | POST | `{ "projectId": 1, "typeId": 1, "title": "Test Number 2" }` | - `correspondenceNumber` ต้องเป็นเลขถัดไป (เช่น **0002**) ห้ามซ้ำกับข้อ 4.1 |
-----
### ✅ Checklist การตรวจสอบใน Server (Files)
1. ไปที่โฟลเดอร์โปรเจกต์
2. ตรวจสอบโฟลเดอร์ `uploads/temp`
3. **สิ่งที่ต้องเจอ:** ไฟล์ที่อัปโหลดในข้อ **1.1** ต้องปรากฏอยู่ในนี้ โดยชื่อไฟล์จะเป็น UUID (ไม่ใช่ชื่อเดิม)
ถ้าผ่านครบทุกข้อนี้ แสดงว่า **Phase 2 (Infrastructure & Integrity)** แข็งแกร่งพร้อมใช้งานครับ\!

3272
T3-0 Setting Project.md Normal file

File diff suppressed because it is too large Load Diff

74
T3-Postman.md Normal file
View File

@@ -0,0 +1,74 @@
# **Manual Integration Testing (Postman Checklist)** สำหรับ **Phase 3: Unified Workflow Engine** ที่คุณสามารถนำไปใช้ทดสอบผ่าน Postman ได้ทันทีครับ
แผนนี้ออกแบบมาเพื่อทดสอบการทำงานร่วมกันของ `Create` -\> `Submit` -\> `Process Action` ให้ครอบคลุมทั้งกรณีปกติ (Happy Path) และกรณีขัดแย้ง (Edge Cases) ครับ
-----
## 📋 Phase 3 Integration Test Plan: Correspondence Workflow
**Pre-requisites (เตรียมข้อมูลก่อนเริ่ม):**
1. **Users:**
* `admin` (Superadmin) - เอาไว้สร้าง Master Data
* `user_org1` (อยู่ Org ID: 1) - เป็นคนสร้างเอกสาร (Originator)
* `user_org2` (อยู่ Org ID: 2) - เป็นคนอนุมัติ (Reviewer)
2. **Master Data:**
* มี `correspondence_types` (เช่น ID: 1 = RFA)
* มี `correspondence_status` (เช่น ID: 1 = DRAFT)
* มี `organizations` (ID: 1 และ 2)
3. **Template:**
* รัน SQL Seed สร้าง Template ID: 1 (Step 1 -\> Org 1, Step 2 -\> Org 2) ตามที่เคยทำไป
-----
### 🧪 Scenario 1: Happy Path (Create -\> Submit -\> Approve -\> Complete)
**เป้าหมาย:** ทดสอบการไหลของงานปกติจนจบกระบวนการ
| Step | Action (API Endpoint) | Method | Actor (Token) | Body (JSON) | Expected Result |
| :------ | :---------------------------------------------------------------------- | :----- | :----------------------------- | :---------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1.1** | **Create Document**<br>`/api/correspondences` | POST | `user_org1` | `{ "projectId": 1, "typeId": 1, "title": "Test Workflow 01", "details": {} }` | - Status: `201 Created`<br>- Response มี `id` (จดไว้ สมมติ `10`)<br>- Response มี `correspondenceNumber` |
| **1.2** | **Submit Document**<br>`/api/correspondences/10/submit` | POST | `user_org1` | `{ "templateId": 1 }` | - Status: `201 Created`<br>- Response คือ `CorrespondenceRouting`<br>- `sequence`: 1<br>- `status`: "SENT"<br>- `toOrganizationId`: 1 (ส่งหาตัวเองก่อนตาม Template) |
| **1.3** | **Approve Step 1**<br>`/api/correspondences/10/workflow/action` | POST | `user_org1` | `{ "action": "APPROVE", "comments": "Review passed" }` | - Status: `201 Created`<br>- **Result:** "Action processed successfully"<br>- มีการสร้าง Step ถัดไป (Sequence 2) ส่งไปหา Org 2 |
| **1.4** | **Approve Step 2 (Final)**<br>`/api/correspondences/10/workflow/action` | POST | `user_org2`<br>*(เปลี่ยน Token)* | `{ "action": "APPROVE", "comments": "Final Approval" }` | - Status: `201 Created`<br>- **Result:** "Action processed successfully"<br>- **ไม่สร้าง Step ถัดไป** (เพราะหมดแล้ว)<br>- Workflow จบสมบูรณ์ |
-----
### 🧪 Scenario 2: Rejection Flow (การปฏิเสธเอกสาร)
**เป้าหมาย:** ทดสอบว่าเมื่อกด Reject แล้ว Workflow ต้องหยุดทันที
| Step | Action (API Endpoint) | Method | Actor (Token) | Body (JSON) | Expected Result |
| :------ | :--------------------------------------------------------------- | :----- | :------------ | :----------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- |
| **2.1** | **Create Document**<br>`/api/correspondences` | POST | `user_org1` | `{ "projectId": 1, "typeId": 1, "title": "Test Reject 01" }` | - ได้ `id` ใหม่ (สมมติ `11`) |
| **2.2** | **Submit Document**<br>`/api/correspondences/11/submit` | POST | `user_org1` | `{ "templateId": 1 }` | - สร้าง Routing Sequence 1 |
| **2.3** | **Reject Document**<br>`/api/correspondences/11/workflow/action` | POST | `user_org1` | `{ "action": "REJECT", "comments": "Invalid Data" }` | - Status: `201 Created`<br>- Step 1 Status เปลี่ยนเป็น `REJECTED`<br>- **ไม่มีการสร้าง Step 2** (Workflow หยุด) |
-----
### 🧪 Scenario 3: Security Check (ข้ามหน้าข้ามตา)
**เป้าหมาย:** ทดสอบว่าคนนอก (User ที่ไม่อยู่ใน Org ปลายทาง) จะไม่สามารถกดอนุมัติได้
| Step | Action (API Endpoint) | Method | Actor (Token) | Body (JSON) | Expected Result |
| :------ | :------------------------------------------------ | :----- | :--------------------------------- | :------------------------ | :------------------------------------------------------------------------------------------------------------- |
| **3.1** | **Create & Submit** | POST | `user_org1` | *(ทำเหมือน 1.1 และ 1.2)* | - ได้ `id` ใหม่ (สมมติ `12`)<br>- Routing Seq 1 (ส่งหา Org 1) |
| **3.2** | **Approve Step 1** | POST | `user_org1` | `{ "action": "APPROVE" }` | - ผ่าน (เพราะ User 1 อยู่ Org 1)<br>- สร้าง Seq 2 (ส่งหา Org 2) |
| **3.3** | **Try to Approve Step 2**<br>*(โดยใช้ User Org 1)* | POST | **`user_org1`**<br>*(เจตนาใช้ผิดคน)* | `{ "action": "APPROVE" }` | - **Status: `400 Bad Request`**<br>- Message: "You are not authorized to process this step"<br>*(ป้องกันสำเร็จ\!)* |
-----
### ✅ Checklist การตรวจสอบผลลัพธ์ใน Database
หลังจากรัน Scenario 1 จบแล้ว ให้ลอง Query ดูใน Database เพื่อความมั่นใจครับ:
```sql
SELECT * FROM correspondence_routings WHERE correspondence_id = 10 ORDER BY sequence;
```
**สิ่งที่ควรเจอ:**
1. Row 1: `sequence`=1, `status`='ACTIONED', `comments`='Review passed'
2. Row 2: `sequence`=2, `status`='ACTIONED', `comments`='Final Approval'
ถ้าผลการทดสอบเป็นไปตามนี้ทั้งหมด แสดงว่า **Phase 3 สมบูรณ์แบบ** พร้อมไปต่อ Phase 4 (RFA) ได้เลยครับ\!

View File

@@ -16,11 +16,22 @@ 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';
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
@Controller('correspondences') @Controller('correspondences')
@UseGuards(JwtAuthGuard, RbacGuard) @UseGuards(JwtAuthGuard, RbacGuard)
export class CorrespondenceController { export class CorrespondenceController {
constructor(private readonly correspondenceService: CorrespondenceService) {} constructor(private readonly correspondenceService: CorrespondenceService) {}
@Post(':id/workflow/action')
@RequirePermission('workflow.action_review') // สิทธิ์ในการกดอนุมัติ/ตรวจสอบ
processAction(
@Param('id', ParseIntPipe) id: number,
@Body() actionDto: WorkflowActionDto,
@Request() req: any,
) {
return this.correspondenceService.processAction(id, actionDto, req.user);
}
@Post() @Post()
@RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง @RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง
create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) { create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) {

View File

@@ -15,7 +15,6 @@ import { DocumentNumberingModule } from '../document-numbering/document-numberin
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([ TypeOrmModule.forFeature([

View File

@@ -18,6 +18,10 @@ import { User } from '../user/entities/user.entity.js';
// DTOs // DTOs
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js'; import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
// Interfaces
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface.js';
// Services // Services
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js'; import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
@@ -46,15 +50,9 @@ export class CorrespondenceService {
private dataSource: DataSource, private dataSource: DataSource,
) {} ) {}
/** // --- 1. CREATE DOCUMENT ---
* สร้างเอกสารใหม่ (Create Correspondence)
* - ตรวจสอบสิทธิ์และข้อมูลพื้นฐาน
* - Validate JSON Details ตาม Type
* - ขอเลขที่เอกสาร (Redis Lock)
* - บันทึกข้อมูลลง DB (Transaction)
*/
async create(createDto: CreateCorrespondenceDto, user: User) { async create(createDto: CreateCorrespondenceDto, user: User) {
// 1. ตรวจสอบข้อมูลพื้นฐาน (Type, Status, Org) // 1.1 Validate Basic Info
const type = await this.typeRepo.findOne({ const type = await this.typeRepo.findOne({
where: { id: createDto.typeId }, where: { id: createDto.typeId },
}); });
@@ -64,39 +62,29 @@ export class CorrespondenceService {
where: { statusCode: 'DRAFT' }, where: { statusCode: 'DRAFT' },
}); });
if (!statusDraft) { if (!statusDraft) {
throw new InternalServerErrorException( throw new InternalServerErrorException('Status DRAFT not found');
'Status DRAFT not found in Master Data',
);
} }
const userOrgId = user.primaryOrganizationId; const userOrgId = user.primaryOrganizationId;
if (!userOrgId) { if (!userOrgId) {
throw new BadRequestException( throw new BadRequestException('User must belong to an organization');
'User must belong to an organization to create documents',
);
} }
// 2. Validate JSON Details (ถ้ามี) // 1.2 Validate JSON Details
if (createDto.details) { if (createDto.details) {
try { try {
// ใช้ Type Code เป็น Key ในการค้นหา Schema (เช่น 'RFA', 'LETTER')
await this.jsonSchemaService.validate(type.typeCode, createDto.details); await this.jsonSchemaService.validate(type.typeCode, createDto.details);
} catch (error: any) { } catch (error: any) {
// บันทึก Warning หรือ Throw Error ตามนโยบาย (ในที่นี้ให้ผ่านไปก่อนถ้ายังไม่สร้าง Schema) console.warn(`Schema validation warning: ${error.message}`);
console.warn(
`Schema validation warning for ${type.typeCode}: ${error.message}`,
);
} }
} }
// 3. เริ่ม Transaction
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
await queryRunner.startTransaction(); await queryRunner.startTransaction();
try { try {
// 3.1 ขอเลขที่เอกสาร (Double-Lock Mechanism) // 1.3 Generate Document Number (Double-Lock)
// Mock ค่า replacements ไว้ก่อน (จริงๆ ต้อง Join เอา Org Code มา)
const docNumber = await this.numberingService.generateNextNumber( const docNumber = await this.numberingService.generateNextNumber(
createDto.projectId, createDto.projectId,
userOrgId, userOrgId,
@@ -104,11 +92,11 @@ export class CorrespondenceService {
new Date().getFullYear(), new Date().getFullYear(),
{ {
TYPE_CODE: type.typeCode, TYPE_CODE: type.typeCode,
ORG_CODE: 'ORG', // TODO: Fetch real organization code ORG_CODE: 'ORG', // In real app, fetch user's org code
}, },
); );
// 3.2 สร้าง Correspondence (หัวจดหมาย) // 1.4 Save Head
const correspondence = queryRunner.manager.create(Correspondence, { const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber, correspondenceNumber: docNumber,
correspondenceTypeId: createDto.typeId, correspondenceTypeId: createDto.typeId,
@@ -119,7 +107,7 @@ export class CorrespondenceService {
}); });
const savedCorr = await queryRunner.manager.save(correspondence); const savedCorr = await queryRunner.manager.save(correspondence);
// 3.3 สร้าง Revision แรก (Rev 0) // 1.5 Save First Revision
const revision = queryRunner.manager.create(CorrespondenceRevision, { const revision = queryRunner.manager.create(CorrespondenceRevision, {
correspondenceId: savedCorr.id, correspondenceId: savedCorr.id,
revisionNumber: 0, revisionNumber: 0,
@@ -132,7 +120,6 @@ export class CorrespondenceService {
}); });
await queryRunner.manager.save(revision); await queryRunner.manager.save(revision);
// 4. Commit Transaction
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return { return {
@@ -140,7 +127,6 @@ export class CorrespondenceService {
currentRevision: revision, currentRevision: revision,
}; };
} catch (err) { } catch (err) {
// Rollback หากเกิดข้อผิดพลาด
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
throw err; throw err;
} finally { } finally {
@@ -148,37 +134,29 @@ export class CorrespondenceService {
} }
} }
/** // --- READ ---
* ดึงข้อมูลเอกสารทั้งหมด (สำหรับ List Page)
*/
async findAll() { async findAll() {
return this.correspondenceRepo.find({ return this.correspondenceRepo.find({
relations: ['revisions', 'type', 'project', 'originator'], relations: ['revisions', 'type', 'project'],
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
}); });
} }
/**
* ดึงข้อมูลเอกสารรายตัว (Detail Page)
*/
async findOne(id: number) { async findOne(id: number) {
const correspondence = await this.correspondenceRepo.findOne({ const correspondence = await this.correspondenceRepo.findOne({
where: { id }, where: { id },
relations: ['revisions', 'type', 'project', 'originator'], relations: ['revisions', 'type', 'project'],
}); });
if (!correspondence) { if (!correspondence) {
throw new NotFoundException(`Correspondence with ID ${id} not found`); throw new NotFoundException(`Correspondence with ID ${id} not found`);
} }
return correspondence; return correspondence;
} }
/** // --- 2. SUBMIT WORKFLOW ---
* ส่งเอกสาร (Submit) เพื่อเริ่ม Workflow การอนุมัติ/ส่งต่อ
*/
async submit(correspondenceId: number, templateId: number, user: User) { async submit(correspondenceId: number, templateId: number, user: User) {
// 1. ดึงข้อมูลเอกสารและหา Revision ปัจจุบัน // 2.1 Get Document & Current Revision
const correspondence = await this.correspondenceRepo.findOne({ const correspondence = await this.correspondenceRepo.findOne({
where: { id: correspondenceId }, where: { id: correspondenceId },
relations: ['revisions'], relations: ['revisions'],
@@ -188,13 +166,12 @@ export class CorrespondenceService {
throw new NotFoundException('Correspondence not found'); throw new NotFoundException('Correspondence not found');
} }
// หา Revision ที่เป็น current
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent); const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
if (!currentRevision) { if (!currentRevision) {
throw new NotFoundException('Current revision not found'); throw new NotFoundException('Current revision not found');
} }
// 2. ดึงข้อมูล Template และ Steps // 2.2 Get Template Config
const template = await this.templateRepo.findOne({ const template = await this.templateRepo.findOne({
where: { id: templateId }, where: { id: templateId },
relations: ['steps'], relations: ['steps'],
@@ -202,12 +179,9 @@ export class CorrespondenceService {
}); });
if (!template || !template.steps?.length) { if (!template || !template.steps?.length) {
throw new BadRequestException( throw new BadRequestException('Invalid routing template');
'Invalid routing template or no steps defined',
);
} }
// 3. เริ่ม Transaction
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
await queryRunner.startTransaction(); await queryRunner.startTransaction();
@@ -215,29 +189,23 @@ export class CorrespondenceService {
try { try {
const firstStep = template.steps[0]; const firstStep = template.steps[0];
// 3.1 สร้าง Routing Record แรก (Log การส่งต่อ) // 2.3 Create First Routing Record
const routing = queryRunner.manager.create(CorrespondenceRouting, { const routing = queryRunner.manager.create(CorrespondenceRouting, {
correspondenceId: currentRevision.id, // เชื่อมกับ Revision ID correspondenceId: currentRevision.id,
templateId: template.id, // ✅ Save templateId for reference
sequence: 1, sequence: 1,
fromOrganizationId: user.primaryOrganizationId, fromOrganizationId: user.primaryOrganizationId,
toOrganizationId: firstStep.toOrganizationId, toOrganizationId: firstStep.toOrganizationId,
stepPurpose: firstStep.stepPurpose, stepPurpose: firstStep.stepPurpose,
status: 'SENT', // สถานะเริ่มต้นของการส่ง status: 'SENT',
dueDate: new Date( dueDate: new Date(
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000, Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
), ),
processedByUserId: user.user_id, // ผู้ส่ง (User ปัจจุบัน) processedByUserId: user.user_id,
processedAt: new Date(), processedAt: new Date(),
}); });
await queryRunner.manager.save(routing); await queryRunner.manager.save(routing);
// 3.2 (Optional) อัปเดตสถานะของ Revision เป็น 'SUBMITTED'
// const statusSubmitted = await this.statusRepo.findOne({ where: { statusCode: 'SUBMITTED' } });
// if (statusSubmitted) {
// currentRevision.statusId = statusSubmitted.id;
// await queryRunner.manager.save(currentRevision);
// }
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return routing; return routing;
} catch (err) { } catch (err) {
@@ -247,4 +215,138 @@ export class CorrespondenceService {
await queryRunner.release(); await queryRunner.release();
} }
} }
// --- 3. PROCESS ACTION (Approve/Reject/Return) ---
async processAction(
correspondenceId: number,
dto: WorkflowActionDto,
user: User,
) {
// 3.1 Find Active Routing Step
// Find correspondence first to ensure it exists
const correspondence = await this.correspondenceRepo.findOne({
where: { id: correspondenceId },
relations: ['revisions'],
});
if (!correspondence)
throw new NotFoundException('Correspondence not found');
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
if (!currentRevision)
throw new NotFoundException('Current revision not found');
// Find the latest routing step
const currentRouting = await this.routingRepo.findOne({
where: {
correspondenceId: currentRevision.id,
// In real scenario, we might check status 'SENT' or 'RECEIVED'
},
order: { sequence: 'DESC' },
relations: ['toOrganization'],
});
if (
!currentRouting ||
currentRouting.status === 'ACTIONED' ||
currentRouting.status === 'REJECTED'
) {
throw new BadRequestException(
'No active workflow step found or step already processed',
);
}
// 3.2 Check Permissions
// User must belong to the target organization of the current step
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
throw new BadRequestException(
'You are not authorized to process this step',
);
}
// 3.3 Load Template to find Next Step Config
if (!currentRouting.templateId) {
throw new InternalServerErrorException(
'Routing record missing templateId',
);
}
const template = await this.templateRepo.findOne({
where: { id: currentRouting.templateId },
relations: ['steps'],
});
if (!template || !template.steps) {
throw new InternalServerErrorException('Template definition not found');
}
const totalSteps = template.steps.length;
const currentSeq = currentRouting.sequence;
// 3.4 Calculate Next State using Workflow Engine
const result = this.workflowEngine.processAction(
currentSeq,
totalSteps,
dto.action,
dto.returnToSequence,
);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 3.5 Update Current Step
currentRouting.status =
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
currentRouting.processedByUserId = user.user_id;
currentRouting.processedAt = new Date();
currentRouting.comments = dto.comments;
await queryRunner.manager.save(currentRouting);
// 3.6 Create Next Step (If exists and not rejected)
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
// ✅ Find config for next step from Template
const nextStepConfig = template.steps.find(
(s) => s.sequence === result.nextStepSequence,
);
if (!nextStepConfig) {
throw new InternalServerErrorException(
`Configuration for step ${result.nextStepSequence} not found`,
);
}
const nextRouting = queryRunner.manager.create(CorrespondenceRouting, {
correspondenceId: currentRevision.id,
templateId: template.id,
sequence: result.nextStepSequence,
fromOrganizationId: user.primaryOrganizationId, // Forwarded by current user
toOrganizationId: nextStepConfig.toOrganizationId, // ✅ Real Target from Template
stepPurpose: nextStepConfig.stepPurpose, // ✅ Real Purpose from Template
status: 'SENT',
dueDate: new Date(
Date.now() +
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000,
),
});
await queryRunner.manager.save(nextRouting);
}
// 3.7 Update Document Status (Optional - if Engine suggests)
if (result.shouldUpdateStatus) {
// Example: Update revision status to APPROVED or REJECTED
// await this.updateDocumentStatus(currentRevision, result.documentStatus);
}
await queryRunner.commitTransaction();
return { message: 'Action processed successfully', result };
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
} }

View File

@@ -0,0 +1,15 @@
import { IsEnum, IsString, IsOptional, IsInt } from 'class-validator';
import { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface.js';
export class WorkflowActionDto {
@IsEnum(WorkflowAction)
action!: WorkflowAction; // APPROVE, REJECT, RETURN, ACKNOWLEDGE
@IsString()
@IsOptional()
comments?: string;
@IsInt()
@IsOptional()
returnToSequence?: number; // ใช้กรณี action = RETURN
}

View File

@@ -9,6 +9,7 @@ import {
import { CorrespondenceRevision } from './correspondence-revision.entity.js'; import { CorrespondenceRevision } from './correspondence-revision.entity.js';
import { Organization } from '../../project/entities/organization.entity.js'; import { Organization } from '../../project/entities/organization.entity.js';
import { User } from '../../user/entities/user.entity.js'; import { User } from '../../user/entities/user.entity.js';
import { RoutingTemplate } from './routing-template.entity.js'; // <--- ✅ เพิ่ม Import นี้ครับ
@Entity('correspondence_routings') @Entity('correspondence_routings')
export class CorrespondenceRouting { export class CorrespondenceRouting {
@@ -16,7 +17,10 @@ export class CorrespondenceRouting {
id!: number; id!: number;
@Column({ name: 'correspondence_id' }) @Column({ name: 'correspondence_id' })
correspondenceId!: number; // FK -> CorrespondenceRevision correspondenceId!: number;
@Column({ name: 'template_id', nullable: true })
templateId?: number;
@Column() @Column()
sequence!: number; sequence!: number;
@@ -31,7 +35,7 @@ export class CorrespondenceRouting {
stepPurpose!: string; stepPurpose!: string;
@Column({ default: 'SENT' }) @Column({ default: 'SENT' })
status!: string; // SENT, RECEIVED, ACTIONED, FORWARDED, REPLIED status!: string;
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
comments?: string; comments?: string;
@@ -53,6 +57,10 @@ export class CorrespondenceRouting {
@JoinColumn({ name: 'correspondence_id' }) @JoinColumn({ name: 'correspondence_id' })
correspondenceRevision?: CorrespondenceRevision; correspondenceRevision?: CorrespondenceRevision;
@ManyToOne(() => RoutingTemplate) // ตอนนี้ TypeScript จะรู้จัก RoutingTemplate แล้ว
@JoinColumn({ name: 'template_id' })
template?: RoutingTemplate;
@ManyToOne(() => Organization) @ManyToOne(() => Organization)
@JoinColumn({ name: 'from_organization_id' }) @JoinColumn({ name: 'from_organization_id' })
fromOrganization?: Organization; fromOrganization?: Organization;

View File

@@ -0,0 +1,24 @@
import { IsInt, IsNotEmpty, IsOptional, ValidateIf } from 'class-validator';
export class AssignRoleDto {
@IsInt()
@IsNotEmpty()
userId!: number;
@IsInt()
@IsNotEmpty()
roleId!: number;
// Scope (ต้องส่งมาอย่างน้อย 1 อัน หรือไม่ส่งเลยถ้าเป็น Global)
@IsInt()
@IsOptional()
organizationId?: number;
@IsInt()
@IsOptional()
projectId?: number;
@IsInt()
@IsOptional()
contractId?: number;
}

View File

@@ -0,0 +1,42 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
JoinColumn,
CreateDateColumn,
} from 'typeorm';
import { User } from './user.entity.js';
// Import Role, Org, Project, Contract entities...
@Entity('user_assignments')
export class UserAssignment {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'user_id' })
userId!: number;
@Column({ name: 'role_id' })
roleId!: number;
@Column({ name: 'organization_id', nullable: true })
organizationId?: number;
@Column({ name: 'project_id', nullable: true })
projectId?: number;
@Column({ name: 'contract_id', nullable: true })
contractId?: number;
@Column({ name: 'assigned_by_user_id', nullable: true })
assignedByUserId?: number;
@CreateDateColumn({ name: 'assigned_at' })
assignedAt!: Date;
// Relation กลับไปหา User (เจ้าของสิทธิ์)
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user?: User;
}

View File

@@ -0,0 +1,38 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserAssignment } from './entities/user-assignment.entity.js'; // ต้องไปสร้าง Entity นี้ก่อน (ดูข้อ 3)
import { AssignRoleDto } from './dto/assign-role.dto.js';
import { User } from './entities/user.entity.js';
@Injectable()
export class UserAssignmentService {
constructor(
@InjectRepository(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,
);
if (scopes.length > 1) {
throw new BadRequestException(
'Cannot assign multiple scopes at once. Choose one of Org, Project, or Contract.',
);
}
// สร้าง Assignment
const assignment = this.assignmentRepo.create({
userId: dto.userId,
roleId: dto.roleId,
organizationId: dto.organizationId,
projectId: dto.projectId,
contractId: dto.contractId,
assignedByUserId: assigner.user_id, // เก็บ Log ว่าใครเป็นคนให้สิทธิ์
});
return this.assignmentRepo.save(assignment);
}
}

View File

@@ -8,40 +8,48 @@ import {
Delete, Delete,
UseGuards, UseGuards,
ParseIntPipe, ParseIntPipe,
Request, // <--- อย่าลืม Import Request
} from '@nestjs/common'; } from '@nestjs/common';
import { UserService } from './user.service.js'; import { UserService } from './user.service.js';
import { CreateUserDto } from './dto/create-user.dto.js'; import { CreateUserDto } from './dto/create-user.dto.js';
import { UpdateUserDto } from './dto/update-user.dto.js'; import { UpdateUserDto } from './dto/update-user.dto.js';
import { AssignRoleDto } from './dto/assign-role.dto.js'; // <--- Import DTO
import { UserAssignmentService } from './user-assignment.service.js'; // <--- Import Service ใหม่
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js'; import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.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';
@Controller('users') @Controller('users')
@UseGuards(JwtAuthGuard, RbacGuard) // 🔒 เพิ่ม RbacGuard ต่อท้าย) // 🔒 บังคับ Login ทุก Endpoints ในนี้ @UseGuards(JwtAuthGuard, RbacGuard)
export class UserController { export class UserController {
constructor(private readonly userService: UserService) {} constructor(
private readonly userService: UserService,
private readonly assignmentService: UserAssignmentService, // <--- ✅ Inject Service เข้ามา
) {}
// --- User CRUD ---
// 1. สร้างผู้ใช้ใหม่
@Post() @Post()
@RequirePermission('user.create') // 🔒 ต้องมีสิทธิ์ user.create ถึงจะเข้าได้ @RequirePermission('user.create')
create(@Body() createUserDto: CreateUserDto) { create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto); return this.userService.create(createUserDto);
} }
// 2. ดูรายชื่อผู้ใช้ทั้งหมด
@Get() @Get()
@RequirePermission('user.view')
findAll() { findAll() {
return this.userService.findAll(); return this.userService.findAll();
} }
// 3. ดูข้อมูลผู้ใช้รายคน (ตาม ID)
@Get(':id') @Get(':id')
@RequirePermission('user.view')
findOne(@Param('id', ParseIntPipe) id: number) { findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id); return this.userService.findOne(id);
} }
// 4. แก้ไขข้อมูลผู้ใช้
@Patch(':id') @Patch(':id')
@RequirePermission('user.edit')
update( update(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto, @Body() updateUserDto: UpdateUserDto,
@@ -49,9 +57,17 @@ export class UserController {
return this.userService.update(id, updateUserDto); return this.userService.update(id, updateUserDto);
} }
// 5. ลบผู้ใช้ (Soft Delete)
@Delete(':id') @Delete(':id')
@RequirePermission('user.delete')
remove(@Param('id', ParseIntPipe) id: number) { remove(@Param('id', ParseIntPipe) id: number) {
return this.userService.remove(id); return this.userService.remove(id);
} }
// --- Role Assignment ---
@Post('assign-role') // <--- ✅ ต้องมี @ เสมอครับ
@RequirePermission('permission.assign')
assignRole(@Body() dto: AssignRoleDto, @Request() req: any) {
return this.assignmentService.assignRole(dto, req.user);
}
} }

View File

@@ -3,12 +3,22 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service.js'; import { UserService } from './user.service.js';
import { UserController } from './user.controller.js'; // 1. Import Controller import { UserController } from './user.controller.js'; // 1. Import Controller
import { User } from './entities/user.entity.js'; import { User } from './entities/user.entity.js';
import { UserAssignmentService } from './user-assignment.service.js';
import { UserAssignment } from './entities/user-assignment.entity.js';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([User])], // จดทะเบียน Entity imports: [
// 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่ // 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment
TypeOrmModule.forFeature([User, UserAssignment]),
], // 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
controllers: [UserController], controllers: [UserController],
providers: [UserService], providers: [
exports: [UserService], // Export ให้ AuthModule เรียกใช้ได้ UserService,
UserAssignmentService, // <--- 4. ลงทะเบียน Service เป็น Provider
],
exports: [
UserService,
UserAssignmentService, // <--- 5. Export เผื่อที่อื่นใช้
], // Export ให้ AuthModule เรียกใช้ได้
}) })
export class UserModule {} export class UserModule {}

View File

@@ -1,5 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { WorkflowEngineService } from './workflow-engine.service'; import { WorkflowEngineService } from './workflow-engine.service';
import { WorkflowAction } from './interfaces/workflow.interface';
import { BadRequestException } from '@nestjs/common';
describe('WorkflowEngineService', () => { describe('WorkflowEngineService', () => {
let service: WorkflowEngineService; let service: WorkflowEngineService;
@@ -15,4 +17,50 @@ describe('WorkflowEngineService', () => {
it('should be defined', () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
describe('processAction', () => {
// 🟢 กรณี: อนุมัติทั่วไป (ไปขั้นต่อไป)
it('should move to next step on APPROVE', () => {
const result = service.processAction(1, 3, WorkflowAction.APPROVE);
expect(result.nextStepSequence).toBe(2);
expect(result.shouldUpdateStatus).toBe(false);
});
// 🟢 กรณี: อนุมัติขั้นตอนสุดท้าย (จบงาน)
it('should complete workflow on APPROVE at last step', () => {
const result = service.processAction(3, 3, WorkflowAction.APPROVE);
expect(result.nextStepSequence).toBeNull(); // ไม่มีขั้นต่อไป
expect(result.shouldUpdateStatus).toBe(true);
expect(result.documentStatus).toBe('COMPLETED');
});
// 🔴 กรณี: ปฏิเสธ (จบงานทันที)
it('should stop workflow on REJECT', () => {
const result = service.processAction(1, 3, WorkflowAction.REJECT);
expect(result.nextStepSequence).toBeNull();
expect(result.shouldUpdateStatus).toBe(true);
expect(result.documentStatus).toBe('REJECTED');
});
// 🟠 กรณี: ส่งกลับ (ย้อนกลับ 1 ขั้น)
it('should return to previous step on RETURN', () => {
const result = service.processAction(2, 3, WorkflowAction.RETURN);
expect(result.nextStepSequence).toBe(1);
expect(result.shouldUpdateStatus).toBe(true);
expect(result.documentStatus).toBe('REVISE_REQUIRED');
});
// 🟠 กรณี: ส่งกลับ (ระบุขั้น)
it('should return to specific step on RETURN', () => {
const result = service.processAction(3, 5, WorkflowAction.RETURN, 1);
expect(result.nextStepSequence).toBe(1);
});
// ❌ กรณี: Error (ส่งกลับต่ำกว่า 1)
it('should throw error if return step is invalid', () => {
expect(() => {
service.processAction(1, 3, WorkflowAction.RETURN);
}).toThrow(BadRequestException);
});
});
}); });

0
lcbp3_dev.session.sql Normal file
View File

16
temp.ts Normal file
View File

@@ -0,0 +1,16 @@
// ใน function submit()
// 2.1 สร้าง Routing Record แรก
const routing = queryRunner.manager.create(CorrespondenceRouting, {
correspondenceId: currentRevision.id,
templateId: template.id, // ✅ บันทึก templateId ไว้ใช้อ้างอิง
sequence: 1,
fromOrganizationId: user.primaryOrganizationId,
toOrganizationId: firstStep.toOrganizationId,
stepPurpose: firstStep.stepPurpose,
status: 'SENT',
dueDate: new Date(
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
),
processedByUserId: user.user_id,
processedAt: new Date(),
});

173
users.sql Normal file
View File

@@ -0,0 +1,173 @@
-- phpMyAdmin SQL Dump
-- version 5.2.3
-- https://www.phpmyadmin.net/
--
-- Host: mariadb
-- Generation Time: Nov 21, 2025 at 03:33 AM
-- Server version: 11.8.5-MariaDB-ubu2404
-- PHP Version: 8.3.27
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */
;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */
;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */
;
/*!40101 SET NAMES utf8mb4 */
;
--
-- Database: `lcbp3_dev`
--
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`user_id` int(11) NOT NULL COMMENT 'ID ของตาราง',
`username` varchar(50) NOT NULL COMMENT 'ชื่อผู้ใช้งาน',
`password_hash` varchar(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)',
`first_name` varchar(50) DEFAULT NULL COMMENT 'ชื่อจริง',
`last_name` varchar(50) DEFAULT NULL COMMENT 'นามสกุล',
`email` varchar(100) NOT NULL COMMENT 'อีเมล',
`line_id` varchar(100) DEFAULT NULL COMMENT 'LINE ID',
`primary_organization_id` int(11) DEFAULT NULL COMMENT 'สังกัดองค์กร',
`is_active` tinyint(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน',
`failed_attempts` int(11) DEFAULT 0 COMMENT 'จำนวนครั้งที่ล็อกอินล้มเหลว',
`locked_until` datetime DEFAULT NULL COMMENT 'ล็อกอินไม่ได้จนถึงเวลา',
`last_login_at` timestamp NULL DEFAULT NULL COMMENT 'วันที่และเวลาที่ล็อกอินล่าสุด',
`created_at` timestamp NULL DEFAULT current_timestamp() COMMENT 'วันที่สร้าง',
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'วันที่แก้ไขล่าสุด',
`deleted_at` datetime DEFAULT NULL COMMENT 'วันที่ลบ'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)';
--
-- Dumping data for table `users`
--
INSERT INTO `users` (
`user_id`,
`username`,
`password_hash`,
`first_name`,
`last_name`,
`email`,
`line_id`,
`primary_organization_id`,
`is_active`,
`failed_attempts`,
`locked_until`,
`last_login_at`,
`created_at`,
`updated_at`,
`deleted_at`
)
VALUES (
1,
'superadmin',
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
'Super',
'Admin',
'superadmin @example.com',
NULL,
NULL,
1,
0,
NULL,
NULL,
'2025-11-19 08:47:47',
'2025-11-21 03:02:20',
NULL
),
(
2,
'editor01',
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
'DC',
'C1',
'editor01 @example.com',
NULL,
41,
1,
0,
NULL,
NULL,
'2025-11-19 08:47:47',
'2025-11-20 02:57:04',
NULL
),
(
3,
'viewer01',
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
'Viewer',
'สคฉ.03',
'viewer01 @example.com',
NULL,
10,
1,
0,
NULL,
NULL,
'2025-11-19 08:47:47',
'2025-11-20 02:55:50',
NULL
),
(
5,
'admin',
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
'Admin',
'คคง.',
'admin@example.com',
NULL,
1,
1,
0,
NULL,
NULL,
'2025-11-19 08:57:20',
'2025-11-21 02:56:02',
NULL
);
--
-- Indexes for dumped tables
--
--
-- Indexes for table `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`user_id`),
ADD UNIQUE KEY `username` (`username`),
ADD UNIQUE KEY `email` (`email`),
ADD KEY `primary_organization_id` (`primary_organization_id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `users`
--
ALTER TABLE `users`
MODIFY `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID ของตาราง',
AUTO_INCREMENT = 6;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `users`
--
ALTER TABLE `users`
ADD CONSTRAINT `users_ibfk_1` FOREIGN KEY (`primary_organization_id`) REFERENCES `organizations` (`id`) ON DELETE
SET NULL;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */
;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */
;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */
;