From 3d9b6e4d0557416ad96d615366e5f0de36758a07 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 24 Nov 2025 08:15:15 +0700 Subject: [PATCH] 251123:2300 Update T1 --- .agent/rules/coding-standards.md | 6 + 2_Backend_Plan_Phase6A_V1_4_3.md | 397 +++++++++++ 2_Backend_Plan_V1_4_3.md | 12 +- 5_Backend_Folder_V1_4_3.md | 478 +++++++++++++ T0-T6.2.md | 436 ++++++++++++ backend/Infrastructure Setup.yml | 76 +++ backend/docker-compose.yml | 16 +- backend/package.json | 6 +- backend/pnpm-lock.yaml | 641 +++++++++++++++++- backend/src/app.module.ts | 96 ++- backend/src/common/aut/aut.controller.spec.ts | 18 - backend/src/common/aut/aut.controller.ts | 4 - backend/src/common/auth/auth.controller.ts | 70 +- backend/src/common/auth/auth.module.ts | 17 +- backend/src/common/auth/auth.service.ts | 95 ++- .../auth/strategies/jwt-refresh.strategy.ts | 32 + .../common/auth/strategies/jwt.strategy.ts | 63 ++ backend/src/common/common.module.ts | 32 +- backend/src/common/config/redis.config.ts | 11 + .../src/common/decorators/audit.decorator.ts | 11 + .../bypass-maintenance.decorator.ts | 10 + .../decorators/idempotency.decorator.ts | 7 + .../src/common/entities/audit-log.entity.ts | 56 ++ .../exceptions/http-exception.filter.ts | 62 +- .../src/common/guards/jwt-refresh.guard.ts | 5 + backend/src/common/guards/jwt.strategy.ts | 34 - .../common/guards/maintenance-mode.guard.ts | 71 ++ .../interceptors/audit-log.interceptor.ts | 80 +++ .../interceptors/idempotency.interceptor.ts | 74 ++ backend/src/common/services/crypto.service.ts | 40 ++ .../services/request-context.service.ts | 35 + backend/src/main.ts | 55 +- .../circulation/circulation.controller.ts | 65 ++ .../modules/circulation/circulation.module.ts | 25 + .../circulation/circulation.service.ts | 96 ++- .../circulation/dto/create-circulation.dto.ts | 3 +- .../circulation/dto/create-transmittal.dto.ts | 34 - .../circulation/dto/search-circulation.dto.ts | 22 + .../dto/update-circulation-routing.dto.ts | 16 + .../correspondence.controller.ts | 4 + .../correspondence/correspondence.module.ts | 2 + .../correspondence/correspondence.service.ts | 16 +- backend/src/modules/drawing/drawing.module.ts | 3 +- .../drawing/shop-drawing.controller.ts | 3 + .../modules/drawing/shop-drawing.service.ts | 40 +- .../json-schema/dto/create-json-schema.dto.ts | 26 + .../json-schema/dto/search-json-schema.dto.ts | 27 + .../json-schema/dto/update-json-schema.dto.ts | 4 + .../json-schema/json-schema.controller.ts | 33 +- .../modules/json-schema/json-schema.module.ts | 15 +- .../json-schema/json-schema.service.ts | 38 +- .../modules/project/dto/create-project.dto.ts | 15 + .../modules/project/dto/search-project.dto.ts | 27 + .../modules/project/dto/update-project.dto.ts | 4 + .../src/modules/project/project.controller.ts | 77 ++- backend/src/modules/project/project.module.ts | 4 +- .../src/modules/project/project.service.ts | 107 ++- .../rfa/entities/rfa-revision.entity.ts | 7 + .../rfa-workflow-template-step.entity.ts | 64 ++ .../entities/rfa-workflow-template.entity.ts | 39 ++ .../rfa/entities/rfa-workflow.entity.ts | 73 ++ backend/src/modules/rfa/rfa.controller.ts | 2 + backend/src/modules/rfa/rfa.module.ts | 11 + backend/src/modules/rfa/rfa.service.ts | 13 + .../modules/search/dto/search-query.dto.ts | 27 + .../src/modules/search/search.controller.ts | 22 + backend/src/modules/search/search.module.ts | 32 + backend/src/modules/search/search.service.ts | 152 +++++ .../transmittal/transmittal.controller.ts | 8 +- .../modules/transmittal/transmittal.module.ts | 3 +- .../transmittal/transmittal.service.ts | 2 + .../modules/user/dto/update-preference.dto.ts | 42 ++ .../src/modules/user/dto/update-user.dto.ts | 23 +- .../user/entities/permission.entity.ts | 27 + .../src/modules/user/entities/role.entity.ts | 29 + .../user/entities/user-assignment.entity.ts | 38 +- .../user/entities/user-preference.entity.ts | 15 +- .../src/modules/user/entities/user.entity.ts | 32 +- .../modules/user/user-preference.service.ts | 47 ++ backend/src/modules/user/user.controller.ts | 79 ++- backend/src/modules/user/user.module.ts | 40 +- 81 files changed, 4232 insertions(+), 347 deletions(-) create mode 100644 .agent/rules/coding-standards.md create mode 100644 2_Backend_Plan_Phase6A_V1_4_3.md create mode 100644 5_Backend_Folder_V1_4_3.md create mode 100644 T0-T6.2.md create mode 100644 backend/Infrastructure Setup.yml delete mode 100644 backend/src/common/aut/aut.controller.spec.ts delete mode 100644 backend/src/common/aut/aut.controller.ts create mode 100644 backend/src/common/auth/strategies/jwt-refresh.strategy.ts create mode 100644 backend/src/common/auth/strategies/jwt.strategy.ts create mode 100644 backend/src/common/config/redis.config.ts create mode 100644 backend/src/common/decorators/audit.decorator.ts create mode 100644 backend/src/common/decorators/bypass-maintenance.decorator.ts create mode 100644 backend/src/common/decorators/idempotency.decorator.ts create mode 100644 backend/src/common/entities/audit-log.entity.ts create mode 100644 backend/src/common/guards/jwt-refresh.guard.ts delete mode 100644 backend/src/common/guards/jwt.strategy.ts create mode 100644 backend/src/common/guards/maintenance-mode.guard.ts create mode 100644 backend/src/common/interceptors/audit-log.interceptor.ts create mode 100644 backend/src/common/interceptors/idempotency.interceptor.ts create mode 100644 backend/src/common/services/crypto.service.ts create mode 100644 backend/src/common/services/request-context.service.ts create mode 100644 backend/src/modules/circulation/circulation.controller.ts create mode 100644 backend/src/modules/circulation/circulation.module.ts delete mode 100644 backend/src/modules/circulation/dto/create-transmittal.dto.ts create mode 100644 backend/src/modules/circulation/dto/search-circulation.dto.ts create mode 100644 backend/src/modules/circulation/dto/update-circulation-routing.dto.ts create mode 100644 backend/src/modules/json-schema/dto/create-json-schema.dto.ts create mode 100644 backend/src/modules/json-schema/dto/search-json-schema.dto.ts create mode 100644 backend/src/modules/json-schema/dto/update-json-schema.dto.ts create mode 100644 backend/src/modules/project/dto/create-project.dto.ts create mode 100644 backend/src/modules/project/dto/search-project.dto.ts create mode 100644 backend/src/modules/project/dto/update-project.dto.ts create mode 100644 backend/src/modules/rfa/entities/rfa-workflow-template-step.entity.ts create mode 100644 backend/src/modules/rfa/entities/rfa-workflow-template.entity.ts create mode 100644 backend/src/modules/rfa/entities/rfa-workflow.entity.ts create mode 100644 backend/src/modules/search/dto/search-query.dto.ts create mode 100644 backend/src/modules/search/search.controller.ts create mode 100644 backend/src/modules/search/search.module.ts create mode 100644 backend/src/modules/search/search.service.ts create mode 100644 backend/src/modules/user/dto/update-preference.dto.ts create mode 100644 backend/src/modules/user/entities/permission.entity.ts create mode 100644 backend/src/modules/user/entities/role.entity.ts create mode 100644 backend/src/modules/user/user-preference.service.ts diff --git a/.agent/rules/coding-standards.md b/.agent/rules/coding-standards.md new file mode 100644 index 0000000..513a1d3 --- /dev/null +++ b/.agent/rules/coding-standards.md @@ -0,0 +1,6 @@ +--- +trigger: always_on +glob: +description: +--- + diff --git a/2_Backend_Plan_Phase6A_V1_4_3.md b/2_Backend_Plan_Phase6A_V1_4_3.md new file mode 100644 index 0000000..6e75a61 --- /dev/null +++ b/2_Backend_Plan_Phase6A_V1_4_3.md @@ -0,0 +1,397 @@ +# “Phase 6A + Technical Design Document : Workflow DSL (Mini-Language)”** +ออกแบบสำหรับระบบ Workflow Engine กลางของโครงการ +**ไม่มีโค้ดผูกกับ Framework** เพื่อให้สามารถนำไป Implement ใน NestJS หรือ Microservice ใด ๆ ได้ + +--- + +## 📌 **Phase 6A – Workflow DSL Implementation Plan** + +### 🎯 เป้าหมายของ Phase 6A + +ใน Phase นี้ จะเริ่มสร้าง “Workflow DSL (Domain-Specific Language)” สำหรับนิยามกฎการเดินงาน (Workflow Transition Rules) ให้สามารถ: + +* แยก **Business Workflow Logic** ออกจาก Source Code +* แก้ไขกฎ Workflow ได้โดย **ไม่ต้องแก้โค้ดและไม่ต้อง Deploy ใหม่** +* รองรับ Document หลายประเภท เช่น + + * Correspondence + * RFA + * Internal Circulation + * Document Transmittal +* รองรับ Multi-step routing, skip, reject, rollback, parallel assignments +* สามารถนำไปใช้งานทั้งใน + + * Backend (NestJS) + * Frontend (UI Driven) + * External Microservices + +--- + +### 📅 ระยะเวลา + +**1 สัปดาห์ (หลัง Phase 6.5)** + +--- + +### 🧩 Output ของ Phase 6A + +* DSL Specification (Grammar) +* JSON Schema for Workflow Definition +* Workflow Rule Interpreter (Parser + Executor) +* Validation Engine (Compile-time and Runtime) +* Storage (DB Table / Registry) +* Execution API: + +| Action | Description | +| -------------------------------- | ------------------------------- | +| compile() | ตรวจ DSL → สร้าง Execution Tree | +| evaluate(state, action, context) | ประมวลผลและส่งสถานะใหม่ | +| preview(state) | คำนวณ Next Possible Transitions | +| validate() | ตรวจว่า DSL ถูกต้อง | + +--- + +## 📘 **Technical Specification – Workflow DSL** + +--- + +### 1️⃣ Requirements + +#### Functional Requirements + +* นิยาม Workflow เป็นภาษาคล้าย State Machine +* แต่ละเอกสารมี **State, Actions, Entry/Exit Events** +* สามารถมี: + + * Required approvals + * Conditional transition + * Auto-transition + * Parallel approval + * Return/rollback + +#### +* Running time: < 20ms ต่อคำสั่ง +* Hot reload ไม่ต้อง Compile ใหม่ทั้ง Backend +* DSL ต้อง Debug ได้ง่าย +* ต้อง Versioned +* ต้องรองรับ Audit 100% + +--- + +### 2️⃣ DSL Format (Human Friendly) + +```yaml +workflow: RFA +version: 1.0 + +states: + - name: DRAFT + initial: true + on: + SUBMIT: + to: IN_REVIEW + require: + - role: ENGINEER + events: + - notify: reviewer + + - name: IN_REVIEW + on: + APPROVE: + to: APPROVED + REJECT: + to: DRAFT + events: + - notify: creator + + - name: APPROVED + terminal: true +``` + +--- + +### 3️⃣ Compiled Execution Model (Normalized JSON) + +```json +{ + "workflow": "RFA", + "version": "1.0", + "states": { + "DRAFT": { + "initial": true, + "transitions": { + "SUBMIT": { + "to": "IN_REVIEW", + "requirements": [ + { "role": "ENGINEER" } + ], + "events": [ + { "type": "notify", "target": "reviewer" } + ] + } + } + }, + "IN_REVIEW": { + "transitions": { + "APPROVE": { "to": "APPROVED" }, + "REJECT": { + "to": "DRAFT", + "events": [ + { "type": "notify", "target": "creator" } + ] + } + } + }, + "APPROVED": { + "terminal": true + } + } +} +``` + +Frontend และ Backend สามารถแชร์ JSON นี้ร่วมกันได้ + +--- + +### 4️⃣ DSL Grammar Definition (EBNF) + +```ebnf +workflow = "workflow" ":" identifier ; +version = "version" ":" number ; + +states = "states:" state_list ; +state_list = { state } ; + +state = "- name:" identifier + [ "initial:" boolean ] + [ "terminal:" boolean ] + [ "on:" transition_list ] ; + +transition_list = { transition } ; + +transition = action ":" + indent "to:" identifier + [ indent "require:" requirements ] + [ indent "events:" event_list ] ; + +requirements = "- role:" identifier | "- user:" identifier ; + +event_list = { event } ; +event = "- notify:" identifier ; +``` + +--- + +### 5️⃣ Validation Rules (Compile-Time) + +#### 5.1 State Rules + +* ต้องมีอย่างน้อย 1 state ที่ `initial: true` +* หาก `terminal: true` → ต้องไม่มี transition ต่อไป + +#### 5.2 Transition Rules + +ตรวจสอบว่า: + +* `to` ชี้ไปยัง state ที่มีอยู่ +* `require.role` ต้องเป็น role ที่ระบบรู้จัก +* Action name ต้องเป็น **UPPER_CASE** + +#### 5.3 Version Safety + +* ทุกชุด Workflow DSL ต้องขึ้นกับ version +* แก้ไขต้องสร้าง version ใหม่ +* ไม่ overwrite version เก่า +* “Document ที่กำลังอยู่ใน step เดิมยังต้องใช้กฎเดิมได้” + +--- + +### 6️⃣ Runtime Validation Rules + +เมื่อ execute(action): + +``` +input: current_state, action, context + +1) ตรวจว่า state มี transition "action" +2) ตรวจว่าผู้ใช้มีสิทธิ์ตาม require[] +3) Compute next state +4) Execute events[] +5) Return next_state +``` + +--- + +### 7️⃣ Context Model + +```ts +interface WorkflowContext { + userId: string; + roles: string[]; + documentId: string; + now: Date; + payload?: any; +} +``` + +--- + +### 8️⃣ Execution API (Abstract) + +```ts +class WorkflowEngine { + + load(dsl: string | object): CompiledWorkflow + + compile(dsl: string | object): CompiledWorkflow + + evaluate(state: string, action: string, context: WorkflowContext): EvalResult + + getAvailableActions(state: string, context: WorkflowContext): string[] +} +``` + +--- + +### 9️⃣ Interpreter Execution Flow + +```mermaid +flowchart TD + A[Receive Action] --> B[Load Compiled Workflow] + B --> C[Check allowed actions] + C -->|Invalid| E[Return Error] + C --> D[Evaluate Requirements] + D --> F[Transition to Next State] + F --> G[Run Events] + G --> H[Return Success] +``` + +--- + +### 🔟 Events System + +รองรับ event หลายประเภท: + +| event.type | ตัวอย่าง | +| ----------- | ------------------------- | +| notify | ส่ง Email/Line | +| assign | เปลี่ยนผู้รับผิดชอบ | +| webhook | ยิง Webhook ไปยังระบบอื่น | +| auto_action | ทำ action ซ้ำโดยอัตโนมัติ | + +--- + +### 11️⃣ Database Schema + +#### Table: `workflow_definition` + +| Field | Type | Description | +| ------------- | ----------- | --------------------- | +| id | UUID | PK | +| workflow_code | varchar(50) | เช่น `RFA`, `CORR` | +| version | int | Version number | +| dsl | JSON | YAML/JSON DSL เก็บดิบ | +| compiled | JSON | JSON ที่ Compile แล้ว | +| created_at | timestamp | | +| is_active | boolean | ใช้อยู่หรือไม่ | + +#### Table: `workflow_history` + +เก็บ audit แบบ immutable append-only + +| Field | Description | +| ----------- | --------------- | +| workflow | Document Type | +| document_id | เอกสารไหน | +| from_state | เดิม | +| to_state | ใหม่ | +| action | คำสั่ง | +| user | ใครเป็นคนทำ | +| timestamp | เวลา | +| metadata | เหตุการณ์อื่น ๆ | + +--- + +### 12️⃣ Error Codes + +| Code | Meaning | +| ----------------------- | ---------------------- | +| WF_NO_TRANSITION | Action นี้ไม่ถูกต้อง | +| WF_RESTRICTED | User ไม่มีสิทธิ์ | +| WF_MISSING_REQUIREMENTS | ไม่ผ่านเงื่อนไข | +| WF_STATE_NOT_FOUND | ไม่มี state ที่อ้างอิง | +| WF_SYNTAX_ERROR | DSL ผิดรูปแบบ | + +--- + +### 13️⃣ Testing Strategy + +#### Unit Tests + +* Parse DSL → JSON +* Invalid syntax → throw error +* Invalid transitions → throw error + +#### Integration Tests + +* Evaluate() ผ่าน 20+ cases +* RFA ย้อนกลับ +* Approve chain +* Parallel review + +#### Load Tests + +* 1,000 documents running workflow +* Evaluate < 20ms ต่อ action + +--- + +### 14️⃣ Deployment Strategy + +#### Hot Reload Options + +* DSL stored in DB +* Cache in Redis +* Touched timestamp triggers: + +``` +invalidate cache → recompile +``` + +#### No downtime required + +--- + +### 15️⃣ Microservice-Ready + +DSL Engine แยกเป็น: + +* `workflow-engine-core` → Pure JS library +* `workflow-service` → NestJS module +* API public: + +``` +POST /workflow/evaluate +GET /workflow/preview +POST /workflow/compile +``` + +ภายหลังสามารถนำไปวางบน: + +* Kubernetes +* Worker Node +* API Gateway + +--- + +## 🎉 Summary + +### สิ่งที่ Phase 6A เพิ่มเข้าในระบบ + +✔ Workflow DSL ที่แก้ไขได้โดยไม่ต้อง Deploy +✔ Parser + Validator + Runtime Interpreter +✔ Database storage + Versioning +✔ Execution API สำหรับ Backend และ Frontend +✔ รองรับ Business Workflow ซับซ้อนทั้งหมด +✔ Ready สำหรับ Microservice model ในอนาคต + diff --git a/2_Backend_Plan_V1_4_3.md b/2_Backend_Plan_V1_4_3.md index 5d3a2b3..c6d8fd4 100644 --- a/2_Backend_Plan_V1_4_3.md +++ b/2_Backend_Plan_V1_4_3.md @@ -127,7 +127,7 @@ src/ - **[ ] T1.1 CommonModule - Base Infrastructure** - - [ ] สร้าง Base Entity (id, created\_at, updated\_at, deleted\_at) + - [ ] สร้าง Base Entity (id, created_at, updated_at, deleted_at) - [ ] สร้าง Global Exception Filter (ไม่เปิดเผย sensitive information) - [ ] สร้าง Response Transform Interceptor - [ ] สร้าง Audit Log Interceptor @@ -150,7 +150,7 @@ src/ - [ ] สร้าง JWT Strategy (Passport) - [ ] สร้าง JwtAuthGuard - [ ] สร้าง Controllers: - - [ ] POST /auth/login → { access\_token, refresh\_token } + - [ ] POST /auth/login → { access_token, refresh_token } - [ ] POST /auth/register → Create User (Admin only) - [ ] POST /auth/refresh → Refresh token - [ ] POST /auth/logout → Revoke token @@ -238,8 +238,8 @@ src/ - [ ] Generate checksum (SHA-256) - [ ] **Cleanup Job:** สร้าง Cron Job ลบไฟล์ใน `temp/` ที่ค้างเกิน 24 ชม. **โดยตรวจสอบจากคอลัมน์ `expires_at` ในตาราง `attachments`** - [ ] สร้าง Controller: - - [ ] POST /files/upload → { temp\_id } (Protected) - - [ ] POST /files/commit → { attachment\_id, url } (Protected) + - [ ] POST /files/upload → { temp_id } (Protected) + - [ ] POST /files/commit → { attachment_id, url } (Protected) - [ ] GET /files/:id/download → File Stream (Protected + Expiration) - [ ] **Security:** Access Control - ตรวจสอบสิทธิ์ผ่าน Junction Table - [ ] **Deliverable:** อัปโหลด/ดาวน์โหลดไฟล์ได้อย่างปลอดภัย แบบ Transactional @@ -259,7 +259,7 @@ src/ 4. Release Redis Lock 5. Retry on Failure ด้วย exponential backoff - [ ] Fallback mechanism เมื่อการขอเลขล้มเหลว - - [ ] Format ตาม Template: {ORG\_CODE}-{TYPE\_CODE}-{YEAR\_SHORT}-{SEQ:4} + - [ ] Format ตาม Template: {ORG_CODE}-{TYPE_CODE}-{YEAR_SHORT}-{SEQ:4} - **ไม่มี Controller** (Internal Service เท่านั้น) - [ ] **Security:** Implement audit log ทุกครั้งที่มีการ generate เลขที่ - [ ] **Deliverable:** Service สร้างเลขที่เอกสารได้ถูกต้องและปลอดภัย ไม่มี Race Condition @@ -503,7 +503,7 @@ src/ - [ ] Implement Caching (Master Data, User Permissions, Search Results) - [ ] Database Optimization (Review Indexes, Query Optimization, Pagination) - - [ ] **Deliverable:** Response Time \< 200ms (90th percentile) + - [ ] **Deliverable:** Response Time < 200ms (90th percentile) ----- diff --git a/5_Backend_Folder_V1_4_3.md b/5_Backend_Folder_V1_4_3.md new file mode 100644 index 0000000..8966546 --- /dev/null +++ b/5_Backend_Folder_V1_4_3.md @@ -0,0 +1,478 @@ +# โครงสร้างโฟลเดอร์และไฟล์ทั้งหมดสำหรับ **Backend (NestJS)** ตามแผนงาน **LCBP3-DMS v1.4.3** ตั้งแต่ Phase 0 ถึง Phase 6 (T0-T6.2) ที่ได้ดำเนินการไปแล้ว + +โครงสร้างนี้ออกแบบตามหลัก **Domain-Driven Design** และ **Modular Architecture** ที่ระบุไว้ในแผนพัฒนา + +--- + +## 📂 **backend/** (Backend Application) + +* [x] `.env` (สำหรับ Local Dev เท่านั้น ห้าม commit) +* [x] `.gitignore` +* [x] `docker-compose.yml` (Configuration หลักสำหรับ Deploy) +* [x] `docker-compose.override.yml` (สำหรับ Inject Secrets ตอน Dev) +* [x] `package.json` +* [x] `pnpm-lock.yaml` +* [x] `tsconfig.json` +* [x] `nest-cli.json` +* [x] `README.md` + +--- + +## 📂 **backend/src/** (Source Code) + +### **📄 Entry Points** + +* [x] `main.ts` (Application Bootstrap, Swagger, Global Pipes) +* [x] `app.module.ts` (Root Module ที่รวมทุก Modules เข้าด้วยกัน) + +### **📁 src/common/** (Shared Resources) + +* [x] **common.module.ts** +* **auth/** + * **dto/** + * [x] **login.dto.ts** + * [x] **register.dto.ts** + * [x] **auth.controller.spec.ts** + * [x] **auth.controller.ts** + * [x] **auth.module.ts** + * [x] **auth.service.spec.ts** + * [x] **auth.service.ts** +* **config/** (Configuration Service) + * [x] **env.validation.ts** +* **decorators/** + * [x] **audit.decorator.ts** + * [x] `current-user.decorator.ts` + * [x] `require-permission.decorator.ts` +* **entities/** + * [x] **audit-log.entity.ts** + * [x] **base.entity.ts** +* **exceptions/** + * [x] `http-exception.filter.ts` (Global Filter) +* **file-storage/** (Two-Phase Storage System) + * **entities/** + * [x] **attachment.entity.ts** + * [x] **file-storage.controller.spec.ts** + * [x] **file-storage.controller.ts** + * [x] **file-storage.module.ts** + * [x] **file-storage.service.spec.ts** + * [x] `file-storage.service.ts` (Upload, Scan Virus, Commit) +* [x] `guards/` + * [x] `jwt-auth.guard.ts` + * [x] **jwt.strategy.ts** + * [x] `rbac.guard.ts` (ตรวจสอบสิทธิ์ 4 ระดับ) +* **interceptors/** + * [x] `audit-log.interceptor.ts` (เก็บ Log ลง DB) + * [x] `transform.interceptor.ts` (Standard Response Format) +* **resilience/** (Circuit Breaker & Retry) + +### **📁 src/modules/** (Feature Modules) + +1. **user/** (User Management & RBAC) + * [x] `dto/` + * [x] **assign-user-role.dto.ts** + * [x] `create-user.dto.ts` + * [x] `update-user.dto.ts` + * [x] `entities/` + * [x] `user.entity.ts` + * [x] `role.entity.ts` + * [x] `permission.entity.ts` + * [x] `user-preference.entity.ts` + * [x] **user-assignment.service.ts** + * [x] `user.controller.ts` + * [x] `user.module.ts` + * [x] `user.service.ts` + * [x] **user.service.spec.ts** + +2. **project/** (Project Structure) + * [x] `dto/` + * [x] **create-project.dto.ts** + * [x] `search-project.dto.ts` + * [x] `update-project.dto.ts` + * [x] `entities/` + * [x] `contract-organization.entity.ts` + * [x] `contract.entity.ts` + * [x] `organization.entity.ts` + * [x] `project-organization.entity.ts` (Junction) + * [x] **project.entity.ts** + * [x] **project.controller.spec.ts** + * [x] `project.controller.ts` + * [x] `project.module.ts` + * [x] **project.service.spec.ts** + * [x] `project.service.ts` + +3. **correspondence/** (Core Document System) + * [x] `dto/` + * [x] `add-reference.dto.ts` + * [x] `create-correspondence.dto.ts` + * [x] `search-correspondence.dto.ts` + * [x] **submit-correspondence.dto.ts** + * [x] `workflow-action.dto.ts` + * [x] `entities/` + * [x] `correspondence-reference.entity.ts` + * [x] `correspondence-revision.entity.ts` + * [x] `correspondence-routing.entity.ts` (Unified Workflow) + * [x] `correspondence-status.entity.ts` + * [x] `correspondence-type.entity.ts` + * [x] `correspondence.entity.ts` + * [x] **routing-template-step.entity.ts** + * [x] `routing-template.entity.ts` + * [x] **correspondence.controller.spec.ts** + * [x] `correspondence.controller.ts` + * [x] `correspondence.module.ts` + * [x] **correspondence.service.spec.ts** + * [x] `correspondence.service.ts` (Impersonation & Workflow Logic) + +4. **drawing/** (Contract & Shop Drawings) + * [x] `dto/` + * [x] `create-contract-drawing.dto.ts` + * [x] `create-shop-drawing-revision.dto.ts` + * [x] `create-shop-drawing.dto.ts` + * [x] `search-contract-drawing.dto.ts` + * [x] `search-shop-drawing.dto.ts` + * [x] `update-contract-drawing.dto.ts` + * [x] `entities/` + * [x] `contract-drawing-sub-category.entity.ts` + * [x] `contract-drawing-volume.entity.ts` + * [x] `contract-drawing.entity.ts` + * [x] `shop-drawing-main-category.entity.ts` + * [x] `shop-drawing-revision.entity.ts` + * [x] `shop-drawing-sub-category.entity.ts` + * [x] `shop-drawing.entity.ts` + * [x] `contract-drawing.controller.ts` + * [x] `contract-drawing.service.ts` + * [x] `drawing-master-data.controller.ts` + * [x] `drawing-master-data.service.ts` + * [x] `drawing.module.ts` + * [x] `shop-drawing.controller.ts` + * [x] `shop-drawing.service.ts` + +4. **rfa/** (Request for Approval & Advanced Workflow) + * [x] `dto/` + * [x] `create-rfa.dto.ts` + * [x] `search-rfa.dto.ts` + * [x] `update-rfa.dto.ts` + * [x] `entities/` + * [x] `rfa-approve-code.entity.ts` + * [x] `rfa-item.entity.ts` + * [x] `rfa-revision.entity.ts` + * [x] `rfa-status-code.entity.ts` + * [x] `rfa-type.entity.ts` + * [x] `rfa-workflow-template-step.entity.ts` + * [x] `rfa-workflow-template.entity.ts` + * [x] `rfa-workflow.entity.ts` + * [x] `rfa.entity.ts` + * [x] `rfa.controller.ts` + * [x] `rfa.module.ts` + * [x] `rfa.service.ts` (Unified Workflow Integration) + +5. **circulation/** (Internal Routing) + * [x] `dto/` + * [x] `create-circulation.dto.ts` + * [x] `update-circulation-routing.dto.ts` + * [x] `search-circulation.dto.ts` + * [x] `entities/` + * [x] `circulation-routing.entity.ts` + * [x] `circulation-status-code.entity.ts` + * [x] `circulation.entity.ts` + * [x] `circulation.controller.ts` + * [x] `circulation.module.ts` + * [x] `circulation.service.ts` + +6. **transmittal/** (Document Forwarding) + * [x] `dto/` + * [x] `create-transmittal.dto.ts` + * [x] `search-transmittal.dto.ts` + * [x] **update-transmittal.dto.ts** + * [x] `entities/` + * [x] `transmittal-item.entity.ts` + * [x] `transmittal.entity.ts` + * [x] `transmittal.controller.ts` + * [x] `transmittal.module.ts` + * [x] `transmittal.service.ts` + +7. **notification/** (System Alerts) + * [x] `dto/` + * [x] `create-notification.dto.ts` + * [x] `search-notification.dto.ts` + * [x] `entities/` + * [x] `notification.entity.ts` + * [x] `notification-cleanup.service.ts` (Cron Job) + * [x] `notification.controller.ts` + * [x] `notification.gateway.ts` + * [x] `notification.module.ts` (Real-time WebSocket) + * [x] `notification.processor.ts` (Consumer/Worker for Email & Line) + * [x] `notification.service.ts` (Producer) + +8. **search/** (Elasticsearch) + * [x] `dto/search-query.dto.ts` + * [x] `search.controller.ts` + * [x] `search.module.ts` + * [x] `search.service.ts` (Indexing & Searching) + +9. **document-numbering/** (Internal Service) + * [x] `entities/` + * [x] `document-number-format.entity.ts` + * [x] `document-number-counter.entity.ts` + * [x] `document-numbering.module.ts` + * [x] **document-numbering.service.spec.ts** + * [x] `document-numbering.service.ts` (Double-Lock Mechanism) + +10. **workflow-engine/** (Unified Logic) + * [x] `interfaces/workflow.interface.ts` + * [x] `workflow-engine.module.ts` + * [x] **workflow-engine.service.spec.ts** + * [x] `workflow-engine.service.ts` (State Machine Logic) + +11. **json-schema/** (Validation) + * [x] `dto/` + * [x] `create-json-schema.dto.ts`+ + * [x] `search-json-schema.dto.ts` + * [x] `update-json-schema.dto.ts` + * [x] `entities/` + * [x] `json-schema.entity.ts` + * [x] **json-schema.controller.spec.ts** + * [x] **json-schema.controller.ts** + * [x] `json-schema.module.ts` + * [x] **json-schema.service.spec.ts** + * [x] `json-schema.service.ts` + +## **Folder Structure ของ Backend (NestJS)** ที่ + +--- + +### 📁 Backend Folder Structure (LCBP3-DMS v1.4.3) + +```text +backend/ +├── .env # Environment variables for local development only (not committed) +├── .gitignore # Git ignore rules +├── docker-compose.yml # Main deployment container configuration +├── docker-compose.override.yml # Dev-time secret/environment injection +├── package.json # Node dependencies and NPM scripts +├── pnpm-lock.yaml # Dependency lock file for pnpm +├── tsconfig.json # TypeScript compiler configuration +├── nest-cli.json # NestJS project configuration +├── README.md # Project documentation +└── src/ + ├── main.ts # Application bootstrap and initialization + ├── app.module.ts # Root application module + │ + │ + ├── common/ # 🛠️ Shared framework resources used across modules + │ ├── common.module.ts # Registers shared providers + │ │ + │ ├── auth/ # 🛡️ Authentication module + │ │ ├── dto/ + │ │ │ ├── login.dto.ts # Login request payload + │ │ │ └── register.dto.ts # Registration payload + │ │ ├── auth.module.ts # Auth DI module + │ │ ├── auth.controller.ts # Auth REST endpoints + │ │ ├── auth.controller.spec.ts # Unit tests for controller + │ │ ├── auth.service.ts # Authentication logic + │ │ └── auth.service.spec.ts # Unit test for service + │ │ + │ ├── config/ # 📄 Configuration + │ │ └── env.validation.ts # Zod/Joi validation for environment variables + │ │ + │ ├── decorators/ # 📡 Decorators for common use cases + │ │ ├── audit.decorator.ts # Enables audit logging for a method + │ │ ├── current-user.decorator.ts # Extracts logged-in user from request + │ │ └── require-permission.decorator.ts # Declares RBAC permission requirement + │ │ + │ ├── entities/ # 📚 Database entities + │ │ ├── audit-log.entity.ts # Audit log database entity + │ │ └── base.entity.ts # Base abstraction containing core columns + │ │ + │ ├── exceptions/ # 🛡️ Global exception trap/formatter + │ │ └── http-exception.filter.ts # Global exception trap/formatter + │ │ + │ ├── file-storage/ # 📂 Two-Phase document storage (upload → scan → commit) + │ │ ├── entities/ + │ │ │ └── attachment.entity.ts # Represents stored file metadata + │ │ ├── file-storage.controller.ts # Upload/download endpoints + │ │ ├── file-storage.controller.spec.ts # Unit tests + │ │ ├── file-storage.module.ts # Module DI bindings + │ │ ├── file-storage.service.ts # File handling logic + │ │ └── file-storage.service.spec.ts + │ │ + │ ├── guards/ # 🛡️ JWT authentication guard + │ │ ├── jwt-auth.guard.ts # JWT authentication guard + │ │ ├── jwt.strategy.ts # JWT strategy configuration + │ │ └── rbac.guard.ts # Role-based access control enforcement + │ │ + │ ├── interceptors/ # 📡 Interceptors for common use cases + │ │ ├── audit-log.interceptor.ts # Automatically logs certain operations + │ │ └── transform.interceptor.ts # Standardized response formatting + │ │ + │ └── resilience/ # 🛡️ Circuit-breaker / retry logic (if implemented) + │ + │ + ├── modules/ # 📦 Module-specific resources + │ ├── user/ # 👤 User + RBAC module + │ │ ├── dto/ + │ │ │ ├── assign-user-role.dto.ts # Assign roles to users + │ │ │ ├── create-user.dto.ts + │ │ │ └── update-user.dto.ts + │ │ ├── entities/ + │ │ │ ├── user.entity.ts # User table definition + │ │ │ ├── role.entity.ts # Role definition + │ │ │ ├── permission.entity.ts # Permission entity + │ │ │ └── user-preference.entity.ts # User preference settings + │ │ ├── user.controller.ts # REST endpoints + │ │ ├── user.service.ts # Business logic + │ │ ├── user.service.spec.ts # Unit tests + │ │ └── user.module.ts # Module DI container + │ │ + │ ├── project/ # 🏢 Project/Organization/Contract structure + │ │ ├── dto/ + │ │ │ ├── create-project.dto.ts + │ │ │ ├── search-project.dto.ts + │ │ │ └── update-project.dto.ts + │ │ ├── entities/ + │ │ │ ├── project.entity.ts + │ │ │ ├── contract.entity.ts + │ │ │ ├── organization.entity.ts + │ │ │ ├── project-organization.entity.ts + │ │ │ └── contract-organization.entity.ts + │ │ ├── project.controller.ts + │ │ ├── project.controller.spec.ts + │ │ └── project.service.ts + │ + │ ├── correspondence/ # ✉️ Formal letters with routing workflow + │ │ ├── dto/ + │ │ │ ├── add-reference.dto.ts + │ │ │ ├── create-correspondence.dto.ts + │ │ │ ├── search-correspondence.dto.ts + │ │ │ ├── submit-correspondence.dto.ts + │ │ │ └── workflow-action.dto.ts + │ │ ├── entities/ + │ │ │ ├── correspondence.entity.ts + │ │ │ ├── correspondence-revision.entity.ts + │ │ │ ├── correspondence-routing.entity.ts + │ │ │ ├── correspondence-status.entity.ts + │ │ │ ├── correspondence-type.entity.ts + │ │ │ ├── correspondence-reference.entity.ts + │ │ │ ├── routing-template.entity.ts + │ │ │ └── routing-template-step.entity.ts + │ │ ├── correspondence.controller.ts + │ │ ├── correspondence.controller.spec.ts + │ │ └── correspondence.service.ts + │ + │ ├── drawing/ # 📐Contract & Shop drawing tracking + │ │ ├── dto/ + │ │ │ ├── create-contract-drawing.dto.ts + │ │ │ ├── create-shop-drawing.dto.ts + │ │ │ ├── create-shop-drawing-revision.dto.ts + │ │ │ ├── search-contract-drawing.dto.ts + │ │ │ ├── search-shop-drawing.dto.ts + │ │ │ └── update-contract-drawing.dto.ts + │ │ ├── entities/ + │ │ │ ├── contract-drawing.entity.ts + │ │ │ ├── contract-drawing-volume.entity.ts + │ │ │ ├── contract-drawing-sub-category.entity.ts + │ │ │ ├── shop-drawing.entity.ts + │ │ │ ├── shop-drawing-revision.entity.ts + │ │ │ ├── shop-drawing-main-category.entity.ts + │ │ │ └── shop-drawing-sub-category.entity.ts + │ │ ├── drawing.module.ts + │ │ ├── contract-drawing.controller.ts + │ │ ├── contract-drawing.service.ts + │ │ ├── drawing-master-data.controller.ts + │ │ ├── drawing-master-data.service.ts + │ │ ├── shop-drawing.controller.ts + │ │ └── shop-drawing.service.ts + │ + │ ├── rfa/ # ✅ Request for Approval (multi-step workflow) + │ │ ├── dto/ + │ │ │ ├── create-rfa.dto.ts + │ │ │ ├── search-rfa.dto.ts + │ │ │ └── update-rfa.dto.ts + │ │ ├── entities/ + │ │ │ ├── rfa.entity.ts + │ │ │ ├── rfa-revision.entity.ts + │ │ │ ├── rfa-item.entity.ts + │ │ │ ├── rfa-type.entity.ts + │ │ │ ├── rfa-status-code.entity.ts + │ │ │ ├── rfa-approve-code.entity.ts + │ │ │ ├── rfa-workflow.entity.ts + │ │ │ ├── rfa-workflow-template.entity.ts + │ │ │ └── rfa-workflow-template-step.entity.ts + │ │ ├── rfa.controller.ts + │ │ ├── rfa.module.ts + │ │ └── rfa.service.ts + │ + │ ├── circulation/ # 🔄 Internal routing workflow + │ │ ├── dto/ + │ │ │ ├── create-circulation.dto.ts + │ │ │ ├── update-circulation-routing.dto.ts + │ │ │ └── search-circulation.dto.ts + │ │ ├── entities/ + │ │ │ ├── circulation.entity.ts + │ │ │ ├── circulation-routing.entity.ts + │ │ │ └── circulation-status-code.entity.ts + │ │ ├── circulation.controller.ts + │ │ ├── circulation.module.ts + │ │ └── circulation.service.ts + │ + │ ├── transmittal/ # 📤 Document forwarding + │ │ ├── dto/ + │ │ │ ├── create-transmittal.dto.ts + │ │ │ ├── search-transmittal.dto.ts + │ │ │ └── update-transmittal.dto.ts + │ │ ├── entities/ + │ │ │ ├── transmittal.entity.ts + │ │ │ └── transmittal-item.entity.ts + │ │ ├── transmittal.controller.ts + │ │ ├── transmittal.module.ts + │ │ └── transmittal.service.ts + │ + │ ├── notification/ # 🔔 Real-Time notification system + │ │ ├── dto/ + │ │ │ ├── create-notification.dto.ts + │ │ │ └── search-notification.dto.ts + │ │ ├── entities/ + │ │ │ └── notification.entity.ts + │ │ ├── notification.module.ts # WebSocket + Processor registration + │ │ ├── notification.controller.ts + │ │ ├── notification.gateway.ts # WebSocket gateway + │ │ ├── notification.processor.ts # Message consumer (e.g. mail worker) + │ │ ├── notification.service.ts + │ │ └── notification-cleanup.service.ts # Cron-based cleanup job + │ + │ ├── search/ # 🔍 Elasticsearch integration + │ │ ├── dto/ + │ │ │ └── search-query.dto.ts + │ │ ├── search.module.ts + │ │ ├── search.controller.ts + │ │ └── search.service.ts # Indexing/search logic + │ + │ ├── document-numbering/ # 🔢 Auto-increment controlled ID generation + │ │ ├── entities/ + │ │ │ ├── document-number-format.entity.ts + │ │ │ └── document-number-counter.entity.ts + │ │ ├── document-numbering.module.ts + │ │ ├── document-numbering.service.ts + │ │ └── document-numbering.service.spec.ts + │ + │ ├── workflow-engine/ # ⚙️ Unified state-machine workflow engine + │ │ ├── interfaces/ + │ │ │ └── workflow.interface.ts + │ │ ├── workflow-engine.module.ts + │ │ ├── workflow-engine.service.ts + │ │ └── workflow-engine.service.spec.ts + │ + │ └── json-schema/ # 📋 Dynamic request schema validation + │ ├── dto/ + │ │ ├── create-json-schema.dto.ts + │ │ ├── update-json-schema.dto.ts + │ │ └── search-json-schema.dto.ts + │ ├── entities/ + │ │ └── json-schema.entity.ts + │ ├── json-schema.module.ts + │ ├── json-schema.controller.ts + │ ├── json-schema.controller.spec.ts + │ ├── json-schema.service.ts + │ └── json-schema.service.spec.ts +``` + +--- \ No newline at end of file diff --git a/T0-T6.2.md b/T0-T6.2.md new file mode 100644 index 0000000..ad6f92c --- /dev/null +++ b/T0-T6.2.md @@ -0,0 +1,436 @@ +# โครงสร้างโฟลเดอร์และไฟล์ทั้งหมดสำหรับ **Backend (NestJS)** ตามแผนงาน **LCBP3-DMS v1.4.3** ตั้งแต่ Phase 0 ถึง Phase 6 (T0-T6.2) ที่ได้ดำเนินการไปแล้ว + +โครงสร้างนี้ออกแบบตามหลัก **Domain-Driven Design** และ **Modular Architecture** ที่ระบุไว้ในแผนพัฒนา + +--- + +## 📂 **backend/** (Backend Application) + +* [x] `.env` (สำหรับ Local Dev เท่านั้น ห้าม commit) +* [x] `.gitignore` +* [x] `docker-compose.yml` (Configuration หลักสำหรับ Deploy) +* [x] `docker-compose.override.yml` (สำหรับ Inject Secrets ตอน Dev) +* [x] `package.json` +* [x] `pnpm-lock.yaml` +* [x] `tsconfig.json` +* [x] `nest-cli.json` +* [x] `README.md` + +--- + +## 📂 **backend/src/** (Source Code) + +### **📄 Entry Points** + +* [x] `main.ts` (Application Bootstrap, Swagger, Global Pipes) +* [x] `app.module.ts` (Root Module ที่รวมทุก Modules เข้าด้วยกัน) + +### **📁 src/common/** (Shared Resources) + +* [x] **common.module.ts** +* **auth/** + * **dto/** + * [x] **login.dto.ts** + * [x] **register.dto.ts** + * [x] **auth.controller.spec.ts** + * [x] **auth.controller.ts** + * [x] **auth.module.ts** + * [x] **auth.service.spec.ts** + * [x] **auth.service.ts** +* **config/** (Configuration Service) + * [x] **env.validation.ts** +* **decorators/** + * [x] **audit.decorator.ts** + * [x] `current-user.decorator.ts` + * [x] `require-permission.decorator.ts` +* **entities/** + * [x] **audit-log.entity.ts** + * [x] **base.entity.ts** +* **exceptions/** + * [x] `http-exception.filter.ts` (Global Filter) +* **file-storage/** (Two-Phase Storage System) + * **entities/** + * [x] **attachment.entity.ts** + * [x] **file-storage.controller.spec.ts** + * [x] **file-storage.controller.ts** + * [x] **file-storage.module.ts** + * [x] **file-storage.service.spec.ts** + * [x] `file-storage.service.ts` (Upload, Scan Virus, Commit) +* [x] `guards/` + * [x] `jwt-auth.guard.ts` + * [x] **jwt.strategy.ts** + * [x] `rbac.guard.ts` (ตรวจสอบสิทธิ์ 4 ระดับ) +* **interceptors/** + * [x] `audit-log.interceptor.ts` (เก็บ Log ลง DB) + * [x] `transform.interceptor.ts` (Standard Response Format) +* **resilience/** (Circuit Breaker & Retry) + +### **📁 src/modules/** (Feature Modules) + +1. **user/** (User Management & RBAC) + * [x] `dto/` + * [x] **assign-user-role.dto.ts** + * [x] `create-user.dto.ts` + * [x] `update-user.dto.ts` + * [x] `entities/` + * [x] `user.entity.ts` + * [x] `role.entity.ts` + * [x] `permission.entity.ts` + * [x] `user-preference.entity.ts` + * [x] **user-assignment.service.ts** + * [x] `user.controller.ts` + * [x] `user.module.ts` + * [x] `user.service.ts` + * [x] **user.service.spec.ts** + +2. **project/** (Project Structure) + * [x] `dto/` + * [x] **create-project.dto.ts** + * [x] `search-project.dto.ts` + * [x] `update-project.dto.ts` + * [x] `entities/` + * [x] `contract-organization.entity.ts` + * [x] `contract.entity.ts` + * [x] `organization.entity.ts` + * [x] `project-organization.entity.ts` (Junction) + * [x] **project.entity.ts** + * [x] **project.controller.spec.ts** + * [x] `project.controller.ts` + * [x] `project.module.ts` + * [x] **project.service.spec.ts** + * [x] `project.service.ts` + +3. **correspondence/** (Core Document System) + * [x] `dto/` + * [x] `add-reference.dto.ts` + * [x] `create-correspondence.dto.ts` + * [x] `search-correspondence.dto.ts` + * [x] **submit-correspondence.dto.ts** + * [x] `workflow-action.dto.ts` + * [x] `entities/` + * [x] `correspondence-reference.entity.ts` + * [x] `correspondence-revision.entity.ts` + * [x] `correspondence-routing.entity.ts` (Unified Workflow) + * [x] `correspondence-status.entity.ts` + * [x] `correspondence-type.entity.ts` + * [x] `correspondence.entity.ts` + * [x] **routing-template-step.entity.ts** + * [x] `routing-template.entity.ts` + * [x] **correspondence.controller.spec.ts** + * [x] `correspondence.controller.ts` + * [x] `correspondence.module.ts` + * [x] **correspondence.service.spec.ts** + * [x] `correspondence.service.ts` (Impersonation & Workflow Logic) + +4. **drawing/** (Contract & Shop Drawings) + * [x] `dto/` + * [x] `create-contract-drawing.dto.ts` + * [x] `create-shop-drawing-revision.dto.ts` + * [x] `create-shop-drawing.dto.ts` + * [x] `search-contract-drawing.dto.ts` + * [x] `search-shop-drawing.dto.ts` + * [x] `update-contract-drawing.dto.ts` + * [x] `entities/` + * [x] `contract-drawing-sub-category.entity.ts` + * [x] `contract-drawing-volume.entity.ts` + * [x] `contract-drawing.entity.ts` + * [x] `shop-drawing-main-category.entity.ts` + * [x] `shop-drawing-revision.entity.ts` + * [x] `shop-drawing-sub-category.entity.ts` + * [x] `shop-drawing.entity.ts` + * [x] `contract-drawing.controller.ts` + * [x] `contract-drawing.service.ts` + * [x] `drawing-master-data.controller.ts` + * [x] `drawing-master-data.service.ts` + * [x] `drawing.module.ts` + * [x] `shop-drawing.controller.ts` + * [x] `shop-drawing.service.ts` + +4. **rfa/** (Request for Approval & Advanced Workflow) + * [x] `dto/` + * [x] `create-rfa.dto.ts` + * [x] `search-rfa.dto.ts` + * [x] `update-rfa.dto.ts` + * [x] `entities/` + * [x] `rfa-approve-code.entity.ts` + * [x] `rfa-item.entity.ts` + * [x] `rfa-revision.entity.ts` + * [x] `rfa-status-code.entity.ts` + * [x] `rfa-type.entity.ts` + * [x] `rfa-workflow-template-step.entity.ts` + * [x] `rfa-workflow-template.entity.ts` + * [x] `rfa-workflow.entity.ts` + * [x] `rfa.entity.ts` + * [x] `rfa.controller.ts` + * [x] `rfa.module.ts` + * [x] `rfa.service.ts` (Unified Workflow Integration) + +5. **circulation/** (Internal Routing) + * [x] `dto/` + * [x] `create-circulation.dto.ts` + * [x] `update-circulation-routing.dto.ts` + * [x] `search-circulation.dto.ts` + * [x] `entities/` + * [x] `circulation-routing.entity.ts` + * [x] `circulation-status-code.entity.ts` + * [x] `circulation.entity.ts` + * [x] `circulation.controller.ts` + * [x] `circulation.module.ts` + * [x] `circulation.service.ts` + +6. **transmittal/** (Document Forwarding) + * [x] `dto/` + * [x] `create-transmittal.dto.ts` + * [x] `search-transmittal.dto.ts` + * [x] **update-transmittal.dto.ts** + * [x] `entities/` + * [x] `transmittal-item.entity.ts` + * [x] `transmittal.entity.ts` + * [x] `transmittal.controller.ts` + * [x] `transmittal.module.ts` + * [x] `transmittal.service.ts` + +7. **notification/** (System Alerts) + * [x] `dto/` + * [x] `create-notification.dto.ts` + * [x] `search-notification.dto.ts` + * [x] `entities/` + * [x] `notification.entity.ts` + * [x] `notification-cleanup.service.ts` (Cron Job) + * [x] `notification.controller.ts` + * [x] `notification.gateway.ts` + * [x] `notification.module.ts` (Real-time WebSocket) + * [x] `notification.processor.ts` (Consumer/Worker for Email & Line) + * [x] `notification.service.ts` (Producer) + +8. **search/** (Elasticsearch) + * [x] `dto/search-query.dto.ts` + * [x] `search.controller.ts` + * [x] `search.module.ts` + * [x] `search.service.ts` (Indexing & Searching) + +9. **document-numbering/** (Internal Service) + * [x] `entities/` + * [x] `document-number-format.entity.ts` + * [x] `document-number-counter.entity.ts` + * [x] `document-numbering.module.ts` + * [x] **document-numbering.service.spec.ts** + * [x] `document-numbering.service.ts` (Double-Lock Mechanism) + +10. **workflow-engine/** (Unified Logic) + * [x] `interfaces/workflow.interface.ts` + * [x] `workflow-engine.module.ts` + * [x] **workflow-engine.service.spec.ts** + * [x] `workflow-engine.service.ts` (State Machine Logic) + +11. **json-schema/** (Validation) + * [x] `dto/` + * [x] `create-json-schema.dto.ts`+ + * [x] `search-json-schema.dto.ts` + * [x] `update-json-schema.dto.ts` + * [x] `entities/` + * [x] `json-schema.entity.ts` + * [x] **json-schema.controller.spec.ts** + * [x] **json-schema.controller.ts** + * [x] `json-schema.module.ts` + * [x] **json-schema.service.spec.ts** + * [x] `json-schema.service.ts` + +--- + +นี่คือโครงสร้าง **Folder Structure** ของโปรเจกต์ Backend (NestJS) ฉบับสมบูรณ์ ที่รวบรวมจาก **Requirements**, **FullStackJS Guidelines**, **Data Dictionary** และสิ่งที่เราได้ **Implement ไปแล้วตั้งแต่ Phase 0 ถึง Phase 6.2** ครับ + +โครงสร้างนี้เป็นแบบ **Domain-Driven Modular Architecture** ครับ + +```text +lcbp3-backend/ +├── .env # Environment variables (Local Dev) +├── .gitignore +├── docker-compose.yml # Main config for services (DB, Redis, ES, App) +├── docker-compose.override.yml # Secrets injection for Dev +├── nest-cli.json +├── package.json +├── pnpm-lock.yaml +├── README.md +├── tsconfig.json +│ +└── src/ + ├── main.ts # Entry point (Swagger, Helmet, Global Pipes) + ├── app.module.ts # Root Module + │ + ├── common/ # 🛠️ Shared Resources + │ ├── auth/ + │ │ ├── guards/ + │ │ │ ├── jwt-auth.guard.ts + │ │ │ └── rbac.guard.ts + │ │ └── strategies/ + │ │ └── jwt.strategy.ts + │ ├── config/ # Config Service + │ ├── decorators/ + │ │ ├── current-user.decorator.ts + │ │ └── require-permission.decorator.ts + │ ├── exceptions/ + │ │ └── http-exception.filter.ts + │ ├── file-storage/ # 📂 Two-Phase Storage System + │ │ ├── dto/ + │ │ ├── entities/ + │ │ │ └── attachment.entity.ts + │ │ ├── file-storage.module.ts + │ │ └── file-storage.service.ts + │ ├── interceptors/ + │ │ ├── audit-log.interceptor.ts + │ │ └── transform.interceptor.ts + │ └── resilience/ # Circuit Breaker & Retry logic + │ + └── modules/ # 📦 Feature Modules + │ + ├── user/ # 👤 User & RBAC + │ ├── dto/ + │ │ ├── create-user.dto.ts + │ │ └── update-user.dto.ts + │ ├── entities/ + │ │ ├── permission.entity.ts + │ │ ├── role.entity.ts + │ │ ├── user-preference.entity.ts + │ │ └── user.entity.ts + │ ├── user.controller.ts + │ ├── user.module.ts + │ └── user.service.ts + │ + ├── project/ # 🏢 Projects & Organizations + │ ├── dto/ + │ │ ├── create-project.dto.ts + │ │ ├── search-project.dto.ts + │ │ └── update-project.dto.ts + │ ├── entities/ + │ │ ├── contract-organization.entity.ts + │ │ ├── contract.entity.ts + │ │ ├── organization.entity.ts + │ │ ├── project-organization.entity.ts + │ │ └── project.entity.ts + │ ├── project.controller.ts + │ ├── project.module.ts + │ └── project.service.ts + │ + ├── correspondence/ # ✉️ Core Document + │ ├── dto/ + │ │ ├── add-reference.dto.ts + │ │ ├── create-correspondence.dto.ts + │ │ ├── search-correspondence.dto.ts + │ │ └── workflow-action.dto.ts + │ ├── entities/ + │ │ ├── correspondence-reference.entity.ts + │ │ ├── correspondence-revision.entity.ts + │ │ ├── correspondence-routing.entity.ts + │ │ ├── correspondence-status.entity.ts + │ │ ├── correspondence-type.entity.ts + │ │ ├── correspondence.entity.ts + │ │ └── routing-template.entity.ts + │ ├── correspondence.controller.ts + │ ├── correspondence.module.ts + │ └── correspondence.service.ts + │ + ├── drawing/ # 📐 Shop & Contract Drawings + │ ├── dto/ + │ │ ├── create-contract-drawing.dto.ts + │ │ ├── create-shop-drawing-revision.dto.ts + │ │ ├── create-shop-drawing.dto.ts + │ │ ├── search-contract-drawing.dto.ts + │ │ ├── search-shop-drawing.dto.ts + │ │ └── update-contract-drawing.dto.ts + │ ├── entities/ + │ │ ├── contract-drawing-sub-category.entity.ts + │ │ ├── contract-drawing-volume.entity.ts + │ │ ├── contract-drawing.entity.ts + │ │ ├── shop-drawing-main-category.entity.ts + │ │ ├── shop-drawing-revision.entity.ts + │ │ ├── shop-drawing-sub-category.entity.ts + │ │ └── shop-drawing.entity.ts + │ ├── contract-drawing.controller.ts + │ ├── contract-drawing.service.ts + │ ├── drawing-master-data.controller.ts + │ ├── drawing-master-data.service.ts + │ ├── drawing.module.ts + │ ├── shop-drawing.controller.ts + │ └── shop-drawing.service.ts + │ + ├── rfa/ # ✅ Request for Approval + │ ├── dto/ + │ │ ├── create-rfa.dto.ts + │ │ ├── search-rfa.dto.ts + │ │ └── update-rfa.dto.ts + │ ├── entities/ + │ │ ├── rfa-approve-code.entity.ts + │ │ ├── rfa-item.entity.ts + │ │ ├── rfa-revision.entity.ts + │ │ ├── rfa-status-code.entity.ts + │ │ ├── rfa-type.entity.ts + │ │ ├── rfa-workflow-template-step.entity.ts + │ │ ├── rfa-workflow-template.entity.ts + │ │ ├── rfa-workflow.entity.ts + │ │ └── rfa.entity.ts + │ ├── rfa.controller.ts + │ ├── rfa.module.ts + │ └── rfa.service.ts + │ + ├── circulation/ # 🔄 Internal Routing + │ ├── dto/ + │ │ ├── create-circulation.dto.ts + │ │ ├── search-circulation.dto.ts + │ │ └── update-circulation-routing.dto.ts + │ ├── entities/ + │ │ ├── circulation-routing.entity.ts + │ │ ├── circulation-status-code.entity.ts + │ │ └── circulation.entity.ts + │ ├── circulation.controller.ts + │ ├── circulation.module.ts + │ └── circulation.service.ts + │ + ├── transmittal/ # 📤 Outgoing Documents + │ ├── dto/ + │ │ ├── create-transmittal.dto.ts + │ │ └── search-transmittal.dto.ts + │ ├── entities/ + │ │ ├── transmittal-item.entity.ts + │ │ └── transmittal.entity.ts + │ ├── transmittal.controller.ts + │ ├── transmittal.module.ts + │ └── transmittal.service.ts + │ + ├── notification/ # 🔔 Real-time & Queue + │ ├── dto/ + │ │ ├── create-notification.dto.ts + │ │ └── search-notification.dto.ts + │ ├── entities/ + │ │ └── notification.entity.ts + │ ├── notification-cleanup.service.ts + │ ├── notification.controller.ts + │ ├── notification.gateway.ts + │ ├── notification.module.ts + │ ├── notification.processor.ts + │ └── notification.service.ts + │ + ├── search/ # 🔍 Elasticsearch + │ ├── dto/ + │ │ └── search-query.dto.ts + │ ├── search.controller.ts + │ ├── search.module.ts + │ └── search.service.ts + │ + ├── document-numbering/ # 🔢 Internal Numbering Service + │ ├── entities/ + │ │ ├── document-number-counter.entity.ts + │ │ └── document-number-format.entity.ts + │ ├── document-numbering.module.ts + │ └── document-numbering.service.ts + │ + ├── workflow-engine/ # ⚙️ Unified Logic + │ ├── interfaces/ + │ │ └── workflow.interface.ts + │ ├── workflow-engine.module.ts + │ └── workflow-engine.service.ts + │ + └── json-schema/ # 📋 Validation Logic + ├── json-schema.module.ts + └── json-schema.service.ts +``` diff --git a/backend/Infrastructure Setup.yml b/backend/Infrastructure Setup.yml new file mode 100644 index 0000000..d00f493 --- /dev/null +++ b/backend/Infrastructure Setup.yml @@ -0,0 +1,76 @@ +version: '3.8' + +services: + # --------------------------------------------------------------------------- + # Redis Service + # ใช้สำหรับ: Caching ข้อมูล, Session Store และ Message Queue (สำหรับ NestJS/BullMQ) + # --------------------------------------------------------------------------- + redis: + image: redis:7-alpine + container_name: lcbp3_redis + restart: always + command: redis-server --save 60 1 --loglevel warning --requirepass "${REDIS_PASSWORD:-redis_password}" + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - lcbp3_net + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis_password}", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + # --------------------------------------------------------------------------- + # Elasticsearch Service + # ใช้สำหรับ: Full-text Search และการวิเคราะห์ข้อมูล (Database Analysis) + # --------------------------------------------------------------------------- + elasticsearch: + image: elasticsearch:8.11.1 + container_name: lcbp3_elasticsearch + restart: always + environment: + - node.name=lcbp3_es01 + - cluster.name=lcbp3_es_cluster + - discovery.type=single-node # รันแบบ Node เดียวสำหรับ Dev/Phase 0 + - bootstrap.memory_lock=true # ล็อคหน่วยความจำเพื่อประสิทธิภาพ + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # กำหนด Heap Size (ปรับเพิ่มได้ตาม Resource เครื่อง) + - xpack.security.enabled=false # ปิด Security ชั่วคราวสำหรับ Phase 0 (ควรเปิดใน Production) + - xpack.security.http.ssl.enabled=false + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - es_data:/usr/share/elasticsearch/data + ports: + - "9200:9200" + networks: + - lcbp3_net + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + +# --------------------------------------------------------------------------- +# Volumes Configuration +# การจัดการพื้นที่จัดเก็บข้อมูลแบบ Persistent Data +# --------------------------------------------------------------------------- +volumes: + redis_data: + driver: local + name: lcbp3_redis_vol + es_data: + driver: local + name: lcbp3_es_vol + +# --------------------------------------------------------------------------- +# Networks Configuration +# เครือข่ายสำหรับเชื่อมต่อ Container ภายใน +# --------------------------------------------------------------------------- +networks: + lcbp3_net: + driver: bridge + name: lcbp3_network \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index ea24128..6c1b4ea 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -41,10 +41,24 @@ services: networks: - lcbp3-net + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.11.1 + container_name: lcbp3-elasticsearch + environment: + - discovery.type=single-node + - xpack.security.enabled=false # ปิด security เพื่อความง่ายใน Dev (Prod ต้องเปิด) + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + ports: + - "9200:9200" + volumes: + - esdata:/usr/share/elasticsearch/data + networks: + - lcbp3-net volumes: db_data: redis_data: # เพิ่ม Volume - + esdata: + networks: lcbp3-net: driver: bridge \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 87078d2..04791cc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,8 +21,9 @@ }, "dependencies": { "@casl/ability": "^6.7.3", - "@elastic/elasticsearch": "^9.2.0", + "@elastic/elasticsearch": "^8.11.1", "@nestjs/bullmq": "^11.0.4", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -43,6 +44,8 @@ "axios": "^1.13.2", "bcrypt": "^6.0.0", "bullmq": "^5.63.2", + "cache-manager": "^7.2.5", + "cache-manager-redis-yet": "^5.1.5", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "fs-extra": "^11.3.2", @@ -69,6 +72,7 @@ "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", "@types/bcrypt": "^6.0.0", + "@types/cache-manager": "^5.0.0", "@types/express": "^5.0.0", "@types/fs-extra": "^11.0.4", "@types/ioredis": "^5.0.0", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 8e7e500..b9d37dd 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -11,9 +11,15 @@ importers: '@casl/ability': specifier: ^6.7.3 version: 6.7.3 + '@elastic/elasticsearch': + specifier: ^8.11.1 + version: 8.19.1 '@nestjs/bullmq': specifier: ^11.0.4 version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.63.2) + '@nestjs/cache-manager': + specifier: ^3.0.1 + version: 3.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(cache-manager@7.2.5)(keyv@5.5.4)(rxjs@7.8.2) '@nestjs/common': specifier: ^11.0.1 version: 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -22,7 +28,10 @@ importers: version: 4.0.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.1 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/elasticsearch': + specifier: ^11.1.0 + version: 11.1.0(@elastic/elasticsearch@8.19.1)(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/jwt': specifier: ^11.0.1 version: 11.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)) @@ -35,6 +44,9 @@ importers: '@nestjs/platform-express': specifier: ^11.0.1 version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/platform-socket.io': + specifier: ^11.1.9 + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.0.1 version: 6.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) @@ -46,7 +58,10 @@ importers: version: 6.4.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))) + version: 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))) + '@nestjs/websockets': + specifier: ^11.1.9 + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/nodemailer': specifier: ^7.0.4 version: 7.0.4 @@ -65,6 +80,12 @@ importers: bullmq: specifier: ^5.63.2 version: 5.63.2 + cache-manager: + specifier: ^7.2.5 + version: 7.2.5 + cache-manager-redis-yet: + specifier: ^5.1.5 + version: 5.1.5 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -107,12 +128,15 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.2 + socket.io: + specifier: ^4.8.1 + version: 4.8.1 swagger-ui-express: specifier: ^5.0.1 version: 5.0.1(express@5.1.0) typeorm: specifier: ^0.3.27 - version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) uuid: specifier: ^13.0.0 version: 13.0.0 @@ -135,6 +159,9 @@ importers: '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 + '@types/cache-manager': + specifier: ^5.0.0 + version: 5.0.0 '@types/express': specifier: ^5.0.0 version: 5.0.5 @@ -535,6 +562,9 @@ packages: '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@cacheable/utils@2.3.1': + resolution: {integrity: sha512-38NJXjIr4W1Sghun8ju+uYWD8h2c61B4dKwfnQHVDFpAJ9oS28RpfqZQJ6Dgd3RceGkILDY9YT+72HJR3LoeSQ==} + '@casl/ability@6.7.3': resolution: {integrity: sha512-A4L28Ko+phJAsTDhRjzCOZWECQWN2jzZnJPnROWWHjJpyMq1h7h9ZqjwS2WbIUa3Z474X1ZPSgW0f1PboZGC0A==} @@ -546,6 +576,14 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@elastic/elasticsearch@8.19.1': + resolution: {integrity: sha512-+1j9NnQVOX+lbWB8LhCM7IkUmjU05Y4+BmSLfusq0msCsQb1Va+OUKFCoOXjCJqQrcgdRdQCjYYyolQ/npQALQ==} + engines: {node: '>=18'} + + '@elastic/transport@8.10.0': + resolution: {integrity: sha512-Xd62ZtgdrJuaunTLk0LqYtkUtJ3D2/NQ4QyLWPYj0c2h97SNUaNkrQH9lzb6r2P0Bdjx/HwKtW3X8kO5LJ7qEQ==} + engines: {node: '>=18'} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -899,6 +937,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -952,6 +993,15 @@ packages: '@nestjs/core': ^10.0.0 || ^11.0.0 bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 + '@nestjs/cache-manager@3.0.1': + resolution: {integrity: sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw==} + peerDependencies: + '@nestjs/common': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 || ^11.0.0 + cache-manager: '>=6' + keyv: '>=5' + rxjs: ^7.8.1 + '@nestjs/cli@11.0.11': resolution: {integrity: sha512-phKImmBK2qc0dqMPz+vnBlb+xcAxyZ5yiCKOdcgq9DwFsswL6jn3l2leKXQLIyM2bqIecE5T3tdWKLZl7wgW0w==} engines: {node: '>= 20.11'} @@ -1002,6 +1052,13 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/elasticsearch@11.1.0': + resolution: {integrity: sha512-NwMakVs8LeXUksaSNp0ejhv223yVCK4w9iqMBrsonKj2gl4sBIBrAgJq/aXhD9bJCNLYb+waoRAsxuuPxYcjXw==} + peerDependencies: + '@elastic/elasticsearch': ^7.4.0 || ^8.0.0 || ^9.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.2.0 + '@nestjs/jwt@11.0.1': resolution: {integrity: sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==} peerDependencies: @@ -1032,6 +1089,13 @@ packages: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 + '@nestjs/platform-socket.io@11.1.9': + resolution: {integrity: sha512-OaAW+voXo5BXbFKd9Ot3SL05tEucRMhZRdw5wdWZf/RpIl9hB6G6OHr8DDxNbUGvuQWzNnZHCDHx3EQJzjcIyA==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + rxjs: ^7.1.0 + '@nestjs/schedule@6.0.1': resolution: {integrity: sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==} peerDependencies: @@ -1089,6 +1153,18 @@ packages: rxjs: ^7.2.0 typeorm: ^0.3.0 + '@nestjs/websockets@11.1.9': + resolution: {integrity: sha512-kkkdeTVcc3X7ZzvVqUVpOAJoh49kTRUjWNUXo5jmG+27OvZoHfs/vuSiqxidrrbIgydSqN15HUsf1wZwQUrxCQ==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/platform-socket.io': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/platform-socket.io': + optional: true + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -1110,6 +1186,20 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.38.0': + resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + engines: {node: '>=14'} + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -1121,6 +1211,35 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} @@ -1305,12 +1424,18 @@ packages: resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} engines: {node: '>=18.0.0'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@tokenizer/inflate@0.3.1': resolution: {integrity: sha512-4oeoZEBQdLdt5WmP/hx1KZ6D3/Oid/0cUb2nk4F0pTDAWy+KCH3/EnAkZF/bvckWo8I33EqBm01lIPgmgc8rCA==} engines: {node: '>=18'} @@ -1351,12 +1476,25 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/cache-manager@5.0.0': + resolution: {integrity: sha512-YaQBBhn2OIShxGMH7l1qzGgXCQJ2TRYTqMhFMXRBkDcXQIlqXv+XFkBGstwXSyv7q+JQm3HbpVhaVF8l5tr+Hg==} + deprecated: This is a stub types definition. cache-manager provides its own type definitions, so you do not need this installed. + + '@types/command-line-args@5.2.3': + resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} + + '@types/command-line-usage@5.0.4': + resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -1421,6 +1559,9 @@ packages: '@types/node@22.19.1': resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/nodemailer@7.0.4': resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==} @@ -1690,6 +1831,10 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1786,6 +1931,10 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + apache-arrow@21.1.0: + resolution: {integrity: sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==} + hasBin: true + app-root-path@3.1.0: resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} engines: {node: '>= 6.0.0'} @@ -1802,6 +1951,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-back@6.2.2: + resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} + engines: {node: '>=12.17'} + array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -1853,6 +2006,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + baseline-browser-mapping@2.8.29: resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==} hasBin: true @@ -1916,6 +2073,18 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cache-manager-redis-yet@5.1.5: + resolution: {integrity: sha512-NYDxrWBoLXxxVPw4JuBriJW0f45+BVOAsgLiozRo4GoJQyoKPbueQWYStWqmO73/AeHJeWrV7Hzvk6vhCGHlqA==} + engines: {node: '>= 18'} + deprecated: With cache-manager v6 we now are using Keyv + + cache-manager@5.7.6: + resolution: {integrity: sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==} + engines: {node: '>= 18'} + + cache-manager@7.2.5: + resolution: {integrity: sha512-Y5LF7olTrcKJn1NoKiWPOvjEiO5DfDVPxqZHETCRMaliC60KBNb4Ge/vEYep5TyaqpXvnpnPPo8zauCe6UzZwA==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1943,6 +2112,10 @@ packages: caniuse-lite@1.0.30001755: resolution: {integrity: sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==} + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2021,6 +2194,19 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + command-line-args@6.0.1: + resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==} + engines: {node: '>=12.20'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true + + command-line-usage@7.0.3: + resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} + engines: {node: '>=12.20.0'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2102,6 +2288,15 @@ packages: dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2202,6 +2397,14 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -2322,6 +2525,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2393,6 +2599,15 @@ packages: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} + find-replace@5.0.2: + resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} + engines: {node: '>=14'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -2405,6 +2620,9 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -2473,6 +2691,10 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2559,6 +2781,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hashery@1.2.0: + resolution: {integrity: sha512-43XJKpwle72Ik5Zpam7MuzRWyNdwwdf6XHlh8wCj2PggvWf+v/Dm5B0dxGZOmddidgeO6Ofu9As/o231Ti/9PA==} + engines: {node: '>=20'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2567,6 +2793,13 @@ packages: resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} engines: {node: '>=18.0.0'} + hookified@1.13.0: + resolution: {integrity: sha512-6sPYUY8olshgM/1LDNW4QZQN0IqgKhtl/1C8koNZBJrKLBk3AZl6chQtNwpNztvfiApHMEwMHek5rv993PRbWw==} + + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2863,6 +3096,10 @@ packages: engines: {node: '>=6'} hasBin: true + json-bignum@0.0.3: + resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} + engines: {node: '>=0.8'} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2902,6 +3139,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.5.4: + resolution: {integrity: sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -2932,6 +3172,12 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -3126,6 +3372,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -3173,6 +3423,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -3320,6 +3574,10 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + promise-coalesce@1.5.0: + resolution: {integrity: sha512-cTJ30U+ur1LD7pMPyQxiKIwxjtAjLsyU7ivRhVWZrX9BNIXtf78pc37vSMc8Vikx7DVzEKNk2SEJ5KWUpSG2ig==} + engines: {node: '>=16'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3371,6 +3629,9 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + redis@4.7.1: + resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} + redlock@5.0.0-beta.2: resolution: {integrity: sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==} engines: {node: '>=12'} @@ -3433,6 +3694,9 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + secure-json-parse@3.0.2: + resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3503,6 +3767,17 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -3630,6 +3905,10 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + table-layout@4.1.1: + resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} + engines: {node: '>=12.17'} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -3842,6 +4121,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + typical@7.3.0: + resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} + engines: {node: '>=12.17'} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -3858,6 +4141,13 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici@6.22.0: + resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} + engines: {node: '>=18.17'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3952,6 +4242,10 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wordwrapjs@5.1.1: + resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} + engines: {node: '>=12.17'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -3971,6 +4265,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -3982,6 +4288,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4647,6 +4956,11 @@ snapshots: '@borewit/text-codec@0.1.1': {} + '@cacheable/utils@2.3.1': + dependencies: + hashery: 1.2.0 + keyv: 5.5.4 + '@casl/ability@6.7.3': dependencies: '@ucast/mongo2js': 1.4.0 @@ -4658,6 +4972,28 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@elastic/elasticsearch@8.19.1': + dependencies: + '@elastic/transport': 8.10.0 + apache-arrow: 21.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@75lb/nature' + - supports-color + + '@elastic/transport@8.10.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + debug: 4.4.3 + hpagent: 1.2.0 + ms: 2.1.3 + secure-json-parse: 3.0.2 + tslib: 2.8.1 + undici: 6.22.0 + transitivePeerDependencies: + - supports-color + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -5122,6 +5458,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@keyv/serialize@1.1.1': {} + '@lukeed/csprng@1.1.0': {} '@microsoft/tsdoc@0.16.0': {} @@ -5154,17 +5492,25 @@ snapshots: '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.63.2)': dependencies: '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) bullmq: 5.63.2 tslib: 2.8.1 + '@nestjs/cache-manager@3.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(cache-manager@7.2.5)(keyv@5.5.4)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cache-manager: 7.2.5 + keyv: 5.5.4 + rxjs: 7.8.2 + '@nestjs/cli@11.0.11(@types/node@22.19.1)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) @@ -5214,7 +5560,7 @@ snapshots: lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 @@ -5227,6 +5573,13 @@ snapshots: uid: 2.0.2 optionalDependencies: '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/websockets': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + + '@nestjs/elasticsearch@11.1.0(@elastic/elasticsearch@8.19.1)(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@elastic/elasticsearch': 8.19.1 + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + rxjs: 7.8.2 '@nestjs/jwt@11.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: @@ -5250,7 +5603,7 @@ snapshots: '@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.5 express: 5.1.0 multer: 2.0.2 @@ -5259,10 +5612,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/platform-socket.io@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + rxjs: 7.8.2 + socket.io: 4.8.1 + tslib: 2.8.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@nestjs/schedule@6.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.3.3 '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': @@ -5280,7 +5645,7 @@ snapshots: dependencies: '@microsoft/tsdoc': 0.16.0 '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) js-yaml: 4.1.1 lodash: 4.17.21 @@ -5294,7 +5659,7 @@ snapshots: '@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) @@ -5302,16 +5667,28 @@ snapshots: '@nestjs/throttler@6.4.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + + '@nestjs/websockets@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + iterare: 1.2.1 + object-hash: 3.0.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-socket.io': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -5331,6 +5708,15 @@ snapshots: dependencies: consola: 3.4.2 + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/semantic-conventions@1.38.0': {} + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -5340,6 +5726,32 @@ snapshots: '@pkgr/core@0.2.9': {} + '@redis/bloom@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/client@1.6.1': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/json@1.0.7(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/search@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/time-series@1.1.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + '@scarf/scarf@1.4.0': {} '@sinclair/typebox@0.34.41': {} @@ -5626,10 +6038,16 @@ snapshots: dependencies: tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} + '@sqltools/formatter@1.2.5': {} '@standard-schema/spec@1.0.0': {} + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + '@tokenizer/inflate@0.3.1': dependencies: debug: 4.4.3 @@ -5683,12 +6101,24 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.19.1 + '@types/cache-manager@5.0.0': + dependencies: + cache-manager: 7.2.5 + + '@types/command-line-args@5.2.3': {} + + '@types/command-line-usage@5.0.4': {} + '@types/connect@3.4.38': dependencies: '@types/node': 22.19.1 '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.19.1 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -5769,6 +6199,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + '@types/nodemailer@7.0.4': dependencies: '@aws-sdk/client-sesv2': 3.938.0 @@ -6085,6 +6519,11 @@ snapshots: '@xtuc/long@4.2.2': {} + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + accepts@2.0.0: dependencies: mime-types: 3.0.1 @@ -6162,6 +6601,20 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + apache-arrow@21.1.0: + dependencies: + '@swc/helpers': 0.5.17 + '@types/command-line-args': 5.2.3 + '@types/command-line-usage': 5.0.4 + '@types/node': 24.10.1 + command-line-args: 6.0.1 + command-line-usage: 7.0.3 + flatbuffers: 25.9.23 + json-bignum: 0.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@75lb/nature' + app-root-path@3.1.0: {} append-field@1.0.0: {} @@ -6174,6 +6627,8 @@ snapshots: argparse@2.0.1: {} + array-back@6.2.2: {} + array-timsort@1.0.3: {} asap@2.0.6: {} @@ -6250,6 +6705,8 @@ snapshots: base64-js@1.5.1: {} + base64id@2.0.0: {} + baseline-browser-mapping@2.8.29: {} bcrypt@6.0.0: @@ -6340,6 +6797,29 @@ snapshots: bytes@3.1.2: {} + cache-manager-redis-yet@5.1.5: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.1) + '@redis/client': 1.6.1 + '@redis/graph': 1.1.1(@redis/client@1.6.1) + '@redis/json': 1.0.7(@redis/client@1.6.1) + '@redis/search': 1.2.0(@redis/client@1.6.1) + '@redis/time-series': 1.1.0(@redis/client@1.6.1) + cache-manager: 5.7.6 + redis: 4.7.1 + + cache-manager@5.7.6: + dependencies: + eventemitter3: 5.0.1 + lodash.clonedeep: 4.5.0 + lru-cache: 10.4.3 + promise-coalesce: 1.5.0 + + cache-manager@7.2.5: + dependencies: + '@cacheable/utils': 2.3.1 + keyv: 5.5.4 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6365,6 +6845,10 @@ snapshots: caniuse-lite@1.0.30001755: {} + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6430,6 +6914,20 @@ snapshots: dependencies: delayed-stream: 1.0.0 + command-line-args@6.0.1: + dependencies: + array-back: 6.2.2 + find-replace: 5.0.2 + lodash.camelcase: 4.3.0 + typical: 7.3.0 + + command-line-usage@7.0.3: + dependencies: + array-back: 6.2.2 + chalk-template: 0.4.0 + table-layout: 4.1.1 + typical: 7.3.0 + commander@2.20.3: {} commander@4.1.1: {} @@ -6500,6 +6998,10 @@ snapshots: dayjs@1.11.19: {} + debug@4.3.7: + dependencies: + ms: 2.1.3 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -6570,6 +7072,24 @@ snapshots: encodeurl@2.0.0: {} + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 22.19.1 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -6695,6 +7215,8 @@ snapshots: etag@1.8.1: {} + eventemitter3@5.0.1: {} + events@3.3.0: {} execa@5.1.1: @@ -6814,6 +7336,8 @@ snapshots: transitivePeerDependencies: - supports-color + find-replace@5.0.2: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -6829,6 +7353,8 @@ snapshots: flatted: 3.3.3 keyv: 4.5.4 + flatbuffers@25.9.23: {} + flatted@3.3.3: {} follow-redirects@1.15.11: {} @@ -6902,6 +7428,8 @@ snapshots: dependencies: is-property: 1.0.2 + generic-pool@3.9.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -6996,12 +7524,20 @@ snapshots: dependencies: has-symbols: 1.1.0 + hashery@1.2.0: + dependencies: + hookified: 1.13.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 helmet@8.1.0: {} + hookified@1.13.0: {} + + hpagent@1.2.0: {} + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -7481,6 +8017,8 @@ snapshots: jsesc@3.1.0: {} + json-bignum@0.0.3: {} + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -7529,6 +8067,10 @@ snapshots: dependencies: json-buffer: 3.0.1 + keyv@5.5.4: + dependencies: + '@keyv/serialize': 1.1.1 + leven@3.1.0: {} levn@0.4.1: @@ -7552,6 +8094,10 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: {} + lodash.defaults@4.2.0: {} lodash.includes@4.3.0: {} @@ -7720,6 +8266,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + negotiator@1.0.0: {} neo-async@2.6.2: {} @@ -7753,6 +8301,8 @@ snapshots: object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} on-finished@2.4.1: @@ -7888,6 +8438,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + promise-coalesce@1.5.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -7934,6 +8486,15 @@ snapshots: dependencies: redis-errors: 1.2.0 + redis@4.7.1: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.1) + '@redis/client': 1.6.1 + '@redis/graph': 1.1.1(@redis/client@1.6.1) + '@redis/json': 1.0.7(@redis/client@1.6.1) + '@redis/search': 1.2.0(@redis/client@1.6.1) + '@redis/time-series': 1.1.0(@redis/client@1.6.1) + redlock@5.0.0-beta.2: dependencies: node-abort-controller: 3.1.1 @@ -7998,6 +8559,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + secure-json-parse@3.0.2: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -8090,6 +8653,36 @@ snapshots: slash@3.0.0: {} + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.7 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -8211,6 +8804,11 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + table-layout@4.1.1: + dependencies: + array-back: 6.2.2 + wordwrapjs: 5.1.1 + tapable@2.3.0: {} terser-webpack-plugin@5.3.14(webpack@5.100.2): @@ -8351,7 +8949,7 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 3.17.0 @@ -8371,6 +8969,7 @@ snapshots: optionalDependencies: ioredis: 5.8.2 mysql2: 3.15.3 + redis: 4.7.1 ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros @@ -8389,6 +8988,8 @@ snapshots: typescript@5.9.3: {} + typical@7.3.0: {} + uglify-js@3.19.3: optional: true @@ -8400,6 +9001,10 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.16.0: {} + + undici@6.22.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -8525,6 +9130,8 @@ snapshots: wordwrap@1.0.0: {} + wordwrapjs@5.1.1: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -8550,12 +9157,16 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@8.17.1: {} + xtend@4.0.2: {} y18n@5.0.8: {} yallist@3.1.1: {} + yallist@4.0.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index ef68e81..b65c469 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,42 +1,73 @@ // File: src/app.module.ts +// บันทึกการแก้ไข: เพิ่ม CacheModule (Redis), Config สำหรับ Idempotency และ Maintenance Mode (T1.1) + import { Module } from '@nestjs/common'; -import { APP_GUARD } from '@nestjs/core'; // <--- เพิ่ม Import นี้ T2.4 +import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { BullModule } from '@nestjs/bullmq'; // Import BullModule -import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; // <--- เพิ่ม Import นี้ T2.4 +import { BullModule } from '@nestjs/bullmq'; +import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; +import { CacheModule } from '@nestjs/cache-manager'; +import { redisStore } from 'cache-manager-redis-yet'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { envValidationSchema } from './common/config/env.validation.js'; // สังเกต .js สำหรับ ESM -// import { CommonModule } from './common/common.module'; +import { envValidationSchema } from './common/config/env.validation.js'; +import redisConfig from './common/config/redis.config'; + +// Entities & Interceptors +import { AuditLog } from './common/entities/audit-log.entity'; +import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor'; +// ✅ Import Guard ใหม่สำหรับ Maintenance Mode +import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard'; +// import { IdempotencyInterceptor } from './common/interceptors/idempotency.interceptor'; // ✅ เตรียมไว้ใช้ (ถ้าต้องการ Global) + +// Modules import { UserModule } from './modules/user/user.module'; import { ProjectModule } from './modules/project/project.module'; import { FileStorageModule } from './common/file-storage/file-storage.module.js'; import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module'; -import { AuthModule } from './common/auth/auth.module.js'; // <--- เพิ่ม Import นี้ T2.4 +import { AuthModule } from './common/auth/auth.module.js'; import { JsonSchemaModule } from './modules/json-schema/json-schema.module.js'; import { WorkflowEngineModule } from './modules/workflow-engine/workflow-engine.module'; import { CorrespondenceModule } from './modules/correspondence/correspondence.module'; + @Module({ imports: [ // 1. Setup Config Module พร้อม Validation ConfigModule.forRoot({ - isGlobal: true, // เรียกใช้ได้ทั่วทั้ง App ไม่ต้อง import ซ้ำ - envFilePath: '.env', // อ่านไฟล์ .env (สำหรับ Dev) - validationSchema: envValidationSchema, // ใช้ Schema ที่เราสร้างเพื่อตรวจสอบ + isGlobal: true, + envFilePath: '.env', + load: [redisConfig], + validationSchema: envValidationSchema, validationOptions: { - // ถ้ามีค่าไหนไม่ผ่าน Validation ให้ Error และหยุดทำงานทันที abortEarly: true, }, }), - // 🛡️ T2.4 1. Setup Throttler Module (Rate Limiting) + + // 🛡️ Setup Throttler Module (Rate Limiting) ThrottlerModule.forRoot([ { - ttl: 60000, // 60 วินาที (Time to Live) - limit: 100, // ยิงได้สูงสุด 100 ครั้ง (Global Default) + ttl: 60000, // 60 วินาที + limit: 100, // ยิงได้สูงสุด 100 ครั้ง }, ]), + // 💾 Setup Cache Module (Redis) + CacheModule.registerAsync({ + isGlobal: true, + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + store: await redisStore({ + socket: { + host: configService.get('redis.host'), + port: configService.get('redis.port'), + }, + ttl: configService.get('redis.ttl'), + }), + }), + inject: [ConfigService], + }), + // 2. Setup TypeORM (MariaDB) TypeOrmModule.forRootAsync({ imports: [ConfigModule], @@ -49,15 +80,14 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo password: configService.get('DB_PASSWORD'), database: configService.get('DB_DATABASE'), autoLoadEntities: true, - // synchronize: true เฉพาะตอน Dev เท่านั้น ห้ามใช้บน Prod - // synchronize: configService.get('NODE_ENV') === 'development', - // แก้บรรทัดนี้เป็น false ครับ - // เพราะเราใช้ SQL Script สร้าง DB แล้ว ไม่ต้องการให้ TypeORM มาแก้ Structure อัตโนมัติ - synchronize: false, // เราใช้ false ตามที่ตกลงกัน + synchronize: false, // Production Ready: false }), }), - // 3. BullMQ (Redis) Setup [NEW] + // ✅ 4. Register AuditLog Entity (Global Scope) + TypeOrmModule.forFeature([AuditLog]), + + // 3. BullMQ (Redis) Setup BullModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], @@ -69,24 +99,46 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo }, }), }), + + // Feature Modules AuthModule, - // CommonModule, UserModule, ProjectModule, FileStorageModule, DocumentNumberingModule, JsonSchemaModule, WorkflowEngineModule, - CorrespondenceModule, // <--- เพิ่ม + CorrespondenceModule, ], controllers: [AppController], providers: [ AppService, - // 🛡️ 2. Register Global Guard + // 🛡️ 1. Register Global Guard (Rate Limit) - ทำงานก่อนเพื่อน { provide: APP_GUARD, useClass: ThrottlerGuard, }, + // 🚧 2. Maintenance Mode Guard - ทำงานต่อมา เพื่อ Block การเข้าถึงถ้าระบบปิดอยู่ + { + provide: APP_GUARD, + useClass: MaintenanceModeGuard, + }, + // 📝 3. Register Global Interceptor (Audit Log) + { + provide: APP_INTERCEPTOR, + useClass: AuditLogInterceptor, + }, + // 🔄 4. Register Idempotency (Uncomment เมื่อต้องการบังคับใช้ Global) + // { + // provide: APP_INTERCEPTOR, + // useClass: IdempotencyInterceptor, + // }, ], }) export class AppModule {} + +/*วิธีใช้งาน +เมื่อต้องการเปิด Maintenance Mode ให้ Admin (หรือคุณ) ยิงคำสั่งเข้า Redis หรือสร้าง API เพื่อ Set ค่า: SET system:maintenance_mode true (หรือ "true") + +ระบบจะตอบกลับด้วย 503 Service Unavailable ทันที ยกเว้น Controller ที่คุณใส่ @BypassMaintenance() ไว้ครับ +*/ diff --git a/backend/src/common/aut/aut.controller.spec.ts b/backend/src/common/aut/aut.controller.spec.ts deleted file mode 100644 index 2b3355e..0000000 --- a/backend/src/common/aut/aut.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AutController } from './aut.controller'; - -describe('AutController', () => { - let controller: AutController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AutController], - }).compile(); - - controller = module.get(AutController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/backend/src/common/aut/aut.controller.ts b/backend/src/common/aut/aut.controller.ts deleted file mode 100644 index 196d7c3..0000000 --- a/backend/src/common/aut/aut.controller.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Controller } from '@nestjs/common'; - -@Controller('aut') -export class AutController {} diff --git a/backend/src/common/auth/auth.controller.ts b/backend/src/common/auth/auth.controller.ts index bcbe8ba..6e1a343 100644 --- a/backend/src/common/auth/auth.controller.ts +++ b/backend/src/common/auth/auth.controller.ts @@ -1,17 +1,34 @@ -import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common'; -import { Throttle } from '@nestjs/throttler'; // <--- ✅ เพิ่มบรรทัดนี้ครับ -import { AuthService } from './auth.service.js'; -import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO -import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO +// File: src/common/auth/auth.controller.ts +// บันทึกการแก้ไข: เพิ่ม Endpoints ให้ครบตามแผน T1.2 (Refresh, Logout, Profile) +import { + Controller, + Post, + Body, + Get, + UseGuards, + UnauthorizedException, + Request, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { AuthService } from './auth.service.js'; +import { LoginDto } from './dto/login.dto.js'; +import { RegisterDto } from './dto/register.dto.js'; +import { JwtAuthGuard } from './guards/jwt-auth.guard.js'; +import { JwtRefreshGuard } from './guards/jwt-refresh.guard.js'; // ต้องสร้าง Guard นี้ +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; // (ถ้าใช้ Swagger) + +@ApiTags('Authentication') @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @Post('login') - // เพิ่มความเข้มงวดให้ Login (กัน Brute Force) - @Throttle({ default: { limit: 10, ttl: 60000 } }) // 🔒 ให้ลองได้แค่ 5 ครั้ง ใน 1 นาที - // เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto + @Throttle({ default: { limit: 5, ttl: 60000 } }) // เข้มงวด: 5 ครั้ง/นาที + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'เข้าสู่ระบบเพื่อรับ Access & Refresh Token' }) async login(@Body() loginDto: LoginDto) { const user = await this.authService.validateUser( loginDto.username, @@ -26,15 +43,38 @@ export class AuthController { } @Post('register-admin') - // เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto + @UseGuards(JwtAuthGuard) // ควรป้องกัน Route นี้ให้เฉพาะ Superadmin + @ApiBearerAuth() + @ApiOperation({ summary: 'สร้างบัญชีผู้ใช้ใหม่ (Admin Only)' }) async register(@Body() registerDto: RegisterDto) { return this.authService.register(registerDto); } - /*ตัวอย่าง: ยกเว้นการนับ (เช่น Health Check) -import { SkipThrottle } from '@nestjs/throttler'; -@SkipThrottle() -@Get('health') -check() { ... } -*/ + @UseGuards(JwtRefreshGuard) + @Post('refresh') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'ขอ Access Token ใหม่ด้วย Refresh Token' }) + async refresh(@Request() req) { + // req.user จะมาจาก JwtRefreshStrategy + return this.authService.refreshToken(req.user.sub, req.user.refreshToken); + } + + @UseGuards(JwtAuthGuard) + @Post('logout') + @HttpCode(HttpStatus.OK) + @ApiBearerAuth() + @ApiOperation({ summary: 'ออกจากระบบ (Revoke Token)' }) + async logout(@Request() req) { + // ดึง Token จาก Header Authorization: Bearer + const token = req.headers.authorization?.split(' ')[1]; + return this.authService.logout(req.user.sub, token); + } + + @UseGuards(JwtAuthGuard) + @Get('profile') + @ApiBearerAuth() + @ApiOperation({ summary: 'ดึงข้อมูลผู้ใช้ปัจจุบัน' }) + getProfile(@Request() req) { + return req.user; + } } diff --git a/backend/src/common/auth/auth.module.ts b/backend/src/common/auth/auth.module.ts index 23f69d0..859e6bf 100644 --- a/backend/src/common/auth/auth.module.ts +++ b/backend/src/common/auth/auth.module.ts @@ -1,3 +1,6 @@ +// File: src/common/auth/auth.module.ts +// บันทึกการแก้ไข: ลงทะเบียน Refresh Strategy และแก้ไข Config + import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; @@ -5,7 +8,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service.js'; import { AuthController } from './auth.controller.js'; import { UserModule } from '../../modules/user/user.module.js'; -import { JwtStrategy } from '../guards/jwt.strategy.js'; +import { JwtStrategy } from './strategies/jwt.strategy.js'; +import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy.js'; @Module({ imports: [ @@ -17,14 +21,17 @@ import { JwtStrategy } from '../guards/jwt.strategy.js'; useFactory: async (configService: ConfigService) => ({ secret: configService.get('JWT_SECRET'), signOptions: { - // Cast เป็น any เพื่อแก้ปัญหา Type ไม่ตรงกับ Library - expiresIn: (configService.get('JWT_EXPIRATION') || - '8h') as any, + // ใช้ Template String หรือค่า Default ที่ปลอดภัย + expiresIn: configService.get('JWT_EXPIRATION') || '15m', }, }), }), ], - providers: [AuthService, JwtStrategy], + providers: [ + AuthService, + JwtStrategy, + JwtRefreshStrategy, // ✅ เพิ่ม Strategy สำหรับ Refresh Token + ], controllers: [AuthController], exports: [AuthService], }) diff --git a/backend/src/common/auth/auth.service.ts b/backend/src/common/auth/auth.service.ts index 7950287..2029b82 100644 --- a/backend/src/common/auth/auth.service.ts +++ b/backend/src/common/auth/auth.service.ts @@ -1,16 +1,31 @@ -import { Injectable } from '@nestjs/common'; +// File: src/common/auth/auth.service.ts +// บันทึกการแก้ไข: เพิ่ม Refresh Token, Logout (Redis Blacklist) และ Profile ตาม T1.2 + +import { + Injectable, + UnauthorizedException, + Inject, + BadRequestException, +} from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; import * as bcrypt from 'bcrypt'; import { UserService } from '../../modules/user/user.service.js'; -import { RegisterDto } from './dto/register.dto.js'; // Import DTO +import { RegisterDto } from './dto/register.dto.js'; +import { User } from '../../modules/user/entities/user.entity.js'; @Injectable() export class AuthService { constructor( private userService: UserService, private jwtService: JwtService, + private configService: ConfigService, + @Inject(CACHE_MANAGER) private cacheManager: Cache, // ใช้ Redis สำหรับ Blacklist ) {} + // 1. ตรวจสอบ Username/Password async validateUser(username: string, pass: string): Promise { const user = await this.userService.findOneByUsername(username); if (user && (await bcrypt.compare(pass, user.password))) { @@ -21,21 +36,91 @@ export class AuthService { return null; } + // 2. Login: สร้าง Access & Refresh Token async login(user: any) { - const payload = { username: user.username, sub: user.user_id }; + const payload = { + username: user.username, + sub: user.user_id, + scope: 'Global', // ตัวอย่าง: ใส่ Scope เริ่มต้น หรือดึงจาก Role + }; + + const [accessToken, refreshToken] = await Promise.all([ + this.jwtService.signAsync(payload, { + secret: this.configService.get('JWT_SECRET'), + expiresIn: this.configService.get('JWT_EXPIRATION') || '15m', + }), + this.jwtService.signAsync(payload, { + secret: this.configService.get('JWT_REFRESH_SECRET'), + expiresIn: + this.configService.get('JWT_REFRESH_EXPIRATION') || '7d', + }), + ]); + return { - access_token: this.jwtService.sign(payload), + access_token: accessToken, + refresh_token: refreshToken, + user: user, // ส่งข้อมูล user กลับไปให้ Frontend ใช้แสดงผลเบื้องต้น }; } + // 3. Register (สำหรับ Admin) async register(userDto: RegisterDto) { + // ตรวจสอบว่ามี user อยู่แล้วหรือไม่ + const existingUser = await this.userService.findOneByUsername( + userDto.username, + ); + if (existingUser) { + throw new BadRequestException('Username already exists'); + } + const salt = await bcrypt.genSalt(); const hashedPassword = await bcrypt.hash(userDto.password, salt); - // ใช้ค่าจาก DTO ที่ Validate มาแล้ว return this.userService.create({ ...userDto, password: hashedPassword, }); } + + // 4. Refresh Token: ออก Token ใหม่ + async refreshToken(userId: number, refreshToken: string) { + // ตรวจสอบความถูกต้องของ Refresh Token (ถ้าใช้ DB เก็บ Refresh Token ก็เช็คตรงนี้) + // ในที่นี้เราเชื่อใจ Signature ของ JWT Refresh Secret + const user = await this.userService.findOne(userId); + if (!user) throw new UnauthorizedException('User not found'); + + // สร้าง Access Token ใหม่ + const payload = { username: user.username, sub: user.user_id }; + const accessToken = await this.jwtService.signAsync(payload, { + secret: this.configService.get('JWT_SECRET'), + expiresIn: this.configService.get('JWT_EXPIRATION') || '15m', + }); + + return { + access_token: accessToken, + // refresh_token: refreshToken, // จะส่งเดิมกลับ หรือ Rotate ใหม่ก็ได้ (แนะนำ Rotate เพื่อความปลอดภัยสูงสุด) + }; + } + + // 5. Logout: นำ Token เข้า Blacklist ใน Redis + async logout(userId: number, accessToken: string) { + // หาเวลาที่เหลือของ Token เพื่อตั้ง TTL ใน Redis + try { + const decoded = this.jwtService.decode(accessToken); + if (decoded && decoded.exp) { + const ttl = decoded.exp - Math.floor(Date.now() / 1000); + if (ttl > 0) { + // Key pattern: blacklist:token:{token_string} + await this.cacheManager.set( + `blacklist:token:${accessToken}`, + true, + ttl * 1000, + ); + } + } + } catch (error) { + // Ignore decoding error + } + return { message: 'Logged out successfully' }; + } } diff --git a/backend/src/common/auth/strategies/jwt-refresh.strategy.ts b/backend/src/common/auth/strategies/jwt-refresh.strategy.ts new file mode 100644 index 0000000..7e35eb8 --- /dev/null +++ b/backend/src/common/auth/strategies/jwt-refresh.strategy.ts @@ -0,0 +1,32 @@ +// File: src/common/auth/strategies/jwt-refresh.strategy.ts +// บันทึกการแก้ไข: Strategy สำหรับ Refresh Token (T1.2) + +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; + +@Injectable() +export class JwtRefreshStrategy extends PassportStrategy( + Strategy, + 'jwt-refresh', +) { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + // ใช้ Secret แยกต่างหากสำหรับ Refresh Token + secretOrKey: configService.get('JWT_REFRESH_SECRET'), + passReqToCallback: true, + }); + } + + async validate(req: Request, payload: any) { + const refreshToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req); + return { + ...payload, + refreshToken, + }; + } +} diff --git a/backend/src/common/auth/strategies/jwt.strategy.ts b/backend/src/common/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..0f94b13 --- /dev/null +++ b/backend/src/common/auth/strategies/jwt.strategy.ts @@ -0,0 +1,63 @@ +// File: src/common/auth/strategies/jwt.strategy.ts +// บันทึกการแก้ไข: ปรับปรุง JwtStrategy ให้ตรวจสอบ Blacklist (Redis) และสถานะ User (T1.2) + +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException, Inject } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; // ✅ ใช้สำหรับ Blacklist +import { Cache } from 'cache-manager'; +import { Request } from 'express'; +import { UserService } from '../../../modules/user/user.service.js'; + +// Interface สำหรับ Payload ใน Token +export interface JwtPayload { + sub: number; + username: string; + scope?: string; // เพิ่ม Scope ถ้ามีการใช้ +} + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + configService: ConfigService, + private userService: UserService, + @Inject(CACHE_MANAGER) private cacheManager: Cache, // ✅ Inject Redis Cache + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET'), + passReqToCallback: true, // ✅ จำเป็นต้องใช้ เพื่อดึง Raw Token มาเช็ค Blacklist + }); + } + + async validate(req: Request, payload: JwtPayload) { + // 1. ดึง Token ออกมาเพื่อตรวจสอบใน Blacklist + const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req); + + // 2. ตรวจสอบว่า Token นี้อยู่ใน Redis Blacklist หรือไม่ (กรณี Logout ไปแล้ว) + const isBlacklisted = await this.cacheManager.get( + `blacklist:token:${token}`, + ); + if (isBlacklisted) { + throw new UnauthorizedException('Token has been revoked (Logged out)'); + } + + // 3. ค้นหา User จาก Database + const user = await this.userService.findOne(payload.sub); + + // 4. ตรวจสอบความถูกต้องของ User + if (!user) { + throw new UnauthorizedException('User not found'); + } + + // 5. (Optional) ตรวจสอบว่า User ยัง Active อยู่หรือไม่ + if (user.is_active === false || user.is_active === 0) { + throw new UnauthorizedException('User account is inactive'); + } + + // คืนค่า User เพื่อนำไปใส่ใน req.user + return user; + } +} diff --git a/backend/src/common/common.module.ts b/backend/src/common/common.module.ts index dd36dc6..1f29dd7 100644 --- a/backend/src/common/common.module.ts +++ b/backend/src/common/common.module.ts @@ -1,9 +1,31 @@ -import { Module } from '@nestjs/common'; -import { AuthModule } from './auth/auth.module'; -import { AutController } from './aut/aut.controller'; +// File: src/common/common.module.ts +// บันทึกการแก้ไข: Module รวม Infrastructure พื้นฐาน (T1.1) +import { Module, Global } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { CryptoService } from './services/crypto.service'; +import { RequestContextService } from './services/request-context.service'; +import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; +import { HttpExceptionFilter } from './exceptions/http-exception.filter'; +import { TransformInterceptor } from './interceptors/transform.interceptor'; +// import { IdempotencyInterceptor } from './interceptors/idempotency.interceptor'; // นำเข้าถ้าต้องการใช้ Global + +@Global() // ทำให้ Module นี้ใช้ได้ทั่วทั้งแอปโดยไม่ต้อง Import ซ้ำ @Module({ - imports: [AuthModule], - controllers: [AutController] + imports: [ConfigModule], + providers: [ + CryptoService, + RequestContextService, + // Register Global Filter & Interceptor ที่นี่ หรือใน AppModule ก็ได้ + { + provide: APP_FILTER, + useClass: HttpExceptionFilter, + }, + { + provide: APP_INTERCEPTOR, + useClass: TransformInterceptor, + }, + ], + exports: [CryptoService, RequestContextService], }) export class CommonModule {} diff --git a/backend/src/common/config/redis.config.ts b/backend/src/common/config/redis.config.ts new file mode 100644 index 0000000..556abe4 --- /dev/null +++ b/backend/src/common/config/redis.config.ts @@ -0,0 +1,11 @@ +// File: src/common/config/redis.config.ts +// บันทึกการแก้ไข: สร้าง Config สำหรับ Redis (T0.2) + +import { registerAs } from '@nestjs/config'; + +export default registerAs('redis', () => ({ + host: process.env.REDIS_HOST || 'cache', // Default เป็นชื่อ Service ใน Docker + port: parseInt(process.env.REDIS_PORT, 10) || 6379, + ttl: parseInt(process.env.REDIS_TTL, 10) || 3600, // Default TTL 1 ชั่วโมง + // password: process.env.REDIS_PASSWORD, // เปิดใช้ถ้ามี Password +})); diff --git a/backend/src/common/decorators/audit.decorator.ts b/backend/src/common/decorators/audit.decorator.ts new file mode 100644 index 0000000..6a9e5ff --- /dev/null +++ b/backend/src/common/decorators/audit.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; + +export const AUDIT_KEY = 'audit'; + +export interface AuditMetadata { + action: string; // ชื่อการกระทำ (เช่น 'rfa.create', 'user.login') + entityType?: string; // ชื่อ Entity (เช่น 'rfa', 'user') - ถ้าไม่ระบุอาจจะพยายามเดา +} + +export const Audit = (action: string, entityType?: string) => + SetMetadata(AUDIT_KEY, { action, entityType }); diff --git a/backend/src/common/decorators/bypass-maintenance.decorator.ts b/backend/src/common/decorators/bypass-maintenance.decorator.ts new file mode 100644 index 0000000..45beba1 --- /dev/null +++ b/backend/src/common/decorators/bypass-maintenance.decorator.ts @@ -0,0 +1,10 @@ +// File: src/common/decorators/bypass-maintenance.decorator.ts +// บันทึกการแก้ไข: ใช้สำหรับยกเว้นการตรวจสอบ Maintenance Mode (T1.1) + +import { SetMetadata } from '@nestjs/common'; + +export const BYPASS_MAINTENANCE_KEY = 'bypass_maintenance'; + +// ใช้ @BypassMaintenance() บน Controller หรือ Method ที่ต้องการให้ทำงานได้แม้ปิดระบบ +export const BypassMaintenance = () => + SetMetadata(BYPASS_MAINTENANCE_KEY, true); diff --git a/backend/src/common/decorators/idempotency.decorator.ts b/backend/src/common/decorators/idempotency.decorator.ts new file mode 100644 index 0000000..567ee39 --- /dev/null +++ b/backend/src/common/decorators/idempotency.decorator.ts @@ -0,0 +1,7 @@ +// File: src/common/decorators/idempotency.decorator.ts +// ใช้สำหรับบังคับว่า Controller นี้ต้องมี Idempotency Key (Optional Enhancement) + +import { SetMetadata } from '@nestjs/common'; + +export const IDEMPOTENCY_KEY = 'idempotency_required'; +export const RequireIdempotency = () => SetMetadata(IDEMPOTENCY_KEY, true); diff --git a/backend/src/common/entities/audit-log.entity.ts b/backend/src/common/entities/audit-log.entity.ts new file mode 100644 index 0000000..bc15f27 --- /dev/null +++ b/backend/src/common/entities/audit-log.entity.ts @@ -0,0 +1,56 @@ +// File: src/common/entities/audit-log.entity.ts +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from '../../modules/user/entities/user.entity'; + +@Entity('audit_logs') +export class AuditLog { + @PrimaryGeneratedColumn({ name: 'audit_id', type: 'bigint' }) + auditId!: string; + + @Column({ name: 'request_id', nullable: true }) + requestId?: string; + + // ✅ ต้องมีบรรทัดนี้ (TypeORM ต้องการเพื่อ Map Column) + @Column({ name: 'user_id', nullable: true }) + userId?: number | null; // ✅ เพิ่ม | null เพื่อรองรับค่า null + + @Column({ length: 100 }) + action!: string; + + @Column({ + type: 'enum', + enum: ['INFO', 'WARN', 'ERROR', 'CRITICAL'], + default: 'INFO', + }) + severity!: string; + + @Column({ name: 'entity_type', length: 50, nullable: true }) + entityType?: string; + + @Column({ name: 'entity_id', length: 50, nullable: true }) + entityId?: string; + + @Column({ name: 'details_json', type: 'json', nullable: true }) + detailsJson?: any; + + @Column({ name: 'ip_address', length: 45, nullable: true }) + ipAddress?: string; + + @Column({ name: 'user_agent', length: 255, nullable: true }) + userAgent?: string; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + // Relations + @ManyToOne(() => User) + @JoinColumn({ name: 'user_id' }) + user?: User; +} diff --git a/backend/src/common/exceptions/http-exception.filter.ts b/backend/src/common/exceptions/http-exception.filter.ts index 9a9eba9..2fafea7 100644 --- a/backend/src/common/exceptions/http-exception.filter.ts +++ b/backend/src/common/exceptions/http-exception.filter.ts @@ -1,3 +1,6 @@ +// File: src/common/exceptions/http-exception.filter.ts +// บันทึกการแก้ไข: ปรับปรุง Global Filter ให้จัดการ Error ปลอดภัยสำหรับ Production และ Log ละเอียดใน Dev (T1.1) + import { ExceptionFilter, Catch, @@ -17,34 +20,65 @@ export class HttpExceptionFilter implements ExceptionFilter { const response = ctx.getResponse(); const request = ctx.getRequest(); + // 1. หา Status Code const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + // 2. หา Error Response Body ต้นฉบับ const exceptionResponse = exception instanceof HttpException ? exception.getResponse() - : 'Internal server error'; + : { message: 'Internal server error' }; - // จัดรูปแบบ Error Message - const message = + // จัดรูปแบบ Error Message ให้เป็น Object เสมอ + let errorBody: any = typeof exceptionResponse === 'string' - ? exceptionResponse - : (exceptionResponse as any).message || exceptionResponse; - // 👇👇 เพิ่มบรรทัดนี้ครับ (สำคัญมาก!) 👇👇 - console.error('💥 REAL ERROR:', exception); + ? { message: exceptionResponse } + : exceptionResponse; - // Log Error (สำคัญมากสำหรับการ Debug แต่ไม่ส่งให้ Client เห็นทั้งหมด) - this.logger.error( - `Http Status: ${status} Error Message: ${JSON.stringify(message)}`, - ); + // 3. 📝 Logging Strategy (แยกตามความรุนแรง) + if (status >= 500) { + // 💥 Critical Error: Log stack trace เต็มๆ + this.logger.error( + `💥 HTTP ${status} Error on ${request.method} ${request.url}`, + exception instanceof Error + ? exception.stack + : JSON.stringify(exception), + ); - response.status(status).json({ + // 👇👇 สิ่งที่คุณต้องการ: Log ดิบๆ ให้เห็นชัดใน Docker Console 👇👇 + console.error('💥 REAL CRITICAL ERROR:', exception); + } else { + // ⚠️ Client Error (400, 401, 403, 404): Log แค่ Warning พอ ไม่ต้อง Stack Trace + this.logger.warn( + `⚠️ HTTP ${status} Error on ${request.method} ${request.url}: ${JSON.stringify(errorBody.message || errorBody)}`, + ); + } + + // 4. 🔒 Security & Response Formatting + // กรณี Production และเป็น Error 500 -> ต้องซ่อนรายละเอียดความผิดพลาดของ Server + if (status === 500 && process.env.NODE_ENV === 'production') { + errorBody = { + message: 'Internal server error', + // อาจเพิ่ม reference code เพื่อให้ user แจ้ง support ได้ เช่น code: 'ERR-500' + }; + } + + // 5. Construct Final Response + const responseBody = { statusCode: status, timestamp: new Date().toISOString(), path: request.url, - message: status === 500 ? 'Internal server error' : message, // ซ่อน Detail กรณี 500 - }); + ...errorBody, // Spread message, error, validation details + }; + + // 🛠️ Development Mode: แถม Stack Trace ไปให้ Frontend Debug ง่ายขึ้น + if (process.env.NODE_ENV !== 'production' && exception instanceof Error) { + responseBody.stack = exception.stack; + } + + response.status(status).json(responseBody); } } diff --git a/backend/src/common/guards/jwt-refresh.guard.ts b/backend/src/common/guards/jwt-refresh.guard.ts new file mode 100644 index 0000000..ed74420 --- /dev/null +++ b/backend/src/common/guards/jwt-refresh.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {} diff --git a/backend/src/common/guards/jwt.strategy.ts b/backend/src/common/guards/jwt.strategy.ts deleted file mode 100644 index 013ba93..0000000 --- a/backend/src/common/guards/jwt.strategy.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { PassportStrategy } from '@nestjs/passport'; -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -// Interface สำหรับ Payload ใน Token -interface JwtPayload { - sub: number; - username: string; -} - -import { UserService } from '../../modules/user/user.service.js'; - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor( - configService: ConfigService, - private userService: UserService, - ) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - secretOrKey: configService.get('JWT_SECRET')!, - }); - } - - async validate(payload: JwtPayload) { - const user = await this.userService.findOne(payload.sub); - if (!user) { - throw new Error('User not found'); - } - return user; - } -} diff --git a/backend/src/common/guards/maintenance-mode.guard.ts b/backend/src/common/guards/maintenance-mode.guard.ts new file mode 100644 index 0000000..f954643 --- /dev/null +++ b/backend/src/common/guards/maintenance-mode.guard.ts @@ -0,0 +1,71 @@ +// File: src/common/guards/maintenance-mode.guard.ts +// บันทึกการแก้ไข: ตรวจสอบ Flag ใน Redis เพื่อ Block API ระหว่างปรับปรุงระบบ (T1.1) + +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, + ServiceUnavailableException, + Logger, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; +import { BYPASS_MAINTENANCE_KEY } from '../decorators/bypass-maintenance.decorator'; + +@Injectable() +export class MaintenanceModeGuard implements CanActivate { + private readonly logger = new Logger(MaintenanceModeGuard.name); + // Key ที่ใช้เก็บสถานะใน Redis (Admin จะเป็นคน Toggle ค่านี้) + private readonly MAINTENANCE_KEY = 'system:maintenance_mode'; + + constructor( + private reflector: Reflector, + @Inject(CACHE_MANAGER) private cacheManager: Cache, + ) {} + + async canActivate(context: ExecutionContext): Promise { + // 1. ตรวจสอบว่า Route นี้ได้รับการยกเว้นหรือไม่ (Bypass) + const isBypassed = this.reflector.getAllAndOverride( + BYPASS_MAINTENANCE_KEY, + [context.getHandler(), context.getClass()], + ); + + if (isBypassed) { + return true; + } + + // 2. ตรวจสอบสถานะจาก Redis + try { + const isMaintenanceOn = await this.cacheManager.get(this.MAINTENANCE_KEY); + + // ถ้า Redis มีค่าเป็น true หรือ string "true" ให้ Block + if (isMaintenanceOn === true || isMaintenanceOn === 'true') { + // (Optional) 3. ตรวจสอบ Backdoor Header สำหรับ Admin (ถ้าต้องการ Bypass ฉุกเฉิน) + const request = context.switchToHttp().getRequest(); + // const bypassToken = request.headers['x-maintenance-bypass']; + // if (bypassToken === process.env.ADMIN_SECRET) return true; + + this.logger.warn( + `Blocked request to ${request.url} due to Maintenance Mode`, + ); + + throw new ServiceUnavailableException({ + statusCode: 503, + message: 'ระบบกำลังปิดปรับปรุงชั่วคราว กรุณาลองใหม่ในภายหลัง', + error: 'Service Unavailable', + }); + } + } catch (error) { + // กรณี Redis ล่ม หรือ Error อื่นๆ ให้ยอมให้ผ่านไปก่อน (Fail Open) หรือ Block (Fail Closed) ตามนโยบาย + // ในที่นี้เลือก Fail Open เพื่อไม่ให้ระบบล่มตาม Redis + if (error instanceof ServiceUnavailableException) { + throw error; + } + this.logger.error('Error checking maintenance mode', error); + } + + return true; + } +} diff --git a/backend/src/common/interceptors/audit-log.interceptor.ts b/backend/src/common/interceptors/audit-log.interceptor.ts new file mode 100644 index 0000000..94adace --- /dev/null +++ b/backend/src/common/interceptors/audit-log.interceptor.ts @@ -0,0 +1,80 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, + Logger, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Request } from 'express'; + +import { AuditLog } from '../entities/audit-log.entity'; +import { AUDIT_KEY, AuditMetadata } from '../decorators/audit.decorator'; +import { User } from '../../modules/user/entities/user.entity'; + +@Injectable() +export class AuditLogInterceptor implements NestInterceptor { + private readonly logger = new Logger(AuditLogInterceptor.name); + + constructor( + private reflector: Reflector, + @InjectRepository(AuditLog) + private auditLogRepo: Repository, + ) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const auditMetadata = this.reflector.getAllAndOverride( + AUDIT_KEY, + [context.getHandler(), context.getClass()], + ); + + if (!auditMetadata) { + return next.handle(); + } + + const request = context.switchToHttp().getRequest(); + const user = (request as any).user as User; + const rawIp = request.ip || request.socket.remoteAddress; + const ip = Array.isArray(rawIp) ? rawIp[0] : rawIp; + const userAgent = request.get('user-agent'); + + return next.handle().pipe( + tap(async (data) => { + try { + let entityId = null; + + if (data && typeof data === 'object') { + if ('id' in data) entityId = String(data.id); + else if ('audit_id' in data) entityId = String(data.audit_id); + else if ('user_id' in data) entityId = String(data.user_id); + } + + if (!entityId && request.params.id) { + entityId = String(request.params.id); + } + + // ✅ FIX: ใช้ user?.user_id || null + const auditLog = this.auditLogRepo.create({ + userId: user ? user.user_id : null, + action: auditMetadata.action, + entityType: auditMetadata.entityType, + entityId: entityId, + ipAddress: ip, + userAgent: userAgent, + severity: 'INFO', + } as unknown as AuditLog); // ✨ Trick: Cast ผ่าน unknown เพื่อล้าง Error ถ้า TS ยังไม่อัปเดต + + await this.auditLogRepo.save(auditLog); + } catch (error) { + this.logger.error( + `Failed to create audit log for ${auditMetadata.action}: ${(error as Error).message}`, + ); + } + }), + ); + } +} diff --git a/backend/src/common/interceptors/idempotency.interceptor.ts b/backend/src/common/interceptors/idempotency.interceptor.ts new file mode 100644 index 0000000..26be6d9 --- /dev/null +++ b/backend/src/common/interceptors/idempotency.interceptor.ts @@ -0,0 +1,74 @@ +// File: src/common/interceptors/idempotency.interceptor.ts +// บันทึกการแก้ไข: สร้าง IdempotencyInterceptor เพื่อป้องกันการทำรายการซ้ำ (T1.1) + +import { + CallHandler, + ExecutionContext, + Inject, + Injectable, + NestInterceptor, + ConflictException, + Logger, +} from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; +import { Observable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { Request } from 'express'; + +@Injectable() +export class IdempotencyInterceptor implements NestInterceptor { + private readonly logger = new Logger(IdempotencyInterceptor.name); + + constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const request = context.switchToHttp().getRequest(); + const method = request.method; + + // 1. ตรวจสอบว่าควรใช้ Idempotency หรือไม่ (เฉพาะ POST, PUT, DELETE) + if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) { + return next.handle(); + } + + // 2. ดึง Idempotency-Key จาก Header + const idempotencyKey = request.headers['idempotency-key'] as string; + + // ถ้าไม่มี Key ส่งมา ให้ทำงานปกติ (หรือจะบังคับให้ Error ก็ได้ ตาม Policy) + if (!idempotencyKey) { + // หมายเหตุ: ในระบบที่ Strict อาจจะ throw BadRequestException ถ้าไม่มี Key สำหรับ Transaction สำคัญ + return next.handle(); + } + + const cacheKey = `idempotency:${idempotencyKey}`; + + // 3. ตรวจสอบใน Redis ว่า Key นี้เคยถูกประมวลผลหรือยัง + const cachedResponse = await this.cacheManager.get(cacheKey); + + if (cachedResponse) { + this.logger.warn( + `Idempotency key detected: ${idempotencyKey}. Returning cached response.`, + ); + // ถ้ามี ให้คืนค่าเดิมกลับไปเลย (เสมือนว่าทำรายการสำเร็จแล้ว) + return of(cachedResponse); + } + + // 4. ถ้ายังไม่มี ให้ประมวลผลต่อ และบันทึกผลลัพธ์ลง Redis + return next.handle().pipe( + tap(async (response) => { + try { + // บันทึก Response ลง Cache (TTL 24 ชั่วโมง หรือตามความเหมาะสม) + await this.cacheManager.set(cacheKey, response, 86400 * 1000); + } catch (err) { + this.logger.error( + `Failed to cache idempotency key ${idempotencyKey}`, + err.stack, + ); + } + }), + ); + } +} diff --git a/backend/src/common/services/crypto.service.ts b/backend/src/common/services/crypto.service.ts new file mode 100644 index 0000000..fd84781 --- /dev/null +++ b/backend/src/common/services/crypto.service.ts @@ -0,0 +1,40 @@ +// File: src/common/services/crypto.service.ts +// บันทึกการแก้ไข: Encryption/Decryption Utility (T1.1) + +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as crypto from 'crypto'; + +@Injectable() +export class CryptoService { + private readonly algorithm = 'aes-256-cbc'; + private readonly key: Buffer; + private readonly ivLength = 16; + + constructor(private configService: ConfigService) { + // Key ต้องมีขนาด 32 bytes (256 bits) + const secret = + this.configService.get('APP_SECRET_KEY') || + 'default-secret-key-32-chars-long!'; + this.key = crypto.scryptSync(secret, 'salt', 32); + } + + encrypt(text: string): string { + const iv = crypto.randomBytes(this.ivLength); + const cipher = crypto.createCipheriv(this.algorithm, this.key, iv); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return `${iv.toString('hex')}:${encrypted}`; + } + + decrypt(text: string): string { + const [ivHex, encryptedHex] = text.split(':'); + if (!ivHex || !encryptedHex) return text; + + const iv = Buffer.from(ivHex, 'hex'); + const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv); + let decrypted = decipher.update(encryptedHex, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } +} diff --git a/backend/src/common/services/request-context.service.ts b/backend/src/common/services/request-context.service.ts new file mode 100644 index 0000000..e4d8b69 --- /dev/null +++ b/backend/src/common/services/request-context.service.ts @@ -0,0 +1,35 @@ +// File: src/common/services/request-context.service.ts +// บันทึกการแก้ไข: เก็บ Context ระหว่าง Request (User, TraceID) (T1.1) + +import { Injectable, Scope } from '@nestjs/common'; +import { AsyncLocalStorage } from 'async_hooks'; + +@Injectable({ scope: Scope.DEFAULT }) +export class RequestContextService { + private static readonly cls = new AsyncLocalStorage>(); + + static run(fn: () => void) { + this.cls.run(new Map(), fn); + } + + static set(key: string, value: any) { + const store = this.cls.getStore(); + if (store) { + store.set(key, value); + } + } + + static get(key: string): T | undefined { + const store = this.cls.getStore(); + return store?.get(key); + } + + // Helper methods + static get currentUserId(): number | null { + return this.get('user_id') || null; + } + + static get requestId(): string | null { + return this.get('request_id') || null; + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts index f746b05..254e36d 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,66 +1,81 @@ +// File: src/main.ts +// บันทึกการแก้ไข: ปรับปรุง main.ts ให้สมบูรณ์ เชื่อมต่อกับ Global Filters/Interceptors และ ConfigService (T1.1) + import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; // ✅ เพิ่ม Import Swagger -import { json, urlencoded } from 'express'; // ✅ เพิ่ม Import Body Parser +import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { json, urlencoded } from 'express'; import helmet from 'helmet'; -// Import ของเดิมของคุณ -import { TransformInterceptor } from './common/interceptors/transform.interceptor.js'; -import { HttpExceptionFilter } from './common/exceptions/http-exception.filter.js'; +// Import Custom Interceptors & Filters ที่สร้างใน T1.1 +import { TransformInterceptor } from './common/interceptors/transform.interceptor'; +import { HttpExceptionFilter } from './common/exceptions/http-exception.filter'; async function bootstrap() { + // 1. Create App const app = await NestFactory.create(AppModule); + + // ดึง ConfigService เพื่อใช้ดึงค่า Environment Variables + const configService = app.get(ConfigService); const logger = new Logger('Bootstrap'); - // 🛡️ 1. Security (Helmet & CORS) + // 🛡️ 2. Security (Helmet & CORS) app.use(helmet()); + + // ตั้งค่า CORS (ใน Production ควรระบุ origin ให้ชัดเจนจาก Config) app.enableCors({ - origin: true, // หรือระบุเช่น ['https://lcbp3.np-dms.work'] + origin: true, // หรือ configService.get('CORS_ORIGIN') methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, }); - // 📁 2. Body Parser Limits (รองรับ File Upload 50MB) + // 📁 3. Body Parser Limits (รองรับ File Upload 50MB ตาม Requirements) app.use(json({ limit: '50mb' })); app.use(urlencoded({ extended: true, limit: '50mb' })); - // 🌐 3. Global Prefix (เช่น /api/v1) + // 🌐 4. Global Prefix app.setGlobalPrefix('api'); - // ⚙️ 4. Global Pipes & Interceptors (ของเดิม) + // ⚙️ 5. Global Pipes & Interceptors & Filters app.useGlobalPipes( new ValidationPipe({ - whitelist: true, // ตัด field ส่วนเกินทิ้ง + whitelist: true, // ตัด field ส่วนเกินทิ้ง (Security) transform: true, // แปลง Type อัตโนมัติ (เช่น string -> number) forbidNonWhitelisted: true, // แจ้ง Error ถ้าส่ง field แปลกปลอมมา + transformOptions: { + enableImplicitConversion: true, // ช่วยแปลง Type ใน Query Params + }, }), ); + + // ลงทะเบียน Global Interceptor และ Filter ที่เราสร้างไว้ app.useGlobalInterceptors(new TransformInterceptor()); app.useGlobalFilters(new HttpExceptionFilter()); - // 📘 5. Swagger Configuration (ส่วนที่ขาดไป) - const config = new DocumentBuilder() + // 📘 6. Swagger Configuration + const swaggerConfig = new DocumentBuilder() .setTitle('LCBP3 DMS API') .setDescription('Document Management System API Documentation') .setVersion('1.4.3') .addBearerAuth() // เพิ่มปุ่มใส่ Token (รูปกุญแจ) .build(); - const document = SwaggerModule.createDocument(app, config); + const document = SwaggerModule.createDocument(app, swaggerConfig); - // ตั้งค่าให้เข้าถึงได้ที่ /docs + // ตั้งค่าให้เข้าถึง Swagger ได้ที่ /docs SwaggerModule.setup('docs', app, document, { swaggerOptions: { - persistAuthorization: true, // จำ Token ไว้ไม่ต้องใส่ใหม่เวลารีเฟรช + persistAuthorization: true, // จำ Token ไว้ไม่ต้องใส่ใหม่เวลารีเฟรชหน้าจอ }, }); - // 🚀 6. Start Server - const port = process.env.PORT || 3000; + // 🚀 7. Start Server + const port = configService.get('PORT') || 3000; await app.listen(port); - logger.log(`Application is running on: http://localhost:${port}/api`); - logger.log(`Swagger UI is available at: http://localhost:${port}/docs`); + logger.log(`Application is running on: ${await app.getUrl()}/api`); + logger.log(`Swagger UI is available at: ${await app.getUrl()}/docs`); } bootstrap(); diff --git a/backend/src/modules/circulation/circulation.controller.ts b/backend/src/modules/circulation/circulation.controller.ts new file mode 100644 index 0000000..85839c2 --- /dev/null +++ b/backend/src/modules/circulation/circulation.controller.ts @@ -0,0 +1,65 @@ +import { + Controller, + Get, + Post, + Body, + Param, + Query, + ParseIntPipe, + UseGuards, + Patch, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; + +import { CirculationService } from './circulation.service'; +import { CreateCirculationDto } from './dto/create-circulation.dto'; +import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto'; +import { SearchCirculationDto } from './dto/search-circulation.dto'; +import { User } from '../user/entities/user.entity'; + +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RbacGuard } from '../../common/guards/rbac.guard'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { Audit } from '../../common/decorators/audit.decorator'; // Import + +@ApiTags('Circulations') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RbacGuard) +@Controller('circulations') +export class CirculationController { + constructor(private readonly circulationService: CirculationService) {} + + @Post() + @ApiOperation({ summary: 'Create internal circulation' }) + @RequirePermission('circulation.create') // สิทธิ์ ID 41 + @Audit('circulation.create', 'circulation') // ✅ แปะตรงนี้ + create(@Body() createDto: CreateCirculationDto, @CurrentUser() user: User) { + return this.circulationService.create(createDto, user); + } + + @Get() + @ApiOperation({ summary: 'List circulations in my organization' }) + @RequirePermission('document.view') + findAll(@Query() searchDto: SearchCirculationDto, @CurrentUser() user: User) { + return this.circulationService.findAll(searchDto, user); + } + + @Get(':id') + @ApiOperation({ summary: 'Get circulation details' }) + @RequirePermission('document.view') + findOne(@Param('id', ParseIntPipe) id: number) { + return this.circulationService.findOne(id); + } + + @Patch('routings/:id') + @ApiOperation({ summary: 'Update my routing task (Complete/Reject)' }) + @RequirePermission('circulation.respond') // สิทธิ์ ID 42 + updateRouting( + @Param('id', ParseIntPipe) id: number, + @Body() updateDto: UpdateCirculationRoutingDto, + @CurrentUser() user: User, + ) { + return this.circulationService.updateRoutingStatus(id, updateDto, user); + } +} diff --git a/backend/src/modules/circulation/circulation.module.ts b/backend/src/modules/circulation/circulation.module.ts new file mode 100644 index 0000000..9f9a224 --- /dev/null +++ b/backend/src/modules/circulation/circulation.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { Circulation } from './entities/circulation.entity'; +import { CirculationRouting } from './entities/circulation-routing.entity'; +import { CirculationStatusCode } from './entities/circulation-status-code.entity'; + +import { CirculationService } from './circulation.service'; +import { CirculationController } from './circulation.controller'; +import { UserModule } from '../user/user.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Circulation, + CirculationRouting, + CirculationStatusCode, + ]), + UserModule, + ], + controllers: [CirculationController], + providers: [CirculationService], + exports: [CirculationService], +}) +export class CirculationModule {} diff --git a/backend/src/modules/circulation/circulation.service.ts b/backend/src/modules/circulation/circulation.service.ts index 09b8165..2b5b935 100644 --- a/backend/src/modules/circulation/circulation.service.ts +++ b/backend/src/modules/circulation/circulation.service.ts @@ -1,11 +1,18 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + BadRequestException, + ForbiddenException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; +import { Repository, DataSource, Not } from 'typeorm'; // เพิ่ม Not import { Circulation } from './entities/circulation.entity'; import { CirculationRouting } from './entities/circulation-routing.entity'; import { User } from '../user/entities/user.entity'; -import { CreateCirculationDto } from './dto/create-circulation.dto'; // ต้องสร้าง DTO นี้ +import { CreateCirculationDto } from './dto/create-circulation.dto'; +import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto'; // Import ใหม่ +import { SearchCirculationDto } from './dto/search-circulation.dto'; // Import ใหม่ @Injectable() export class CirculationService { @@ -18,13 +25,16 @@ export class CirculationService { ) {} async create(createDto: CreateCirculationDto, user: User) { + if (!user.primaryOrganizationId) { + throw new BadRequestException('User must belong to an organization'); + } + const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { - // 1. Create Master Circulation - // TODO: Generate Circulation No. logic here (Simple format) + // Generate No. (Mock Logic) -> ควรใช้ NumberingService จริงในอนาคต const circulationNo = `CIR-${Date.now()}`; const circulation = queryRunner.manager.create(Circulation, { @@ -37,13 +47,12 @@ export class CirculationService { }); const savedCirculation = await queryRunner.manager.save(circulation); - // 2. Create Routings (Assignees) if (createDto.assigneeIds && createDto.assigneeIds.length > 0) { const routings = createDto.assigneeIds.map((userId, index) => queryRunner.manager.create(CirculationRouting, { circulationId: savedCirculation.id, stepNumber: index + 1, - organizationId: user.primaryOrganizationId, // Internal routing + organizationId: user.primaryOrganizationId, assignedTo: userId, status: 'PENDING', }), @@ -61,23 +70,84 @@ export class CirculationService { } } + async findAll(searchDto: SearchCirculationDto, user: User) { + const { search, status, page = 1, limit = 20 } = searchDto; + const query = this.circulationRepo + .createQueryBuilder('c') + .leftJoinAndSelect('c.creator', 'creator') + .where('c.organizationId = :orgId', { + orgId: user.primaryOrganizationId, + }); + + if (status) { + query.andWhere('c.statusCode = :status', { status }); + } + + if (search) { + query.andWhere( + '(c.circulationNo LIKE :search OR c.subject LIKE :search)', + { search: `%${search}%` }, + ); + } + + query + .orderBy('c.createdAt', 'DESC') + .skip((page - 1) * limit) + .take(limit); + + const [data, total] = await query.getManyAndCount(); + return { data, meta: { total, page, limit } }; + } + async findOne(id: number) { const circulation = await this.circulationRepo.findOne({ where: { id }, - relations: ['routings', 'routings.assignee', 'correspondence'], + relations: ['routings', 'routings.assignee', 'correspondence', 'creator'], + order: { routings: { stepNumber: 'ASC' } }, }); if (!circulation) throw new NotFoundException('Circulation not found'); return circulation; } - // Method update status (Complete task) + // ✅ Logic อัปเดตสถานะและปิดงาน async updateRoutingStatus( routingId: number, - status: string, - comments: string, + dto: UpdateCirculationRoutingDto, user: User, ) { - // Logic to update routing status - // and Check if all routings are completed -> Close Circulation + const routing = await this.routingRepo.findOne({ + where: { id: routingId }, + relations: ['circulation'], + }); + + if (!routing) throw new NotFoundException('Routing task not found'); + + // Check Permission: คนทำต้องเป็นเจ้าของ Task + if (routing.assignedTo !== user.user_id) { + throw new ForbiddenException('You are not assigned to this task'); + } + + // Update Routing + routing.status = dto.status; + routing.comments = dto.comments; + routing.completedAt = new Date(); + await this.routingRepo.save(routing); + + // Check: ถ้าทุกคนทำเสร็จแล้ว ให้ปิดใบเวียน (Master) + const pendingCount = await this.routingRepo.count({ + where: { + circulationId: routing.circulationId, + status: 'PENDING', // หรือ status ที่ยังไม่เสร็จ + }, + }); + + if (pendingCount === 0) { + await this.circulationRepo.update(routing.circulationId, { + statusCode: 'COMPLETED', + closedAt: new Date(), + }); + } + + return routing; } } diff --git a/backend/src/modules/circulation/dto/create-circulation.dto.ts b/backend/src/modules/circulation/dto/create-circulation.dto.ts index 33ffe3c..b4a754e 100644 --- a/backend/src/modules/circulation/dto/create-circulation.dto.ts +++ b/backend/src/modules/circulation/dto/create-circulation.dto.ts @@ -4,6 +4,7 @@ import { IsNotEmpty, IsArray, IsOptional, + ArrayMinSize, // ✅ เพิ่ม } from 'class-validator'; export class CreateCirculationDto { @@ -17,7 +18,7 @@ export class CreateCirculationDto { @IsArray() @IsInt({ each: true }) - @IsNotEmpty() + @ArrayMinSize(1) // ✅ ต้องมีผู้รับอย่างน้อย 1 คน assigneeIds!: number[]; // รายชื่อ User ID ที่ต้องการส่งให้ (ผู้รับผิดชอบ) @IsString() diff --git a/backend/src/modules/circulation/dto/create-transmittal.dto.ts b/backend/src/modules/circulation/dto/create-transmittal.dto.ts deleted file mode 100644 index bf8ea91..0000000 --- a/backend/src/modules/circulation/dto/create-transmittal.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - IsInt, - IsString, - IsOptional, - IsArray, - IsNotEmpty, - IsEnum, -} from 'class-validator'; - -export enum TransmittalPurpose { - FOR_APPROVAL = 'FOR_APPROVAL', - FOR_INFORMATION = 'FOR_INFORMATION', - FOR_REVIEW = 'FOR_REVIEW', - OTHER = 'OTHER', -} - -export class CreateTransmittalDto { - @IsInt() - @IsNotEmpty() - projectId!: number; // จำเป็นสำหรับการออกเลขที่เอกสาร - - @IsEnum(TransmittalPurpose) - @IsOptional() - purpose?: TransmittalPurpose; // วัตถุประสงค์ - - @IsString() - @IsOptional() - remarks?: string; // หมายเหตุ - - @IsArray() - @IsInt({ each: true }) - @IsNotEmpty() - itemIds!: number[]; // ID ของเอกสาร (Correspondence IDs) ที่จะแนบไปใน Transmittal -} diff --git a/backend/src/modules/circulation/dto/search-circulation.dto.ts b/backend/src/modules/circulation/dto/search-circulation.dto.ts new file mode 100644 index 0000000..2545926 --- /dev/null +++ b/backend/src/modules/circulation/dto/search-circulation.dto.ts @@ -0,0 +1,22 @@ +import { IsInt, IsOptional, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class SearchCirculationDto { + @IsOptional() + @IsString() + search?: string; // ค้นหาจาก Subject หรือ No. + + @IsOptional() + @IsString() + status?: string; // OPEN, COMPLETED + + @IsOptional() + @IsInt() + @Type(() => Number) + page: number = 1; + + @IsOptional() + @IsInt() + @Type(() => Number) + limit: number = 20; +} diff --git a/backend/src/modules/circulation/dto/update-circulation-routing.dto.ts b/backend/src/modules/circulation/dto/update-circulation-routing.dto.ts new file mode 100644 index 0000000..8d58236 --- /dev/null +++ b/backend/src/modules/circulation/dto/update-circulation-routing.dto.ts @@ -0,0 +1,16 @@ +import { IsString, IsOptional, IsEnum } from 'class-validator'; + +export enum CirculationAction { + COMPLETED = 'COMPLETED', + REJECTED = 'REJECTED', + // IN_PROGRESS อาจจะไม่ต้องส่งมา เพราะเป็น auto state ตอนเริ่มดู +} + +export class UpdateCirculationRoutingDto { + @IsEnum(CirculationAction) + status!: string; // สถานะที่ต้องการอัปเดต + + @IsString() + @IsOptional() + comments?: string; // ความคิดเห็นเพิ่มเติม +} diff --git a/backend/src/modules/correspondence/correspondence.controller.ts b/backend/src/modules/correspondence/correspondence.controller.ts index 72f12c1..8173590 100644 --- a/backend/src/modules/correspondence/correspondence.controller.ts +++ b/backend/src/modules/correspondence/correspondence.controller.ts @@ -21,6 +21,8 @@ import { WorkflowActionDto } from './dto/workflow-action.dto.js'; import { AddReferenceDto } from './dto/add-reference.dto.js'; import { SearchCorrespondenceDto } from './dto/search-correspondence.dto.js'; import { Query, Delete } from '@nestjs/common'; // เพิ่ม Query, Delete +import { Audit } from '../../common/decorators/audit.decorator'; // Import + @Controller('correspondences') @UseGuards(JwtAuthGuard, RbacGuard) export class CorrespondenceController { @@ -38,6 +40,7 @@ export class CorrespondenceController { @Post() @RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง + @Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้ create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) { return this.correspondenceService.create(createDto, req.user); } @@ -52,6 +55,7 @@ export class CorrespondenceController { // ✅ เพิ่ม Endpoint นี้ครับ @Post(':id/submit') @RequirePermission('correspondence.create') // หรือจะสร้าง Permission ใหม่ 'workflow.submit' ก็ได้ + @Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้ submit( @Param('id', ParseIntPipe) id: number, @Body() submitDto: SubmitCorrespondenceDto, diff --git a/backend/src/modules/correspondence/correspondence.module.ts b/backend/src/modules/correspondence/correspondence.module.ts index 48d3d0d..07dc292 100644 --- a/backend/src/modules/correspondence/correspondence.module.ts +++ b/backend/src/modules/correspondence/correspondence.module.ts @@ -16,6 +16,7 @@ import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ import { CorrespondenceReference } from './entities/correspondence-reference.entity.js'; +import { SearchModule } from '../search/search.module'; // ✅ 1. เพิ่ม Import SearchModule @Module({ imports: [ @@ -33,6 +34,7 @@ import { CorrespondenceReference } from './entities/correspondence-reference.ent JsonSchemaModule, // Import เพื่อ Validate JSON UserModule, // <--- 2. ใส่ UserModule ใน imports เพื่อให้ RbacGuard ทำงานได้ WorkflowEngineModule, // <--- Import WorkflowEngine + SearchModule, // ✅ 2. ใส่ SearchModule ที่นี่ ], controllers: [CorrespondenceController], providers: [CorrespondenceService], diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index 738fa50..09f423f 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -35,7 +35,7 @@ import { DocumentNumberingService } from '../document-numbering/document-numberi import { JsonSchemaService } from '../json-schema/json-schema.service.js'; import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service.js'; import { UserService } from '../user/user.service.js'; - +import { SearchService } from '../search/search.service'; // Import SearchService @Injectable() export class CorrespondenceService { private readonly logger = new Logger(CorrespondenceService.name); @@ -61,6 +61,7 @@ export class CorrespondenceService { private workflowEngine: WorkflowEngineService, private userService: UserService, private dataSource: DataSource, + private searchService: SearchService, // Inject ) {} /** @@ -182,7 +183,18 @@ export class CorrespondenceService { }); await queryRunner.manager.save(revision); - await queryRunner.commitTransaction(); + await queryRunner.commitTransaction(); // Transaction จบแล้ว ข้อมูลชัวร์แล้ว + // 🔥 Fire & Forget: ไม่ต้อง await ผลลัพธ์เพื่อความเร็ว (หรือใช้ Queue ก็ได้) + this.searchService.indexDocument({ + id: savedCorr.id, + type: 'correspondence', + docNumber: docNumber, + title: createDto.title, + description: createDto.description, + status: 'DRAFT', + projectId: createDto.projectId, + createdAt: new Date(), + }); return { ...savedCorr, diff --git a/backend/src/modules/drawing/drawing.module.ts b/backend/src/modules/drawing/drawing.module.ts index bba7842..8e16120 100644 --- a/backend/src/modules/drawing/drawing.module.ts +++ b/backend/src/modules/drawing/drawing.module.ts @@ -28,7 +28,7 @@ import { ContractDrawingController } from './contract-drawing.controller'; import { DrawingMasterDataController } from './drawing-master-data.controller'; // Modules import { FileStorageModule } from '../../common/file-storage/file-storage.module'; - +import { UserModule } from '../user/user.module'; @Module({ imports: [ TypeOrmModule.forFeature([ @@ -47,6 +47,7 @@ import { FileStorageModule } from '../../common/file-storage/file-storage.module Attachment, ]), FileStorageModule, + UserModule, ], providers: [ ShopDrawingService, diff --git a/backend/src/modules/drawing/shop-drawing.controller.ts b/backend/src/modules/drawing/shop-drawing.controller.ts index 75f7b32..82f3538 100644 --- a/backend/src/modules/drawing/shop-drawing.controller.ts +++ b/backend/src/modules/drawing/shop-drawing.controller.ts @@ -20,6 +20,7 @@ import { RbacGuard } from '../../common/guards/rbac.guard'; import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { User } from '../user/entities/user.entity'; +import { Audit } from '../../common/decorators/audit.decorator'; // Import @ApiTags('Shop Drawings') @ApiBearerAuth() @@ -31,6 +32,7 @@ export class ShopDrawingController { @Post() @ApiOperation({ summary: 'Create new Shop Drawing with initial revision' }) @RequirePermission('drawing.create') // อ้างอิง Permission จาก Seed + @Audit('drawing.create', 'shop_drawing') // ✅ แปะตรงนี้ create(@Body() createDto: CreateShopDrawingDto, @CurrentUser() user: User) { return this.shopDrawingService.create(createDto, user); } @@ -52,6 +54,7 @@ export class ShopDrawingController { @Post(':id/revisions') @ApiOperation({ summary: 'Add new revision to existing Shop Drawing' }) @RequirePermission('drawing.create') // หรือ drawing.edit ตาม Logic องค์กร + @Audit('drawing.create', 'shop_drawing') // ✅ แปะตรงนี้ createRevision( @Param('id', ParseIntPipe) id: number, @Body() createRevisionDto: CreateShopDrawingRevisionDto, diff --git a/backend/src/modules/drawing/shop-drawing.service.ts b/backend/src/modules/drawing/shop-drawing.service.ts index 8739184..df7e20c 100644 --- a/backend/src/modules/drawing/shop-drawing.service.ts +++ b/backend/src/modules/drawing/shop-drawing.service.ts @@ -43,10 +43,9 @@ export class ShopDrawingService { /** * สร้าง Shop Drawing ใหม่ พร้อม Revision แรก (Rev 0) - * ทำงานภายใต้ Database Transaction เดียวกัน */ async create(createDto: CreateShopDrawingDto, user: User) { - // 1. ตรวจสอบเลขที่แบบซ้ำ (Unique Check) + // 1. Check Duplicate const exists = await this.shopDrawingRepo.findOne({ where: { drawingNumber: createDto.drawingNumber }, }); @@ -61,7 +60,7 @@ export class ShopDrawingService { await queryRunner.startTransaction(); try { - // 2. เตรียมข้อมูล Relations (Contract Drawings & Attachments) + // 2. Prepare Relations let contractDrawings: ContractDrawing[] = []; if (createDto.contractDrawingIds?.length) { contractDrawings = await this.contractDrawingRepo.findBy({ @@ -76,7 +75,7 @@ export class ShopDrawingService { }); } - // 3. สร้าง Master Shop Drawing + // 3. Create Master Shop Drawing const shopDrawing = queryRunner.manager.create(ShopDrawing, { projectId: createDto.projectId, drawingNumber: createDto.drawingNumber, @@ -87,23 +86,22 @@ export class ShopDrawingService { }); const savedShopDrawing = await queryRunner.manager.save(shopDrawing); - // 4. สร้าง First Revision (Rev 0) + // 4. Create First Revision (Rev 0) const revision = queryRunner.manager.create(ShopDrawingRevision, { shopDrawingId: savedShopDrawing.id, - revisionNumber: 0, // เริ่มต้นที่ 0 เสมอ + revisionNumber: 0, revisionLabel: createDto.revisionLabel || '0', revisionDate: createDto.revisionDate ? new Date(createDto.revisionDate) : new Date(), description: createDto.description, - contractDrawings: contractDrawings, // ผูก M:N Relation - attachments: attachments, // ผูก M:N Relation + contractDrawings: contractDrawings, + attachments: attachments, }); await queryRunner.manager.save(revision); - // 5. Commit Files (ย้ายไฟล์จาก Temp -> Permanent) + // 5. Commit Files if (createDto.attachmentIds?.length) { - // ✅ FIX: ใช้ commitFiles และแปลง number[] -> string[] await this.fileStorageService.commit( createDto.attachmentIds.map(String), ); @@ -111,13 +109,13 @@ export class ShopDrawingService { await queryRunner.commitTransaction(); + // ✅ FIX: Return ข้อมูลของ ShopDrawing และ Revision (ไม่ใช่ savedCorr หรือ docNumber) return { ...savedShopDrawing, currentRevision: revision, }; } catch (err) { await queryRunner.rollbackTransaction(); - // ✅ FIX: Cast err เป็น Error this.logger.error( `Failed to create shop drawing: ${(err as Error).message}`, ); @@ -128,14 +126,12 @@ export class ShopDrawingService { } /** - * เพิ่ม Revision ใหม่ให้กับ Shop Drawing เดิม (Add Revision) - * เช่น Rev 0 -> Rev A + * เพิ่ม Revision ใหม่ (Add Revision) */ async createRevision( shopDrawingId: number, createDto: CreateShopDrawingRevisionDto, ) { - // 1. ตรวจสอบว่ามี Master Drawing อยู่จริง const shopDrawing = await this.shopDrawingRepo.findOneBy({ id: shopDrawingId, }); @@ -143,7 +139,6 @@ export class ShopDrawingService { throw new NotFoundException('Shop Drawing not found'); } - // 2. ตรวจสอบ Label ซ้ำใน Drawing เดียวกัน const exists = await this.revisionRepo.findOne({ where: { shopDrawingId, revisionLabel: createDto.revisionLabel }, }); @@ -158,7 +153,6 @@ export class ShopDrawingService { await queryRunner.startTransaction(); try { - // 3. เตรียม Relations let contractDrawings: ContractDrawing[] = []; if (createDto.contractDrawingIds?.length) { contractDrawings = await this.contractDrawingRepo.findBy({ @@ -173,14 +167,12 @@ export class ShopDrawingService { }); } - // 4. หา Revision Number ล่าสุดเพื่อ +1 (Running Number ภายใน) const latestRev = await this.revisionRepo.findOne({ where: { shopDrawingId }, order: { revisionNumber: 'DESC' }, }); const nextRevNum = (latestRev?.revisionNumber ?? -1) + 1; - // 5. บันทึก Revision ใหม่ const revision = queryRunner.manager.create(ShopDrawingRevision, { shopDrawingId, revisionNumber: nextRevNum, @@ -194,9 +186,7 @@ export class ShopDrawingService { }); await queryRunner.manager.save(revision); - // 6. Commit Files if (createDto.attachmentIds?.length) { - // ✅ FIX: ใช้ commitFiles และแปลง number[] -> string[] await this.fileStorageService.commit( createDto.attachmentIds.map(String), ); @@ -206,7 +196,6 @@ export class ShopDrawingService { return revision; } catch (err) { await queryRunner.rollbackTransaction(); - // ✅ FIX: Cast err เป็น Error this.logger.error(`Failed to create revision: ${(err as Error).message}`); throw err; } finally { @@ -215,8 +204,7 @@ export class ShopDrawingService { } /** - * ค้นหา Shop Drawing (Search & Filter) - * รองรับการค้นหาด้วย Text และกรองตาม Category + * ค้นหา Shop Drawing */ async findAll(searchDto: SearchShopDrawingDto) { const { @@ -260,7 +248,7 @@ export class ShopDrawingService { const [items, total] = await query.getManyAndCount(); - // Transform Data: เลือก Revision ล่าสุดมาแสดงเป็น currentRevision + // Transform Data const transformedItems = items.map((item) => { item.revisions.sort((a, b) => b.revisionNumber - a.revisionNumber); const currentRevision = item.revisions[0]; @@ -283,7 +271,7 @@ export class ShopDrawingService { } /** - * ดูรายละเอียด Shop Drawing (Get One) + * ดูรายละเอียด Shop Drawing */ async findOne(id: number) { const shopDrawing = await this.shopDrawingRepo.findOne({ @@ -308,7 +296,7 @@ export class ShopDrawingService { } /** - * ลบ Shop Drawing (Soft Delete) + * ลบ Shop Drawing */ async remove(id: number, user: User) { const shopDrawing = await this.findOne(id); diff --git a/backend/src/modules/json-schema/dto/create-json-schema.dto.ts b/backend/src/modules/json-schema/dto/create-json-schema.dto.ts new file mode 100644 index 0000000..c6f2cc4 --- /dev/null +++ b/backend/src/modules/json-schema/dto/create-json-schema.dto.ts @@ -0,0 +1,26 @@ +import { + IsString, + IsNotEmpty, + IsInt, + IsOptional, + IsBoolean, + IsObject, +} from 'class-validator'; + +export class CreateJsonSchemaDto { + @IsString() + @IsNotEmpty() + schemaCode!: string; // รหัส Schema (ต้องไม่ซ้ำ เช่น 'RFA_DWG_V1') + + @IsInt() + @IsOptional() + version?: number; // เวอร์ชัน (Default: 1) + + @IsObject() + @IsNotEmpty() + schemaDefinition!: Record; // โครงสร้าง JSON Schema (Standard Format) + + @IsBoolean() + @IsOptional() + isActive?: boolean; // สถานะการใช้งาน +} diff --git a/backend/src/modules/json-schema/dto/search-json-schema.dto.ts b/backend/src/modules/json-schema/dto/search-json-schema.dto.ts new file mode 100644 index 0000000..55962f2 --- /dev/null +++ b/backend/src/modules/json-schema/dto/search-json-schema.dto.ts @@ -0,0 +1,27 @@ +import { IsString, IsOptional, IsBoolean, IsInt } from 'class-validator'; +import { Type, Transform } from 'class-transformer'; + +export class SearchJsonSchemaDto { + @IsString() + @IsOptional() + search?: string; // ค้นหาจาก schemaCode + + @IsBoolean() + @IsOptional() + @Transform(({ value }) => { + if (value === 'true') return true; + if (value === 'false') return false; + return value; + }) + isActive?: boolean; + + @IsOptional() + @IsInt() + @Type(() => Number) + page: number = 1; + + @IsOptional() + @IsInt() + @Type(() => Number) + limit: number = 20; +} diff --git a/backend/src/modules/json-schema/dto/update-json-schema.dto.ts b/backend/src/modules/json-schema/dto/update-json-schema.dto.ts new file mode 100644 index 0000000..7d17d50 --- /dev/null +++ b/backend/src/modules/json-schema/dto/update-json-schema.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateJsonSchemaDto } from './create-json-schema.dto'; + +export class UpdateJsonSchemaDto extends PartialType(CreateJsonSchemaDto) {} diff --git a/backend/src/modules/json-schema/json-schema.controller.ts b/backend/src/modules/json-schema/json-schema.controller.ts index 6d369ab..a9c8152 100644 --- a/backend/src/modules/json-schema/json-schema.controller.ts +++ b/backend/src/modules/json-schema/json-schema.controller.ts @@ -1,25 +1,36 @@ import { Controller, Post, Body, Param, UseGuards } from '@nestjs/common'; -import { JsonSchemaService } from './json-schema.service.js'; -import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js'; -import { RbacGuard } from '../../common/guards/rbac.guard.js'; -import { RequirePermission } from '../../common/decorators/require-permission.decorator.js'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; -@Controller('json-schemas') +import { JsonSchemaService } from './json-schema.service'; +// ✅ FIX: Import DTO +import { CreateJsonSchemaDto } from './dto/create-json-schema.dto'; +// ✅ FIX: แก้ไข Path ของ Guards +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RbacGuard } from '../../common/guards/rbac.guard'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; + +@ApiTags('JSON Schemas') // ✅ Add Swagger Tag +@ApiBearerAuth() @UseGuards(JwtAuthGuard, RbacGuard) +@Controller('json-schemas') export class JsonSchemaController { constructor(private readonly schemaService: JsonSchemaService) {} - @Post(':code') - @RequirePermission('system.manage_all') // เฉพาะ Superadmin หรือผู้มีสิทธิ์จัดการ System - create(@Param('code') code: string, @Body() definition: any) { - return this.schemaService.createOrUpdate(code, definition); + @Post() + @ApiOperation({ summary: 'Create or Update JSON Schema' }) + @RequirePermission('system.manage_all') // Admin Only + create(@Body() createDto: CreateJsonSchemaDto) { + return this.schemaService.createOrUpdate( + createDto.schemaCode, + createDto.schemaDefinition, + ); } - // Endpoint สำหรับ Test Validate (Optional) @Post(':code/validate') + @ApiOperation({ summary: 'Test validation against a schema' }) @RequirePermission('document.view') async validate(@Param('code') code: string, @Body() data: any) { const isValid = await this.schemaService.validate(code, data); - return { valid: isValid }; + return { valid: isValid, message: 'Validation passed' }; } } diff --git a/backend/src/modules/json-schema/json-schema.module.ts b/backend/src/modules/json-schema/json-schema.module.ts index 087528b..175f535 100644 --- a/backend/src/modules/json-schema/json-schema.module.ts +++ b/backend/src/modules/json-schema/json-schema.module.ts @@ -1,17 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { JsonSchemaService } from './json-schema.service.js'; -import { JsonSchemaController } from './json-schema.controller.js'; -import { JsonSchema } from './entities/json-schema.entity.js'; -import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule +import { JsonSchemaService } from './json-schema.service'; +import { JsonSchemaController } from './json-schema.controller'; +import { JsonSchema } from './entities/json-schema.entity'; +import { UserModule } from '../user/user.module'; @Module({ - imports: [ - TypeOrmModule.forFeature([JsonSchema]), - UserModule, // <--- 2. ใส่ UserModule ใน imports - ], + imports: [TypeOrmModule.forFeature([JsonSchema]), UserModule], controllers: [JsonSchemaController], providers: [JsonSchemaService], - exports: [JsonSchemaService], // Export ให้ Module อื่นเรียกใช้ .validate() + exports: [JsonSchemaService], }) export class JsonSchemaModule {} diff --git a/backend/src/modules/json-schema/json-schema.service.ts b/backend/src/modules/json-schema/json-schema.service.ts index 6dc5e21..d5ece1b 100644 --- a/backend/src/modules/json-schema/json-schema.service.ts +++ b/backend/src/modules/json-schema/json-schema.service.ts @@ -3,31 +3,32 @@ import { OnModuleInit, BadRequestException, NotFoundException, + Logger, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; -import { JsonSchema } from './entities/json-schema.entity.js'; +import { JsonSchema } from './entities/json-schema.entity'; // ลบ .js @Injectable() export class JsonSchemaService implements OnModuleInit { private ajv: Ajv; - // Cache ตัว Validator ที่ Compile แล้ว เพื่อประสิทธิภาพ private validators = new Map(); + private readonly logger = new Logger(JsonSchemaService.name); constructor( @InjectRepository(JsonSchema) private schemaRepo: Repository, ) { - // ตั้งค่า AJV - this.ajv = new Ajv({ allErrors: true, strict: false }); // strict: false เพื่อยืดหยุ่นกับ custom keywords - addFormats(this.ajv); // รองรับ format เช่น email, date-time + this.ajv = new Ajv({ allErrors: true, strict: false }); + addFormats(this.ajv); } - onModuleInit() { - // (Optional) โหลด Schema ทั้งหมดมา Cache ตอนเริ่ม App ก็ได้ - // แต่ตอนนี้ใช้วิธี Lazy Load (โหลดเมื่อใช้) ไปก่อน + async onModuleInit() { + // Pre-load schemas (Optional for performance) + // const schemas = await this.schemaRepo.find({ where: { isActive: true } }); + // schemas.forEach(s => this.createValidator(s.schemaCode, s.schemaDefinition)); } /** @@ -36,7 +37,6 @@ export class JsonSchemaService implements OnModuleInit { async validate(schemaCode: string, data: any): Promise { let validate = this.validators.get(schemaCode); - // ถ้ายังไม่มีใน Cache หรือต้องการตัวล่าสุด ให้ดึงจาก DB if (!validate) { const schema = await this.schemaRepo.findOne({ where: { schemaCode, isActive: true }, @@ -59,19 +59,21 @@ export class JsonSchemaService implements OnModuleInit { const valid = validate(data); if (!valid) { - // รวบรวม Error ทั้งหมดส่งกลับไป const errors = validate.errors ?.map((e: any) => `${e.instancePath} ${e.message}`) .join(', '); + // โยน Error กลับไปเพื่อให้ Controller/Service ปลายทางจัดการ throw new BadRequestException(`JSON Validation Failed: ${errors}`); } return true; } - // ฟังก์ชันสำหรับสร้าง/อัปเดต Schema (สำหรับ Admin) - async createOrUpdate(schemaCode: string, definition: any) { - // ตรวจสอบก่อนว่า Definition เป็น JSON Schema ที่ถูกต้องไหม + /** + * สร้างหรืออัปเดต Schema + */ + async createOrUpdate(schemaCode: string, definition: Record) { + // 1. ตรวจสอบว่า Definition เป็น JSON Schema ที่ถูกต้องไหม try { this.ajv.compile(definition); } catch (error: any) { @@ -80,6 +82,7 @@ export class JsonSchemaService implements OnModuleInit { ); } + // 2. บันทึกลง DB let schema = await this.schemaRepo.findOne({ where: { schemaCode } }); if (schema) { @@ -93,9 +96,12 @@ export class JsonSchemaService implements OnModuleInit { }); } - // Clear Cache เก่า - this.validators.delete(schemaCode); + const savedSchema = await this.schemaRepo.save(schema); - return this.schemaRepo.save(schema); + // 3. Clear Cache เพื่อให้ครั้งหน้าโหลดตัวใหม่ + this.validators.delete(schemaCode); + this.logger.log(`Schema '${schemaCode}' updated (v${savedSchema.version})`); + + return savedSchema; } } diff --git a/backend/src/modules/project/dto/create-project.dto.ts b/backend/src/modules/project/dto/create-project.dto.ts new file mode 100644 index 0000000..52b45a9 --- /dev/null +++ b/backend/src/modules/project/dto/create-project.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator'; + +export class CreateProjectDto { + @IsString() + @IsNotEmpty() + projectCode!: string; // รหัสโครงการ (เช่น LCBP3) + + @IsString() + @IsNotEmpty() + projectName!: string; // ชื่อโครงการ + + @IsBoolean() + @IsOptional() + isActive?: boolean; // สถานะการใช้งาน (Default: true) +} diff --git a/backend/src/modules/project/dto/search-project.dto.ts b/backend/src/modules/project/dto/search-project.dto.ts new file mode 100644 index 0000000..10d871e --- /dev/null +++ b/backend/src/modules/project/dto/search-project.dto.ts @@ -0,0 +1,27 @@ +import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator'; +import { Type, Transform } from 'class-transformer'; + +export class SearchProjectDto { + @IsString() + @IsOptional() + search?: string; // ค้นหาจาก Project Code หรือ Name + + @IsOptional() + @IsBoolean() + @Transform(({ value }) => { + if (value === 'true') return true; + if (value === 'false') return false; + return value; + }) + isActive?: boolean; // กรองตามสถานะ Active + + @IsOptional() + @IsInt() + @Type(() => Number) + page: number = 1; + + @IsOptional() + @IsInt() + @Type(() => Number) + limit: number = 20; +} diff --git a/backend/src/modules/project/dto/update-project.dto.ts b/backend/src/modules/project/dto/update-project.dto.ts new file mode 100644 index 0000000..5f55c29 --- /dev/null +++ b/backend/src/modules/project/dto/update-project.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateProjectDto } from './create-project.dto'; + +export class UpdateProjectDto extends PartialType(CreateProjectDto) {} diff --git a/backend/src/modules/project/project.controller.ts b/backend/src/modules/project/project.controller.ts index 66980ae..a3d71ae 100644 --- a/backend/src/modules/project/project.controller.ts +++ b/backend/src/modules/project/project.controller.ts @@ -1,4 +1,75 @@ -import { Controller } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + UseGuards, + ParseIntPipe, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; -@Controller('project') -export class ProjectController {} +import { ProjectService } from './project.service.js'; +import { CreateProjectDto } from './dto/create-project.dto.js'; +import { UpdateProjectDto } from './dto/update-project.dto.js'; +import { SearchProjectDto } from './dto/search-project.dto.js'; + +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js'; +import { RbacGuard } from '../../common/guards/rbac.guard.js'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator.js'; + +@ApiTags('Projects') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RbacGuard) +@Controller('projects') // แนะนำให้ใช้ plural noun (projects) +export class ProjectController { + constructor(private readonly projectService: ProjectService) {} + + @Post() + @ApiOperation({ summary: 'Create new Project' }) + @RequirePermission('project.create') + create(@Body() createDto: CreateProjectDto) { + return this.projectService.create(createDto); + } + + @Get() + @ApiOperation({ summary: 'Search Projects' }) + @RequirePermission('project.view') + findAll(@Query() searchDto: SearchProjectDto) { + return this.projectService.findAll(searchDto); + } + + @Get('organizations') + @ApiOperation({ summary: 'List All Organizations (Master Data)' }) + // @RequirePermission('organization.view') // หรือเปิดให้ดูได้ทั่วไปถ้าจำเป็น + findAllOrgs() { + return this.projectService.findAllOrganizations(); + } + + @Get(':id') + @ApiOperation({ summary: 'Get Project Details' }) + @RequirePermission('project.view') + findOne(@Param('id', ParseIntPipe) id: number) { + return this.projectService.findOne(id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update Project' }) + @RequirePermission('project.edit') + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateDto: UpdateProjectDto, + ) { + return this.projectService.update(id, updateDto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete Project (Soft Delete)' }) + @RequirePermission('project.delete') + remove(@Param('id', ParseIntPipe) id: number) { + return this.projectService.remove(id); + } +} diff --git a/backend/src/modules/project/project.module.ts b/backend/src/modules/project/project.module.ts index 159ed1a..af0f422 100644 --- a/backend/src/modules/project/project.module.ts +++ b/backend/src/modules/project/project.module.ts @@ -7,7 +7,8 @@ import { Organization } from './entities/organization.entity.js'; import { Contract } from './entities/contract.entity.js'; import { ProjectOrganization } from './entities/project-organization.entity.js'; // เพิ่ม import { ContractOrganization } from './entities/contract-organization.entity.js'; // เพิ่ม - +// Modules +import { UserModule } from '../user/user.module'; // ✅ 1. Import UserModule @Module({ imports: [ TypeOrmModule.forFeature([ @@ -17,6 +18,7 @@ import { ContractOrganization } from './entities/contract-organization.entity.js ProjectOrganization, // ลงทะเบียน ContractOrganization, // ลงทะเบียน ]), + UserModule, // ✅ 2. เพิ่ม UserModule เข้าไปใน imports ], controllers: [ProjectController], providers: [ProjectService], diff --git a/backend/src/modules/project/project.service.ts b/backend/src/modules/project/project.service.ts index 79f5a9e..e0dc5dc 100644 --- a/backend/src/modules/project/project.service.ts +++ b/backend/src/modules/project/project.service.ts @@ -1,11 +1,25 @@ -import { Injectable } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + ConflictException, + Logger, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, Like } from 'typeorm'; + +// Entities import { Project } from './entities/project.entity.js'; import { Organization } from './entities/organization.entity.js'; +// DTOs +import { CreateProjectDto } from './dto/create-project.dto.js'; +import { UpdateProjectDto } from './dto/update-project.dto.js'; +import { SearchProjectDto } from './dto/search-project.dto.js'; + @Injectable() export class ProjectService { + private readonly logger = new Logger(ProjectService.name); + constructor( @InjectRepository(Project) private projectRepository: Repository, @@ -13,13 +27,92 @@ export class ProjectService { private organizationRepository: Repository, ) {} - // ดึงรายการ Project ทั้งหมด - async findAllProjects() { - return this.projectRepository.find(); + // --- CRUD Operations --- + + async create(createDto: CreateProjectDto) { + // 1. เช็คชื่อ/รหัสซ้ำ (ถ้าจำเป็น) + const existing = await this.projectRepository.findOne({ + where: { projectCode: createDto.projectCode }, + }); + if (existing) { + throw new ConflictException( + `Project Code "${createDto.projectCode}" already exists`, + ); + } + + // 2. สร้าง Project + const project = this.projectRepository.create(createDto); + return this.projectRepository.save(project); } - // ดึงรายการ Organization ทั้งหมด + async findAll(searchDto: SearchProjectDto) { + const { search, isActive, page = 1, limit = 20 } = searchDto; + const skip = (page - 1) * limit; + + // สร้าง Query Builder + const query = this.projectRepository.createQueryBuilder('project'); + + if (isActive !== undefined) { + query.andWhere('project.isActive = :isActive', { isActive }); + } + + if (search) { + query.andWhere( + '(project.projectCode LIKE :search OR project.projectName LIKE :search)', + { search: `%${search}%` }, + ); + } + + query.orderBy('project.created_at', 'DESC'); + query.skip(skip).take(limit); + + const [items, total] = await query.getManyAndCount(); + + return { + data: items, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; + } + + async findOne(id: number) { + const project = await this.projectRepository.findOne({ + where: { id }, + relations: ['contracts'], // ดึงสัญญาที่เกี่ยวข้องมาด้วย + }); + + if (!project) { + throw new NotFoundException(`Project ID ${id} not found`); + } + + return project; + } + + async update(id: number, updateDto: UpdateProjectDto) { + const project = await this.findOne(id); + + // Merge ข้อมูลใหม่ใส่ข้อมูลเดิม + this.projectRepository.merge(project, updateDto); + + return this.projectRepository.save(project); + } + + async remove(id: number) { + const project = await this.findOne(id); + // ใช้ Soft Delete + return this.projectRepository.softRemove(project); + } + + // --- Organization Helper --- + async findAllOrganizations() { - return this.organizationRepository.find(); + return this.organizationRepository.find({ + where: { isActive: true }, + order: { organizationCode: 'ASC' }, + }); } } diff --git a/backend/src/modules/rfa/entities/rfa-revision.entity.ts b/backend/src/modules/rfa/entities/rfa-revision.entity.ts index cc1f27d..02348dc 100644 --- a/backend/src/modules/rfa/entities/rfa-revision.entity.ts +++ b/backend/src/modules/rfa/entities/rfa-revision.entity.ts @@ -15,6 +15,7 @@ import { RfaStatusCode } from './rfa-status-code.entity'; import { RfaApproveCode } from './rfa-approve-code.entity'; import { User } from '../../user/entities/user.entity'; import { RfaItem } from './rfa-item.entity'; +import { RfaWorkflow } from './rfa-workflow.entity'; // Import เพิ่ม @Entity('rfa_revisions') @Unique(['rfaId', 'revisionNumber']) @@ -96,4 +97,10 @@ export class RfaRevision { // Items (Shop Drawings inside this RFA) @OneToMany(() => RfaItem, (item) => item.rfaRevision, { cascade: true }) items!: RfaItem[]; + + // Workflows + @OneToMany(() => RfaWorkflow, (workflow) => workflow.rfaRevision, { + cascade: true, + }) + workflows!: RfaWorkflow[]; } diff --git a/backend/src/modules/rfa/entities/rfa-workflow-template-step.entity.ts b/backend/src/modules/rfa/entities/rfa-workflow-template-step.entity.ts new file mode 100644 index 0000000..c3d07e1 --- /dev/null +++ b/backend/src/modules/rfa/entities/rfa-workflow-template-step.entity.ts @@ -0,0 +1,64 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { RfaWorkflowTemplate } from './rfa-workflow-template.entity'; +import { Organization } from '../../project/entities/organization.entity'; +import { Role } from '../../user/entities/role.entity'; + +// ✅ 1. สร้าง Enum เพื่อให้ Type Safe +export enum RfaActionType { + REVIEW = 'REVIEW', + APPROVE = 'APPROVE', + ACKNOWLEDGE = 'ACKNOWLEDGE', +} + +@Entity('rfa_workflow_template_steps') +export class RfaWorkflowTemplateStep { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'template_id' }) + templateId!: number; + + @Column({ name: 'step_number' }) + stepNumber!: number; + + @Column({ name: 'organization_id' }) + organizationId!: number; + + @Column({ name: 'role_id', nullable: true }) + roleId?: number; + + @Column({ + name: 'action_type', + type: 'enum', + enum: RfaActionType, // ✅ 2. ใช้ Enum ตรงนี้ + nullable: true, + }) + actionType?: RfaActionType; // ✅ 3. เปลี่ยน type จาก string เป็น Enum + + @Column({ name: 'duration_days', nullable: true }) + durationDays?: number; + + @Column({ name: 'is_optional', default: false }) + isOptional!: boolean; + + // Relations + @ManyToOne(() => RfaWorkflowTemplate, (template) => template.steps, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'template_id' }) + template!: RfaWorkflowTemplate; + + @ManyToOne(() => Organization) + @JoinColumn({ name: 'organization_id' }) + organization!: Organization; + + @ManyToOne(() => Role) + @JoinColumn({ name: 'role_id' }) + role?: Role; +} diff --git a/backend/src/modules/rfa/entities/rfa-workflow-template.entity.ts b/backend/src/modules/rfa/entities/rfa-workflow-template.entity.ts new file mode 100644 index 0000000..450e872 --- /dev/null +++ b/backend/src/modules/rfa/entities/rfa-workflow-template.entity.ts @@ -0,0 +1,39 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, +} from 'typeorm'; +import { RfaWorkflowTemplateStep } from './rfa-workflow-template-step.entity'; + +@Entity('rfa_workflow_templates') +export class RfaWorkflowTemplate { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'template_name', length: 100 }) + templateName!: string; + + @Column({ type: 'text', nullable: true }) + description?: string; + + @Column({ name: 'is_active', default: true }) + isActive!: boolean; + + @Column({ type: 'json', nullable: true }) + workflowConfig?: Record; // Configuration เพิ่มเติม + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; + + // Relations + @OneToMany(() => RfaWorkflowTemplateStep, (step) => step.template, { + cascade: true, + }) + steps!: RfaWorkflowTemplateStep[]; +} diff --git a/backend/src/modules/rfa/entities/rfa-workflow.entity.ts b/backend/src/modules/rfa/entities/rfa-workflow.entity.ts new file mode 100644 index 0000000..b43b64f --- /dev/null +++ b/backend/src/modules/rfa/entities/rfa-workflow.entity.ts @@ -0,0 +1,73 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { RfaRevision } from './rfa-revision.entity'; +import { Organization } from '../../project/entities/organization.entity'; +import { User } from '../../user/entities/user.entity'; + +@Entity('rfa_workflows') +export class RfaWorkflow { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'rfa_revision_id' }) + rfaRevisionId!: number; + + @Column({ name: 'step_number' }) + stepNumber!: number; + + @Column({ name: 'organization_id' }) + organizationId!: number; + + @Column({ name: 'assigned_to', nullable: true }) + assignedTo?: number; + + @Column({ + name: 'action_type', + type: 'enum', + enum: ['REVIEW', 'APPROVE', 'ACKNOWLEDGE'], + nullable: true, + }) + actionType?: string; + + @Column({ + type: 'enum', + enum: ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'REJECTED'], + nullable: true, + }) + status?: string; + + @Column({ type: 'text', nullable: true }) + comments?: string; + + @Column({ name: 'completed_at', type: 'datetime', nullable: true }) + completedAt?: Date; + + @Column({ type: 'json', nullable: true }) + stateContext?: Record; // เก็บ Snapshot ข้อมูล ณ ขณะนั้น + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; + + // Relations + @ManyToOne(() => RfaRevision, (rev) => rev.workflows, { onDelete: 'CASCADE' }) // ต้องไปเพิ่ม Property workflows ใน RfaRevision ด้วย + @JoinColumn({ name: 'rfa_revision_id' }) + rfaRevision!: RfaRevision; + + @ManyToOne(() => Organization) + @JoinColumn({ name: 'organization_id' }) + organization!: Organization; + + @ManyToOne(() => User) + @JoinColumn({ name: 'assigned_to' }) + assignee?: User; +} diff --git a/backend/src/modules/rfa/rfa.controller.ts b/backend/src/modules/rfa/rfa.controller.ts index 7398248..4c16fd2 100644 --- a/backend/src/modules/rfa/rfa.controller.ts +++ b/backend/src/modules/rfa/rfa.controller.ts @@ -18,6 +18,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { RbacGuard } from '../../common/guards/rbac.guard'; import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { Audit } from '../../common/decorators/audit.decorator'; // Import @ApiTags('RFA (Request for Approval)') @ApiBearerAuth() @@ -29,6 +30,7 @@ export class RfaController { @Post() @ApiOperation({ summary: 'Create new RFA (Draft)' }) @RequirePermission('rfa.create') // สิทธิ์ ID 37 + @Audit('rfa.create', 'rfa') // ✅ แปะตรงนี้ create(@Body() createDto: CreateRfaDto, @CurrentUser() user: User) { return this.rfaService.create(createDto, user); } diff --git a/backend/src/modules/rfa/rfa.module.ts b/backend/src/modules/rfa/rfa.module.ts index 45f2aed..ab8ce5d 100644 --- a/backend/src/modules/rfa/rfa.module.ts +++ b/backend/src/modules/rfa/rfa.module.ts @@ -21,6 +21,12 @@ import { RfaController } from './rfa.controller'; import { DocumentNumberingModule } from '../document-numbering/document-numbering.module'; import { UserModule } from '../user/user.module'; +// ... imports +import { RfaWorkflow } from './entities/rfa-workflow.entity'; +import { RfaWorkflowTemplate } from './entities/rfa-workflow-template.entity'; +import { RfaWorkflowTemplateStep } from './entities/rfa-workflow-template-step.entity'; + +import { SearchModule } from '../search/search.module'; // ✅ เพิ่ม @Module({ imports: [ TypeOrmModule.forFeature([ @@ -32,9 +38,14 @@ import { UserModule } from '../user/user.module'; RfaApproveCode, Correspondence, ShopDrawingRevision, + // ... (ตัวเดิม) + RfaWorkflow, + RfaWorkflowTemplate, + RfaWorkflowTemplateStep, ]), DocumentNumberingModule, UserModule, + SearchModule, ], providers: [RfaService], controllers: [RfaController], diff --git a/backend/src/modules/rfa/rfa.service.ts b/backend/src/modules/rfa/rfa.service.ts index 3459cb5..075c17a 100644 --- a/backend/src/modules/rfa/rfa.service.ts +++ b/backend/src/modules/rfa/rfa.service.ts @@ -34,6 +34,7 @@ import { DocumentNumberingService } from '../document-numbering/document-numberi import { UserService } from '../user/user.service'; import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service'; import { NotificationService } from '../notification/notification.service'; +import { SearchService } from '../search/search.service'; // Import SearchService @Injectable() export class RfaService { @@ -66,6 +67,7 @@ export class RfaService { private workflowEngine: WorkflowEngineService, private notificationService: NotificationService, private dataSource: DataSource, + private searchService: SearchService, // Inject ) {} /** @@ -166,6 +168,17 @@ export class RfaService { } await queryRunner.commitTransaction(); + // 🔥 Fire & Forget: ไม่ต้อง await ผลลัพธ์เพื่อความเร็ว (หรือใช้ Queue ก็ได้) + this.searchService.indexDocument({ + id: savedCorr.id, + type: 'correspondence', + docNumber: docNumber, + title: createDto.title, + description: createDto.description, + status: 'DRAFT', + projectId: createDto.projectId, + createdAt: new Date(), + }); return { ...savedRfa, diff --git a/backend/src/modules/search/dto/search-query.dto.ts b/backend/src/modules/search/dto/search-query.dto.ts new file mode 100644 index 0000000..d0f155e --- /dev/null +++ b/backend/src/modules/search/dto/search-query.dto.ts @@ -0,0 +1,27 @@ +import { IsString, IsOptional, IsInt, IsNotEmpty } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class SearchQueryDto { + @IsString() + @IsOptional() + q?: string; // คำค้นหา (Query) + + @IsString() + @IsOptional() + type?: string; // กรองประเภท: 'rfa', 'correspondence', 'drawing' + + @IsInt() + @Type(() => Number) + @IsOptional() + projectId?: number; + + @IsInt() + @Type(() => Number) + @IsOptional() + page: number = 1; + + @IsInt() + @Type(() => Number) + @IsOptional() + limit: number = 20; +} diff --git a/backend/src/modules/search/search.controller.ts b/backend/src/modules/search/search.controller.ts new file mode 100644 index 0000000..8eb9abb --- /dev/null +++ b/backend/src/modules/search/search.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { SearchService } from './search.service'; +import { SearchQueryDto } from './dto/search-query.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RbacGuard } from '../../common/guards/rbac.guard'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; + +@ApiTags('Search') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RbacGuard) +@Controller('search') +export class SearchController { + constructor(private readonly searchService: SearchService) {} + + @Get() + @ApiOperation({ summary: 'Advanced Search across all documents' }) + @RequirePermission('search.advanced') // สิทธิ์ ID 48 + search(@Query() queryDto: SearchQueryDto) { + return this.searchService.search(queryDto); + } +} diff --git a/backend/src/modules/search/search.module.ts b/backend/src/modules/search/search.module.ts new file mode 100644 index 0000000..4458173 --- /dev/null +++ b/backend/src/modules/search/search.module.ts @@ -0,0 +1,32 @@ +import { Module } from '@nestjs/common'; +import { ElasticsearchModule } from '@nestjs/elasticsearch'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { SearchService } from './search.service'; +import { SearchController } from './search.controller'; +import { UserModule } from '../user/user.module'; // ✅ 1. Import UserModule + +@Module({ + imports: [ + ConfigModule, + // ✅ 2. เพิ่ม UserModule เข้าไปใน imports + UserModule, + + ElasticsearchModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + node: + configService.get('ELASTICSEARCH_NODE') || + 'http://localhost:9200', + auth: { + username: configService.get('ELASTICSEARCH_USERNAME') || '', + password: configService.get('ELASTICSEARCH_PASSWORD') || '', + }, + }), + inject: [ConfigService], + }), + ], + controllers: [SearchController], + providers: [SearchService], + exports: [SearchService], +}) +export class SearchModule {} diff --git a/backend/src/modules/search/search.service.ts b/backend/src/modules/search/search.service.ts new file mode 100644 index 0000000..162ec93 --- /dev/null +++ b/backend/src/modules/search/search.service.ts @@ -0,0 +1,152 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { ConfigService } from '@nestjs/config'; +import { SearchQueryDto } from './dto/search-query.dto'; + +@Injectable() +export class SearchService implements OnModuleInit { + private readonly logger = new Logger(SearchService.name); + private readonly indexName = 'dms_documents'; + + constructor( + private readonly esService: ElasticsearchService, + private readonly configService: ConfigService, + ) {} + + async onModuleInit() { + await this.createIndexIfNotExists(); + } + + /** + * สร้าง Index และกำหนด Mapping (Schema) + */ + private async createIndexIfNotExists() { + try { + const indexExists = await this.esService.indices.exists({ + index: this.indexName, + }); + + if (!indexExists) { + // ✅ FIX: Cast 'body' เป็น any เพื่อแก้ปัญหา Type Mismatch ของ Library + await this.esService.indices.create({ + index: this.indexName, + body: { + mappings: { + properties: { + id: { type: 'integer' }, + type: { type: 'keyword' }, // correspondence, rfa, drawing + docNumber: { type: 'text' }, + title: { type: 'text', analyzer: 'standard' }, + description: { type: 'text', analyzer: 'standard' }, + status: { type: 'keyword' }, + projectId: { type: 'integer' }, + createdAt: { type: 'date' }, + tags: { type: 'text' }, + }, + }, + } as any, + }); + this.logger.log(`Elasticsearch index '${this.indexName}' created.`); + } + } catch (error) { + this.logger.error(`Failed to create index: ${(error as Error).message}`); + } + } + + /** + * Index เอกสาร (Create/Update) + */ + async indexDocument(doc: any) { + try { + return await this.esService.index({ + index: this.indexName, + id: `${doc.type}_${doc.id}`, // Unique ID: rfa_101 + document: doc, // ✅ Library รุ่นใหม่ใช้ 'document' แทน 'body' ในบางเวอร์ชัน + }); + } catch (error) { + this.logger.error( + `Failed to index document: ${(error as Error).message}`, + ); + } + } + + /** + * ลบเอกสารออกจาก Index + */ + async removeDocument(type: string, id: number) { + try { + await this.esService.delete({ + index: this.indexName, + id: `${type}_${id}`, + }); + } catch (error) { + this.logger.error( + `Failed to remove document: ${(error as Error).message}`, + ); + } + } + + /** + * ค้นหาเอกสาร (Full-text Search) + */ + async search(queryDto: SearchQueryDto) { + const { q, type, projectId, page = 1, limit = 20 } = queryDto; + const from = (page - 1) * limit; + + const mustQueries: any[] = []; + + // 1. Full-text search logic + if (q) { + mustQueries.push({ + multi_match: { + query: q, + fields: ['title^3', 'docNumber^2', 'description', 'tags'], // Boost ความสำคัญ + fuzziness: 'AUTO', + }, + }); + } else { + mustQueries.push({ match_all: {} }); + } + + // 2. Filter logic + const filterQueries: any[] = []; + if (type) filterQueries.push({ term: { type } }); + if (projectId) filterQueries.push({ term: { projectId } }); + + try { + const result = await this.esService.search({ + index: this.indexName, + from, + size: limit, + // ✅ ส่ง Query Structure โดยตรง + query: { + bool: { + must: mustQueries, + filter: filterQueries, + }, + }, + sort: [{ createdAt: { order: 'desc' } }], + }); + + // 3. Format Result + const hits = result.hits.hits; + const total = + typeof result.hits.total === 'number' + ? result.hits.total + : result.hits.total?.value || 0; + + return { + data: hits.map((hit) => hit._source), + meta: { + total, + page, + limit, + took: result.took, + }, + }; + } catch (error) { + this.logger.error(`Search failed: ${(error as Error).message}`); + return { data: [], meta: { total: 0, page, limit, took: 0 } }; + } + } +} diff --git a/backend/src/modules/transmittal/transmittal.controller.ts b/backend/src/modules/transmittal/transmittal.controller.ts index 4b51b00..f0a0dbc 100644 --- a/backend/src/modules/transmittal/transmittal.controller.ts +++ b/backend/src/modules/transmittal/transmittal.controller.ts @@ -19,6 +19,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { RbacGuard } from '../../common/guards/rbac.guard'; import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { Audit } from '../../common/decorators/audit.decorator'; // Import @ApiTags('Transmittals') @ApiBearerAuth() @@ -30,24 +31,23 @@ export class TransmittalController { @Post() @ApiOperation({ summary: 'Create new Transmittal' }) @RequirePermission('transmittal.create') // สิทธิ์ ID 40 + @Audit('transmittal.create', 'transmittal') // ✅ แปะตรงนี้ create(@Body() createDto: CreateTransmittalDto, @CurrentUser() user: User) { return this.transmittalService.create(createDto, user); } // เพิ่ม Endpoint พื้นฐานสำหรับการค้นหา (Optional) - /* @Get() @ApiOperation({ summary: 'Search Transmittals' }) @RequirePermission('document.view') findAll(@Query() searchDto: SearchTransmittalDto) { - // return this.transmittalService.findAll(searchDto); + // return this.transmittalService.findAll(searchDto); } @Get(':id') @ApiOperation({ summary: 'Get Transmittal details' }) @RequirePermission('document.view') findOne(@Param('id', ParseIntPipe) id: number) { - // return this.transmittalService.findOne(id); + // return this.transmittalService.findOne(id); } - */ } diff --git a/backend/src/modules/transmittal/transmittal.module.ts b/backend/src/modules/transmittal/transmittal.module.ts index 8574ec8..935b0ac 100644 --- a/backend/src/modules/transmittal/transmittal.module.ts +++ b/backend/src/modules/transmittal/transmittal.module.ts @@ -6,11 +6,12 @@ import { Correspondence } from '../correspondence/entities/correspondence.entity import { TransmittalService } from './transmittal.service'; import { TransmittalController } from './transmittal.controller'; import { DocumentNumberingModule } from '../document-numbering/document-numbering.module'; - +import { UserModule } from '../user/user.module'; @Module({ imports: [ TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]), DocumentNumberingModule, + UserModule, ], controllers: [TransmittalController], providers: [TransmittalService], diff --git a/backend/src/modules/transmittal/transmittal.service.ts b/backend/src/modules/transmittal/transmittal.service.ts index 4280660..39b4fe9 100644 --- a/backend/src/modules/transmittal/transmittal.service.ts +++ b/backend/src/modules/transmittal/transmittal.service.ts @@ -12,6 +12,7 @@ import { Correspondence } from '../correspondence/entities/correspondence.entity import { CreateTransmittalDto } from './dto/create-transmittal.dto'; // ต้องสร้าง DTO import { User } from '../user/entities/user.entity'; import { DocumentNumberingService } from '../document-numbering/document-numbering.service'; +import { SearchService } from '../search/search.service'; // Import SearchService @Injectable() export class TransmittalService { @@ -24,6 +25,7 @@ export class TransmittalService { private correspondenceRepo: Repository, private numberingService: DocumentNumberingService, private dataSource: DataSource, + private searchService: SearchService, // Inject ) {} async create(createDto: CreateTransmittalDto, user: User) { diff --git a/backend/src/modules/user/dto/update-preference.dto.ts b/backend/src/modules/user/dto/update-preference.dto.ts new file mode 100644 index 0000000..d8504e3 --- /dev/null +++ b/backend/src/modules/user/dto/update-preference.dto.ts @@ -0,0 +1,42 @@ +// File: src/modules/user/dto/update-preference.dto.ts +// บันทึกการแก้ไข: DTO สำหรับตรวจสอบข้อมูลการอัปเดต User Preferences (T1.3) + +import { IsBoolean, IsOptional, IsString, IsIn } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; // ใช้สำหรับสร้าง API Documentation (Swagger) + +export class UpdatePreferenceDto { + @ApiPropertyOptional({ + description: 'รับการแจ้งเตือนทางอีเมลหรือไม่', + default: true, + }) + @IsOptional() + @IsBoolean() + notifyEmail?: boolean; + + @ApiPropertyOptional({ + description: 'รับการแจ้งเตือนทาง LINE หรือไม่', + default: true, + }) + @IsOptional() + @IsBoolean() + notifyLine?: boolean; + + @ApiPropertyOptional({ + description: + 'รับการแจ้งเตือนแบบรวม (Digest) แทน Real-time เพื่อลดจำนวนข้อความ', + default: false, + }) + @IsOptional() + @IsBoolean() + digestMode?: boolean; + + @ApiPropertyOptional({ + description: 'ธีมของหน้าจอ (light, dark, หรือ system)', + default: 'light', + enum: ['light', 'dark', 'system'], + }) + @IsOptional() + @IsString() + @IsIn(['light', 'dark', 'system']) // บังคับว่าต้องเป็นค่าใดค่าหนึ่งในนี้เท่านั้น + uiTheme?: string; +} diff --git a/backend/src/modules/user/dto/update-user.dto.ts b/backend/src/modules/user/dto/update-user.dto.ts index 912cdc5..2ff85ab 100644 --- a/backend/src/modules/user/dto/update-user.dto.ts +++ b/backend/src/modules/user/dto/update-user.dto.ts @@ -1,4 +1,21 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreateUserDto } from './create-user.dto.js'; +// File: src/modules/user/dto/update-preference.dto.ts +import { IsBoolean, IsOptional, IsString, IsIn } from 'class-validator'; -export class UpdateUserDto extends PartialType(CreateUserDto) {} +export class UpdatePreferenceDto { + @IsOptional() + @IsBoolean() + notifyEmail?: boolean; + + @IsOptional() + @IsBoolean() + notifyLine?: boolean; + + @IsOptional() + @IsBoolean() + digestMode?: boolean; + + @IsOptional() + @IsString() + @IsIn(['light', 'dark', 'system']) + uiTheme?: string; +} diff --git a/backend/src/modules/user/entities/permission.entity.ts b/backend/src/modules/user/entities/permission.entity.ts new file mode 100644 index 0000000..0c648fb --- /dev/null +++ b/backend/src/modules/user/entities/permission.entity.ts @@ -0,0 +1,27 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('permissions') +export class Permission { + @PrimaryGeneratedColumn({ name: 'permission_id' }) + permissionId!: number; + + @Column({ name: 'permission_name', length: 100, unique: true }) + permissionName!: string; // e.g., 'rfa.create' + + @Column({ type: 'text', nullable: true }) + description?: string; + + @Column({ length: 50, nullable: true }) + module?: string; // e.g., 'rfa', 'user' + + @Column({ + name: 'scope_level', + type: 'enum', + enum: ['GLOBAL', 'ORG', 'PROJECT'], + nullable: true, + }) + scopeLevel?: string; + + @Column({ name: 'is_active', default: true }) + isActive!: boolean; +} diff --git a/backend/src/modules/user/entities/role.entity.ts b/backend/src/modules/user/entities/role.entity.ts new file mode 100644 index 0000000..0fb97d0 --- /dev/null +++ b/backend/src/modules/user/entities/role.entity.ts @@ -0,0 +1,29 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +export enum RoleScope { + GLOBAL = 'Global', + ORGANIZATION = 'Organization', + PROJECT = 'Project', + CONTRACT = 'Contract', +} + +@Entity('roles') +export class Role { + @PrimaryGeneratedColumn({ name: 'role_id' }) + roleId!: number; + + @Column({ name: 'role_name', length: 100 }) + roleName!: string; + + @Column({ + type: 'enum', + enum: RoleScope, + }) + scope!: RoleScope; + + @Column({ type: 'text', nullable: true }) + description?: string; + + @Column({ name: 'is_system', default: false }) + isSystem!: boolean; +} diff --git a/backend/src/modules/user/entities/user-assignment.entity.ts b/backend/src/modules/user/entities/user-assignment.entity.ts index 40fa16c..d283e39 100644 --- a/backend/src/modules/user/entities/user-assignment.entity.ts +++ b/backend/src/modules/user/entities/user-assignment.entity.ts @@ -1,3 +1,6 @@ +// File: src/modules/user/entities/user-assignment.entity.ts +// บันทึกการแก้ไข: Entity สำหรับการมอบหมาย Role ให้กับ User ตาม Scope (T1.3, RBAC 4-Level) + import { Entity, Column, @@ -6,8 +9,11 @@ import { JoinColumn, CreateDateColumn, } from 'typeorm'; -import { User } from './user.entity.js'; -// Import Role, Org, Project, Contract entities... +import { User } from './user.entity'; +import { Role } from './role.entity'; +import { Organization } from '../../project/entities/organization.entity'; // ปรับ Path ให้ตรงกับ ProjectModule +import { Project } from '../../project/entities/project.entity'; // ปรับ Path ให้ตรงกับ ProjectModule +import { Contract } from '../../project/entities/contract.entity'; // ปรับ Path ให้ตรงกับ ProjectModule @Entity('user_assignments') export class UserAssignment { @@ -20,6 +26,7 @@ export class UserAssignment { @Column({ name: 'role_id' }) roleId!: number; + // --- Scopes (เลือกได้เพียง 1 หรือเป็น NULL ทั้งหมดสำหรับ Global) --- @Column({ name: 'organization_id', nullable: true }) organizationId?: number; @@ -35,8 +42,29 @@ export class UserAssignment { @CreateDateColumn({ name: 'assigned_at' }) assignedAt!: Date; - // Relation กลับไปหา User (เจ้าของสิทธิ์) - @ManyToOne(() => User) + // --- Relations --- + + @ManyToOne(() => User, (user) => user.assignments, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'user_id' }) - user?: User; + user!: User; + + @ManyToOne(() => Role, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'role_id' }) + role!: Role; + + @ManyToOne(() => Organization, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'organization_id' }) + organization?: Organization; + + @ManyToOne(() => Project, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'project_id' }) + project?: Project; + + @ManyToOne(() => Contract, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'contract_id' }) + contract?: Contract; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'assigned_by_user_id' }) + assignedBy?: User; } diff --git a/backend/src/modules/user/entities/user-preference.entity.ts b/backend/src/modules/user/entities/user-preference.entity.ts index 11e0423..19444e1 100644 --- a/backend/src/modules/user/entities/user-preference.entity.ts +++ b/backend/src/modules/user/entities/user-preference.entity.ts @@ -1,7 +1,10 @@ +// File: src/modules/user/entities/user-preference.entity.ts +// บันทึกการแก้ไข: Entity สำหรับเก็บการตั้งค่าส่วนตัวของผู้ใช้ แยกจากตาราง Users (T1.3) + import { Entity, - PrimaryColumn, Column, + PrimaryColumn, UpdateDateColumn, OneToOne, JoinColumn, @@ -10,7 +13,6 @@ import { User } from './user.entity'; @Entity('user_preferences') export class UserPreference { - // ใช้ user_id เป็น Primary Key และ Foreign Key ในตัวเดียวกัน (1:1 Relation) @PrimaryColumn({ name: 'user_id' }) userId!: number; @@ -20,18 +22,17 @@ export class UserPreference { @Column({ name: 'notify_line', default: true }) notifyLine!: boolean; - @Column({ name: 'digest_mode', default: true }) + @Column({ name: 'digest_mode', default: false }) digestMode!: boolean; // รับแจ้งเตือนแบบรวม (Digest) แทน Real-time - @Column({ name: 'ui_theme', length: 20, default: 'light' }) + @Column({ name: 'ui_theme', default: 'light', length: 20 }) uiTheme!: string; @UpdateDateColumn({ name: 'updated_at' }) updatedAt!: Date; - // --- Relations --- - - @OneToOne(() => User) + // --- Relation --- + @OneToOne(() => User, (user) => user.preference, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'user_id' }) user!: User; } diff --git a/backend/src/modules/user/entities/user.entity.ts b/backend/src/modules/user/entities/user.entity.ts index 553e83f..718a958 100644 --- a/backend/src/modules/user/entities/user.entity.ts +++ b/backend/src/modules/user/entities/user.entity.ts @@ -1,3 +1,6 @@ +// File: src/modules/user/entities/user.entity.ts +// บันทึกการแก้ไข: เพิ่ม Relations กับ UserAssignment และ UserPreference (T1.3) + import { Entity, Column, @@ -5,10 +8,14 @@ import { CreateDateColumn, UpdateDateColumn, DeleteDateColumn, - ManyToOne, // <--- เพิ่มตรงนี้ - JoinColumn, // <--- เพิ่มตรงนี้ + ManyToOne, + OneToMany, + OneToOne, + JoinColumn, } from 'typeorm'; -import { Organization } from '../../project/entities/organization.entity.js'; // อย่าลืม import Organization +import { Organization } from '../../project/entities/organization.entity'; // Adjust path as needed +import { UserAssignment } from './user-assignment.entity'; +import { UserPreference } from './user-preference.entity'; @Entity('users') export class User { @@ -18,7 +25,7 @@ export class User { @Column({ unique: true, length: 50 }) username!: string; - @Column({ name: 'password_hash' }) + @Column({ name: 'password_hash', select: false }) // ไม่ Select Password โดย Default password!: string; @Column({ unique: true, length: 100 }) @@ -33,15 +40,26 @@ export class User { @Column({ name: 'is_active', default: true }) isActive!: boolean; - // Relation กับ Organization + @Column({ name: 'line_id', nullable: true, length: 100 }) + lineId?: string; + + // Relation กับ Organization (สังกัดหลัก) @Column({ name: 'primary_organization_id', nullable: true }) primaryOrganizationId?: number; - @ManyToOne(() => Organization) + @ManyToOne(() => Organization, { nullable: true, onDelete: 'SET NULL' }) @JoinColumn({ name: 'primary_organization_id' }) organization?: Organization; - // Base Entity Fields (ที่เราแยกมาเขียนเองเพราะเรื่อง deleted_at) + // Relation กับ Assignments (RBAC) + @OneToMany(() => UserAssignment, (assignment) => assignment.user) + assignments?: UserAssignment[]; + + // Relation กับ Preferences (1:1) + @OneToOne(() => UserPreference, (pref) => pref.user, { cascade: true }) + preference?: UserPreference; + + // Base Entity Fields @CreateDateColumn({ name: 'created_at' }) createdAt!: Date; diff --git a/backend/src/modules/user/user-preference.service.ts b/backend/src/modules/user/user-preference.service.ts new file mode 100644 index 0000000..05fb865 --- /dev/null +++ b/backend/src/modules/user/user-preference.service.ts @@ -0,0 +1,47 @@ +// File: src/modules/user/user-preference.service.ts +// บันทึกการแก้ไข: Service จัดการการตั้งค่าส่วนตัว (T1.3) + +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserPreference } from './entities/user-preference.entity'; +import { UpdatePreferenceDto } from './dto/update-preference.dto'; + +@Injectable() +export class UserPreferenceService { + constructor( + @InjectRepository(UserPreference) + private prefRepo: Repository, + ) {} + + // ดึง Preference ของ User (ถ้าไม่มีให้สร้าง Default) + async findByUser(userId: number): Promise { + let pref = await this.prefRepo.findOne({ where: { userId } }); + + if (!pref) { + pref = this.prefRepo.create({ + userId, + notifyEmail: true, + notifyLine: true, + digestMode: false, + uiTheme: 'light', + }); + await this.prefRepo.save(pref); + } + + return pref; + } + + // อัปเดต Preference + async update( + userId: number, + dto: UpdatePreferenceDto, + ): Promise { + const pref = await this.findByUser(userId); + + // Merge ข้อมูลใหม่ + this.prefRepo.merge(pref, dto); + + return this.prefRepo.save(pref); + } +} diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts index 4b33738..57cc538 100644 --- a/backend/src/modules/user/user.controller.ts +++ b/backend/src/modules/user/user.controller.ts @@ -1,3 +1,6 @@ +// File: src/modules/user/user.controller.ts +// บันทึกการแก้ไข: เพิ่ม Endpoints สำหรับ User Preferences (T1.3) + import { Controller, Get, @@ -8,47 +11,86 @@ import { Delete, UseGuards, ParseIntPipe, - Request, // <--- อย่าลืม Import Request } from '@nestjs/common'; -import { UserService } from './user.service.js'; -import { CreateUserDto } from './dto/create-user.dto.js'; -import { UpdateUserDto } from './dto/update-user.dto.js'; -import { AssignRoleDto } from './dto/assign-role.dto.js'; // <--- Import DTO -import { UserAssignmentService } from './user-assignment.service.js'; // <--- Import Service ใหม่ +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js'; -import { RbacGuard } from '../../common/guards/rbac.guard.js'; -import { RequirePermission } from '../../common/decorators/require-permission.decorator.js'; +import { UserService } from './user.service'; +import { UserAssignmentService } from './user-assignment.service'; +import { UserPreferenceService } from './user-preference.service'; // ✅ เพิ่ม +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { AssignRoleDto } from './dto/assign-role.dto'; +import { UpdatePreferenceDto } from './dto/update-preference.dto'; // ✅ เพิ่ม DTO +import { JwtAuthGuard } from '../../common/auth/guards/jwt-auth.guard'; +import { RbacGuard } from '../../common/guards/rbac.guard'; // สมมติว่ามีแล้ว ถ้ายังไม่มีให้คอมเมนต์ไว้ก่อน +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { User } from './entities/user.entity'; + +@ApiTags('Users') +@ApiBearerAuth() @Controller('users') -@UseGuards(JwtAuthGuard, RbacGuard) +@UseGuards(JwtAuthGuard, RbacGuard) // RbacGuard จะเช็ค permission export class UserController { constructor( private readonly userService: UserService, - private readonly assignmentService: UserAssignmentService, // <--- ✅ Inject Service เข้ามา + private readonly assignmentService: UserAssignmentService, + private readonly preferenceService: UserPreferenceService, // ✅ Inject Service ) {} - // --- User CRUD --- + // --- User Preferences (Me) --- + // ต้องวางไว้ก่อน :id เพื่อไม่ให้ route ชนกัน + + @Get('me/preferences') + @ApiOperation({ summary: 'Get my preferences' }) + @UseGuards(JwtAuthGuard) // Bypass RBAC check for self + getMyPreferences(@CurrentUser() user: User) { + return this.preferenceService.findByUser(user.user_id); + } + + @Patch('me/preferences') + @ApiOperation({ summary: 'Update my preferences' }) + @UseGuards(JwtAuthGuard) // Bypass RBAC check for self + updateMyPreferences( + @CurrentUser() user: User, + @Body() dto: UpdatePreferenceDto, + ) { + return this.preferenceService.update(user.user_id, dto); + } + + @Get('me/permissions') + @ApiOperation({ summary: 'Get my permissions' }) + @UseGuards(JwtAuthGuard) + getMyPermissions(@CurrentUser() user: User) { + return this.userService.getUserPermissions(user.user_id); + } + + // --- User CRUD (Admin) --- @Post() + @ApiOperation({ summary: 'Create new user' }) @RequirePermission('user.create') create(@Body() createUserDto: CreateUserDto) { return this.userService.create(createUserDto); } @Get() + @ApiOperation({ summary: 'List all users' }) @RequirePermission('user.view') findAll() { return this.userService.findAll(); } @Get(':id') + @ApiOperation({ summary: 'Get user details' }) @RequirePermission('user.view') findOne(@Param('id', ParseIntPipe) id: number) { return this.userService.findOne(id); } @Patch(':id') + @ApiOperation({ summary: 'Update user' }) @RequirePermission('user.edit') update( @Param('id', ParseIntPipe) id: number, @@ -58,6 +100,7 @@ export class UserController { } @Delete(':id') + @ApiOperation({ summary: 'Delete user (Soft delete)' }) @RequirePermission('user.delete') remove(@Param('id', ParseIntPipe) id: number) { return this.userService.remove(id); @@ -65,14 +108,10 @@ export class UserController { // --- Role Assignment --- - @Post('assign-role') // <--- ✅ ต้องมี @ เสมอครับ + @Post('assign-role') + @ApiOperation({ summary: 'Assign role to user' }) @RequirePermission('permission.assign') - assignRole(@Body() dto: AssignRoleDto, @Request() req: any) { - return this.assignmentService.assignRole(dto, req.user); - } - @Get('me/permissions') - @UseGuards(JwtAuthGuard) // No RbacGuard here to avoid circular dependency check issues - getMyPermissions(@Request() req: any) { - return this.userService.getUserPermissions(req.user.user_id); + assignRole(@Body() dto: AssignRoleDto, @CurrentUser() user: User) { + return this.assignmentService.assignRole(dto, user); } } diff --git a/backend/src/modules/user/user.module.ts b/backend/src/modules/user/user.module.ts index f9bc775..c5f12e1 100644 --- a/backend/src/modules/user/user.module.ts +++ b/backend/src/modules/user/user.module.ts @@ -1,24 +1,42 @@ +// File: src/modules/user/user.module.ts +// บันทึกการแก้ไข: รวม UserPreferenceService และ RoleService (T1.3) + import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserService } from './user.service.js'; -import { UserController } from './user.controller.js'; // 1. Import Controller -import { User } from './entities/user.entity.js'; -import { UserAssignmentService } from './user-assignment.service.js'; -import { UserAssignment } from './entities/user-assignment.entity.js'; + +import { UserService } from './user.service'; +import { UserController } from './user.controller'; +import { UserAssignmentService } from './user-assignment.service'; +import { UserPreferenceService } from './user-preference.service'; // ✅ เพิ่ม + +// Entities +import { User } from './entities/user.entity'; +import { UserAssignment } from './entities/user-assignment.entity'; +import { UserPreference } from './entities/user-preference.entity'; +import { Role } from './entities/role.entity'; +import { Permission } from './entities/permission.entity'; @Module({ imports: [ - // 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment - TypeOrmModule.forFeature([User, UserAssignment]), - ], // 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่ + // ลงทะเบียน Entity ให้ครบ + TypeOrmModule.forFeature([ + User, + UserAssignment, + UserPreference, + Role, + Permission, + ]), + ], controllers: [UserController], providers: [ UserService, - UserAssignmentService, // <--- 4. ลงทะเบียน Service เป็น Provider + UserAssignmentService, + UserPreferenceService, // ✅ เพิ่ม Provider ], exports: [ UserService, - UserAssignmentService, // <--- 5. Export เผื่อที่อื่นใช้ - ], // Export ให้ AuthModule เรียกใช้ได้ + UserAssignmentService, + UserPreferenceService, // ✅ Export ให้ Module อื่นใช้ (เช่น Notification) + ], }) export class UserModule {}