260222:1053 20260222 refactor specs/ #1 03-Data-and-Storage
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s

This commit is contained in:
admin
2026-02-22 10:53:23 +07:00
parent fd9be92b9d
commit c90a664f53
105 changed files with 2561 additions and 0 deletions

View File

@@ -1,505 +0,0 @@
# 🛠️ Section 2: System Architecture (สถาปัตยกรรมและเทคโนโลยี)
---
title: 'System Architecture'
version: 1.8.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2026-01-26
related: -
specs/01-objectives.md
---
ชื่อกำหนด สถาปัตยกรรมแบบ Headless/API-First ที่ทันสมัย ทำงานทั้งหมดบน QNAP Server ผ่าน Container Station เพื่อความสะดวกในการจัดการและบำรุงรักษา
## **2.1 Infrastructure & Environment:**
- Domain: `np-dms.work`, `www.np-dms.work`
- IP: 159.192.126.103
- Server: QNAP TS-473A, RAM: 32GB, CPU: AMD Ryzen V1500B, HDD: 4TBx4nos. RAID 5, SSD: 1TB ใช้เป็น caching, มี port 2.5Gbps 2 port
- Server: AS5304T, RAM: 16GB, CPU: Intel Celeron CPU @ 2.00GH, HDD: 6TBx3nos. RAID 5, SSD: 1TB ใช้เป็น caching, มี port 2.5Gbps 2 port
- Rotuter: TP-LINK ER7206, WAN/LAN port 1 SFP, WAN port 2, WAN/LAN 10/100/1000 port 3-6
- Core Switch: TP-LINK TL-SG2428P, LAN port 1-24 10/100/1000, SFP port 25-28 1Gbps
- Server Switch: AMPCOM, LAN port 1-8 10/100/1000/2500, SFP+ port 9 10Gbps
- Admin Switch: TP-LINK ES205G, LAN port 1-5 10/100/1000
- CCTV Switch: TP-LINK TL-SL1226P port 1-24 PoE+ 100Mbps, SFP port 24-25 1Gbps
- IP Phone Switch: TP-LINK TL-SG1210P port 1-8 PoE+ 100Mbps , Uplink1 10/100/1000, Uplink2 SFP 1Gbps
- Controller: TP-LINK OC200
- Wireless Access point: TP-LINK EAP610 16 ตัว
- CCTV: HikVision (DS-7732NXI-K4) + กล้อง 6 ตัว
- IP Phone: YeaLink 8 ตัว
- Admin Desktop: Windows 11, LAN port 10/100/1000/2500
- Printer: Kyocera CS 3554ci, LAN port 10/100/1000
- 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 Netwrok Configuration**
**VLAN Networks**
| VLAN ID | Name | Purpose | Gateway/Subnet | DHCP | IP Range | DNS | Lease Time | ARP Detection | IGMP Snooping | MLD Snooping | Notes |
| ------- | ------ | --------- | --------------- | ---- | ------------------ | ------- | ---------- | ------------- | ------------- | ------------ | --------------- |
| 10 | SERVER | Interface | 192.168.10.1/24 | No | - | Custom | - | - | - | - | Static servers |
| 20 | MGMT | Interface | 192.168.20.1/24 | No | - | Custom | - | Enable | Enable | - | Management only |
| 30 | USER | Interface | 192.168.30.1/24 | Yes | 192.168.30.10-254 | Auto | 7 Days | - | Enable | - | User devices |
| 40 | CCTV | Interface | 192.168.40.1/24 | Yes | 192.168.40.100-150 | Auto | 7 Days | - | Enable | - | CCTV & NVR |
| 50 | VOICE | Interface | 192.168.50.1/24 | Yes | 192.168.50.201-250 | Auto | 7 Days | - | - | - | IP Phones |
| 60 | DMZ | Interface | 192.168.60.1/24 | No | - | 1.1.1.1 | - | - | - | - | Public services |
| 70 | GUEST | Interface | 192.168.70.1/24 | Yes | 192.168.70.200-250 | Auto | 1 Day | - | - | - | Guest |
**Switch Profiles**
| Profile Name | Native Network | Tagged Networks | Untagged Networks | Voice Network | Loopback Control | Usage |
| ---------------- | -------------- | --------------------- | ----------------- | ------------- | ---------------- | ----------------------- |
| 01_CORE_TRUNK | MGMT (20) | 10,30,40,50,60,70 | MGMT (20) | - | Spanning Tree | Router & switch uplinks |
| 02_MGMT_ONLY | MGMT (20) | MGMT (20) | - | - | Spanning Tree | Management only |
| 03_SERVER_ACCESS | SERVER (10) | MGMT (20) | SERVER (10) | - | Spanning Tree | QNAP / ASUSTOR |
| 04_CCTV_ACCESS | CCTV (40) | - | CCTV (40) | - | Spanning Tree | CCTV cameras |
| 05_USER_ACCESS | USER (30) | - | USER (30) | - | Spanning Tree | PC / Printer |
| 06_AP_TRUNK | MGMT (20) | USER (30), GUEST (70) | MGMT (20) | - | Spanning Tree | EAP610 Access Points |
| 07_VOICE_ACCESS | USER (30) | VOICE (50) | USER (30) | VOICE (50) | Spanning Tree | IP Phones |
**ER7206 Port Mapping**
| Port | Connected Device | Port | Description |
| ---- | ---------------- | ------------- | ----------- |
| 1 | - | - | - |
| 2 | WAN | - | Internet |
| 3 | SG2428P | PVID MGMT(20) | Core Switch |
| 4 | - | - | - |
| 5 | - | - | - |
| 6 | - | - | - |
**AMPCOM Port Aggregate Setting**
| Aggregate Group ID | Type | Member port | Aggregated Port |
| ------------------ | ---- | ----------- | --------------- |
| Trunk1 | LACP | 3,4 | 3,4 |
| Trunk2 | LACP | 5,6 | 5,6 |
**AMPCOM Port VLAN Mapping**
| Port | Connected Device | Port vlan type | Access VLAN | Native VLAN | Trunk vlan |
| ------ | ---------------- | -------------- | ----------- | ----------- | -------------------- |
| 1 | SG2428P | Trunk | - | 20 | 10,20,30,40,50,60,70 |
| 2 | - | Trunk | - | 20 | 10,20,30,40,50,60,70 |
| 7 | - | Access | 20 | - | - |
| 8 | Admin Desktop | Access | 20 | - | - |
| Trunk1 | QNAP | Trunk | - | 10 | 10,20,30,40,50,60,70 |
| Trunk2 | ASUSTOR | Trunk | - | 10 | 10,20,30,40,50,60,70 |
**NAS NIC Bonding Configuration**
| Device | Bonding Mode | Member Ports | VLAN Mode | Tagged VLAN | IP Address | Gateway | Notes |
| ------- | ------------------- | ------------ | --------- | ----------- | --------------- | ------------ | ---------------------- |
| QNAP | IEEE 802.3ad (LACP) | Adapter 1, 2 | Untagged | 10 (SERVER) | 192.168.10.8/24 | 192.168.10.1 | Primary NAS for DMS |
| ASUSTOR | IEEE 802.3ad (LACP) | Port 1, 2 | Untagged | 10 (SERVER) | 192.168.10.9/24 | 192.168.10.1 | Backup / Secondary NAS |
> **หมายเหตุ**: NAS ทั้งสองตัวใช้ LACP bonding เพื่อเพิ่ม bandwidth และ redundancy โดยต้อง config ให้ตรงกับ AMPCOM Switch (Trunk1)
**SG2428P Port Mapping**
| Port | Connected Device | Switch Profile | Description |
| ---- | ------------------------- | -------------------- | ------------- |
| 1 | ER7206 | 01_CORE_TRUNK | Internet |
| 2 | OC200 | 01_CORE_TRUNK | Controller |
| 3 | Ampcom 2.5G Switch Port 1 | LAG1 (01_CORE_TRUNK) | Uplink |
| 4 | - | LAG1 (01_CORE_TRUNK) | Reserved |
| 5 | EAP610-01 | 06_AP_TRUNK | Access Point |
| 6 | EAP610-02 | 06_AP_TRUNK | Access Point |
| 7 | EAP610-03 | 06_AP_TRUNK | Access Point |
| 8 | EAP610-04 | 06_AP_TRUNK | Access Point |
| 9 | EAP610-05 | 06_AP_TRUNK | Access Point |
| 10 | EAP610-06 | 06_AP_TRUNK | Access Point |
| 11 | EAP610-07 | 06_AP_TRUNK | Access Point |
| 12 | EAP610-08 | 06_AP_TRUNK | Access Point |
| 13 | EAP610-09 | 06_AP_TRUNK | Access Point |
| 14 | EAP610-10 | 06_AP_TRUNK | Access Point |
| 15 | EAP610-11 | 06_AP_TRUNK | Access Point |
| 16 | EAP610-12 | 06_AP_TRUNK | Access Point |
| 17 | EAP610-13 | 06_AP_TRUNK | Access Point |
| 18 | EAP610-14 | 06_AP_TRUNK | Access Point |
| 19 | EAP610-15 | 06_AP_TRUNK | Access Point |
| 20 | EAP610-16 | 06_AP_TRUNK | Access Point |
| 21 | Reserved | 01_CORE_TRUNK | |
| 22 | Reserved | 01_CORE_TRUNK | |
| 23 | Printer | 05_USER_ACCESS | Printer |
| 24 | ES205G | 01_CORE_TRUNK | Management PC |
| 25 | TL-SL1226P | 01_CORE_TRUNK | Uplink |
| 26 | SG1210P | 01_CORE_TRUNK | Uplink |
| 27 | Reserved | 01_CORE_TRUNK | |
| 28 | Reserved | 01_CORE_TRUNK | |
**ES205G Port Mapping (Admin Switch)**
| Port | Connected Device | VLAN | Description |
| ---- | ---------------- | ----------- | ----------- |
| 1 | SG2428P Port 24 | Trunk (All) | Uplink |
| 2 | Admin Desktop | MGMT (20) | Admin PC |
| 3 | Reserved | MGMT (20) | |
| 4 | Reserved | MGMT (20) | |
| 5 | Reserved | MGMT (20) | |
> **หมายเหตุ**: ES205G เป็น Unmanaged Switch ไม่รองรับ VLAN tagging ดังนั้นทุก port จะอยู่ใน Native VLAN (20) ของ uplink
**TL-SL1226P Port Mapping (CCTV Switch)**
| Port | Connected Device | PoE | VLAN | Description |
| ---- | ---------------- | ---- | --------- | ----------- |
| 1 | Camera-01 | PoE+ | CCTV (40) | CCTV Camera |
| 2 | Camera-02 | PoE+ | CCTV (40) | CCTV Camera |
| 3 | Camera-03 | PoE+ | CCTV (40) | CCTV Camera |
| 4 | Camera-04 | PoE+ | CCTV (40) | CCTV Camera |
| 5 | Camera-05 | PoE+ | CCTV (40) | CCTV Camera |
| 6 | Camera-06 | PoE+ | CCTV (40) | CCTV Camera |
| 7-23 | Reserved | PoE+ | CCTV (40) | |
| 24 | HikVision NVR | - | CCTV (40) | NVR |
| 25 | SG2428P Port 25 | - | Trunk | SFP Uplink |
| 26 | Reserved | - | Trunk | SFP |
**SG1210P Port Mapping (IP Phone Switch)**
| Port | Connected Device | PoE | Data VLAN | Voice VLAN | Description |
| ------- | ---------------- | ---- | --------- | ---------- | ----------- |
| 1 | IP Phone-01 | PoE+ | USER (30) | VOICE (50) | IP Phone |
| 2 | IP Phone-02 | PoE+ | USER (30) | VOICE (50) | IP Phone |
| 3 | IP Phone-03 | PoE+ | USER (30) | VOICE (50) | IP Phone |
| 4 | IP Phone-04 | PoE+ | USER (30) | VOICE (50) | IP Phone |
| 5 | IP Phone-05 | PoE+ | USER (30) | VOICE (50) | IP Phone |
| 6 | IP Phone-06 | PoE+ | USER (30) | VOICE (50) | IP Phone |
| 7 | IP Phone-07 | PoE+ | USER (30) | VOICE (50) | IP Phone |
| 8 | IP Phone-08 | PoE+ | USER (30) | VOICE (50) | IP Phone |
| Uplink1 | Reserved | - | Trunk | - | RJ45 Uplink |
| Uplink2 | SG2428P Port 26 | - | Trunk | - | SFP Uplink |
> **หมายเหตุ**: SG1210P รองรับ Voice VLAN ทำให้ IP Phone ใช้ VLAN 50 สำหรับ voice traffic และ passthrough VLAN 30 สำหรับ PC ที่ต่อผ่าน phone
**Static IP Allocation**
| VLAN | Device | IP Address | MAC Address | Notes |
| ---------- | --------------- | ------------------ | ----------- | ---------------- |
| SERVER(10) | QNAP | 192.168.10.8 | - | Primary NAS |
| SERVER(10) | ASUSTOR | 192.168.10.9 | - | Backup NAS |
| SERVER(10) | Docker Host | 192.168.10.10 | - | Containers |
| MGMT(20) | ER7206 | 192.168.20.1 | - | Gateway/Router |
| MGMT(20) | SG2428P | 192.168.20.2 | - | Core Switch |
| MGMT(20) | AMPCOM | 192.168.20.3 | - | Server Switch |
| MGMT(20) | TL-SL1226P | 192.168.20.4 | - | CCTV Switch |
| MGMT(20) | SG1210P | 192.168.20.5 | - | Phone Switch |
| MGMT(20) | OC200 | 192.168.20.250 | - | Omada Controller |
| MGMT(20) | Admin Desktop | 192.168.20.100 | - | Admin PC |
| USER(30) | Printer | 192.168.30.222 | - | Kyocera CS3554ci |
| CCTV(40) | NVR | 192.168.40.100 | - | HikVision NVR |
| CCTV(40) | Camera-01 to 06 | 192.168.40.101-106 | - | CCTV Cameras |
| USER(30) | Admin Desktop | 192.168.30.100 | - | Admin PC (USER) |
**2.8 DHCP Reservation (MAC Mapping)**
**CCTV MAC Address Mapping (VLAN 40)**
| Device Name | IP Address | MAC Address | Port (Switch) | Notes |
| ------------- | -------------- | ----------- | ------------- | ---------- |
| HikVision NVR | 192.168.40.100 | | Port 24 | Master NVR |
| Camera-01 | 192.168.40.101 | | Port 1 | |
| Camera-02 | 192.168.40.102 | | Port 2 | |
| Camera-03 | 192.168.40.103 | | Port 3 | |
| Camera-04 | 192.168.40.104 | | Port 4 | |
| Camera-05 | 192.168.40.105 | | Port 5 | |
| Camera-06 | 192.168.40.106 | | Port 6 | |
**IP Phone MAC Address Mapping (VLAN 50)**
| Device Name | IP Address | MAC Address | Port (Switch) | Notes |
| ----------- | -------------- | ----------- | ------------- | ------- |
| IP Phone-01 | 192.168.50.201 | | Port 1 | Yealink |
| IP Phone-02 | 192.168.50.202 | | Port 2 | Yealink |
| IP Phone-03 | 192.168.50.203 | | Port 3 | Yealink |
| IP Phone-04 | 192.168.50.204 | | Port 4 | Yealink |
| IP Phone-05 | 192.168.50.205 | | Port 5 | Yealink |
| IP Phone-06 | 192.168.50.206 | | Port 6 | Yealink |
| IP Phone-07 | 192.168.50.207 | | Port 7 | Yealink |
| IP Phone-08 | 192.168.50.208 | | Port 8 | Yealink |
**Wireless SSID Mapping (OC200 Controller)**
| SSID Name | Band | VLAN | Security | Portal Auth | Notes |
| --------- | ------- | ---------- | --------- | ----------- | ----------------------- |
| PSLCBP3 | 2.4G/5G | USER (30) | WPA2/WPA3 | No | Staff WiFi |
| GUEST | 2.4G/5G | GUEST (70) | WPA2 | Yes | Guest WiFi with Captive |
> **หมายเหตุ**: ทุก SSID broadcast ผ่าน EAP610 ทั้ง 16 ตัว โดยใช้ 06_AP_TRUNK profile ที่ tag VLAN 30 และ 70
**Gateway ACL (ER7206 Firewall Rules)**
*Inter-VLAN Routing Policy*
| # | Name | Source | Destination | Service | Action | Log | Notes |
| --- | ----------------- | --------------- | ---------------- | -------------- | ------ | --- | --------------------------- |
| 1 | MGMT-to-ALL | VLAN20 (MGMT) | Any | Any | Allow | No | Admin full access |
| 2 | SERVER-to-ALL | VLAN10 (SERVER) | Any | Any | Allow | No | Servers outbound access |
| 3 | USER-to-SERVER | VLAN30 (USER) | VLAN10 (SERVER) | HTTP/HTTPS/SSH | Allow | No | Users access web apps |
| 4 | USER-to-DMZ | VLAN30 (USER) | VLAN60 (DMZ) | HTTP/HTTPS | Allow | No | Users access DMZ services |
| 5 | USER-to-MGMT | VLAN30 (USER) | VLAN20 (MGMT) | Any | Deny | Yes | Block users from management |
| 6 | USER-to-CCTV | VLAN30 (USER) | VLAN40 (CCTV) | Any | Deny | Yes | Isolate CCTV |
| 7 | USER-to-VOICE | VLAN30 (USER) | VLAN50 (VOICE) | Any | Deny | No | Isolate Voice |
| 8 | USER-to-GUEST | VLAN30 (USER) | VLAN70 (GUEST) | Any | Deny | No | Isolate Guest |
| 9 | CCTV-to-INTERNET | VLAN40 (CCTV) | WAN | HTTPS (443) | Allow | No | NVR cloud backup (optional) |
| 10 | CCTV-to-ALL | VLAN40 (CCTV) | Any (except WAN) | Any | Deny | Yes | CCTV isolated |
| 11 | VOICE-to-SIP | VLAN50 (VOICE) | SIP Server IP | SIP/RTP | Allow | No | Voice to SIP trunk |
| 12 | VOICE-to-ALL | VLAN50 (VOICE) | Any | Any | Deny | No | Voice isolated |
| 13 | DMZ-to-ALL | VLAN60 (DMZ) | Any (internal) | Any | Deny | Yes | DMZ cannot reach internal |
| 14 | GUEST-to-INTERNET | VLAN70 (GUEST) | WAN | HTTP/HTTPS/DNS | Allow | No | Guest internet only |
| 15 | GUEST-to-ALL | VLAN70 (GUEST) | Any (internal) | Any | Deny | Yes | Guest isolated |
| 99 | DEFAULT-DENY | Any | Any | Any | Deny | Yes | Catch-all deny |
*WAN Inbound Rules (Port Forwarding)*
| # | Name | WAN Port | Internal IP | Internal Port | Protocol | Notes |
| --- | --------- | -------- | ------------ | ------------- | -------- | ------------------- |
| 1 | HTTPS-NPM | 443 | 192.168.10.8 | 443 | TCP | Nginx Proxy Manager |
| 2 | HTTP-NPM | 80 | 192.168.10.8 | 80 | TCP | HTTP redirect |
> **หมายเหตุ**: ER7206 ใช้หลักการ Default Deny - Rules ประมวลผลจากบนลงล่าง
**Switch ACL (SG2428P Layer 2 Rules)**
*Port-Based Access Control*
| # | Name | Source Port | Source MAC/VLAN | Destination | Action | Notes |
| --- | --------------- | --------------- | --------------- | ------------------- | ------ | ------------------------ |
| 1 | CCTV-Isolation | Port 25 (CCTV) | VLAN 40 | VLAN 10,20,30 | Deny | CCTV cannot reach others |
| 2 | Guest-Isolation | Port 5-20 (APs) | VLAN 70 | VLAN 10,20,30,40,50 | Deny | Guest isolation |
| 3 | Voice-QoS | Port 26 (Phone) | VLAN 50 | Any | Allow | QoS priority DSCP EF |
*Storm Control (per port)*
| Port Range | Broadcast | Multicast | Unknown Unicast | Notes |
| ---------- | --------- | --------- | --------------- | ----------------------- |
| 1-28 | 10% | 10% | 10% | Prevent broadcast storm |
*Spanning Tree Configuration*
| Setting | Value | Notes |
| -------------------- | --------- | ------------------------------ |
| STP Mode | RSTP | Rapid Spanning Tree |
| Root Bridge Priority | 4096 | SG2428P as root |
| Port Fast | Port 5-24 | Edge ports (APs, endpoints) |
| BPDU Guard | Port 5-24 | Protect against rogue switches |
> **หมายเหตุ**: SG2428P เป็น L2+ switch, ACL ทำได้จำกัด ให้ใช้ ER7206 เป็น primary firewall
**EAP ACL (Omada Controller - Wireless Rules)**
*SSID: PSLCBP3 (Staff WiFi)*
| # | Name | Source | Destination | Service | Action | Schedule | Notes |
| --- | ------------------- | ---------- | ---------------- | -------- | ------ | -------- | ----------------- |
| 1 | Allow-DNS | Any Client | 8.8.8.8, 1.1.1.1 | DNS (53) | Allow | Always | DNS resolution |
| 2 | Allow-Server | Any Client | 192.168.10.0/24 | Any | Allow | Always | Access to servers |
| 3 | Allow-Printer | Any Client | 192.168.30.222 | 9100,631 | Allow | Always | Print services |
| 4 | Allow-Internet | Any Client | WAN | Any | Allow | Always | Internet access |
| 5 | Block-MGMT | Any Client | 192.168.20.0/24 | Any | Deny | Always | No management |
| 6 | Block-CCTV | Any Client | 192.168.40.0/24 | Any | Deny | Always | No CCTV access |
| 7 | Block-Voice | Any Client | 192.168.50.0/24 | Any | Deny | Always | No Voice access |
| 8 | Block-Client2Client | Any Client | Any Client | Any | Deny | Always | Client isolation |
*SSID: GUEST (Guest WiFi)*
| # | Name | Source | Destination | Service | Action | Schedule | Notes |
| --- | ------------------- | ---------- | ---------------- | ---------- | ------ | -------- | ------------------ |
| 1 | Allow-DNS | Any Client | 8.8.8.8, 1.1.1.1 | DNS (53) | Allow | Always | DNS resolution |
| 2 | Allow-HTTP | Any Client | WAN | HTTP/HTTPS | Allow | Always | Web browsing |
| 3 | Block-RFC1918 | Any Client | 10.0.0.0/8 | Any | Deny | Always | No private IPs |
| 4 | Block-RFC1918-2 | Any Client | 172.16.0.0/12 | Any | Deny | Always | No private IPs |
| 5 | Block-RFC1918-3 | Any Client | 192.168.0.0/16 | Any | Deny | Always | No internal access |
| 6 | Block-Client2Client | Any Client | Any Client | Any | Deny | Always | Client isolation |
*Rate Limiting*
| SSID | Download Limit | Upload Limit | Notes |
| ------- | -------------- | ------------ | ----------------------- |
| PSLCBP3 | Unlimited | Unlimited | Staff full speed |
| GUEST | 10 Mbps | 5 Mbps | Guest bandwidth control |
*Captive Portal (GUEST SSID)*
| Setting | Value | Notes |
| ---------------- | --------------- | ---------------------- |
| Portal Type | Simple Password | Single shared password |
| Session Timeout | 8 Hours | Re-auth after 8 hours |
| Idle Timeout | 30 Minutes | Disconnect if idle |
| Terms of Service | Enabled | User must accept ToS |
> **หมายเหตุ**: EAP ACL ทำงานที่ Layer 3 บน Omada Controller ช่วยลด load บน ER7206
**Network Topology Diagram**
```mermaid
graph TB
subgraph Internet
WAN[("🌐 Internet<br/>WAN")]
end
subgraph Router["ER7206 Router"]
R[("🔲 ER7206<br/>192.168.20.1")]
end
subgraph CoreSwitch["SG2428P Core Switch"]
CS[("🔲 SG2428P<br/>192.168.20.2")]
end
subgraph ServerSwitch["AMPCOM 2.5G Switch"]
SS[("🔲 AMPCOM<br/>192.168.20.3")]
end
subgraph Servers["VLAN 10 - Servers"]
QNAP[("💾 QNAP<br/>192.168.10.10")]
ASUSTOR[("💾 ASUSTOR<br/>192.168.10.11")]
end
subgraph AccessPoints["EAP610 x16"]
AP[("📶 WiFi APs")]
end
subgraph OtherSwitches["Distribution"]
CCTV_SW[("🔲 TL-SL1226P<br/>CCTV")]
PHONE_SW[("🔲 SG1210P<br/>IP Phone")]
ADMIN_SW[("🔲 ES205G<br/>Admin")]
end
WAN --> R
R -->|Port 3| CS
CS -->|LAG Port 3-4| SS
SS -->|Port 3-4 LACP| QNAP
SS -->|Port 5-6 LACP| ASUSTOR
SS -->|Port 7| ADMIN_SW
CS -->|Port 5-20| AP
CS -->|SFP 25| CCTV_SW
CS -->|SFP 26| PHONE_SW
CS -->|Port 24| ADMIN_SW
```
**OC200 Omada Controller Configuration**
| Setting | Value | Notes |
| --------------- | -------------------------- | ------------------------------ |
| Controller IP | 192.168.20.10 | Static IP in MGMT VLAN |
| Controller Port | 8043 (HTTPS) | Management Web UI |
| Adoption URL | https://192.168.20.10:8043 | URL for AP adoption |
| Site Name | LCBP3 | Single site configuration |
| Managed Devices | 16x EAP610 | All APs managed centrally |
| Firmware Update | Manual | Test before production rollout |
| Backup Schedule | Weekly (Sunday 2AM) | Auto backup to QNAP |
## **2.3 การจัดการ 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)
- Docker Network: ทุก Service จะเชื่อมต่อผ่านเครือข่ายกลางชื่อ lcbp3 เพื่อให้สามารถสื่อสารกันได้
## **2.4 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 11.8
- 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.5 Business Logic & Consistency (ปรับปรุง):**
- 2.5.1 Unified Workflow Engine (หลัก):
- ระบบการเดินเอกสารทั้งหมด (Correspondence, RFA, Circulation) ต้อง ใช้ Engine กลางเดียวกัน โดยกำหนด Logic ผ่าน Workflow DSL (JSON Configuration) แทนการเขียน Hard-coded ลงในตาราง
- Workflow Versioning (เพิ่ม): ระบบต้องรองรับการกำหนด Version ของ Workflow Definition โดยเอกสารที่เริ่มกระบวนการไปแล้ว (In-progress instances) จะต้องใช้ Workflow Version เดิม จนกว่าจะสิ้นสุดกระบวนการ หรือได้รับคำสั่ง Migrate จาก Admin เพื่อป้องกันความขัดแย้งของ State
- 2.5.2 Separation of Concerns:
- Module ต่างๆ (Correspondence, RFA, Circulation) จะเก็บเฉพาะข้อมูลของเอกสาร (Data) ส่วนสถานะและการเปลี่ยนสถานะ (State Transition) จะถูกจัดการโดย Workflow Engine
- 2.5.3 Idempotency & Locking:
- ใช้กลไกเดิมในการป้องกันการทำรายการซ้ำ
- 2.5.4 Optimistic Locking:
- ใช้ Version Column ใน Database ควบคู่กับ Redis Lock สำหรับการสร้างเลขที่เอกสาร เพื่อเป็น Safety Net ชั้นสุดท้าย
- 2.5.5 จะไม่มีการใช้ SQL Triggers
- เพื่อป้องกันตรรกะซ่อนเร้น (Hidden Logic) และความซับซ้อนในการดีบัก
## **2.6 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.7 กลยุทธ์ความทนทานและการจัดการข้อผิดพลาด (Resilience & Error Handling Strategy)**
- 2.7.1 Circuit Breaker Pattern: ใช้สำหรับ external service calls (Email, LINE, Elasticsearch)
- 2.7.2 Retry Mechanism: ด้วย exponential backoff สำหรับ transient failures
- 2.7.3 Fallback Strategies: Graceful degradation เมื่อบริการภายนอกล้มเหลว
- 2.7.4 Error Handling: Error messages ต้องไม่เปิดเผยข้อมูล sensitive
- 2.6.5 Monitoring: Centralized error monitoring และ alerting system

View File

@@ -1,39 +0,0 @@
# 3.10 File Handling Management (การจัดการไฟล์)
---
title: 'Functional Requirements: File Handling Management'
version: 1.5.0
status: first-draft
owner: Nattanin Peancharoen
last_updated: 2025-11-30
related:
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
---
## 3.10.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.10.2 Security:
- Virus Scan (ClamAV) ก่อนย้ายเข้า permanent
- Whitelist File Types: PDF, DWG, DOCX, XLSX, ZIP
- Max Size: 50MB
- Access Control: ตรวจสอบสิทธิ์ผ่าน Junction Table ก่อนให้ Download Link
## 3.10.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 ทุกครั้งที่มีการดาวน์โหลดไฟล์สำคัญ

View File

@@ -0,0 +1,821 @@
# Document Numbering Implementation Guide (Combined)
---
title: 'Implementation Guide: Document Numbering System'
version: 1.6.2
status: APPROVED
owner: Development Team
last_updated: 2025-12-17
related:
- specs/01-requirements/03.11-document-numbering.md
- specs/04-operations/document-numbering-operations.md
- specs/05-decisions/ADR-002-document-numbering-strategy.md
---
## Overview
เอกสารนี้รวบรวม implementation details สำหรับระบบ Document Numbering โดยผนวกข้อมูลจาก:
- `document-numbering.md` - Core implementation และ database schema
- `document-numbering-add.md` - Extended features (Reservation, Manual Override, Monitoring)
---
## Technology Stack
| Component | Technology |
| ----------------- | -------------------- |
| Backend Framework | NestJS 10.x |
| ORM | TypeORM 0.3.x |
| Database | MariaDB 11.8 |
| Cache/Lock | Redis 7.x + Redlock |
| Message Queue | BullMQ |
| Monitoring | Prometheus + Grafana |
---
## 1. Module Structure
```
backend/src/modules/document-numbering/
├── document-numbering.module.ts
├── controllers/
│ ├── document-numbering.controller.ts # General endpoints
│ ├── document-numbering-admin.controller.ts # Admin endpoints
│ └── numbering-metrics.controller.ts # Metrics endpoints
├── services/
│ ├── document-numbering.service.ts # Main orchestration
│ ├── document-numbering-lock.service.ts # Redis Lock
│ ├── counter.service.ts # Sequence counter logic
│ ├── reservation.service.ts # Two-phase commit
│ ├── manual-override.service.ts # Manual number handling
│ ├── format.service.ts # Template formatting
│ ├── template.service.ts # Template CRUD
│ ├── audit.service.ts # Audit logging
│ ├── metrics.service.ts # Prometheus metrics
│ └── migration.service.ts # Legacy import
├── entities/
│ ├── document-number-counter.entity.ts
│ ├── document-number-format.entity.ts
│ ├── document-number-audit.entity.ts
│ ├── document-number-error.entity.ts
│ └── document-number-reservation.entity.ts
├── dto/
│ ├── generate-number.dto.ts
│ ├── preview-number.dto.ts
│ ├── reserve-number.dto.ts
│ ├── confirm-reservation.dto.ts
│ ├── manual-override.dto.ts
│ ├── void-document.dto.ts
│ └── bulk-import.dto.ts
├── validators/
│ └── template.validator.ts
├── guards/
│ └── manual-override.guard.ts
├── decorators/
│ └── audit-numbering.decorator.ts
├── jobs/
│ └── counter-reset.job.ts
└── tests/
├── unit/
├── integration/
└── e2e/
```
---
## 2. Database Schema
### 2.1 Format Template Table
```sql
CREATE TABLE document_number_formats (
id INT AUTO_INCREMENT PRIMARY KEY,
project_id INT NOT NULL,
correspondence_type_id INT NULL, -- NULL = default format for project
format_template VARCHAR(100) NOT NULL,
reset_sequence_yearly TINYINT(1) DEFAULT 1,
description VARCHAR(255),
created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
UNIQUE KEY idx_unique_project_type (project_id, correspondence_type_id),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE
) ENGINE=InnoDB COMMENT='Document Number Format Templates';
```
### 2.2 Counter Table
```sql
CREATE TABLE document_number_counters (
project_id INT NOT NULL,
correspondence_type_id INT NULL,
originator_organization_id INT NOT NULL,
recipient_organization_id INT NOT NULL DEFAULT 0, -- 0 = no recipient (RFA)
sub_type_id INT DEFAULT 0,
rfa_type_id INT DEFAULT 0,
discipline_id INT DEFAULT 0,
reset_scope VARCHAR(20) NOT NULL,
last_number INT DEFAULT 0 NOT NULL,
version INT DEFAULT 0 NOT NULL,
created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (
project_id,
originator_organization_id,
recipient_organization_id,
correspondence_type_id,
sub_type_id,
rfa_type_id,
discipline_id,
reset_scope
),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE,
INDEX idx_counter_lookup (project_id, correspondence_type_id, reset_scope),
INDEX idx_counter_org (originator_organization_id, reset_scope),
INDEX idx_counter_updated (updated_at),
CONSTRAINT chk_last_number_positive CHECK (last_number >= 0),
CONSTRAINT chk_reset_scope_format CHECK (
reset_scope = 'NONE' OR
reset_scope LIKE 'YEAR_%'
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='Running Number Counters';
```
### 2.3 Audit Table
```sql
CREATE TABLE document_number_audit (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
document_id INT NULL COMMENT 'FK to documents (NULL initially)',
document_type VARCHAR(50),
document_number VARCHAR(100) NOT NULL,
operation ENUM('RESERVE', 'CONFIRM', 'CANCEL', 'MANUAL_OVERRIDE', 'VOID', 'GENERATE') NOT NULL,
status ENUM('RESERVED', 'CONFIRMED', 'CANCELLED', 'VOID', 'MANUAL'),
counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)',
reservation_token VARCHAR(36) NULL,
originator_organization_id INT NULL,
recipient_organization_id INT NULL,
template_used VARCHAR(200) NOT NULL,
old_value TEXT NULL,
new_value TEXT NULL,
user_id INT NULL COMMENT 'FK to users (Allow NULL for system generation)',
ip_address VARCHAR(45),
user_agent TEXT,
is_success BOOLEAN DEFAULT TRUE,
retry_count INT DEFAULT 0,
lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds',
total_duration_ms INT COMMENT 'Total generation time',
fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE',
metadata JSON NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_document_id (document_id),
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_operation (operation),
INDEX idx_document_number (document_number),
INDEX idx_created_at (created_at),
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail';
```
### 2.4 Error Log Table
```sql
CREATE TABLE document_number_errors (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
error_type ENUM(
'LOCK_TIMEOUT',
'VERSION_CONFLICT',
'DB_ERROR',
'REDIS_ERROR',
'VALIDATION_ERROR',
'SEQUENCE_EXHAUSTED',
'RESERVATION_EXPIRED',
'DUPLICATE_NUMBER'
) NOT NULL,
error_message TEXT,
stack_trace TEXT,
context_data JSON COMMENT 'Request context (user, project, etc.)',
user_id INT,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resolved_at TIMESTAMP NULL,
INDEX idx_error_type (error_type),
INDEX idx_created_at (created_at),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB COMMENT='Document Numbering Error Log';
```
### 2.5 Reservation Table
```sql
CREATE TABLE document_number_reservations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
-- Reservation Details
token VARCHAR(36) NOT NULL UNIQUE COMMENT 'UUID v4',
document_number VARCHAR(100) NOT NULL UNIQUE,
status ENUM('RESERVED', 'CONFIRMED', 'CANCELLED', 'VOID') NOT NULL DEFAULT 'RESERVED',
-- Linkage
document_id INT NULL COMMENT 'FK to documents (NULL until confirmed)',
-- Context (for debugging)
project_id INT NOT NULL,
correspondence_type_id INT NOT NULL,
originator_organization_id INT NOT NULL,
recipient_organization_id INT DEFAULT 0,
user_id INT NOT NULL,
-- Timestamps
reserved_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
expires_at DATETIME(6) NOT NULL,
confirmed_at DATETIME(6) NULL,
cancelled_at DATETIME(6) NULL,
-- Audit
ip_address VARCHAR(45),
user_agent TEXT,
metadata JSON NULL COMMENT 'Additional context',
-- Indexes
INDEX idx_token (token),
INDEX idx_status (status),
INDEX idx_status_expires (status, expires_at),
INDEX idx_document_id (document_id),
INDEX idx_user_id (user_id),
INDEX idx_reserved_at (reserved_at),
-- Foreign Keys
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE SET NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='Document Number Reservations - Two-Phase Commit';
```
---
## 3. Core Services
### 3.1 Number Generation Process
```mermaid
sequenceDiagram
participant C as Client
participant S as NumberingService
participant L as LockService
participant CS as CounterService
participant DB as Database
participant R as Redis
C->>S: generateDocumentNumber(dto)
S->>L: acquireLock(counterKey)
L->>R: REDLOCK acquire
R-->>L: lock acquired
L-->>S: lock handle
S->>CS: incrementCounter(counterKey)
CS->>DB: BEGIN TRANSACTION
CS->>DB: SELECT FOR UPDATE
CS->>DB: UPDATE last_number
CS->>DB: COMMIT
DB-->>CS: newNumber
CS-->>S: sequence
S->>S: formatNumber(template, seq)
S->>L: releaseLock()
L->>R: REDLOCK release
S-->>C: documentNumber
```
### 3.2 Two-Phase Commit (Reserve/Confirm)
```mermaid
sequenceDiagram
participant C as Client
participant RS as ReservationService
participant SS as SequenceService
participant R as Redis
Note over C,R: Phase 1: Reserve
C->>RS: reserve(documentType)
RS->>SS: getNextSequence()
SS-->>RS: documentNumber
RS->>R: SETEX reservation:{token} (TTL: 5min)
RS-->>C: {token, documentNumber, expiresAt}
Note over C,R: Phase 2: Confirm
C->>RS: confirm(token)
RS->>R: GET reservation:{token}
R-->>RS: reservationData
RS->>R: DEL reservation:{token}
RS-->>C: documentNumber (confirmed)
```
### 3.3 Counter Service Implementation
```typescript
// services/counter.service.ts
@Injectable()
export class CounterService {
private readonly logger = new Logger(CounterService.name);
constructor(
@InjectRepository(DocumentNumberCounter)
private counterRepo: Repository<DocumentNumberCounter>,
private dataSource: DataSource,
) {}
async incrementCounter(counterKey: CounterKey): Promise<number> {
const MAX_RETRIES = 2;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return await this.dataSource.transaction(async (manager) => {
// ใช้ Optimistic Locking
const counter = await manager.findOne(DocumentNumberCounter, {
where: this.buildWhereClause(counterKey),
});
if (!counter) {
const newCounter = manager.create(DocumentNumberCounter, {
...counterKey,
lastNumber: 1,
version: 0,
});
await manager.save(newCounter);
return 1;
}
counter.lastNumber += 1;
await manager.save(counter); // Auto-check version
return counter.lastNumber;
});
} catch (error) {
if (error instanceof OptimisticLockVersionMismatchError) {
this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`);
if (attempt === MAX_RETRIES - 1) {
throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่');
}
continue;
}
throw error;
}
}
}
}
```
### 3.4 Redis Lock Service
```typescript
// services/document-numbering-lock.service.ts
@Injectable()
export class DocumentNumberingLockService {
private readonly logger = new Logger(DocumentNumberingLockService.name);
private redlock: Redlock;
constructor(@InjectRedis() private readonly redis: Redis) {
this.redlock = new Redlock([redis], {
driftFactor: 0.01,
retryCount: 5,
retryDelay: 100,
retryJitter: 50,
});
}
async acquireLock(counterKey: CounterKey): Promise<Redlock.Lock> {
const lockKey = this.buildLockKey(counterKey);
const ttl = 5000; // 5 seconds
try {
const lock = await this.redlock.acquire([lockKey], ttl);
this.logger.debug(`Acquired lock: ${lockKey}`);
return lock;
} catch (error) {
this.logger.error(`Failed to acquire lock: ${lockKey}`, error);
throw error;
}
}
async releaseLock(lock: Redlock.Lock): Promise<void> {
try {
await lock.release();
} catch (error) {
this.logger.warn('Failed to release lock (may have expired)', error);
}
}
private buildLockKey(key: CounterKey): string {
return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` +
`${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` +
`${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`;
}
}
```
### 3.5 Reservation Service
```typescript
// services/reservation.service.ts
@Injectable()
export class ReservationService {
private readonly TTL = 300; // 5 minutes
constructor(
private redis: Redis,
private sequenceService: SequenceService,
private auditService: AuditService,
) {}
async reserve(
documentType: string,
scopeValue?: string,
metadata?: Record<string, any>,
): Promise<Reservation> {
// 1. Generate next number
const documentNumber = await this.sequenceService.getNextSequence(
documentType,
scopeValue,
);
// 2. Generate reservation token
const token = uuidv4();
const expiresAt = new Date(Date.now() + this.TTL * 1000);
// 3. Save to Redis
const reservation: Reservation = {
token,
document_number: documentNumber,
document_type: documentType,
scope_value: scopeValue,
expires_at: expiresAt,
metadata,
};
await this.redis.setex(
`reservation:${token}`,
this.TTL,
JSON.stringify(reservation),
);
// 4. Audit log
await this.auditService.log({
operation: 'RESERVE',
document_type: documentType,
document_number: documentNumber,
metadata: { token, scope_value: scopeValue },
});
return reservation;
}
async confirm(token: string, userId: number): Promise<string> {
const reservation = await this.getReservation(token);
if (!reservation) {
throw new ReservationExpiredError(
'Reservation not found or expired. Please reserve a new number.',
);
}
await this.redis.del(`reservation:${token}`);
await this.auditService.log({
operation: 'CONFIRM',
document_type: reservation.document_type,
document_number: reservation.document_number,
user_id: userId,
metadata: { token },
});
return reservation.document_number;
}
async cancel(token: string, userId: number): Promise<void> {
const reservation = await this.getReservation(token);
if (reservation) {
await this.redis.del(`reservation:${token}`);
await this.auditService.log({
operation: 'CANCEL',
document_type: reservation.document_type,
document_number: reservation.document_number,
user_id: userId,
metadata: { token },
});
}
}
@Cron('0 */5 * * * *') // Every 5 minutes
async cleanupExpired(): Promise<void> {
const keys = await this.redis.keys('reservation:*');
for (const key of keys) {
const ttl = await this.redis.ttl(key);
if (ttl <= 0) {
await this.redis.del(key);
}
}
}
}
```
---
## 4. Template System
### 4.1 Supported Tokens
| Token | Description | Example Output |
| -------------- | ---------------------------- | -------------- |
| `{PROJECT}` | Project Code | `LCBP3` |
| `{ORIGINATOR}` | Originator Organization Code | `คคง.` |
| `{RECIPIENT}` | Recipient Organization Code | `สคฉ.3` |
| `{CORR_TYPE}` | Correspondence Type Code | `L` |
| `{SUB_TYPE}` | Sub Type Code | `TD` |
| `{RFA_TYPE}` | RFA Type Code | `RFA` |
| `{DISCIPLINE}` | Discipline Code | `CV` |
| `{SEQ:n}` | Sequence Number (n digits) | `0001` |
| `{YEAR:CE}` | Year (Common Era) | `2025` |
| `{YEAR:BE}` | Year (Buddhist Era) | `2568` |
| `{REV}` | Revision Number | `A` |
### 4.2 Template Validation
```typescript
// validators/template.validator.ts
@Injectable()
export class TemplateValidator {
private readonly ALLOWED_TOKENS = [
'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE',
'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV',
];
validate(template: string, correspondenceType: string): ValidationResult {
const tokens = this.extractTokens(template);
const errors: string[] = [];
// ตรวจสอบ Token ที่ไม่รู้จัก
for (const token of tokens) {
if (!this.ALLOWED_TOKENS.includes(token.name)) {
errors.push(`Unknown token: {${token.name}}`);
}
}
// กฎพิเศษสำหรับแต่ละประเภท
if (correspondenceType === 'RFA') {
if (!tokens.some((t) => t.name === 'PROJECT')) {
errors.push('RFA template ต้องมี {PROJECT}');
}
if (!tokens.some((t) => t.name === 'DISCIPLINE')) {
errors.push('RFA template ต้องมี {DISCIPLINE}');
}
}
if (correspondenceType === 'TRANSMITTAL') {
if (!tokens.some((t) => t.name === 'SUB_TYPE')) {
errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}');
}
}
// ทุก template ต้องมี {SEQ}
if (!tokens.some((t) => t.name.startsWith('SEQ'))) {
errors.push('Template ต้องมี {SEQ:n}');
}
return { valid: errors.length === 0, errors };
}
}
```
---
## 5. API Endpoints
### 5.1 General Endpoints (`/document-numbering`)
| Endpoint | Method | Permission | Description |
| --------------- | ------ | ------------------------ | --------------------------------- |
| `/logs/audit` | GET | `system.view_logs` | Get audit logs |
| `/logs/errors` | GET | `system.view_logs` | Get error logs |
| `/sequences` | GET | `correspondence.read` | Get counter sequences |
| `/counters/:id` | PATCH | `system.manage_settings` | Update counter value |
| `/preview` | POST | `correspondence.read` | Preview number without generating |
| `/reserve` | POST | `correspondence.create` | Reserve a document number |
| `/confirm` | POST | `correspondence.create` | Confirm a reservation |
| `/cancel` | POST | `correspondence.create` | Cancel a reservation |
### 5.2 Admin Endpoints (`/admin/document-numbering`)
| Endpoint | Method | Permission | Description |
| ------------------- | ------ | ------------------------ | ----------------------- |
| `/templates` | GET | `system.manage_settings` | Get all templates |
| `/templates` | POST | `system.manage_settings` | Create/update template |
| `/templates/:id` | DELETE | `system.manage_settings` | Delete template |
| `/metrics` | GET | `system.view_logs` | Get metrics |
| `/manual-override` | POST | `system.manage_settings` | Override counter value |
| `/void-and-replace` | POST | `system.manage_settings` | Void and replace number |
| `/cancel` | POST | `system.manage_settings` | Cancel a number |
| `/bulk-import` | POST | `system.manage_settings` | Bulk import counters |
---
## 6. Monitoring & Observability
### 6.1 Prometheus Metrics
```typescript
@Injectable()
export class NumberingMetrics {
// Counter: Total numbers generated
private readonly numbersGenerated = new Counter({
name: 'numbering_sequences_total',
help: 'Total document numbers generated',
labelNames: ['document_type'],
});
// Gauge: Sequence utilization (%)
private readonly sequenceUtilization = new Gauge({
name: 'numbering_sequence_utilization',
help: 'Sequence utilization percentage',
labelNames: ['document_type'],
});
// Histogram: Lock wait time
private readonly lockWaitTime = new Histogram({
name: 'numbering_lock_wait_seconds',
help: 'Time spent waiting for lock acquisition',
labelNames: ['document_type'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
});
// Counter: Lock failures
private readonly lockFailures = new Counter({
name: 'numbering_lock_failures_total',
help: 'Total lock acquisition failures',
labelNames: ['document_type', 'reason'],
});
}
```
### 6.2 Alert Rules
| Alert | Condition | Severity | Action |
| ------------------ | ------------------ | -------- | ---------------------- |
| `SequenceCritical` | Utilization > 95% | Critical | Extend max_value |
| `SequenceWarning` | Utilization > 90% | Warning | Plan extension |
| `HighLockWaitTime` | p95 > 1s | Warning | Check Redis health |
| `RedisUnavailable` | Redis cluster down | Critical | Switch to DB-only mode |
| `HighErrorRate` | > 10 errors/sec | Warning | Check logs |
---
## 7. Error Handling
### 7.1 Error Codes
| Code | Name | Description |
| ----- | --------------------------- | -------------------------- |
| NB001 | CONFIG_NOT_FOUND | Config not found for type |
| NB002 | SEQUENCE_EXHAUSTED | Sequence reached max value |
| NB003 | LOCK_TIMEOUT | Failed to acquire lock |
| NB004 | RESERVATION_EXPIRED | Reservation token expired |
| NB005 | DUPLICATE_NUMBER | Number already exists |
| NB006 | INVALID_FORMAT | Number format invalid |
| NB007 | MANUAL_OVERRIDE_NOT_ALLOWED | Manual override disabled |
| NB008 | REDIS_UNAVAILABLE | Redis connection failed |
### 7.2 Fallback Strategy
```mermaid
flowchart TD
A[Generate Number Request] --> B{Redis Available?}
B -->|Yes| C[Acquire Redlock]
B -->|No| D[Use DB-only Lock]
C --> E{Lock Acquired?}
E -->|Yes| F[Increment Counter]
E -->|No| G{Retry < 3?}
G -->|Yes| C
G -->|No| H[Fallback to DB Lock]
D --> F
H --> F
F --> I[Format Number]
I --> J[Return Number]
```
---
## 8. Testing
### 8.1 Unit Tests
```bash
# Run unit tests
pnpm test:watch -- --testPathPattern=document-numbering
```
### 8.2 Integration Tests
```bash
# Run integration tests
pnpm test:e2e -- --testPathPattern=numbering
```
### 8.3 Concurrency Test
```typescript
// tests/load/concurrency.spec.ts
it('should handle 1000 concurrent requests without duplicates', async () => {
const promises = Array.from({ length: 1000 }, () =>
request(app.getHttpServer())
.post('/document-numbering/reserve')
.send({ document_type: 'COR' })
);
const results = await Promise.all(promises);
const numbers = results.map(r => r.body.data.document_number);
const uniqueNumbers = new Set(numbers);
expect(uniqueNumbers.size).toBe(1000);
});
```
---
## 9. Best Practices
### 9.1 DO's ✅
- ✅ Always use two-phase commit (reserve + confirm)
- ✅ Implement fallback to DB-only if Redis fails
- ✅ Log every operation to audit trail
- ✅ Monitor sequence utilization (alert at 90%)
- ✅ Test under concurrent load (1000+ req/s)
- ✅ Use pessimistic locking in database
- ✅ Set reasonable TTL for reservations (5 min)
- ✅ Validate manual override format
- ✅ Skip cancelled numbers (never reuse)
### 9.2 DON'Ts ❌
- ❌ Never skip validation for manual override
- ❌ Never reuse cancelled numbers
- ❌ Never trust client-generated numbers
- ❌ Never increase sequence without transaction
- ❌ Never deploy without load testing
- ❌ Never modify sequence table directly
- ❌ Never skip audit logging
---
## 10. Environment Variables
```bash
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_CLUSTER_NODES=redis-1:6379,redis-2:6379,redis-3:6379
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=lcbp3
DB_PASSWORD=
DB_DATABASE=lcbp3_db
DB_POOL_SIZE=20
# Numbering Configuration
NUMBERING_LOCK_TIMEOUT=5000 # 5 seconds
NUMBERING_RESERVATION_TTL=300 # 5 minutes
NUMBERING_RETRY_ATTEMPTS=3
NUMBERING_RETRY_DELAY=200 # milliseconds
# Monitoring
PROMETHEUS_PORT=9090
GRAFANA_PORT=3000
```
---
## References
- [Requirements](../01-requirements/01-03.11-document-numbering.md)
- [Operations Guide](../04-operations/04-08-document-numbering-operations.md)
- [ADR-018 Document Numbering](file:///d:/nap-dms.lcbp3/specs/05-decisions/adr-018-document-numbering.md)
- [Backend Guidelines](03-02-backend-guidelines.md)
---
**Document Version**: 2.0.0
**Created By**: Development Team
**Last Updated**: 2025-12-17

View File

@@ -0,0 +1,423 @@
# ADR-004: RBAC Implementation with 4-Level Scope
**Status:** Accepted
**Date:** 2025-11-30
**Decision Makers:** Development Team, Security Team
**Related Documents:**
- [System Architecture](../02-architecture/02-01-system-architecture.md)
- [Access Control Requirements](../01-requirements/01-04-access-control.md)
---
## Context and Problem Statement
LCBP3-DMS ต้องจัดการสิทธิ์การเข้าถึงที่ซับซ้อน:
- **Multi-Organization:** หลายองค์กรใช้ระบบร่วมกัน แต่ต้องแยกข้อมูล
- **Project-Based:** แต่ละ Project มี Contracts แยกกัน
- **Hierarchical Permissions:** สิทธิ์ระดับบนครอบคลุมระดับล่าง
- **Dynamic Roles:** Role และ Permission ต้องปรับได้โดยไม่ต้อง Deploy
### Key Requirements
1. User หนึ่งคนสามารถมีหลาย Roles ในหลาย Scopes
2. Permission Inheritance (Global → Organization → Project → Contract)
3. Fine-grained Access Control (e.g., "ดู Correspondence ได้เฉพาะ Project A")
4. Performance (Check permission ต้องเร็ว < 10ms)
---
## Decision Drivers
- **Security:** ป้องกันการเข้าถึงข้อมูลที่ไม่มีสิทธิ์
- **Flexibility:** ปรับ Roles/Permissions ได้ง่าย
- **Performance:** Check permission รวดเร็ว
- **Usability:** Admin กำหนดสิทธิ์ได้ง่าย
- **Scalability:** รองรับ Users/Organizations จำนวนมาก
---
## Considered Options
### Option 1: Simple Role-Based (No Scope)
**แนวทาง:** Users มี Roles (Admin, Editor, Viewer) เท่านั้น ไม่มี Scope
**Pros:**
- ✅ Very simple implementation
- ✅ Easy to understand
**Cons:**
- ❌ ไม่รองรับ Multi-organization
- ❌ Superadmin เห็นข้อมูลทุก Organization
- ❌ ไม่ยืดหยุ่น
### Option 2: Organization-Only Scope
**แนวทาง:** Roles ผูกกับ Organization เท่านั้น
**Pros:**
- ✅ แยกข้อมูลระหว่าง Organizations ได้
- ✅ Moderate complexity
**Cons:**
- ❌ ไม่รองรับ Project/Contract level permissions
- ❌ User ใน Organization เห็นทุก Project
### Option 3: **4-Level Hierarchical RBAC** ⭐ (Selected)
**แนวทาง:** Global → Organization → Project → Contract
**Pros:**
-**Maximum Flexibility:** ครอบคลุมทุก Use Case
-**Inheritance:** Global Admin เห็นทุกอย่าง
-**Isolation:** Project Manager เห็นแค่ Project ของตน
-**Fine-grained:** Contract Admin จัดการแค่ Contract เดียว
-**Dynamic:** Roles/Permissions configurable
**Cons:**
- ❌ Complex implementation
- ❌ Performance concern (need optimization)
- ❌ Learning curve for admins
---
## Decision Outcome
**Chosen Option:** Option 3 - 4-Level Hierarchical RBAC
### Rationale
เลือก 4-Level RBAC เนื่องจาก:
1. **Business Requirements:** Project มีหลาย Contracts ที่ต้องแยกสิทธิ์
2. **Future-proof:** รองรับการเติบโตในอนาคต
3. **CASL Integration:** ใช้ library ที่รองรับ complex permissions
4. **Redis Caching:** แก้ปัญหา Performance ด้วย Cache
---
## Implementation Details
### Database Schema
```sql
-- Roles with Scope
CREATE TABLE roles (
role_id INT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(100) NOT NULL,
scope ENUM('Global', 'Organization', 'Project', 'Contract') NOT NULL,
description TEXT,
is_system BOOLEAN DEFAULT FALSE
);
-- Permissions
CREATE TABLE permissions (
permission_id INT PRIMARY KEY AUTO_INCREMENT,
permission_name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
module VARCHAR(50),
scope_level ENUM('GLOBAL', 'ORG', 'PROJECT')
);
-- Role-Permission Mapping
CREATE TABLE role_permissions (
role_id INT,
permission_id INT,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ON DELETE CASCADE
);
-- User Role Assignments with Scope Context
CREATE TABLE user_assignments (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
role_id INT NOT NULL,
organization_id INT NULL,
project_id INT NULL,
contract_id INT NULL,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE,
CONSTRAINT chk_scope CHECK (
(organization_id IS NOT NULL AND project_id IS NULL AND contract_id IS NULL) OR
(organization_id IS NULL AND project_id IS NOT NULL AND contract_id IS NULL) OR
(organization_id IS NULL AND project_id IS NULL AND contract_id IS NOT NULL) OR
(organization_id IS NULL AND project_id IS NULL AND contract_id IS NULL)
)
);
```
### CASL Ability Rules
```typescript
// ability.factory.ts
import { AbilityBuilder, PureAbility } from '@casl/ability';
export type AppAbility = PureAbility<[string, any]>;
@Injectable()
export class AbilityFactory {
async createForUser(user: User): Promise<AppAbility> {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(PureAbility);
// Get user assignments (from cache or DB)
const assignments = await this.getUserAssignments(user.user_id);
for (const assignment of assignments) {
const role = await this.getRole(assignment.role_id);
const permissions = await this.getRolePermissions(role.role_id);
for (const permission of permissions) {
// permission format: 'correspondence.create', 'project.view'
const [subject, action] = permission.permission_name.split('.');
// Apply scope-based conditions
switch (assignment.scope) {
case 'Global':
can(action, subject);
break;
case 'Organization':
can(action, subject, {
organization_id: assignment.organization_id,
});
break;
case 'Project':
can(action, subject, {
project_id: assignment.project_id,
});
break;
case 'Contract':
can(action, subject, {
contract_id: assignment.contract_id,
});
break;
}
}
}
return build();
}
}
```
### Permission Guard
```typescript
// permission.guard.ts
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(
private reflector: Reflector,
private abilityFactory: AbilityFactory,
private redis: Redis
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Get required permission from decorator
const permission = this.reflector.get<string>(
'permission',
context.getHandler()
);
if (!permission) return true;
const request = context.switchToHttp().getRequest();
const user = request.user;
// Check cache first (30 min TTL)
const cacheKey = `user:${user.user_id}:permissions`;
let ability = await this.redis.get(cacheKey);
if (!ability) {
ability = await this.abilityFactory.createForUser(user);
await this.redis.set(cacheKey, JSON.stringify(ability.rules), 'EX', 1800);
}
const [action, subject] = permission.split('.');
const resource = request.params || request.body;
return ability.can(action, subject, resource);
}
}
```
### Usage Example
```typescript
@Controller('correspondences')
@UseGuards(JwtAuthGuard, PermissionGuard)
export class CorrespondenceController {
@Post()
@RequirePermission('correspondence.create')
async create(@Body() dto: CreateCorrespondenceDto) {
// Only users with create permission can access
}
@Get(':id')
@RequirePermission('correspondence.view')
async findOne(@Param('id') id: string) {
// Check if user has view permission for this project
}
}
```
---
## Permission Checking Flow
```mermaid
sequenceDiagram
participant Client
participant Guard as Permission Guard
participant Redis as Redis Cache
participant Factory as Ability Factory
participant DB as Database
Client->>Guard: Request with JWT
Guard->>Redis: Get user permissions (cache)
alt Cache Hit
Redis-->>Guard: Cached permissions
else Cache Miss
Guard->>Factory: createForUser(user)
Factory->>DB: Get user_assignments
Factory->>DB: Get role_permissions
Factory->>Factory: Build CASL ability
Factory-->>Guard: Ability object
Guard->>Redis: Cache permissions (TTL: 30min)
end
Guard->>Guard: Check permission.can(action, subject, context)
alt Permission Granted
Guard-->>Client: Allow access
else Permission Denied
Guard-->>Client: 403 Forbidden
end
```
---
## 4-Level Scope Hierarchy
```
Global (ทั้งระบบ)
├─ Organization (ระดับองค์กร)
│ ├─ Project (ระดับโครงการ)
│ │ └─ Contract (ระดับสัญญา)
│ │
│ └─ Project B
│ └─ Contract B
└─ Organization 2
└─ Project C
```
### Example Assignments
```typescript
// User A: Superadmin (Global)
{
user_id: 1,
role_id: 1, // Superadmin
organization_id: null,
project_id: null,
contract_id: null
}
// Can access EVERYTHING
// User B: Document Control in TEAM Organization
{
user_id: 2,
role_id: 3, // Document Control
organization_id: 3, // TEAM
project_id: null,
contract_id: null
}
// Can manage documents in TEAM organization (all projects)
// User C: Project Manager for LCBP3
{
user_id: 3,
role_id: 6, // Project Manager
organization_id: null,
project_id: 1, // LCBP3
contract_id: null
}
// Can manage only LCBP3 project (all contracts within)
// User D: Contract Admin for Contract-1
{
user_id: 4,
role_id: 7, // Contract Admin
organization_id: null,
project_id: null,
contract_id: 5 // Contract-1
}
// Can manage only Contract-1
```
---
## Consequences
### Positive
1.**Fine-grained Control:** แยกสิทธิ์ได้ละเอียดมาก
2.**Flexible:** User มีหลาย Roles ใน Scopes ต่างกันได้
3.**Inheritance:** Global → Org → Project → Contract
4.**Performant:** Redis cache ทำให้เร็ว (< 10ms)
5.**Auditable:** ทุก Assignment บันทึกใน DB
### Negative
1.**Complexity:** ซับซ้อนในการ Setup และ Maintain
2.**Cache Invalidation:** ต้อง Invalidate ถูกต้องเมื่อเปลี่ยน Roles
3.**Learning Curve:** Admin ต้องเข้าใจ Scope hierarchy
4.**Testing:** ต้อง Test ทุก Combination
### Mitigation Strategies
- **Complexity:** สร้าง Admin UI ที่ใช้งานง่าย
- **Cache:** Auto-invalidate เมื่อมีการเปลี่ยนแปลง
- **Documentation:** เขียน Guide ชัดเจน
- **Testing:** Integration tests ครอบคลุม Permissions
---
## Compliance
เป็นไปตาม:
- [Requirements Section 4](../01-requirements/01-04-access-control.md) - Access Control
- [Backend Plan Section 2 RBAC](../../docs/2_Backend_Plan_V1_4_5.md#rbac)
---
## Related ADRs
- [ADR-005: Redis Usage Strategy](./ADR-005-redis-usage-strategy.md) - Permission caching
- [ADR-001: Unified Workflow Engine](./ADR-001-unified-workflow-engine.md) - Workflow permission guards
---
## References
- [CASL Documentation](https://casl.js.org/v6/en/guide/intro)
- [RBAC Best Practices](https://csrc.nist.gov/publications/detail/sp/800-162/final)