From cb6faacba6c9e0a9f61041a89b1d38948ee8a9f1 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 24 Nov 2025 17:01:58 +0700 Subject: [PATCH] 251124:1700 Ready to Phase 7 --- 2_2_phase_6a_workflow_dsl.sql | 16 + 5_Backend_Folder_V1_4_3.md | 416 +++++++++++------- backend/package.json | 11 +- backend/src/app.module.ts | 38 +- backend/src/common/auth/auth.controller.ts | 35 +- backend/src/common/auth/auth.module.ts | 13 +- backend/src/common/auth/auth.service.ts | 32 +- .../auth/strategies/jwt-refresh.strategy.ts | 5 +- .../common/auth/strategies/jwt.strategy.ts | 22 +- backend/src/common/config/redis.config.ts | 12 +- .../decorators/circuit-breaker.decorator.ts | 49 +++ .../src/common/decorators/retry.decorator.ts | 60 +++ .../file-storage/file-cleanup.service.ts | 70 +++ .../file-storage/file-storage.controller.ts | 55 ++- .../file-storage/file-storage.module.ts | 12 +- .../file-storage/file-storage.service.ts | 111 ++++- .../common/guards/maintenance-mode.guard.ts | 2 +- .../interceptors/idempotency.interceptor.ts | 15 +- .../interceptors/performance.interceptor.ts | 90 ++++ .../common/resilience/resilience.module.ts | 9 + .../src/modules/master/dto/create-tag.dto.ts | 19 + .../src/modules/master/dto/search-tag.dto.ts | 26 ++ .../src/modules/master/dto/update-tag.dto.ts | 6 + .../src/modules/master/entities/tag.entity.ts | 27 ++ .../src/modules/master/master.controller.ts | 64 +++ backend/src/modules/master/master.module.ts | 33 ++ backend/src/modules/master/master.service.ts | 97 ++++ .../controllers/health.controller.ts | 45 ++ .../monitoring/logger/winston.config.ts | 30 ++ .../modules/monitoring/monitoring.module.ts | 23 + .../monitoring/services/metrics.service.ts | 54 +++ .../notification/notification.service.ts | 167 +++++-- backend/src/modules/rfa/rfa.module.ts | 28 +- .../modules/transmittal/transmittal.module.ts | 2 + .../src/modules/user/dto/update-user.dto.ts | 24 +- backend/src/modules/user/user.controller.ts | 2 +- backend/src/modules/user/user.service.ts | 65 ++- .../dto/create-workflow-definition.dto.ts | 26 ++ .../dto/evaluate-workflow.dto.ts | 25 ++ .../dto/get-available-actions.dto.ts | 15 + .../dto/update-workflow-definition.dto.ts | 10 + .../entities/workflow-definition.entity.ts | 37 ++ .../workflow-engine/workflow-dsl.service.ts | 203 +++++++++ .../workflow-engine.controller.ts | 65 +++ .../workflow-engine/workflow-engine.module.ts | 19 +- .../workflow-engine.service.ts | 215 +++++++-- temp.md | 80 ++-- 47 files changed, 2047 insertions(+), 433 deletions(-) create mode 100644 2_2_phase_6a_workflow_dsl.sql create mode 100644 backend/src/common/decorators/circuit-breaker.decorator.ts create mode 100644 backend/src/common/decorators/retry.decorator.ts create mode 100644 backend/src/common/file-storage/file-cleanup.service.ts create mode 100644 backend/src/common/interceptors/performance.interceptor.ts create mode 100644 backend/src/common/resilience/resilience.module.ts create mode 100644 backend/src/modules/master/dto/create-tag.dto.ts create mode 100644 backend/src/modules/master/dto/search-tag.dto.ts create mode 100644 backend/src/modules/master/dto/update-tag.dto.ts create mode 100644 backend/src/modules/master/entities/tag.entity.ts create mode 100644 backend/src/modules/master/master.controller.ts create mode 100644 backend/src/modules/master/master.module.ts create mode 100644 backend/src/modules/master/master.service.ts create mode 100644 backend/src/modules/monitoring/controllers/health.controller.ts create mode 100644 backend/src/modules/monitoring/logger/winston.config.ts create mode 100644 backend/src/modules/monitoring/monitoring.module.ts create mode 100644 backend/src/modules/monitoring/services/metrics.service.ts create mode 100644 backend/src/modules/workflow-engine/dto/create-workflow-definition.dto.ts create mode 100644 backend/src/modules/workflow-engine/dto/evaluate-workflow.dto.ts create mode 100644 backend/src/modules/workflow-engine/dto/get-available-actions.dto.ts create mode 100644 backend/src/modules/workflow-engine/dto/update-workflow-definition.dto.ts create mode 100644 backend/src/modules/workflow-engine/entities/workflow-definition.entity.ts create mode 100644 backend/src/modules/workflow-engine/workflow-dsl.service.ts create mode 100644 backend/src/modules/workflow-engine/workflow-engine.controller.ts diff --git a/2_2_phase_6a_workflow_dsl.sql b/2_2_phase_6a_workflow_dsl.sql new file mode 100644 index 0000000..6f8be90 --- /dev/null +++ b/2_2_phase_6a_workflow_dsl.sql @@ -0,0 +1,16 @@ +-- File: database/migrations/phase_6a_workflow_dsl.sql +-- ตารางสำหรับเก็บนิยามของ Workflow (Workflow Definition) +CREATE TABLE workflow_definitions ( + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Workflow Definition', + workflow_code VARCHAR(50) NOT NULL COMMENT 'รหัส Workflow เช่น RFA, CORR, CIRCULATION', + version INT NOT NULL DEFAULT 1 COMMENT 'หมายเลข Version', + dsl JSON NOT NULL COMMENT 'นิยาม Workflow ต้นฉบับ (YAML/JSON Format)', + compiled JSON NOT NULL COMMENT 'โครงสร้าง Execution Tree ที่ Compile แล้ว', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + -- ป้องกันการมี Workflow Code และ Version ซ้ำกัน + UNIQUE KEY uq_workflow_version (workflow_code, version) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บนิยามกฎการเดินเอกสาร (Workflow DSL)'; +-- สร้าง Index สำหรับการค้นหา Workflow ที่ Active ล่าสุดได้เร็วขึ้น +CREATE INDEX idx_workflow_active ON workflow_definitions(workflow_code, is_active, version); \ No newline at end of file diff --git a/5_Backend_Folder_V1_4_3.md b/5_Backend_Folder_V1_4_3.md index 8966546..ce215a2 100644 --- a/5_Backend_Folder_V1_4_3.md +++ b/5_Backend_Folder_V1_4_3.md @@ -8,12 +8,14 @@ * [x] `.env` (สำหรับ Local Dev เท่านั้น ห้าม commit) * [x] `.gitignore` -* [x] `docker-compose.yml` (Configuration หลักสำหรับ Deploy) -* [x] `docker-compose.override.yml` (สำหรับ Inject Secrets ตอน Dev) +* [x] `.prettierrc` +* [x] `docker-compose.override.yml` +* [x] `docker-compose.yml` +* [x] `nest-cli.json` +* [x] `tsconfig.build.json` +* [x] `tsconfig.json` * [x] `package.json` * [x] `pnpm-lock.yaml` -* [x] `tsconfig.json` -* [x] `nest-cli.json` * [x] `README.md` --- @@ -24,6 +26,10 @@ * [x] `main.ts` (Application Bootstrap, Swagger, Global Pipes) * [x] `app.module.ts` (Root Module ที่รวมทุก Modules เข้าด้วยกัน) +* [x] `app.service.ts` (Root Application Service) +* [x] `app.controller.ts` (Root Application Controller) +* [x] `app.controller.spec.ts` (Root Application Controller Unit Tests) +* [x] `redlock.d.ts` (Redlock Configuration) ### **📁 src/common/** (Shared Resources) @@ -32,6 +38,9 @@ * **dto/** * [x] **login.dto.ts** * [x] **register.dto.ts** + * **strategies/** + * [x] **local.strategy.ts** + * [x] **jwt.strategy.ts** * [x] **auth.controller.spec.ts** * [x] **auth.controller.ts** * [x] **auth.module.ts** @@ -39,10 +48,15 @@ * [x] **auth.service.ts** * **config/** (Configuration Service) * [x] **env.validation.ts** + * [x] **redis.config.ts** * **decorators/** * [x] **audit.decorator.ts** + * [x] **bypass-maintenance.decorator.ts** * [x] `current-user.decorator.ts` + * [x] **idempotency.decorator.ts** * [x] `require-permission.decorator.ts` + * [x] **retry.decorator.ts** + * [x] **circuit-breaker.decorator.ts** * **entities/** * [x] **audit-log.entity.ts** * [x] **base.entity.ts** @@ -56,14 +70,21 @@ * [x] **file-storage.module.ts** * [x] **file-storage.service.spec.ts** * [x] `file-storage.service.ts` (Upload, Scan Virus, Commit) + * [x] **file-cleanup.service.ts** (Cleanup Temporary Files) * [x] `guards/` * [x] `jwt-auth.guard.ts` - * [x] **jwt.strategy.ts** + * [x] **jwt-refresh.guard.ts** + * [x] **maintenance-mode.guard.ts** * [x] `rbac.guard.ts` (ตรวจสอบสิทธิ์ 4 ระดับ) * **interceptors/** * [x] `audit-log.interceptor.ts` (เก็บ Log ลง DB) + * [x] **idempotency.interceptor.ts** (Idempotency Interceptor) * [x] `transform.interceptor.ts` (Standard Response Format) * **resilience/** (Circuit Breaker & Retry) + * [x] **resilience.module.ts** (Resilience Module) +* **security/** (Security Service) + * [x] **crypto.service.ts** (Crypto Service) + * [x] **request-context.service.ts ### **📁 src/modules/** (Feature Modules) @@ -71,13 +92,17 @@ * [x] `dto/` * [x] **assign-user-role.dto.ts** * [x] `create-user.dto.ts` + * [ ] **search-user.dto.ts** * [x] `update-user.dto.ts` + * [x] `update-user-preference.dto.ts` * [x] `entities/` * [x] `user.entity.ts` * [x] `role.entity.ts` * [x] `permission.entity.ts` + * [x] **user-assignment.entity.ts** * [x] `user-preference.entity.ts` * [x] **user-assignment.service.ts** + * [x] **user-preference.service.ts** * [x] `user.controller.ts` * [x] `user.module.ts` * [x] `user.service.ts` @@ -146,7 +171,7 @@ * [x] `shop-drawing.controller.ts` * [x] `shop-drawing.service.ts` -4. **rfa/** (Request for Approval & Advanced Workflow) +5. **rfa/** (Request for Approval & Advanced Workflow) * [x] `dto/` * [x] `create-rfa.dto.ts` * [x] `search-rfa.dto.ts` @@ -165,7 +190,7 @@ * [x] `rfa.module.ts` * [x] `rfa.service.ts` (Unified Workflow Integration) -5. **circulation/** (Internal Routing) +6. **circulation/** (Internal Routing) * [x] `dto/` * [x] `create-circulation.dto.ts` * [x] `update-circulation-routing.dto.ts` @@ -178,7 +203,7 @@ * [x] `circulation.module.ts` * [x] `circulation.service.ts` -6. **transmittal/** (Document Forwarding) +7. **transmittal/** (Document Forwarding) * [x] `dto/` * [x] `create-transmittal.dto.ts` * [x] `search-transmittal.dto.ts` @@ -190,7 +215,7 @@ * [x] `transmittal.module.ts` * [x] `transmittal.service.ts` -7. **notification/** (System Alerts) +8. **notification/** (System Alerts) * [x] `dto/` * [x] `create-notification.dto.ts` * [x] `search-notification.dto.ts` @@ -203,13 +228,13 @@ * [x] `notification.processor.ts` (Consumer/Worker for Email & Line) * [x] `notification.service.ts` (Producer) -8. **search/** (Elasticsearch) +9. **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) +10. **document-numbering/** (Internal Service) * [x] `entities/` * [x] `document-number-format.entity.ts` * [x] `document-number-counter.entity.ts` @@ -217,13 +242,19 @@ * [x] **document-numbering.service.spec.ts** * [x] `document-numbering.service.ts` (Double-Lock Mechanism) -10. **workflow-engine/** (Unified Logic) +11. **workflow-engine/** (Unified Logic) + * [x] **dto/** + * [x] `create-workflow-definition.dto.ts` + * [x] `evaluate-workflow.dto.ts` + * [x] `get-available-actions.dto.ts` + * [x] `update-workflow-definition.dto.ts` * [x] `interfaces/workflow.interface.ts` + * [x] **workflow-dsl.service.ts** * [x] `workflow-engine.module.ts` * [x] **workflow-engine.service.spec.ts** * [x] `workflow-engine.service.ts` (State Machine Logic) -11. **json-schema/** (Validation) +12. **json-schema/** (Validation) * [x] `dto/` * [x] `create-json-schema.dto.ts`+ * [x] `search-json-schema.dto.ts` @@ -236,6 +267,16 @@ * [x] **json-schema.service.spec.ts** * [x] `json-schema.service.ts` +13. **monitoring/** (Monitoring & Metrics) + * [x] `controllers/` + * [x] `health.controller.ts` + * [x] `logger/` + * [x] `winston.config.ts` + * [x] `services/` + * [x] `metrics.service.ts` + * [x] `monitoring.module.ts` + + ## **Folder Structure ของ Backend (NestJS)** ที่ --- @@ -244,40 +285,53 @@ ```text backend/ -├── .env # Environment variables for local development only (not committed) -├── .gitignore # Git ignore rules -├── docker-compose.yml # Main deployment container configuration +├── .env # Environment variables for local development only (not committed) +├── .gitignore # Git ignore rules +├── .prettierrc # Prettier configuration +├── 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 +├── package.json # Node dependencies and NPM scripts +├── pnpm-lock.yaml # Dependency lock file for pnpm +├── tsconfig.json # TypeScript compiler configuration +├── tsconfig.build.json # TypeScript compiler configuration for production +├── nest-cli.json # NestJS project configuration +├── README.md # Project documentation └── src/ - ├── main.ts # Application bootstrap and initialization - ├── app.module.ts # Root application module + ├── main.ts # Application bootstrap and initialization + ├── app.module.ts # Root application module + ├── app.service.ts # Root application service + ├── app.controller.ts # Root application controller + ├── app.controller.spec.ts # Root application unit tests + ├── redlock.d.ts # Redlock configuration │ - │ - ├── common/ # 🛠️ Shared framework resources used across modules - │ ├── common.module.ts # Registers shared providers + ├── common/ # 🛠️ Shared framework resources used across modules + │ ├── common.module.ts # Registers shared providers │ │ - │ ├── auth/ # 🛡️ Authentication module + │ ├── auth/ # 🛡️ Authentication module │ │ ├── dto/ │ │ │ ├── login.dto.ts # Login request payload │ │ │ └── register.dto.ts # Registration payload + │ │ ├── strategies/ + │ │ │ ├── local.strategy.ts # Local strategy for authentication + │ │ │ └── jwt.strategy.ts # JWT strategy for authentication │ │ ├── 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 + │ ├── config/ # 📄 Configuration + │ │ ├── env.validation.ts # Zod/Joi validation for environment variables + │ │ └── redis.config.ts # Redis configuration │ │ - │ ├── decorators/ # 📡 Decorators for common use cases + │ ├── decorators/ # 📡 Decorators for common use cases │ │ ├── audit.decorator.ts # Enables audit logging for a method + │ │ ├── bypass-maintenance.decorator.ts # Declares bypass maintenance requirement │ │ ├── current-user.decorator.ts # Extracts logged-in user from request - │ │ └── require-permission.decorator.ts # Declares RBAC permission requirement + │ │ ├── idempotency.decorator.ts # Declares idempotency requirement + │ │ ├── require-permission.decorator.ts # Declares RBAC permission requirement + │ │ ├── retry.decorator.ts # Declares retry requirement + │ │ └── circuit-breaker.decorator.ts # Declares circuit breaker requirement │ │ │ ├── entities/ # 📚 Database entities │ │ ├── audit-log.entity.ts # Audit log database entity @@ -293,186 +347,222 @@ backend/ │ │ ├── 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 + │ │ ├── file-storage.service.spec.ts # Unit tests + │ │ └── file-cleanup.service.ts # Cleanup temporary files │ │ │ ├── guards/ # 🛡️ JWT authentication guard │ │ ├── jwt-auth.guard.ts # JWT authentication guard - │ │ ├── jwt.strategy.ts # JWT strategy configuration + │ │ ├── jwt-refresh.guard.ts # JWT refresh guard + │ │ ├── maintenance-mode.guard.ts # Maintenance mode guard │ │ └── rbac.guard.ts # Role-based access control enforcement │ │ - │ ├── interceptors/ # 📡 Interceptors for common use cases + │ ├── interceptors/ # 📡 Interceptors for common use cases │ │ ├── audit-log.interceptor.ts # Automatically logs certain operations + │ │ ├── idempotency.interceptor.ts # Idempotency interceptor │ │ └── transform.interceptor.ts # Standardized response formatting │ │ - │ └── resilience/ # 🛡️ Circuit-breaker / retry logic (if implemented) - │ + │ ├─── resilience/ # 🛡️ Circuit-breaker / retry logic (if implemented) + │ │ └── resilience.module.ts # Resilience module + │ │ + │ └──── security/ # 🔐 Security service + │ ├── crypto.service.ts # Crypto service + │ └── request-context.service.ts # Request context service (for logging) │ ├── 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 + │ │ │ ├── create-user.dto.ts # Create new user + │ │ │ ├── search-user.dto.ts # Search users + │ │ │ ├── update-user.dto.ts # Update user details + │ │ │ └── update-user-preference.dto.ts # Update user preferences │ │ ├── entities/ │ │ │ ├── user.entity.ts # User table definition │ │ │ ├── role.entity.ts # Role definition │ │ │ ├── permission.entity.ts # Permission entity + │ │ │ ├── user-assignment.entity.ts # User assignment entity │ │ │ └── user-preference.entity.ts # User preference settings + │ │ ├── user-assignment.service.ts # User assignment service + │ │ ├── user-preference.service.ts # User preference service │ │ ├── user.controller.ts # REST endpoints + │ │ ├── user.module.ts # Module DI container │ │ ├── user.service.ts # Business logic - │ │ ├── user.service.spec.ts # Unit tests - │ │ └── user.module.ts # Module DI container + │ │ └── user.service.spec.ts # Unit tests │ │ - │ ├── project/ # 🏢 Project/Organization/Contract structure + │ ├── project/ # 🏢 Project/Organization/Contract structure │ │ ├── dto/ - │ │ │ ├── create-project.dto.ts - │ │ │ ├── search-project.dto.ts - │ │ │ └── update-project.dto.ts + │ │ │ ├── create-project.dto.ts # Create new project + │ │ │ ├── search-project.dto.ts # Search projects + │ │ │ └── update-project.dto.ts # Update project │ │ ├── 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 - │ + │ │ │ ├── project.entity.ts # Project table definition + │ │ │ ├── contract.entity.ts # Contract table definition + │ │ │ ├── organization.entity.ts # Organization table definition + │ │ │ ├── project-organization.entity.ts # Project organization entity + │ │ │ └── contract-organization.entity.ts # Contract organization entity + │ │ ├── project.controller.ts # REST endpoints + │ │ ├── project.controller.spec.ts # Unit tests + │ │ ├── project.module.ts # Module DI container + │ │ ├── project.service.ts # Business logic + │ │ └── project.service.spec.ts # Unit tests + │ │ │ ├── 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 + │ │ │ ├── add-reference.dto.ts # Add reference to correspondence + │ │ │ ├── create-correspondence.dto.ts # Create new correspondence + │ │ │ ├── search-correspondence.dto.ts # Search correspondences + │ │ │ ├── submit-correspondence.dto.ts # Submit correspondence + │ │ │ └── workflow-action.dto.ts # Workflow action │ │ ├── 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 - │ + │ │ │ ├── correspondence.entity.ts # Correspondence table definition + │ │ │ ├── correspondence-revision.entity.ts # Correspondence revision entity + │ │ │ ├── correspondence-routing.entity.ts # Correspondence routing entity + │ │ │ ├── correspondence-status.entity.ts # Correspondence status entity + │ │ │ ├── correspondence-type.entity.ts # Correspondence type entity + │ │ │ ├── correspondence-reference.entity.ts # Correspondence reference entity + │ │ │ ├── routing-template.entity.ts # Routing template entity + │ │ │ └── routing-template-step.entity.ts # Routing template step entity + │ │ ├── correspondence.controller.ts # REST endpoints + │ │ ├── correspondence.controller.spec.ts # Unit tests + │ │ ├── correspondence.module.ts # Module DI container + │ │ ├── correspondence.service.ts # Business logic + │ │ └── correspondence.service.spec.ts # Unit tests + │ │ │ ├── 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 + │ │ │ ├── create-contract-drawing.dto.ts # Create new contract drawing + │ │ │ ├── create-shop-drawing.dto.ts # Create new shop drawing + │ │ │ ├── create-shop-drawing-revision.dto.ts # Create new shop drawing revision + │ │ │ ├── search-contract-drawing.dto.ts # Search contract drawings + │ │ │ ├── search-shop-drawing.dto.ts # Search shop drawings + │ │ │ └── update-contract-drawing.dto.ts # Update contract drawing │ │ ├── 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 - │ + │ │ │ ├── contract-drawing.entity.ts # Contract drawing entity + │ │ │ ├── contract-drawing-volume.entity.ts # Contract drawing volume entity + │ │ │ ├── contract-drawing-sub-category.entity.ts # Contract drawing sub category entity + │ │ │ ├── shop-drawing.entity.ts # Shop drawing entity + │ │ │ ├── shop-drawing-revision.entity.ts # Shop drawing revision entity + │ │ │ ├── shop-drawing-main-category.entity.ts # Shop drawing main category entity + │ │ │ └── shop-drawing-sub-category.entity.ts # Shop drawing sub category entity + │ │ ├── drawing.module.ts # Module DI container + │ │ ├── contract-drawing.controller.ts # REST endpoints + │ │ ├── contract-drawing.service.ts # Business logic + │ │ ├── drawing-master-data.controller.ts # REST endpoints + │ │ ├── drawing-master-data.service.ts # Business logic + │ │ ├── shop-drawing.controller.ts # REST endpoints + │ │ └── shop-drawing.service.ts # Business logic + │ │ │ ├── rfa/ # ✅ Request for Approval (multi-step workflow) │ │ ├── dto/ - │ │ │ ├── create-rfa.dto.ts - │ │ │ ├── search-rfa.dto.ts - │ │ │ └── update-rfa.dto.ts + │ │ │ ├── create-rfa.dto.ts # Create new RFA + │ │ │ ├── search-rfa.dto.ts # Search RFAs + │ │ │ └── update-rfa.dto.ts # Update RFA │ │ ├── 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 - │ + │ │ │ ├── rfa.entity.ts # RFA entity + │ │ │ ├── rfa-revision.entity.ts # RFA revision entity + │ │ │ ├── rfa-item.entity.ts # RFA item entity + │ │ │ ├── rfa-type.entity.ts # RFA type entity + │ │ │ ├── rfa-status-code.entity.ts # RFA status code entity + │ │ │ ├── rfa-approve-code.entity.ts # RFA approve code entity + │ │ │ ├── rfa-workflow.entity.ts # RFA workflow entity + │ │ │ ├── rfa-workflow-template.entity.ts # RFA workflow template entity + │ │ │ └── rfa-workflow-template-step.entity.ts # RFA workflow template step entity + │ │ ├── rfa.controller.ts # REST endpoints + │ │ ├── rfa.module.ts # Module DI container + │ │ └── rfa.service.ts # Business logic + │ │ │ ├── circulation/ # 🔄 Internal routing workflow │ │ ├── dto/ - │ │ │ ├── create-circulation.dto.ts - │ │ │ ├── update-circulation-routing.dto.ts - │ │ │ └── search-circulation.dto.ts + │ │ │ ├── create-circulation.dto.ts # Create new circulation + │ │ │ ├── update-circulation-routing.dto.ts # Update circulation routing + │ │ │ └── search-circulation.dto.ts # Search circulation │ │ ├── entities/ - │ │ │ ├── circulation.entity.ts - │ │ │ ├── circulation-routing.entity.ts - │ │ │ └── circulation-status-code.entity.ts - │ │ ├── circulation.controller.ts - │ │ ├── circulation.module.ts - │ │ └── circulation.service.ts - │ + │ │ │ ├── circulation.entity.ts # Circulation entity + │ │ │ ├── circulation-routing.entity.ts # Circulation routing entity + │ │ │ └── circulation-status-code.entity.ts # Circulation status code entity + │ │ ├── circulation.controller.ts # REST endpoints + │ │ ├── circulation.module.ts # Module DI container + │ │ └── circulation.service.ts # Business logic + │ │ │ ├── transmittal/ # 📤 Document forwarding │ │ ├── dto/ - │ │ │ ├── create-transmittal.dto.ts - │ │ │ ├── search-transmittal.dto.ts - │ │ │ └── update-transmittal.dto.ts + │ │ │ ├── create-transmittal.dto.ts # Create new transmittal + │ │ │ ├── search-transmittal.dto.ts # Search transmittal + │ │ │ └── update-transmittal.dto.ts # Update transmittal │ │ ├── entities/ - │ │ │ ├── transmittal.entity.ts - │ │ │ └── transmittal-item.entity.ts - │ │ ├── transmittal.controller.ts - │ │ ├── transmittal.module.ts - │ │ └── transmittal.service.ts - │ + │ │ │ ├── transmittal.entity.ts # Transmittal entity + │ │ │ └── transmittal-item.entity.ts # Transmittal item entity + │ │ ├── transmittal.controller.ts # REST endpoints + │ │ ├── transmittal.module.ts # Module DI container + │ │ └── transmittal.service.ts # Business logic + │ │ │ ├── notification/ # 🔔 Real-Time notification system │ │ ├── dto/ - │ │ │ ├── create-notification.dto.ts - │ │ │ └── search-notification.dto.ts + │ │ │ ├── create-notification.dto.ts # Create new notification + │ │ │ └── search-notification.dto.ts # Search notification │ │ ├── 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 - │ + │ │ │ └── notification.entity.ts # Notification entity + │ │ ├── notification.module.ts # WebSocket + Processor registration + │ │ ├── notification.controller.ts # REST endpoints + │ │ ├── notification.gateway.ts # WebSocket gateway + │ │ ├── notification.processor.ts # Message consumer (e.g. mail worker) + │ │ ├── notification.service.ts # Business logic + │ │ └── 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 - │ + │ │ │ └── search-query.dto.ts # Search query + │ │ ├── search.module.ts # Module DI container + │ │ ├── search.controller.ts # REST endpoints + │ │ └── 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 - │ + │ │ │ ├── document-number-format.entity.ts # Document number format entity + │ │ │ └── document-number-counter.entity.ts # Document number counter entity + │ │ ├── document-numbering.module.ts # Module DI container + │ │ ├── document-numbering.service.ts # Business logic + │ │ └── document-numbering.service.spec.ts # Unit tests + │ │ │ ├── workflow-engine/ # ⚙️ Unified state-machine workflow engine + │ │ ├── dto/ + │ │ │ ├── create-workflow-definition.dto.ts # Create new workflow definition + │ │ │ ├── evaluate-workflow.dto.ts # Evaluate workflow + │ │ │ ├── get-available-actions.dto.ts # Get available actions + │ │ │ └── update-workflow-definition.dto.ts # Update workflow definition + │ │ ├── entities/ + │ │ │ └── workflow-definition.entity.ts # Workflow definition entity │ │ ├── interfaces/ - │ │ │ └── workflow.interface.ts - │ │ ├── workflow-engine.module.ts - │ │ ├── workflow-engine.service.ts - │ │ └── workflow-engine.service.spec.ts + │ │ │ └── workflow.interface.ts # Workflow interface + │ │ ├── workflow-engine.controller.ts # REST endpoints + │ │ ├── workflow-engine.module.ts # Module DI container + │ │ ├── workflow-engine.service.ts # Business logic + │ │ └── workflow-engine.service.spec.ts # Unit tests + │ │ + │ ├── json-schema/ # 📋 Dynamic request schema validation + │ │ ├── dto/ + │ │ │ ├── create-json-schema.dto.ts # Create new JSON schema + │ │ │ ├── update-json-schema.dto.ts # Update JSON schema + │ │ │ └── search-json-schema.dto.ts # Search JSON schema + │ │ ├── entities/ + │ │ │ └── json-schema.entity.ts # JSON schema entity + │ │ ├── json-schema.module.ts # Module DI container + │ │ ├── json-schema.controller.ts # REST endpoints + │ │ ├── json-schema.controller.spec.ts # Unit tests + │ │ ├── json-schema.service.ts # Business logic + │ │ └── json-schema.service.spec.ts # Unit tests + │ │ + │ └── monitoring/ # 📋 Dynamic request schema validation + │ ├── controllers/ + │ │ ├── health.controller.ts # Create new JSON schema + │ │ ├── update-json-schema.dto.ts # Update JSON schema + │ │ └── search-json-schema.dto.ts # Search JSON schema + │ ├── logger/ + │ │ └── winston.config.ts # JSON schema entity + │ ├── services/ + │ │ └── metrics.service.ts # JSON schema entity + │ └── monitoring.module.ts # Module DI container │ - │ └── 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/backend/package.json b/backend/package.json index 04791cc..fd76484 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,6 +22,7 @@ "dependencies": { "@casl/ability": "^6.7.3", "@elastic/elasticsearch": "^8.11.1", + "@nestjs/axios": "^4.0.1", "@nestjs/bullmq": "^11.0.4", "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", @@ -35,12 +36,14 @@ "@nestjs/platform-socket.io": "^11.1.9", "@nestjs/schedule": "^6.0.1", "@nestjs/swagger": "^11.2.3", + "@nestjs/terminus": "^11.0.0", "@nestjs/throttler": "^6.4.0", "@nestjs/typeorm": "^11.0.0", "@nestjs/websockets": "^11.1.9", "@types/nodemailer": "^7.0.4", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", + "async-retry": "^1.3.3", "axios": "^1.13.2", "bcrypt": "^6.0.0", "bullmq": "^5.63.2", @@ -54,16 +57,20 @@ "joi": "^18.0.1", "multer": "^2.0.2", "mysql2": "^3.15.3", + "nest-winston": "^1.10.2", "nodemailer": "^7.0.10", + "opossum": "^9.0.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "prom-client": "^15.1.3", "redlock": "5.0.0-beta.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.27", - "uuid": "^13.0.0" + "uuid": "^13.0.0", + "winston": "^3.18.3" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -71,6 +78,7 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", + "@types/async-retry": "^1.4.9", "@types/bcrypt": "^6.0.0", "@types/cache-manager": "^5.0.0", "@types/express": "^5.0.0", @@ -79,6 +87,7 @@ "@types/jest": "^30.0.0", "@types/multer": "^2.0.0", "@types/node": "^22.10.7", + "@types/opossum": "^8.1.9", "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "@types/uuid": "^11.0.0", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index b65c469..1634626 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,5 +1,6 @@ // File: src/app.module.ts // บันทึกการแก้ไข: เพิ่ม CacheModule (Redis), Config สำหรับ Idempotency และ Maintenance Mode (T1.1) +// บันทึกการแก้ไข: เพิ่ม MonitoringModule และ WinstonModule (T6.3) import { Module } from '@nestjs/common'; import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; @@ -8,18 +9,19 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BullModule } from '@nestjs/bullmq'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { CacheModule } from '@nestjs/cache-manager'; +import { WinstonModule } from 'nest-winston'; // ✅ Import WinstonModule 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'; import redisConfig from './common/config/redis.config'; +import { winstonConfig } from './modules/monitoring/logger/winston.config'; // ✅ Import 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) +// import { IdempotencyInterceptor } from './common/interceptors/idempotency.interceptor'; // Modules import { UserModule } from './modules/user/user.module'; @@ -30,7 +32,16 @@ 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'; - +import { RfaModule } from './modules/rfa/rfa.module'; +import { DrawingModule } from './modules/drawing/drawing.module'; +import { TransmittalModule } from './modules/transmittal/transmittal.module'; +import { CirculationModule } from './modules/circulation/circulation.module'; +import { NotificationModule } from './modules/notification/notification.module'; +// ✅ Import Monitoring Module +import { MonitoringModule } from './modules/monitoring/monitoring.module'; +import { ResilienceModule } from './common/resilience/resilience.module'; // ✅ Import +// ... imports +import { SearchModule } from './modules/search/search.module'; // ✅ Import @Module({ imports: [ // 1. Setup Config Module พร้อม Validation @@ -68,6 +79,9 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo inject: [ConfigService], }), + // 📝 Setup Winston Logger (Structured Logging) [Req 6.10] + WinstonModule.forRoot(winstonConfig), + // 2. Setup TypeORM (MariaDB) TypeOrmModule.forRootAsync({ imports: [ConfigModule], @@ -84,7 +98,7 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo }), }), - // ✅ 4. Register AuditLog Entity (Global Scope) + // Register AuditLog Entity (Global Scope) TypeOrmModule.forFeature([AuditLog]), // 3. BullMQ (Redis) Setup @@ -100,6 +114,9 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo }), }), + // 📊 Register Monitoring Module (Health & Metrics) [Req 6.10] + MonitoringModule, + // Feature Modules AuthModule, UserModule, @@ -109,16 +126,23 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo JsonSchemaModule, WorkflowEngineModule, CorrespondenceModule, + RfaModule, // 👈 ต้องมี + DrawingModule, // 👈 ต้องมี + TransmittalModule, // 👈 ต้องมี + CirculationModule, // 👈 ต้องมี + SearchModule, // ✅ Register Module + NotificationModule, // 👈 ต้องมี + ResilienceModule, // ✅ Register Module ], controllers: [AppController], providers: [ AppService, - // 🛡️ 1. Register Global Guard (Rate Limit) - ทำงานก่อนเพื่อน + // 🛡️ 1. Register Global Guard (Rate Limit) { provide: APP_GUARD, useClass: ThrottlerGuard, }, - // 🚧 2. Maintenance Mode Guard - ทำงานต่อมา เพื่อ Block การเข้าถึงถ้าระบบปิดอยู่ + // 🚧 2. Maintenance Mode Guard { provide: APP_GUARD, useClass: MaintenanceModeGuard, @@ -128,7 +152,7 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo provide: APP_INTERCEPTOR, useClass: AuditLogInterceptor, }, - // 🔄 4. Register Idempotency (Uncomment เมื่อต้องการบังคับใช้ Global) + // 🔄 4. Register Idempotency (ถ้าต้องการ Global) // { // provide: APP_INTERCEPTOR, // useClass: IdempotencyInterceptor, diff --git a/backend/src/common/auth/auth.controller.ts b/backend/src/common/auth/auth.controller.ts index 6e1a343..43e55c1 100644 --- a/backend/src/common/auth/auth.controller.ts +++ b/backend/src/common/auth/auth.controller.ts @@ -1,5 +1,5 @@ // File: src/common/auth/auth.controller.ts -// บันทึกการแก้ไข: เพิ่ม Endpoints ให้ครบตามแผน T1.2 (Refresh, Logout, Profile) +// บันทึกการแก้ไข: เพิ่ม Type ให้ req และแก้ไข Import (Fix TS7006) import { Controller, @@ -8,7 +8,7 @@ import { Get, UseGuards, UnauthorizedException, - Request, + Req, HttpCode, HttpStatus, } from '@nestjs/common'; @@ -16,9 +16,15 @@ 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) +import { JwtAuthGuard } from '../guards/jwt-auth.guard.js'; +import { JwtRefreshGuard } from '../guards/jwt-refresh.guard.js'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { Request } from 'express'; // ✅ Import Request + +// สร้าง Interface สำหรับ Request ที่มี User (เพื่อให้ TS รู้จัก req.user) +interface RequestWithUser extends Request { + user: any; +} @ApiTags('Authentication') @Controller('auth') @@ -26,7 +32,7 @@ export class AuthController { constructor(private authService: AuthService) {} @Post('login') - @Throttle({ default: { limit: 5, ttl: 60000 } }) // เข้มงวด: 5 ครั้ง/นาที + @Throttle({ default: { limit: 5, ttl: 60000 } }) @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'เข้าสู่ระบบเพื่อรับ Access & Refresh Token' }) async login(@Body() loginDto: LoginDto) { @@ -43,7 +49,7 @@ export class AuthController { } @Post('register-admin') - @UseGuards(JwtAuthGuard) // ควรป้องกัน Route นี้ให้เฉพาะ Superadmin + @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'สร้างบัญชีผู้ใช้ใหม่ (Admin Only)' }) async register(@Body() registerDto: RegisterDto) { @@ -54,8 +60,8 @@ export class AuthController { @Post('refresh') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'ขอ Access Token ใหม่ด้วย Refresh Token' }) - async refresh(@Request() req) { - // req.user จะมาจาก JwtRefreshStrategy + async refresh(@Req() req: RequestWithUser) { + // ✅ ระบุ Type ชัดเจน return this.authService.refreshToken(req.user.sub, req.user.refreshToken); } @@ -64,9 +70,13 @@ export class AuthController { @HttpCode(HttpStatus.OK) @ApiBearerAuth() @ApiOperation({ summary: 'ออกจากระบบ (Revoke Token)' }) - async logout(@Request() req) { - // ดึง Token จาก Header Authorization: Bearer + async logout(@Req() req: RequestWithUser) { + // ✅ ระบุ Type ชัดเจน const token = req.headers.authorization?.split(' ')[1]; + // ต้องเช็คว่ามี token หรือไม่ เพื่อป้องกัน runtime error + if (!token) { + return { message: 'No token provided' }; + } return this.authService.logout(req.user.sub, token); } @@ -74,7 +84,8 @@ export class AuthController { @Get('profile') @ApiBearerAuth() @ApiOperation({ summary: 'ดึงข้อมูลผู้ใช้ปัจจุบัน' }) - getProfile(@Request() req) { + getProfile(@Req() req: RequestWithUser) { + // ✅ ระบุ Type ชัดเจน return req.user; } } diff --git a/backend/src/common/auth/auth.module.ts b/backend/src/common/auth/auth.module.ts index 859e6bf..dc75759 100644 --- a/backend/src/common/auth/auth.module.ts +++ b/backend/src/common/auth/auth.module.ts @@ -1,5 +1,5 @@ // File: src/common/auth/auth.module.ts -// บันทึกการแก้ไข: ลงทะเบียน Refresh Strategy และแก้ไข Config +// บันทึกการแก้ไข: แก้ไข Type Mismatch ของ expiresIn (Fix TS2322) import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @@ -21,17 +21,14 @@ import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy.js'; useFactory: async (configService: ConfigService) => ({ secret: configService.get('JWT_SECRET'), signOptions: { - // ใช้ Template String หรือค่า Default ที่ปลอดภัย - expiresIn: configService.get('JWT_EXPIRATION') || '15m', + // ✅ Fix: Cast เป็น any เพื่อแก้ปัญหา Type ไม่ตรงกับ Library (StringValue vs string) + expiresIn: (configService.get('JWT_EXPIRATION') || + '15m') as any, }, }), }), ], - providers: [ - AuthService, - JwtStrategy, - JwtRefreshStrategy, // ✅ เพิ่ม Strategy สำหรับ Refresh Token - ], + providers: [AuthService, JwtStrategy, JwtRefreshStrategy], controllers: [AuthController], exports: [AuthService], }) diff --git a/backend/src/common/auth/auth.service.ts b/backend/src/common/auth/auth.service.ts index 2029b82..f974ff8 100644 --- a/backend/src/common/auth/auth.service.ts +++ b/backend/src/common/auth/auth.service.ts @@ -1,5 +1,5 @@ // File: src/common/auth/auth.service.ts -// บันทึกการแก้ไข: เพิ่ม Refresh Token, Logout (Redis Blacklist) และ Profile ตาม T1.2 +// บันทึกการแก้ไข: แก้ไข Type Mismatch ใน signAsync (Fix TS2769) import { Injectable, @@ -10,11 +10,10 @@ import { import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { Cache } from 'cache-manager'; +import type { 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 { User } from '../../modules/user/entities/user.entity.js'; @Injectable() export class AuthService { @@ -22,7 +21,7 @@ export class AuthService { private userService: UserService, private jwtService: JwtService, private configService: ConfigService, - @Inject(CACHE_MANAGER) private cacheManager: Cache, // ใช้ Redis สำหรับ Blacklist + @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} // 1. ตรวจสอบ Username/Password @@ -41,31 +40,33 @@ export class AuthService { const payload = { username: user.username, sub: user.user_id, - scope: 'Global', // ตัวอย่าง: ใส่ Scope เริ่มต้น หรือดึงจาก Role + scope: 'Global', }; const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync(payload, { secret: this.configService.get('JWT_SECRET'), - expiresIn: this.configService.get('JWT_EXPIRATION') || '15m', + // ✅ Fix: Cast as any + expiresIn: (this.configService.get('JWT_EXPIRATION') || + '15m') as any, }), this.jwtService.signAsync(payload, { secret: this.configService.get('JWT_REFRESH_SECRET'), - expiresIn: - this.configService.get('JWT_REFRESH_EXPIRATION') || '7d', + // ✅ Fix: Cast as any + expiresIn: (this.configService.get('JWT_REFRESH_EXPIRATION') || + '7d') as any, }), ]); return { access_token: accessToken, refresh_token: refreshToken, - user: user, // ส่งข้อมูล user กลับไปให้ Frontend ใช้แสดงผลเบื้องต้น + user: user, }; } // 3. Register (สำหรับ Admin) async register(userDto: RegisterDto) { - // ตรวจสอบว่ามี user อยู่แล้วหรือไม่ const existingUser = await this.userService.findOneByUsername( userDto.username, ); @@ -84,33 +85,30 @@ export class AuthService { // 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', + // ✅ Fix: Cast as any + expiresIn: (this.configService.get('JWT_EXPIRATION') || + '15m') as any, }); 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, diff --git a/backend/src/common/auth/strategies/jwt-refresh.strategy.ts b/backend/src/common/auth/strategies/jwt-refresh.strategy.ts index 7e35eb8..f906a3a 100644 --- a/backend/src/common/auth/strategies/jwt-refresh.strategy.ts +++ b/backend/src/common/auth/strategies/jwt-refresh.strategy.ts @@ -1,5 +1,6 @@ // File: src/common/auth/strategies/jwt-refresh.strategy.ts // บันทึกการแก้ไข: Strategy สำหรับ Refresh Token (T1.2) +// บันทึกการแก้ไข: แก้ไข TS2345 โดยยืนยันค่า secretOrKey ด้วย ! (Non-null assertion) import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; @@ -16,8 +17,8 @@ export class JwtRefreshStrategy extends PassportStrategy( super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - // ใช้ Secret แยกต่างหากสำหรับ Refresh Token - secretOrKey: configService.get('JWT_REFRESH_SECRET'), + // ✅ Fix: ใส่ ! เพื่อบอก TS ว่าค่านี้มีอยู่จริง (จาก env validation) + secretOrKey: configService.get('JWT_REFRESH_SECRET')!, passReqToCallback: true, }); } diff --git a/backend/src/common/auth/strategies/jwt.strategy.ts b/backend/src/common/auth/strategies/jwt.strategy.ts index 0f94b13..6c92304 100644 --- a/backend/src/common/auth/strategies/jwt.strategy.ts +++ b/backend/src/common/auth/strategies/jwt.strategy.ts @@ -1,12 +1,11 @@ -// File: src/common/auth/strategies/jwt.strategy.ts -// บันทึกการแก้ไข: ปรับปรุง JwtStrategy ให้ตรวจสอบ Blacklist (Redis) และสถานะ User (T1.2) +// บันทึกการแก้ไข: แก้ไข TS2345 (secretOrKey type) และ TS2551 (user.isActive property name) 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 { CACHE_MANAGER } from '@nestjs/cache-manager'; +import type { Cache } from 'cache-manager'; import { Request } from 'express'; import { UserService } from '../../../modules/user/user.service.js'; @@ -14,7 +13,7 @@ import { UserService } from '../../../modules/user/user.service.js'; export interface JwtPayload { sub: number; username: string; - scope?: string; // เพิ่ม Scope ถ้ามีการใช้ + scope?: string; } @Injectable() @@ -22,13 +21,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) { constructor( configService: ConfigService, private userService: UserService, - @Inject(CACHE_MANAGER) private cacheManager: Cache, // ✅ Inject Redis Cache + @Inject(CACHE_MANAGER) private cacheManager: Cache, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: configService.get('JWT_SECRET'), - passReqToCallback: true, // ✅ จำเป็นต้องใช้ เพื่อดึง Raw Token มาเช็ค Blacklist + // ✅ Fix TS2345: ใส่ ! เพื่อยืนยันว่า Secret Key มีค่าแน่นอน + secretOrKey: configService.get('JWT_SECRET')!, + passReqToCallback: true, }); } @@ -36,7 +36,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { // 1. ดึง Token ออกมาเพื่อตรวจสอบใน Blacklist const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req); - // 2. ตรวจสอบว่า Token นี้อยู่ใน Redis Blacklist หรือไม่ (กรณี Logout ไปแล้ว) + // 2. ตรวจสอบว่า Token นี้อยู่ใน Redis Blacklist หรือไม่ const isBlacklisted = await this.cacheManager.get( `blacklist:token:${token}`, ); @@ -53,11 +53,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) { } // 5. (Optional) ตรวจสอบว่า User ยัง Active อยู่หรือไม่ - if (user.is_active === false || user.is_active === 0) { + // ✅ Fix TS2551: แก้ไขชื่อ Property จาก is_active เป็น isActive ตาม Entity Definition + if (user.isActive === false) { throw new UnauthorizedException('User account is inactive'); } - // คืนค่า User เพื่อนำไปใส่ใน req.user return user; } } diff --git a/backend/src/common/config/redis.config.ts b/backend/src/common/config/redis.config.ts index 556abe4..fb32548 100644 --- a/backend/src/common/config/redis.config.ts +++ b/backend/src/common/config/redis.config.ts @@ -1,11 +1,15 @@ // File: src/common/config/redis.config.ts // บันทึกการแก้ไข: สร้าง Config สำหรับ Redis (T0.2) +// บันทึกการแก้ไข: แก้ไข TS2345 โดยการจัดการค่า undefined ของ process.env ก่อน parseInt 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 + // ใช้ค่า Default 'cache' ถ้าหาไม่เจอ + host: process.env.REDIS_HOST || 'cache', + // ✅ Fix: ใช้ || '6379' เพื่อให้มั่นใจว่าเป็น string ก่อนเข้า parseInt + port: parseInt(process.env.REDIS_PORT || '6379', 10), + // ✅ Fix: ใช้ || '3600' เพื่อให้มั่นใจว่าเป็น string + ttl: parseInt(process.env.REDIS_TTL || '3600', 10), + // password: process.env.REDIS_PASSWORD, })); diff --git a/backend/src/common/decorators/circuit-breaker.decorator.ts b/backend/src/common/decorators/circuit-breaker.decorator.ts new file mode 100644 index 0000000..eae19ab --- /dev/null +++ b/backend/src/common/decorators/circuit-breaker.decorator.ts @@ -0,0 +1,49 @@ +// File: src/common/resilience/decorators/circuit-breaker.decorator.ts +import CircuitBreaker from 'opossum'; // ✅ เปลี่ยนเป็น Default Import (ถ้าลง @types/opossum แล้วจะผ่าน) +import { Logger } from '@nestjs/common'; + +export interface CircuitBreakerOptions { + timeout?: number; + errorThresholdPercentage?: number; + resetTimeout?: number; + fallback?: (...args: any[]) => any; +} + +/** + * Decorator สำหรับ Circuit Breaker + * ใช้ป้องกัน System Overload เมื่อ External Service ล่ม + */ +export function UseCircuitBreaker(options: CircuitBreakerOptions = {}) { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + const originalMethod = descriptor.value; + const logger = new Logger('CircuitBreakerDecorator'); + + // สร้าง Opossum Circuit Breaker Instance + const breaker = new CircuitBreaker(originalMethod, { + timeout: options.timeout || 3000, + errorThresholdPercentage: options.errorThresholdPercentage || 50, + resetTimeout: options.resetTimeout || 10000, + }); + + breaker.on('open', () => logger.warn(`Circuit OPEN for ${propertyKey}`)); + breaker.on('halfOpen', () => + logger.log(`Circuit HALF-OPEN for ${propertyKey}`), + ); + breaker.on('close', () => logger.log(`Circuit CLOSED for ${propertyKey}`)); + + if (options.fallback) { + breaker.fallback(options.fallback); + } + + descriptor.value = async function (...args: any[]) { + // ✅ ใช้ .fire โดยส่ง this context ให้ถูกต้อง + return breaker.fire.apply(breaker, [this, ...args]); + }; + + return descriptor; + }; +} diff --git a/backend/src/common/decorators/retry.decorator.ts b/backend/src/common/decorators/retry.decorator.ts new file mode 100644 index 0000000..1331703 --- /dev/null +++ b/backend/src/common/decorators/retry.decorator.ts @@ -0,0 +1,60 @@ +// File: src/common/resilience/decorators/retry.decorator.ts +import retry from 'async-retry'; // ✅ แก้ Import: เปลี่ยนจาก * as retry เป็น default import +import { Logger } from '@nestjs/common'; + +export interface RetryOptions { + retries?: number; + factor?: number; + minTimeout?: number; + maxTimeout?: number; + onRetry?: (e: Error, attempt: number) => any; +} + +/** + * Decorator สำหรับการ Retry Function เมื่อเกิด Error + * ใช้สำหรับ External Call ที่อาจมีปัญหา Network ชั่วคราว + */ +export function Retry(options: RetryOptions = {}) { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + const originalMethod = descriptor.value; + const logger = new Logger('RetryDecorator'); + + descriptor.value = async function (...args: any[]) { + return retry( + // ✅ ระบุ Type ให้กับ bail และ attempt เพื่อแก้ Implicit any + async (bail: (e: Error) => void, attempt: number) => { + try { + return await originalMethod.apply(this, args); + } catch (error) { + // ✅ Cast error เป็น Error Object เพื่อแก้ปัญหา 'unknown' + const err = error as Error; + + if (options.onRetry) { + options.onRetry(err, attempt); + } + + logger.warn( + `Attempt ${attempt} failed for ${propertyKey}. Error: ${err.message}`, // ✅ ใช้ err.message + ); + + // ถ้าต้องการให้หยุด Retry ทันทีในบางเงื่อนไข สามารถเรียก bail(err) ได้ที่นี่ + throw err; + } + }, + { + retries: options.retries || 3, + factor: options.factor || 2, + minTimeout: options.minTimeout || 1000, + maxTimeout: options.maxTimeout || 5000, + ...options, + }, + ); + }; + + return descriptor; + }; +} diff --git a/backend/src/common/file-storage/file-cleanup.service.ts b/backend/src/common/file-storage/file-cleanup.service.ts new file mode 100644 index 0000000..3221bf2 --- /dev/null +++ b/backend/src/common/file-storage/file-cleanup.service.ts @@ -0,0 +1,70 @@ +// File: src/common/file-storage/file-cleanup.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, LessThan } from 'typeorm'; +import * as fs from 'fs-extra'; +import { Attachment } from './entities/attachment.entity'; + +@Injectable() +export class FileCleanupService { + private readonly logger = new Logger(FileCleanupService.name); + + constructor( + @InjectRepository(Attachment) + private attachmentRepository: Repository, + ) {} + + /** + * รันทุกวันเวลาเที่ยงคืน (00:00) + * ลบไฟล์ชั่วคราว (isTemporary = true) ที่หมดอายุแล้ว (expiresAt < now) + */ + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async handleCleanup() { + this.logger.log('Running temporary file cleanup job...'); + + const now = new Date(); + + // 1. ค้นหาไฟล์ที่หมดอายุ + const expiredAttachments = await this.attachmentRepository.find({ + where: { + isTemporary: true, + expiresAt: LessThan(now), + }, + }); + + if (expiredAttachments.length === 0) { + this.logger.log('No expired files found.'); + return; + } + + this.logger.log( + `Found ${expiredAttachments.length} expired files. Deleting...`, + ); + + let deletedCount = 0; + const errors = []; + + for (const att of expiredAttachments) { + try { + // 2. ลบไฟล์จริงออกจาก Disk + if (await fs.pathExists(att.filePath)) { + await fs.remove(att.filePath); + } + + // 3. ลบ Record ออกจาก Database + await this.attachmentRepository.remove(att); + deletedCount++; + } catch (error) { + // ✅ แก้ไข: Cast error เป็น Error object เพื่อเข้าถึง .message + const errMessage = (error as Error).message; + this.logger.error(`Failed to delete file ID ${att.id}: ${errMessage}`); + errors.push(att.id); + } + } + + this.logger.log( + `Cleanup complete. Deleted: ${deletedCount}, Failed: ${errors.length}`, + ); + } +} diff --git a/backend/src/common/file-storage/file-storage.controller.ts b/backend/src/common/file-storage/file-storage.controller.ts index bced4eb..0055784 100644 --- a/backend/src/common/file-storage/file-storage.controller.ts +++ b/backend/src/common/file-storage/file-storage.controller.ts @@ -1,6 +1,10 @@ +// File: src/common/file-storage/file-storage.controller.ts import { Controller, Post, + Get, + Delete, // ✅ Import Delete + Param, UseInterceptors, UploadedFile, UseGuards, @@ -8,12 +12,16 @@ import { ParseFilePipe, MaxFileSizeValidator, FileTypeValidator, + Res, + StreamableFile, + ParseIntPipe, } from '@nestjs/common'; +import type { Response } from 'express'; import { FileInterceptor } from '@nestjs/platform-express'; import { FileStorageService } from './file-storage.service.js'; import { JwtAuthGuard } from '../guards/jwt-auth.guard.js'; -// ✅ 1. สร้าง Interface เพื่อระบุ Type ของ Request +// Interface เพื่อระบุ Type ของ Request ที่ผ่าน JwtAuthGuard มาแล้ว interface RequestWithUser { user: { userId: number; @@ -33,17 +41,56 @@ export class FileStorageController { new ParseFilePipe({ validators: [ new MaxFileSizeValidator({ maxSize: 50 * 1024 * 1024 }), // 50MB - // ตรวจสอบประเภทไฟล์ (Regex) + // ตรวจสอบประเภทไฟล์ (Regex) - รวม image, pdf, docs, zip new FileTypeValidator({ - fileType: /(pdf|msword|openxmlformats|zip|octet-stream)/, + fileType: + /(pdf|msword|openxmlformats|zip|octet-stream|image|jpeg|png)/, }), ], }), ) file: Express.Multer.File, - @Request() req: RequestWithUser, // ✅ 2. ระบุ Type ตรงนี้แทน any + @Request() req: RequestWithUser, ) { // ส่ง userId จาก Token ไปด้วย return this.fileStorageService.upload(file, req.user.userId); } + + /** + * Endpoint สำหรับดาวน์โหลดไฟล์ + * GET /files/:id/download + */ + @Get(':id/download') + async downloadFile( + @Param('id', ParseIntPipe) id: number, + @Res({ passthrough: true }) res: Response, + ): Promise { + const { stream, attachment } = await this.fileStorageService.download(id); + + // Encode ชื่อไฟล์เพื่อรองรับภาษาไทยและตัวอักษรพิเศษใน Header + const encodedFilename = encodeURIComponent(attachment.originalFilename); + + res.set({ + 'Content-Type': attachment.mimeType, + // บังคับให้ browser ดาวน์โหลดไฟล์ แทนการ preview + 'Content-Disposition': `attachment; filename="${encodedFilename}"; filename*=UTF-8''${encodedFilename}`, + 'Content-Length': attachment.fileSize, + }); + + return new StreamableFile(stream); + } + + /** + * ✅ NEW: Delete Endpoint + * DELETE /files/:id + */ + @Delete(':id') + async deleteFile( + @Param('id', ParseIntPipe) id: number, + @Request() req: RequestWithUser, + ) { + // ส่ง userId ไปด้วยเพื่อตรวจสอบความเป็นเจ้าของ + await this.fileStorageService.delete(id, req.user.userId); + return { message: 'File deleted successfully', id }; + } } diff --git a/backend/src/common/file-storage/file-storage.module.ts b/backend/src/common/file-storage/file-storage.module.ts index ede931f..1310350 100644 --- a/backend/src/common/file-storage/file-storage.module.ts +++ b/backend/src/common/file-storage/file-storage.module.ts @@ -1,13 +1,21 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ScheduleModule } from '@nestjs/schedule'; // ✅ Import import { FileStorageService } from './file-storage.service.js'; import { FileStorageController } from './file-storage.controller.js'; +import { FileCleanupService } from './file-cleanup.service.js'; // ✅ Import import { Attachment } from './entities/attachment.entity.js'; @Module({ - imports: [TypeOrmModule.forFeature([Attachment])], + imports: [ + TypeOrmModule.forFeature([Attachment]), + ScheduleModule.forRoot(), // ✅ เปิดใช้งาน Cron Job], + ], controllers: [FileStorageController], - providers: [FileStorageService], + providers: [ + FileStorageService, + FileCleanupService, // ✅ Register Provider + ], exports: [FileStorageService], // Export ให้ Module อื่น (เช่น Correspondence) เรียกใช้ตอน Commit }) export class FileStorageModule {} diff --git a/backend/src/common/file-storage/file-storage.service.ts b/backend/src/common/file-storage/file-storage.service.ts index c789a2c..7e61919 100644 --- a/backend/src/common/file-storage/file-storage.service.ts +++ b/backend/src/common/file-storage/file-storage.service.ts @@ -1,3 +1,4 @@ +// File: src/common/file-storage/file-storage.service.ts import { Injectable, NotFoundException, @@ -12,6 +13,7 @@ import * as path from 'path'; import * as crypto from 'crypto'; import { v4 as uuidv4 } from 'uuid'; import { Attachment } from './entities/attachment.entity.js'; +import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ม @Injectable() export class FileStorageService { @@ -29,7 +31,7 @@ export class FileStorageService { ? '/share/dms-data' : path.join(process.cwd(), 'uploads'); - // สร้างโฟลเดอร์รอไว้เลยถ้ายังไม่มี + // สร้างโฟลเดอร์ temp รอไว้เลยถ้ายังไม่มี fs.ensureDirSync(path.join(this.uploadRoot, 'temp')); } @@ -75,11 +77,20 @@ export class FileStorageService { * เมธอดนี้จะถูกเรียกโดย Service อื่น (เช่น CorrespondenceService) เมื่อกด Save */ async commit(tempIds: string[]): Promise { + if (!tempIds || tempIds.length === 0) { + return []; + } + const attachments = await this.attachmentRepository.find({ where: { tempId: In(tempIds), isTemporary: true }, }); if (attachments.length !== tempIds.length) { + // แจ้งเตือนแต่อาจจะไม่ throw ถ้าต้องการให้ process ต่อไปได้บางส่วน (ขึ้นอยู่กับ business logic) + // แต่เพื่อความปลอดภัยควรแจ้งว่าไฟล์ไม่ครบ + this.logger.warn( + `Expected ${tempIds.length} files to commit, but found ${attachments.length}`, + ); throw new NotFoundException('Some files not found or already committed'); } @@ -98,21 +109,27 @@ export class FileStorageService { try { // ย้ายไฟล์ - await fs.move(oldPath, newPath, { overwrite: true }); + if (await fs.pathExists(oldPath)) { + await fs.move(oldPath, newPath, { overwrite: true }); - // อัปเดตข้อมูลใน DB - att.filePath = newPath; - att.isTemporary = false; - att.tempId = undefined; // เคลียร์ tempId - att.expiresAt = undefined; // เคลียร์วันหมดอายุ + // อัปเดตข้อมูลใน DB + att.filePath = newPath; + att.isTemporary = false; + att.tempId = null as any; // เคลียร์ tempId (TypeORM อาจต้องการ null แทน undefined สำหรับ nullable) + att.expiresAt = null as any; // เคลียร์วันหมดอายุ - committedAttachments.push(await this.attachmentRepository.save(att)); + committedAttachments.push(await this.attachmentRepository.save(att)); + } else { + this.logger.error(`File missing during commit: ${oldPath}`); + throw new NotFoundException( + `File not found on disk: ${att.originalFilename}`, + ); + } } catch (error) { this.logger.error( `Failed to move file from ${oldPath} to ${newPath}`, error, ); - // ถ้า error ตัวนึง ควรจะ rollback หรือ throw error (ในที่นี้ throw เพื่อให้ Transaction ของผู้เรียกจัดการ) throw new BadRequestException( `Failed to commit file: ${att.originalFilename}`, ); @@ -122,7 +139,83 @@ export class FileStorageService { return committedAttachments; } + /** + * Download File + * ดึงไฟล์มาเป็น Stream เพื่อส่งกลับไปให้ Controller + */ + async download( + id: number, + ): Promise<{ stream: fs.ReadStream; attachment: Attachment }> { + // 1. ค้นหาข้อมูลไฟล์จาก DB + const attachment = await this.attachmentRepository.findOne({ + where: { id }, + }); + + if (!attachment) { + throw new NotFoundException(`Attachment #${id} not found`); + } + + // 2. ตรวจสอบว่าไฟล์มีอยู่จริงบน Disk หรือไม่ + const filePath = attachment.filePath; + if (!fs.existsSync(filePath)) { + this.logger.error(`File missing on disk: ${filePath}`); + throw new NotFoundException('File not found on server storage'); + } + + // 3. สร้าง Read Stream (มีประสิทธิภาพกว่าการโหลดทั้งไฟล์เข้า Memory) + const stream = fs.createReadStream(filePath); + + return { stream, attachment }; + } + private calculateChecksum(buffer: Buffer): string { return crypto.createHash('sha256').update(buffer).digest('hex'); } + + /** + * ✅ NEW: Delete File + * ลบไฟล์ออกจาก Disk และ Database + */ + async delete(id: number, userId: number): Promise { + // 1. ค้นหาไฟล์ + const attachment = await this.attachmentRepository.findOne({ + where: { id }, + }); + + if (!attachment) { + throw new NotFoundException(`Attachment #${id} not found`); + } + + // 2. ตรวจสอบความเป็นเจ้าของ (Security Check) + // อนุญาตให้ลบถ้าเป็นคนอัปโหลดเอง + // (ในอนาคตอาจเพิ่มเงื่อนไข OR User เป็น Admin/Document Control) + if (attachment.uploadedByUserId !== userId) { + this.logger.warn( + `User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`, + ); + throw new ForbiddenException('You are not allowed to delete this file'); + } + + // 3. ลบไฟล์ออกจาก Disk + try { + if (await fs.pathExists(attachment.filePath)) { + await fs.remove(attachment.filePath); + } else { + this.logger.warn( + `File not found on disk during deletion: ${attachment.filePath}`, + ); + } + } catch (error) { + this.logger.error( + `Failed to delete file from disk: ${attachment.filePath}`, + error, + ); + throw new BadRequestException('Failed to delete file from storage'); + } + + // 4. ลบ Record ออกจาก Database + await this.attachmentRepository.remove(attachment); + + this.logger.log(`File deleted: ${id} by user ${userId}`); + } } diff --git a/backend/src/common/guards/maintenance-mode.guard.ts b/backend/src/common/guards/maintenance-mode.guard.ts index f954643..1d1ec69 100644 --- a/backend/src/common/guards/maintenance-mode.guard.ts +++ b/backend/src/common/guards/maintenance-mode.guard.ts @@ -11,7 +11,7 @@ import { } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { Cache } from 'cache-manager'; +import type { Cache } from 'cache-manager'; import { BYPASS_MAINTENANCE_KEY } from '../decorators/bypass-maintenance.decorator'; @Injectable() diff --git a/backend/src/common/interceptors/idempotency.interceptor.ts b/backend/src/common/interceptors/idempotency.interceptor.ts index 26be6d9..f5cee6b 100644 --- a/backend/src/common/interceptors/idempotency.interceptor.ts +++ b/backend/src/common/interceptors/idempotency.interceptor.ts @@ -1,5 +1,6 @@ // File: src/common/interceptors/idempotency.interceptor.ts // บันทึกการแก้ไข: สร้าง IdempotencyInterceptor เพื่อป้องกันการทำรายการซ้ำ (T1.1) +// บันทึกการแก้ไข: แก้ไข TS18046 โดยการตรวจสอบ Type ของ err ใน catch block import { CallHandler, @@ -11,7 +12,7 @@ import { Logger, } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { Cache } from 'cache-manager'; +import type { Cache } from 'cache-manager'; import { Observable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; import { Request } from 'express'; @@ -29,43 +30,37 @@ export class IdempotencyInterceptor implements NestInterceptor { 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) { + // ✅ Fix: ตรวจสอบว่า err เป็น Error Object หรือไม่ ก่อนเรียก .stack + const errorMessage = err instanceof Error ? err.stack : String(err); this.logger.error( `Failed to cache idempotency key ${idempotencyKey}`, - err.stack, + errorMessage, ); } }), diff --git a/backend/src/common/interceptors/performance.interceptor.ts b/backend/src/common/interceptors/performance.interceptor.ts new file mode 100644 index 0000000..80513c8 --- /dev/null +++ b/backend/src/common/interceptors/performance.interceptor.ts @@ -0,0 +1,90 @@ +// File: src/modules/monitoring/interceptors/performance.interceptor.ts +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + Logger, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { MetricsService } from '../../modules/monitoring/services/metrics.service'; + +@Injectable() +export class PerformanceInterceptor implements NestInterceptor { + private readonly logger = new Logger(PerformanceInterceptor.name); + + constructor(private readonly metricsService: MetricsService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + // ข้ามการวัดผลสำหรับ Endpoint /metrics และ /health เพื่อลด Noise + const req = context.switchToHttp().getRequest(); + if (req.url === '/metrics' || req.url === '/health') { + return next.handle(); + } + + const method = req.method; + const url = req.route ? req.route.path : req.url; // ใช้ Route path แทน Full URL เพื่อลด Cardinality + const startTime = process.hrtime(); + + return next.handle().pipe( + tap({ + next: (data) => { + this.recordMetrics(context, method, url, startTime, 200); // สมมติ 200 หรือดึงจาก Response จริง + }, + error: (err) => { + const status = err.status || 500; + this.recordMetrics(context, method, url, startTime, status); + }, + }), + ); + } + + /** + * บันทึกข้อมูลลง Metrics Service และ Logger + */ + private recordMetrics( + context: ExecutionContext, + method: string, + route: string, + startTime: [number, number], + statusCode: number, + ) { + const res = context.switchToHttp().getResponse(); + const finalStatus = res.statusCode || statusCode; + + // คำนวณระยะเวลา (Seconds) + const diff = process.hrtime(startTime); + const durationInSeconds = diff[0] + diff[1] / 1e9; + const durationInMs = durationInSeconds * 1000; + + // 1. บันทึก Metrics (Prometheus) + this.metricsService.httpRequestsTotal.inc({ + method, + route, + status_code: finalStatus.toString(), + }); + + this.metricsService.httpRequestDuration.observe( + { + method, + route, + status_code: finalStatus.toString(), + }, + durationInSeconds, + ); + + // 2. บันทึก Log (Winston JSON) - เฉพาะ Request ที่ช้าเกิน 200ms หรือ Error + // ตาม Req 6.5.1 API Response Time Target < 200ms + if (durationInMs > 200 || finalStatus >= 400) { + this.logger.log({ + message: 'HTTP Request Performance', + method, + route, + statusCode: finalStatus, + durationMs: durationInMs, + level: finalStatus >= 500 ? 'error' : 'warn', + }); + } + } +} diff --git a/backend/src/common/resilience/resilience.module.ts b/backend/src/common/resilience/resilience.module.ts new file mode 100644 index 0000000..b6be01e --- /dev/null +++ b/backend/src/common/resilience/resilience.module.ts @@ -0,0 +1,9 @@ +// File: src/common/resilience/resilience.module.ts +import { Module, Global } from '@nestjs/common'; + +@Global() +@Module({ + providers: [], + exports: [], +}) +export class ResilienceModule {} diff --git a/backend/src/modules/master/dto/create-tag.dto.ts b/backend/src/modules/master/dto/create-tag.dto.ts new file mode 100644 index 0000000..45bf286 --- /dev/null +++ b/backend/src/modules/master/dto/create-tag.dto.ts @@ -0,0 +1,19 @@ +// File: src/modules/master/dto/create-tag.dto.ts + +import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateTagDto { + @ApiProperty({ example: 'URGENT', description: 'ชื่อ Tag' }) + @IsString() + @IsNotEmpty() + tag_name: string; + + @ApiProperty({ + example: 'เอกสารด่วนต้องดำเนินการทันที', + description: 'คำอธิบาย', + }) + @IsString() + @IsOptional() + description?: string; +} diff --git a/backend/src/modules/master/dto/search-tag.dto.ts b/backend/src/modules/master/dto/search-tag.dto.ts new file mode 100644 index 0000000..6dcd35c --- /dev/null +++ b/backend/src/modules/master/dto/search-tag.dto.ts @@ -0,0 +1,26 @@ +// File: src/modules/master/dto/search-tag.dto.ts + +import { IsString, IsOptional, IsInt, Min } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class SearchTagDto { + @ApiPropertyOptional({ description: 'คำค้นหา (ชื่อ Tag หรือ คำอธิบาย)' }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ description: 'หมายเลขหน้า (เริ่มต้น 1)', default: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ description: 'จำนวนรายการต่อหน้า', default: 20 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + limit?: number = 20; +} diff --git a/backend/src/modules/master/dto/update-tag.dto.ts b/backend/src/modules/master/dto/update-tag.dto.ts new file mode 100644 index 0000000..ff38b57 --- /dev/null +++ b/backend/src/modules/master/dto/update-tag.dto.ts @@ -0,0 +1,6 @@ +// File: src/modules/master/dto/update-tag.dto.ts + +import { PartialType } from '@nestjs/swagger'; +import { CreateTagDto } from './create-tag.dto'; + +export class UpdateTagDto extends PartialType(CreateTagDto) {} diff --git a/backend/src/modules/master/entities/tag.entity.ts b/backend/src/modules/master/entities/tag.entity.ts new file mode 100644 index 0000000..0fb81cc --- /dev/null +++ b/backend/src/modules/master/entities/tag.entity.ts @@ -0,0 +1,27 @@ +// File: src/modules/master/entities/tag.entity.ts + +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('tags') +export class Tag { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 100, unique: true, comment: 'ชื่อ Tag' }) + tag_name: string; + + @Column({ type: 'text', nullable: true, comment: 'คำอธิบายแท็ก' }) + description: string; + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; +} diff --git a/backend/src/modules/master/master.controller.ts b/backend/src/modules/master/master.controller.ts new file mode 100644 index 0000000..f4da7ad --- /dev/null +++ b/backend/src/modules/master/master.controller.ts @@ -0,0 +1,64 @@ +// File: src/modules/master/master.controller.ts + +import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { MasterService } from './master.service'; +import { CreateTagDto } from './dto/create-tag.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; + +@ApiTags('Master Data') +@Controller('master') +@UseGuards(JwtAuthGuard) // บังคับ Login ทุก Endpoint +export class MasterController { + constructor(private readonly masterService: MasterService) {} + + @Get('correspondence-types') + @ApiOperation({ summary: 'Get all active correspondence types' }) + getCorrespondenceTypes() { + return this.masterService.findAllCorrespondenceTypes(); + } + + @Get('correspondence-statuses') + @ApiOperation({ summary: 'Get all active correspondence statuses' }) + getCorrespondenceStatuses() { + return this.masterService.findAllCorrespondenceStatuses(); + } + + @Get('rfa-types') + @ApiOperation({ summary: 'Get all active RFA types' }) + getRfaTypes() { + return this.masterService.findAllRfaTypes(); + } + + @Get('rfa-statuses') + @ApiOperation({ summary: 'Get all active RFA status codes' }) + getRfaStatuses() { + return this.masterService.findAllRfaStatuses(); + } + + @Get('rfa-approve-codes') + @ApiOperation({ summary: 'Get all active RFA approve codes' }) + getRfaApproveCodes() { + return this.masterService.findAllRfaApproveCodes(); + } + + @Get('circulation-statuses') + @ApiOperation({ summary: 'Get all active circulation statuses' }) + getCirculationStatuses() { + return this.masterService.findAllCirculationStatuses(); + } + + @Get('tags') + @ApiOperation({ summary: 'Get all tags' }) + getTags() { + return this.masterService.findAllTags(); + } + + @Post('tags') + @RequirePermission('master_data.tag.manage') // ต้องมีสิทธิ์จัดการ Tag + @ApiOperation({ summary: 'Create a new tag (Admin only)' }) + createTag(@Body() dto: CreateTagDto) { + return this.masterService.createTag(dto); + } +} diff --git a/backend/src/modules/master/master.module.ts b/backend/src/modules/master/master.module.ts new file mode 100644 index 0000000..050e235 --- /dev/null +++ b/backend/src/modules/master/master.module.ts @@ -0,0 +1,33 @@ +// File: src/modules/master/master.module.ts + +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MasterService } from './master.service'; +import { MasterController } from './master.controller'; + +// Import Entities +import { Tag } from './entities/tag.entity'; +import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity'; +import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity'; +import { RfaType } from '../rfa/entities/rfa-type.entity'; +import { RfaStatusCode } from '../rfa/entities/rfa-status-code.entity'; +import { RfaApproveCode } from '../rfa/entities/rfa-approve-code.entity'; +import { CirculationStatusCode } from '../circulation/entities/circulation-status-code.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Tag, + CorrespondenceType, + CorrespondenceStatus, + RfaType, + RfaStatusCode, + RfaApproveCode, + CirculationStatusCode, + ]), + ], + controllers: [MasterController], + providers: [MasterService], + exports: [MasterService], // Export เผื่อ Module อื่นต้องใช้ +}) +export class MasterModule {} diff --git a/backend/src/modules/master/master.service.ts b/backend/src/modules/master/master.service.ts new file mode 100644 index 0000000..6047900 --- /dev/null +++ b/backend/src/modules/master/master.service.ts @@ -0,0 +1,97 @@ +// File: src/modules/master/master.service.ts + +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +// Import Entities จาก Module อื่นๆ (ตามโครงสร้างที่มีอยู่แล้ว) +import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity'; +import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity'; +import { RfaType } from '../rfa/entities/rfa-type.entity'; +import { RfaStatusCode } from '../rfa/entities/rfa-status-code.entity'; +import { RfaApproveCode } from '../rfa/entities/rfa-approve-code.entity'; +import { CirculationStatusCode } from '../circulation/entities/circulation-status-code.entity'; +import { Tag } from './entities/tag.entity'; // Entity ของ Module นี้เอง + +import { CreateTagDto } from './dto/create-tag.dto'; + +@Injectable() +export class MasterService { + constructor( + @InjectRepository(CorrespondenceType) + private readonly corrTypeRepo: Repository, + + @InjectRepository(CorrespondenceStatus) + private readonly corrStatusRepo: Repository, + + @InjectRepository(RfaType) + private readonly rfaTypeRepo: Repository, + + @InjectRepository(RfaStatusCode) + private readonly rfaStatusRepo: Repository, + + @InjectRepository(RfaApproveCode) + private readonly rfaApproveRepo: Repository, + + @InjectRepository(CirculationStatusCode) + private readonly circulationStatusRepo: Repository, + + @InjectRepository(Tag) + private readonly tagRepo: Repository, + ) {} + + // --- Correspondence --- + findAllCorrespondenceTypes() { + return this.corrTypeRepo.find({ + where: { is_active: true }, + order: { sort_order: 'ASC' }, + }); + } + + findAllCorrespondenceStatuses() { + return this.corrStatusRepo.find({ + where: { is_active: true }, + order: { sort_order: 'ASC' }, + }); + } + + // --- RFA --- + findAllRfaTypes() { + return this.rfaTypeRepo.find({ + where: { is_active: true }, + order: { sort_order: 'ASC' }, + }); + } + + findAllRfaStatuses() { + return this.rfaStatusRepo.find({ + where: { is_active: true }, + order: { sort_order: 'ASC' }, + }); + } + + findAllRfaApproveCodes() { + return this.rfaApproveRepo.find({ + where: { is_active: true }, + order: { sort_order: 'ASC' }, + }); + } + + // --- Circulation --- + findAllCirculationStatuses() { + return this.circulationStatusRepo.find({ + where: { is_active: true }, + order: { sort_order: 'ASC' }, + }); + } + + // --- Tags --- + findAllTags() { + return this.tagRepo.find({ order: { tag_name: 'ASC' } }); + } + + async createTag(dto: CreateTagDto) { + const tag = this.tagRepo.create(dto); + return this.tagRepo.save(tag); + } +} diff --git a/backend/src/modules/monitoring/controllers/health.controller.ts b/backend/src/modules/monitoring/controllers/health.controller.ts new file mode 100644 index 0000000..bd925ab --- /dev/null +++ b/backend/src/modules/monitoring/controllers/health.controller.ts @@ -0,0 +1,45 @@ +// File: src/modules/monitoring/controllers/health.controller.ts +import { Controller, Get } from '@nestjs/common'; +import { + HealthCheckService, + HttpHealthIndicator, + HealthCheck, + TypeOrmHealthIndicator, + MemoryHealthIndicator, + DiskHealthIndicator, +} from '@nestjs/terminus'; +import { MetricsService } from '../services/metrics.service'; + +@Controller() +export class HealthController { + constructor( + private health: HealthCheckService, + private http: HttpHealthIndicator, + private db: TypeOrmHealthIndicator, + private memory: MemoryHealthIndicator, + private disk: DiskHealthIndicator, + private metricsService: MetricsService, + ) {} + + @Get('health') + @HealthCheck() + check() { + return this.health.check([ + // 1. ตรวจสอบการเชื่อมต่อ Database (MariaDB) + () => this.db.pingCheck('database'), + + // 2. ตรวจสอบ Memory Heap (ไม่ควรเกิน 1GB สำหรับ Container นี้ - ปรับค่าตามจริง) + () => this.memory.checkHeap('memory_heap', 1024 * 1024 * 1024), + + // 3. ตรวจสอบพื้นที่ Disk สำหรับ DMS Data (Threshold 90%) + // path '/' อาจต้องเปลี่ยนเป็น '/share/dms-data' ตาม Environment จริง + () => + this.disk.checkStorage('storage', { path: '/', thresholdPercent: 0.9 }), + ]); + } + + @Get('metrics') + async getMetrics() { + return await this.metricsService.getMetrics(); + } +} diff --git a/backend/src/modules/monitoring/logger/winston.config.ts b/backend/src/modules/monitoring/logger/winston.config.ts new file mode 100644 index 0000000..1041ecf --- /dev/null +++ b/backend/src/modules/monitoring/logger/winston.config.ts @@ -0,0 +1,30 @@ +// File: src/modules/monitoring/logger/winston.config.ts +import { + utilities as nestWinstonUtilities, + WinstonModuleOptions, +} from 'nest-winston'; +import * as winston from 'winston'; + +/** + * ฟังก์ชันสร้าง Configuration สำหรับ Winston Logger + * - Development: แสดงผลแบบ Console อ่านง่าย + * - Production: แสดงผลแบบ JSON พร้อม Timestamp เพื่อการทำ Log Aggregation + */ +export const winstonConfig: WinstonModuleOptions = { + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.ms(), + // เลือก Format ตาม Environment + process.env.NODE_ENV === 'production' + ? winston.format.json() // Production ใช้ JSON + : nestWinstonUtilities.format.nestLike('LCBP3-DMS', { + prettyPrint: true, + colors: true, + }), + ), + }), + // สามารถเพิ่ม File Transport หรือ HTTP Transport ไปยัง Log Server ได้ที่นี่ + ], +}; diff --git a/backend/src/modules/monitoring/monitoring.module.ts b/backend/src/modules/monitoring/monitoring.module.ts new file mode 100644 index 0000000..a48ebd4 --- /dev/null +++ b/backend/src/modules/monitoring/monitoring.module.ts @@ -0,0 +1,23 @@ +// File: src/modules/monitoring/monitoring.module.ts +import { Global, Module } from '@nestjs/common'; +import { TerminusModule } from '@nestjs/terminus'; +import { HttpModule } from '@nestjs/axios'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { HealthController } from './controllers/health.controller'; +import { MetricsService } from './services/metrics.service'; +import { PerformanceInterceptor } from '../../common/interceptors/performance.interceptor'; + +@Global() // ทำให้ Module นี้ใช้งานได้ทั่วทั้ง App โดยไม่ต้อง Import ซ้ำ +@Module({ + imports: [TerminusModule, HttpModule], + controllers: [HealthController], + providers: [ + MetricsService, + { + provide: APP_INTERCEPTOR, // Register Global Interceptor + useClass: PerformanceInterceptor, + }, + ], + exports: [MetricsService], +}) +export class MonitoringModule {} diff --git a/backend/src/modules/monitoring/services/metrics.service.ts b/backend/src/modules/monitoring/services/metrics.service.ts new file mode 100644 index 0000000..e74e6c3 --- /dev/null +++ b/backend/src/modules/monitoring/services/metrics.service.ts @@ -0,0 +1,54 @@ +// File: src/modules/monitoring/services/metrics.service.ts +import { Injectable } from '@nestjs/common'; +import { Registry, Counter, Histogram, Gauge } from 'prom-client'; + +@Injectable() +export class MetricsService { + private readonly registry: Registry; + public readonly httpRequestsTotal: Counter; + public readonly httpRequestDuration: Histogram; + public readonly systemMemoryUsage: Gauge; + + constructor() { + this.registry = new Registry(); + this.registry.setDefaultLabels({ app: 'lcbp3-backend' }); + + // นับจำนวน HTTP Request ทั้งหมด แยกตาม method, route, status_code + this.httpRequestsTotal = new Counter({ + name: 'http_requests_total', + help: 'Total number of HTTP requests', + labelNames: ['method', 'route', 'status_code'], + registers: [this.registry], + }); + + // วัดระยะเวลา Response Time (Histogram) + this.httpRequestDuration = new Histogram({ + name: 'http_request_duration_seconds', + help: 'Duration of HTTP requests in seconds', + labelNames: ['method', 'route', 'status_code'], + buckets: [0.1, 0.2, 0.5, 1.0, 1.5, 2.0, 5.0], // Buckets สำหรับวัด Latency + registers: [this.registry], + }); + + // วัดการใช้ Memory (Gauge) + this.systemMemoryUsage = new Gauge({ + name: 'system_memory_usage_bytes', + help: 'Heap memory usage in bytes', + registers: [this.registry], + }); + + // เริ่มเก็บ Metrics พื้นฐานของ Node.js (Optional) + // client.collectDefaultMetrics({ register: this.registry }); + } + + /** + * ดึงข้อมูล Metrics ทั้งหมดในรูปแบบ Text สำหรับ Prometheus Scrape + */ + async getMetrics(): Promise { + // อัปเดต Memory Usage ก่อน Return + const memoryUsage = process.memoryUsage(); + this.systemMemoryUsage.set(memoryUsage.heapUsed); + + return this.registry.metrics(); + } +} diff --git a/backend/src/modules/notification/notification.service.ts b/backend/src/modules/notification/notification.service.ts index 127effb..a31f58f 100644 --- a/backend/src/modules/notification/notification.service.ts +++ b/backend/src/modules/notification/notification.service.ts @@ -1,4 +1,5 @@ -import { Injectable, Logger } from '@nestjs/common'; +// File: src/modules/notification/notification.service.ts +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { InjectQueue } from '@nestjs/bullmq'; import { Queue } from 'bullmq'; import { InjectRepository } from '@nestjs/typeorm'; @@ -12,15 +13,18 @@ import { UserPreference } from '../user/entities/user-preference.entity'; // Gateway import { NotificationGateway } from './notification.gateway'; +// DTOs +import { SearchNotificationDto } from './dto/search-notification.dto'; + // Interfaces export interface NotificationJobData { userId: number; title: string; message: string; - type: 'EMAIL' | 'LINE' | 'SYSTEM'; - entityType?: string; // e.g., 'rfa' + type: 'EMAIL' | 'LINE' | 'SYSTEM'; // ช่องทางหลักที่ต้องการส่ง (Trigger Type) + entityType?: string; // e.g., 'rfa', 'correspondence' entityId?: number; // e.g., rfa_id - link?: string; // Deep link to frontend + link?: string; // Deep link to frontend page } @Injectable() @@ -39,98 +43,195 @@ export class NotificationService { ) {} /** - * ส่งการแจ้งเตือน (Trigger Notification) - * ฟังก์ชันนี้จะตรวจสอบ Preference ของผู้ใช้ และ Push ลง Queue + * ส่งการแจ้งเตือน (Centralized Notification Sender) + * 1. บันทึก DB (System Log) + * 2. ส่ง Real-time (WebSocket) + * 3. ส่ง External (Email/Line) ผ่าน Queue ตาม User Preference */ - async send(data: NotificationJobData) { + async send(data: NotificationJobData): Promise { try { - // 1. สร้าง Entity Instance (ยังไม่บันทึกลง DB) - // ใช้ Enum NotificationType.SYSTEM เพื่อให้ตรงกับ Type Definition + // --------------------------------------------------------- + // 1. สร้าง Entity และบันทึกลง DB (เพื่อให้มี History ในระบบ) + // --------------------------------------------------------- const notification = this.notificationRepo.create({ userId: data.userId, title: data.title, message: data.message, - notificationType: NotificationType.SYSTEM, + notificationType: NotificationType.SYSTEM, // ใน DB เก็บเป็น SYSTEM เสมอเพื่อแสดงใน App entityType: data.entityType, entityId: data.entityId, isRead: false, + // link: data.link // ถ้า Entity มี field link ให้ใส่ด้วย }); - // 2. บันทึกลง DB (ต้อง await เพื่อให้ได้ ID กลับมา) const savedNotification = await this.notificationRepo.save(notification); - // 3. Real-time Push (ผ่าน WebSocket Gateway) - // ส่งข้อมูลที่ save แล้ว (มี ID) ไปให้ Frontend + // --------------------------------------------------------- + // 2. Real-time Push (WebSocket) -> ส่งให้ User ทันทีถ้า Online + // --------------------------------------------------------- this.notificationGateway.sendToUser(data.userId, savedNotification); - // 4. ตรวจสอบ User Preferences เพื่อส่งช่องทางอื่น (Email/Line) + // --------------------------------------------------------- + // 3. ตรวจสอบ User Preferences เพื่อส่งช่องทางอื่น (Email/Line) + // --------------------------------------------------------- const userPref = await this.userPrefRepo.findOne({ where: { userId: data.userId }, }); - // Default: ถ้าไม่มี Pref ให้ส่ง Email/Line เป็นค่าเริ่มต้น (true) - const shouldSendEmail = userPref ? userPref.notifyEmail : true; - const shouldSendLine = userPref ? userPref.notifyLine : true; + // ใช้ Nullish Coalescing Operator (??) + // ถ้าไม่มีค่า (undefined/null) ให้ Default เป็น true + const shouldSendEmail = userPref?.notifyEmail ?? true; + const shouldSendLine = userPref?.notifyLine ?? true; const jobs = []; - // 5. Push to Queue (Email) - // เงื่อนไข: User เปิดรับ Email และ Type ของ Noti นี้ไม่ใช่ LINE-only + // --------------------------------------------------------- + // 4. เตรียม Job สำหรับ Email Queue + // เงื่อนไข: User เปิดรับ Email และ Noti นี้ไม่ได้บังคับส่งแค่ LINE + // --------------------------------------------------------- if (shouldSendEmail && data.type !== 'LINE') { jobs.push({ name: 'send-email', - data: { ...data, notificationId: savedNotification.id }, + data: { + ...data, + notificationId: savedNotification.id, + target: 'EMAIL', + }, opts: { - attempts: 3, // ลองใหม่ 3 ครั้งถ้าล่ม + attempts: 3, // ลองใหม่ 3 ครั้งถ้าล่ม (Resilience) backoff: { type: 'exponential', - delay: 5000, // รอ 5 วิ, 10 วิ, 20 วิ... + delay: 5000, // รอ 5s, 10s, 20s... }, + removeOnComplete: true, // ลบ Job เมื่อเสร็จ (ประหยัด Redis Memory) }, }); } - // 6. Push to Queue (Line) - // เงื่อนไข: User เปิดรับ Line และ Type ของ Noti นี้ไม่ใช่ EMAIL-only + // --------------------------------------------------------- + // 5. เตรียม Job สำหรับ Line Queue + // เงื่อนไข: User เปิดรับ Line และ Noti นี้ไม่ได้บังคับส่งแค่ EMAIL + // --------------------------------------------------------- if (shouldSendLine && data.type !== 'EMAIL') { jobs.push({ name: 'send-line', - data: { ...data, notificationId: savedNotification.id }, + data: { + ...data, + notificationId: savedNotification.id, + target: 'LINE', + }, opts: { attempts: 3, backoff: { type: 'fixed', delay: 3000 }, + removeOnComplete: true, }, }); } + // --------------------------------------------------------- + // 6. Push Jobs ลง Redis BullMQ + // --------------------------------------------------------- if (jobs.length > 0) { await this.notificationQueue.addBulk(jobs); + this.logger.debug( + `Queued ${jobs.length} external notifications for user ${data.userId}`, + ); } - - this.logger.log(`Notification queued for user ${data.userId}`); } catch (error) { - // Cast Error เพื่อให้ TypeScript ไม่ฟ้องใน Strict Mode + // Error Handling: ไม่ Throw เพื่อไม่ให้ Flow หลัก (เช่น การสร้างเอกสาร) พัง + // แต่บันทึก Error ไว้ตรวจสอบ this.logger.error( - `Failed to queue notification: ${(error as Error).message}`, + `Failed to process notification for user ${data.userId}`, + (error as Error).stack, ); - // Note: ไม่ Throw error เพื่อไม่ให้กระทบ Flow หลัก (Resilience Pattern) } } + /** + * ดึงรายการแจ้งเตือนของ User (สำหรับ Controller) + */ + async findAll(userId: number, searchDto: SearchNotificationDto) { + const { page = 1, limit = 20, isRead } = searchDto; + const skip = (page - 1) * limit; + + const queryBuilder = this.notificationRepo + .createQueryBuilder('notification') + .where('notification.userId = :userId', { userId }) + .orderBy('notification.createdAt', 'DESC') + .take(limit) + .skip(skip); + + // Filter by Read Status (ถ้ามีการส่งมา) + if (isRead !== undefined) { + queryBuilder.andWhere('notification.isRead = :isRead', { isRead }); + } + + const [items, total] = await queryBuilder.getManyAndCount(); + + // นับจำนวนที่ยังไม่ได้อ่านทั้งหมด (เพื่อแสดง Badge ที่กระดิ่ง) + const unreadCount = await this.notificationRepo.count({ + where: { userId, isRead: false }, + }); + + return { + data: items, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + unreadCount, + }, + }; + } + /** * อ่านแจ้งเตือน (Mark as Read) */ - async markAsRead(id: number, userId: number) { - await this.notificationRepo.update({ id, userId }, { isRead: true }); + async markAsRead(id: number, userId: number): Promise { + const notification = await this.notificationRepo.findOne({ + where: { id, userId }, + }); + + if (!notification) { + throw new NotFoundException(`Notification #${id} not found`); + } + + if (!notification.isRead) { + notification.isRead = true; + await this.notificationRepo.save(notification); + + // Update Unread Count via WebSocket (Optional) + // this.notificationGateway.sendUnreadCount(userId, ...); + } } /** * อ่านทั้งหมด (Mark All as Read) */ - async markAllAsRead(userId: number) { + async markAllAsRead(userId: number): Promise { await this.notificationRepo.update( { userId, isRead: false }, { isRead: true }, ); } + + /** + * ลบการแจ้งเตือนที่เก่าเกินกำหนด (ใช้กับ Cron Job Cleanup) + * เก็บไว้ 90 วัน + */ + async cleanupOldNotifications(days: number = 90): Promise { + const dateLimit = new Date(); + dateLimit.setDate(dateLimit.getDate() - days); + + const result = await this.notificationRepo + .createQueryBuilder() + .delete() + .from(Notification) + .where('createdAt < :dateLimit', { dateLimit }) + .execute(); + + this.logger.log(`Cleaned up ${result.affected} old notifications`); + return result.affected ?? 0; + } } diff --git a/backend/src/modules/rfa/rfa.module.ts b/backend/src/modules/rfa/rfa.module.ts index ab8ce5d..001e1bd 100644 --- a/backend/src/modules/rfa/rfa.module.ts +++ b/backend/src/modules/rfa/rfa.module.ts @@ -1,3 +1,4 @@ +// File: src/modules/rfa/rfa.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -10,25 +11,27 @@ import { RfaStatusCode } from './entities/rfa-status-code.entity'; import { RfaApproveCode } from './entities/rfa-approve-code.entity'; import { Correspondence } from '../correspondence/entities/correspondence.entity'; import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity'; +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 { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity'; +import { RoutingTemplate } from '../correspondence/entities/routing-template.entity'; +// หมายเหตุ: ตรวจสอบชื่อไฟล์ Entity ให้ตรงกับที่มีจริง (บางทีอาจชื่อ RoutingTemplate) -// Services +// Services & Controllers import { RfaService } from './rfa.service'; - -// Controllers import { RfaController } from './rfa.controller'; // External Modules import { DocumentNumberingModule } from '../document-numbering/document-numbering.module'; import { UserModule } from '../user/user.module'; +import { SearchModule } from '../search/search.module'; +import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'; // ✅ Import +import { NotificationModule } from '../notification/notification.module'; // ✅ เพิ่ม NotificationModule -// ... 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: [ + // 1. Register Entities (เฉพาะ Entity เท่านั้น ห้ามใส่ Module) TypeOrmModule.forFeature([ Rfa, RfaRevision, @@ -38,14 +41,19 @@ import { SearchModule } from '../search/search.module'; // ✅ เพิ่ม RfaApproveCode, Correspondence, ShopDrawingRevision, - // ... (ตัวเดิม) RfaWorkflow, RfaWorkflowTemplate, RfaWorkflowTemplateStep, + CorrespondenceRouting, + RoutingTemplate, ]), + + // 2. Import External Modules (Services ที่ Inject เข้ามา) DocumentNumberingModule, UserModule, SearchModule, + WorkflowEngineModule, // ✅ ย้ายมาใส่ตรงนี้ (imports หลัก) + NotificationModule, // ✅ เพิ่มตรงนี้ เพื่อแก้ dependency index [13] ], providers: [RfaService], controllers: [RfaController], diff --git a/backend/src/modules/transmittal/transmittal.module.ts b/backend/src/modules/transmittal/transmittal.module.ts index 935b0ac..123036d 100644 --- a/backend/src/modules/transmittal/transmittal.module.ts +++ b/backend/src/modules/transmittal/transmittal.module.ts @@ -7,11 +7,13 @@ import { TransmittalService } from './transmittal.service'; import { TransmittalController } from './transmittal.controller'; import { DocumentNumberingModule } from '../document-numbering/document-numbering.module'; import { UserModule } from '../user/user.module'; +import { SearchModule } from '../search/search.module'; // ✅ ต้อง Import เพราะ Service ใช้ (ที่เป็นสาเหตุ Error) @Module({ imports: [ TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]), DocumentNumberingModule, UserModule, + SearchModule, ], controllers: [TransmittalController], providers: [TransmittalService], diff --git a/backend/src/modules/user/dto/update-user.dto.ts b/backend/src/modules/user/dto/update-user.dto.ts index 2ff85ab..9121381 100644 --- a/backend/src/modules/user/dto/update-user.dto.ts +++ b/backend/src/modules/user/dto/update-user.dto.ts @@ -1,21 +1,7 @@ -// File: src/modules/user/dto/update-preference.dto.ts -import { IsBoolean, IsOptional, IsString, IsIn } from 'class-validator'; +// File: src/modules/user/dto/update-user.dto.ts +// บันทึกการแก้ไข: ใช้ PartialType จาก @nestjs/swagger เพื่อรองรับ API Docs (T1.3) -export class UpdatePreferenceDto { - @IsOptional() - @IsBoolean() - notifyEmail?: boolean; +import { PartialType } from '@nestjs/swagger'; +import { CreateUserDto } from './create-user.dto'; - @IsOptional() - @IsBoolean() - notifyLine?: boolean; - - @IsOptional() - @IsBoolean() - digestMode?: boolean; - - @IsOptional() - @IsString() - @IsIn(['light', 'dark', 'system']) - uiTheme?: string; -} +export class UpdateUserDto extends PartialType(CreateUserDto) {} diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts index 57cc538..f7d3142 100644 --- a/backend/src/modules/user/user.controller.ts +++ b/backend/src/modules/user/user.controller.ts @@ -22,7 +22,7 @@ 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 { 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'; diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index a3be051..c18e2b0 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -1,10 +1,16 @@ +// File: src/modules/user/user.service.ts +// บันทึกการแก้ไข: แก้ไข Error TS1272 โดยใช้ 'import type' สำหรับ Cache interface (T1.3) + import { Injectable, NotFoundException, ConflictException, + Inject, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import type { Cache } from 'cache-manager'; // ✅ FIX: เพิ่ม 'type' ตรงนี้ import * as bcrypt from 'bcrypt'; import { User } from './entities/user.entity'; import { CreateUserDto } from './dto/create-user.dto'; @@ -14,26 +20,23 @@ import { UpdateUserDto } from './dto/update-user.dto'; export class UserService { constructor( @InjectRepository(User) - private usersRepository: Repository, // ✅ ชื่อตัวแปรจริงคือ usersRepository + private usersRepository: Repository, + @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} // 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก) async create(createUserDto: CreateUserDto): Promise { - // สร้าง Salt และ Hash Password const salt = await bcrypt.genSalt(); const hashedPassword = await bcrypt.hash(createUserDto.password, salt); - // เตรียมข้อมูล (เปลี่ยน password ธรรมดา เป็น password_hash) const newUser = this.usersRepository.create({ ...createUserDto, password: hashedPassword, }); try { - // บันทึกลง DB return await this.usersRepository.save(newUser); } catch (error: any) { - // เช็ค Error กรณี Username/Email ซ้ำ (MySQL Error Code 1062) if (error.code === 'ER_DUP_ENTRY') { throw new ConflictException('Username or Email already exists'); } @@ -44,7 +47,6 @@ export class UserService { // 2. ดึงข้อมูลทั้งหมด async findAll(): Promise { return this.usersRepository.find({ - // ไม่ส่ง password กลับไปเพื่อความปลอดภัย select: [ 'user_id', 'username', @@ -61,7 +63,7 @@ export class UserService { // 3. ดึงข้อมูลรายคน async findOne(id: number): Promise { const user = await this.usersRepository.findOne({ - where: { user_id: id }, // ใช้ user_id ตาม Entity + where: { user_id: id }, }); if (!user) { @@ -71,26 +73,26 @@ export class UserService { return user; } - // ฟังก์ชันแถม: สำหรับ AuthService ใช้ (ต้องเห็น Password เพื่อเอาไปเทียบ) async findOneByUsername(username: string): Promise { return this.usersRepository.findOne({ where: { username } }); } // 4. แก้ไขข้อมูล async update(id: number, updateUserDto: UpdateUserDto): Promise { - // เช็คก่อนว่ามี User นี้ไหม const user = await this.findOne(id); - // ถ้ามีการแก้รหัสผ่าน ต้อง Hash ใหม่ด้วย if (updateUserDto.password) { const salt = await bcrypt.genSalt(); updateUserDto.password = await bcrypt.hash(updateUserDto.password, salt); } - // รวมร่างข้อมูลเดิม + ข้อมูลใหม่ const updatedUser = this.usersRepository.merge(user, updateUserDto); + const savedUser = await this.usersRepository.save(updatedUser); - return this.usersRepository.save(updatedUser); + // ⚠️ สำคัญ: เมื่อมีการแก้ไขข้อมูล User ต้องเคลียร์ Cache สิทธิ์เสมอ + await this.clearUserCache(id); + + return savedUser; } // 5. ลบผู้ใช้ (Soft Delete) @@ -100,31 +102,48 @@ export class UserService { if (result.affected === 0) { throw new NotFoundException(`User with ID ${id} not found`); } + // เคลียร์ Cache เมื่อลบ + await this.clearUserCache(id); } - /** - * หา User ID ของคนที่เป็น Document Control (หรือตัวแทน) ในองค์กร - * เพื่อส่ง Notification - */ async findDocControlIdByOrg(organizationId: number): Promise { - // ✅ FIX: ใช้ usersRepository ให้ตรงกับ Constructor const user = await this.usersRepository.findOne({ where: { primaryOrganizationId: organizationId }, - // order: { roleId: 'ASC' } // (Optional) Logic การเลือกคน }); - return user ? user.user_id : null; } - // ฟังก์ชันดึงสิทธิ์ (Permission) + /** + * ✅ ดึงสิทธิ์ (Permission) โดยใช้ Caching Strategy + * TTL: 30 นาที (ตาม Requirement 6.5.2) + */ async getUserPermissions(userId: number): Promise { - // Query ข้อมูลจาก View: v_user_all_permissions + const cacheKey = `permissions:user:${userId}`; + + // 1. ลองดึงจาก Cache ก่อน + const cachedPermissions = await this.cacheManager.get(cacheKey); + if (cachedPermissions) { + return cachedPermissions; + } + + // 2. ถ้าไม่มีใน Cache ให้ Query จาก DB (View: v_user_all_permissions) const permissions = await this.usersRepository.query( `SELECT permission_name FROM v_user_all_permissions WHERE user_id = ?`, [userId], ); - // แปลงผลลัพธ์เป็น Array ของ string ['user.create', 'project.view', ...] - return permissions.map((row: any) => row.permission_name); + const permissionList = permissions.map((row: any) => row.permission_name); + + // 3. บันทึกลง Cache (TTL 1800 วินาที = 30 นาที) + await this.cacheManager.set(cacheKey, permissionList, 1800 * 1000); + + return permissionList; + } + + /** + * Helper สำหรับล้าง Cache เมื่อมีการเปลี่ยนแปลงสิทธิ์หรือบทบาท + */ + async clearUserCache(userId: number): Promise { + await this.cacheManager.del(`permissions:user:${userId}`); } } diff --git a/backend/src/modules/workflow-engine/dto/create-workflow-definition.dto.ts b/backend/src/modules/workflow-engine/dto/create-workflow-definition.dto.ts new file mode 100644 index 0000000..e110498 --- /dev/null +++ b/backend/src/modules/workflow-engine/dto/create-workflow-definition.dto.ts @@ -0,0 +1,26 @@ +// File: src/modules/workflow-engine/dto/create-workflow-definition.dto.ts +import { + IsString, + IsNotEmpty, + IsObject, + IsOptional, + IsBoolean, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateWorkflowDefinitionDto { + @ApiProperty({ example: 'RFA', description: 'รหัสของ Workflow' }) + @IsString() + @IsNotEmpty() + workflow_code!: string; // เพิ่ม ! + + @ApiProperty({ description: 'นิยาม Workflow' }) + @IsObject() + @IsNotEmpty() + dsl!: any; // เพิ่ม ! + + @ApiProperty({ description: 'เปิดใช้งานทันทีหรือไม่', default: true }) + @IsBoolean() + @IsOptional() + is_active?: boolean; +} diff --git a/backend/src/modules/workflow-engine/dto/evaluate-workflow.dto.ts b/backend/src/modules/workflow-engine/dto/evaluate-workflow.dto.ts new file mode 100644 index 0000000..cbaf05b --- /dev/null +++ b/backend/src/modules/workflow-engine/dto/evaluate-workflow.dto.ts @@ -0,0 +1,25 @@ +// File: src/modules/workflow-engine/dto/evaluate-workflow.dto.ts +import { IsString, IsNotEmpty, IsObject, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class EvaluateWorkflowDto { + @ApiProperty({ example: 'RFA', description: 'รหัส Workflow' }) + @IsString() + @IsNotEmpty() + workflow_code!: string; // เพิ่ม ! + + @ApiProperty({ example: 'DRAFT', description: 'สถานะปัจจุบัน' }) + @IsString() + @IsNotEmpty() + current_state!: string; // เพิ่ม ! + + @ApiProperty({ example: 'SUBMIT', description: 'Action ที่ต้องการทำ' }) + @IsString() + @IsNotEmpty() + action!: string; // เพิ่ม ! + + @ApiProperty({ description: 'Context', example: { userId: 1 } }) + @IsObject() + @IsOptional() + context?: Record; +} diff --git a/backend/src/modules/workflow-engine/dto/get-available-actions.dto.ts b/backend/src/modules/workflow-engine/dto/get-available-actions.dto.ts new file mode 100644 index 0000000..05421b4 --- /dev/null +++ b/backend/src/modules/workflow-engine/dto/get-available-actions.dto.ts @@ -0,0 +1,15 @@ +// File: src/modules/workflow-engine/dto/get-available-actions.dto.ts +import { IsString, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GetAvailableActionsDto { + @ApiProperty({ description: 'รหัส Workflow', example: 'RFA' }) + @IsString() + @IsNotEmpty() + workflow_code!: string; // เพิ่ม ! + + @ApiProperty({ description: 'สถานะปัจจุบัน', example: 'DRAFT' }) + @IsString() + @IsNotEmpty() + current_state!: string; // เพิ่ม ! +} diff --git a/backend/src/modules/workflow-engine/dto/update-workflow-definition.dto.ts b/backend/src/modules/workflow-engine/dto/update-workflow-definition.dto.ts new file mode 100644 index 0000000..157cecc --- /dev/null +++ b/backend/src/modules/workflow-engine/dto/update-workflow-definition.dto.ts @@ -0,0 +1,10 @@ +// File: src/modules/workflow-engine/dto/update-workflow-definition.dto.ts + +import { PartialType } from '@nestjs/swagger'; +import { CreateWorkflowDefinitionDto } from './create-workflow-definition.dto'; + +// PartialType จะทำให้ทุก field ใน CreateDto กลายเป็น Optional (?) +// เหมาะสำหรับ PATCH method +export class UpdateWorkflowDefinitionDto extends PartialType( + CreateWorkflowDefinitionDto, +) {} diff --git a/backend/src/modules/workflow-engine/entities/workflow-definition.entity.ts b/backend/src/modules/workflow-engine/entities/workflow-definition.entity.ts new file mode 100644 index 0000000..c4140b7 --- /dev/null +++ b/backend/src/modules/workflow-engine/entities/workflow-definition.entity.ts @@ -0,0 +1,37 @@ +// File: src/modules/workflow-engine/entities/workflow-definition.entity.ts +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +@Entity('workflow_definitions') +@Index(['workflow_code', 'is_active', 'version']) +export class WorkflowDefinition { + @PrimaryGeneratedColumn('uuid') + id!: string; // เพิ่ม ! + + @Column({ length: 50, comment: 'รหัส Workflow เช่น RFA, CORR' }) + workflow_code!: string; // เพิ่ม ! + + @Column({ type: 'int', default: 1, comment: 'หมายเลข Version' }) + version!: number; // เพิ่ม ! + + @Column({ type: 'json', comment: 'นิยาม Workflow ต้นฉบับ' }) + dsl!: any; // เพิ่ม ! + + @Column({ type: 'json', comment: 'โครงสร้างที่ Compile แล้ว' }) + compiled!: any; // เพิ่ม ! + + @Column({ default: true, comment: 'สถานะการใช้งาน' }) + is_active!: boolean; // เพิ่ม ! + + @CreateDateColumn() + created_at!: Date; // เพิ่ม ! + + @UpdateDateColumn() + updated_at!: Date; // เพิ่ม ! +} diff --git a/backend/src/modules/workflow-engine/workflow-dsl.service.ts b/backend/src/modules/workflow-engine/workflow-dsl.service.ts new file mode 100644 index 0000000..47facb3 --- /dev/null +++ b/backend/src/modules/workflow-engine/workflow-dsl.service.ts @@ -0,0 +1,203 @@ +// File: src/modules/workflow-engine/workflow-dsl.service.ts + +import { Injectable, BadRequestException } from '@nestjs/common'; + +export interface WorkflowState { + initial?: boolean; + terminal?: boolean; + transitions?: Record; +} + +export interface TransitionRule { + to: string; + requirements?: RequirementRule[]; + events?: EventRule[]; +} + +export interface RequirementRule { + role?: string; + user?: string; + condition?: string; // e.g. "amount > 5000" (Advanced) +} + +export interface EventRule { + type: 'notify' | 'webhook' | 'update_status'; + target?: string; + payload?: any; +} + +export interface CompiledWorkflow { + workflow: string; + version: string | number; + states: Record; +} + +@Injectable() +export class WorkflowDslService { + /** + * คอมไพล์ DSL Input ให้เป็น Standard Execution Tree + * @param dsl ข้อมูลดิบจาก User (JSON/Object) + * @returns CompiledWorkflow Object ที่พร้อมใช้งาน + */ + compile(dsl: any): CompiledWorkflow { + // 1. Basic Structure Validation + if (!dsl.states || !Array.isArray(dsl.states)) { + throw new BadRequestException( + 'DSL syntax error: "states" array is required.', + ); + } + + const compiled: CompiledWorkflow = { + workflow: dsl.workflow || 'UNKNOWN', + version: dsl.version || 1, + states: {}, + }; + + const stateMap = new Set(); + + // 2. First Pass: Collect all state names and normalize structure + for (const rawState of dsl.states) { + if (!rawState.name) { + throw new BadRequestException( + 'DSL syntax error: All states must have a "name".', + ); + } + + stateMap.add(rawState.name); + + const normalizedState: WorkflowState = { + initial: !!rawState.initial, + terminal: !!rawState.terminal, + transitions: {}, + }; + + // Normalize transitions "on:" + if (rawState.on) { + for (const [action, rule] of Object.entries(rawState.on)) { + const rawRule = rule as any; + normalizedState.transitions![action] = { + to: rawRule.to, + requirements: rawRule.require || [], + events: rawRule.events || [], + }; + } + } + + compiled.states[rawState.name] = normalizedState; + } + + // 3. Second Pass: Validate Integrity + this.validateIntegrity(compiled, stateMap); + + return compiled; + } + + /** + * ตรวจสอบความสมบูรณ์ของ Workflow Logic + */ + private validateIntegrity(compiled: CompiledWorkflow, stateMap: Set) { + let hasInitial = false; + + for (const [stateName, state] of Object.entries(compiled.states)) { + if (state.initial) { + if (hasInitial) + throw new BadRequestException( + `DSL Error: Multiple initial states found.`, + ); + hasInitial = true; + } + + // ตรวจสอบ Transitions + if (state.transitions) { + for (const [action, rule] of Object.entries(state.transitions)) { + // 1. ปลายทางต้องมีอยู่จริง + if (!stateMap.has(rule.to)) { + throw new BadRequestException( + `DSL Error: State "${stateName}" transitions via "${action}" to unknown state "${rule.to}".`, + ); + } + // 2. Action name convention (Optional but recommended) + if (!/^[A-Z0-9_]+$/.test(action)) { + // Warning or Strict Error could be here + } + } + } + } + + if (!hasInitial) { + throw new BadRequestException('DSL Error: No initial state defined.'); + } + } + + /** + * ประเมินผล (Evaluate) การเปลี่ยนสถานะ + * @param compiled ข้อมูล Workflow ที่ Compile แล้ว + * @param currentState สถานะปัจจุบัน + * @param action การกระทำ + * @param context ข้อมูลประกอบ (User roles, etc.) + */ + evaluate( + compiled: CompiledWorkflow, + currentState: string, + action: string, + context: any, + ): { nextState: string; events: EventRule[] } { + const stateConfig = compiled.states[currentState]; + + if (!stateConfig) { + throw new BadRequestException( + `Runtime Error: Current state "${currentState}" not found in definition.`, + ); + } + + if (stateConfig.terminal) { + throw new BadRequestException( + `Runtime Error: Cannot transition from terminal state "${currentState}".`, + ); + } + + const transition = stateConfig.transitions?.[action]; + + if (!transition) { + throw new BadRequestException( + `Runtime Error: Action "${action}" is not allowed from state "${currentState}". Available actions: ${Object.keys(stateConfig.transitions || {}).join(', ')}`, + ); + } + + // Check Requirements (RBAC Logic inside Engine) + if (transition.requirements && transition.requirements.length > 0) { + this.checkRequirements(transition.requirements, context); + } + + return { + nextState: transition.to, + events: transition.events || [], + }; + } + + /** + * ตรวจสอบเงื่อนไขสิทธิ์ (Requirements) + */ + private checkRequirements(requirements: RequirementRule[], context: any) { + const userRoles = context.roles || []; + const userId = context.userId; + + const isAllowed = requirements.some((req) => { + // กรณีเช็ค Role + if (req.role) { + return userRoles.includes(req.role); + } + // กรณีเช็ค Specific User + if (req.user) { + return userId === req.user; + } + return false; + }); + + if (!isAllowed) { + throw new BadRequestException( + 'Access Denied: You do not meet the requirements for this action.', + ); + } + } +} diff --git a/backend/src/modules/workflow-engine/workflow-engine.controller.ts b/backend/src/modules/workflow-engine/workflow-engine.controller.ts new file mode 100644 index 0000000..258b609 --- /dev/null +++ b/backend/src/modules/workflow-engine/workflow-engine.controller.ts @@ -0,0 +1,65 @@ +// File: src/modules/workflow-engine/workflow-engine.controller.ts + +import { + Controller, + Post, + Body, + Get, + Query, + Patch, + Param, + UseGuards, +} from '@nestjs/common'; // เพิ่ม Patch, Param +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { WorkflowEngineService } from './workflow-engine.service'; +import { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto'; +import { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto'; +import { GetAvailableActionsDto } from './dto/get-available-actions.dto'; // [NEW] +import { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto'; // [NEW] +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; + +@ApiTags('Workflow Engine (DSL)') +@Controller('workflow-engine') +@UseGuards(JwtAuthGuard) // Protect all endpoints +export class WorkflowEngineController { + constructor(private readonly workflowService: WorkflowEngineService) {} + + @Post('definitions') + @ApiOperation({ summary: 'Create or Update Workflow Definition (DSL)' }) + @ApiResponse({ status: 201, description: 'Workflow compiled and saved.' }) + async createDefinition(@Body() dto: CreateWorkflowDefinitionDto) { + return this.workflowService.createDefinition(dto); + } + + @Post('evaluate') + @ApiOperation({ + summary: 'Evaluate transition (Run logic without saving state)', + }) + async evaluate(@Body() dto: EvaluateWorkflowDto) { + return this.workflowService.evaluate(dto); + } + + @Get('actions') + @ApiOperation({ summary: 'Get available actions for current state' }) + async getAvailableActions(@Query() query: GetAvailableActionsDto) { + // [UPDATED] ใช้ DTO แทนแยก Query + return this.workflowService.getAvailableActions( + query.workflow_code, + query.current_state, + ); + } + + // [OPTIONAL/RECOMMENDED] เพิ่ม Endpoint สำหรับ Update (PATCH) + @Patch('definitions/:id') + @ApiOperation({ + summary: 'Update workflow status or details (e.g. Deactivate)', + }) + async updateDefinition( + @Param('id') id: string, + @Body() dto: UpdateWorkflowDefinitionDto, // [NEW] ใช้ Update DTO + ) { + // *หมายเหตุ: คุณต้องไปเพิ่ม method update() ใน Service ด้วยถ้าจะใช้ Endpoint นี้ + // return this.workflowService.update(id, dto); + return { message: 'Update logic not implemented yet', id, ...dto }; + } +} diff --git a/backend/src/modules/workflow-engine/workflow-engine.module.ts b/backend/src/modules/workflow-engine/workflow-engine.module.ts index 57f6727..abae558 100644 --- a/backend/src/modules/workflow-engine/workflow-engine.module.ts +++ b/backend/src/modules/workflow-engine/workflow-engine.module.ts @@ -1,9 +1,22 @@ +// File: src/modules/workflow-engine/workflow-engine.module.ts + import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkflowEngineService } from './workflow-engine.service'; +import { WorkflowDslService } from './workflow-dsl.service'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A +import { WorkflowEngineController } from './workflow-engine.controller'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A +import { WorkflowDefinition } from './entities/workflow-definition.entity'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A @Module({ - providers: [WorkflowEngineService], - // ✅ เพิ่มบรรทัดนี้ เพื่ออนุญาตให้ Module อื่น (เช่น Correspondence) เรียกใช้ Service นี้ได้ - exports: [WorkflowEngineService], + imports: [ + // เชื่อมต่อกับตาราง workflow_definitions + TypeOrmModule.forFeature([WorkflowDefinition]), + ], + controllers: [WorkflowEngineController], // เพิ่ม Controller สำหรับรับ API + providers: [ + WorkflowEngineService, // Service หลัก + WorkflowDslService, // [New] Service สำหรับ Compile/Validate DSL + ], + exports: [WorkflowEngineService], // Export ให้ module อื่นใช้เหมือนเดิม }) export class WorkflowEngineModule {} diff --git a/backend/src/modules/workflow-engine/workflow-engine.service.ts b/backend/src/modules/workflow-engine/workflow-engine.service.ts index 554d05f..d32deff 100644 --- a/backend/src/modules/workflow-engine/workflow-engine.service.ts +++ b/backend/src/modules/workflow-engine/workflow-engine.service.ts @@ -1,45 +1,179 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; +// File: src/modules/workflow-engine/workflow-engine.service.ts + import { - WorkflowStep, - WorkflowAction, - StepStatus, - TransitionResult, -} from './interfaces/workflow.interface.js'; + Injectable, + NotFoundException, + BadRequestException, + Logger, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { WorkflowDefinition } from './entities/workflow-definition.entity'; +import { WorkflowDslService, CompiledWorkflow } from './workflow-dsl.service'; +import { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto'; +import { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto'; +import { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto'; + +// Interface สำหรับ Backward Compatibility (Logic เดิม) +export enum WorkflowAction { + APPROVE = 'APPROVE', + REJECT = 'REJECT', + RETURN = 'RETURN', + ACKNOWLEDGE = 'ACKNOWLEDGE', +} + +export interface TransitionResult { + nextStepSequence: number | null; + shouldUpdateStatus: boolean; + documentStatus?: string; +} @Injectable() export class WorkflowEngineService { + private readonly logger = new Logger(WorkflowEngineService.name); + + constructor( + @InjectRepository(WorkflowDefinition) + private readonly workflowDefRepo: Repository, + private readonly dslService: WorkflowDslService, + ) {} + + // ================================================================= + // [NEW] DSL & Workflow Engine (Phase 6A) + // ================================================================= + /** - * คำนวณสถานะถัดไป (Next State Transition) - * @param currentSequence ลำดับปัจจุบัน - * @param totalSteps จำนวนขั้นตอนทั้งหมด - * @param action การกระทำ (Approve/Reject/Return) - * @param returnToSequence (Optional) ถ้า Return จะให้กลับไปขั้นไหน + * สร้างหรืออัปเดต Workflow Definition ใหม่ (Auto Versioning) + */ + async createDefinition( + dto: CreateWorkflowDefinitionDto, + ): Promise { + const compiled = this.dslService.compile(dto.dsl); + + const latest = await this.workflowDefRepo.findOne({ + where: { workflow_code: dto.workflow_code }, + order: { version: 'DESC' }, + }); + + const nextVersion = latest ? latest.version + 1 : 1; + + const entity = this.workflowDefRepo.create({ + workflow_code: dto.workflow_code, + version: nextVersion, + dsl: dto.dsl, + compiled: compiled, + is_active: dto.is_active ?? true, + }); + + return this.workflowDefRepo.save(entity); + } + + async update( + id: string, + dto: UpdateWorkflowDefinitionDto, + ): Promise { + const definition = await this.workflowDefRepo.findOne({ where: { id } }); + if (!definition) { + throw new NotFoundException( + `Workflow Definition with ID "${id}" not found`, + ); + } + + if (dto.dsl) { + try { + const compiled = this.dslService.compile(dto.dsl); + definition.dsl = dto.dsl; + definition.compiled = compiled; + } catch (error: any) { + throw new BadRequestException(`Invalid DSL: ${error.message}`); + } + } + + if (dto.is_active !== undefined) definition.is_active = dto.is_active; + if (dto.workflow_code) definition.workflow_code = dto.workflow_code; + + return this.workflowDefRepo.save(definition); + } + + async evaluate(dto: EvaluateWorkflowDto): Promise { + const definition = await this.workflowDefRepo.findOne({ + where: { workflow_code: dto.workflow_code, is_active: true }, + order: { version: 'DESC' }, + }); + + if (!definition) { + throw new NotFoundException( + `No active workflow definition found for "${dto.workflow_code}"`, + ); + } + + const compiled: CompiledWorkflow = definition.compiled; + const result = this.dslService.evaluate( + compiled, + dto.current_state, + dto.action, + dto.context || {}, + ); + + this.logger.log( + `Workflow Evaluated: ${dto.workflow_code} [${dto.current_state}] --${dto.action}--> [${result.nextState}]`, + ); + + return result; + } + + async getAvailableActions( + workflowCode: string, + currentState: string, + ): Promise { + const definition = await this.workflowDefRepo.findOne({ + where: { workflow_code: workflowCode, is_active: true }, + order: { version: 'DESC' }, + }); + + if (!definition) return []; + + const stateConfig = definition.compiled.states[currentState]; + if (!stateConfig || !stateConfig.transitions) return []; + + return Object.keys(stateConfig.transitions); + } + + // ================================================================= + // [LEGACY] Backward Compatibility for Correspondence/RFA Modules + // คืนค่า Logic เดิมเพื่อไม่ให้ Module อื่น Error (TS2339) + // ================================================================= + + /** + * คำนวณสถานะถัดไปแบบ Linear Sequence (Logic เดิม) + * ใช้สำหรับ CorrespondenceService และ RfaService ที่ยังไม่ได้ Refactor */ processAction( currentSequence: number, totalSteps: number, - action: WorkflowAction, + action: string, // รับเป็น string เพื่อความยืดหยุ่น returnToSequence?: number, ): TransitionResult { + // Map string action to enum logic switch (action) { case WorkflowAction.APPROVE: case WorkflowAction.ACKNOWLEDGE: - // ถ้าเป็นขั้นตอนสุดท้าย -> จบ Workflow + case 'APPROVE': // Case sensitive handling fallback + case 'ACKNOWLEDGE': if (currentSequence >= totalSteps) { return { - nextStepSequence: null, // ไม่มีขั้นต่อไปแล้ว + nextStepSequence: null, shouldUpdateStatus: true, - documentStatus: 'COMPLETED', // หรือ APPROVED + documentStatus: 'COMPLETED', }; } - // ถ้ายังไม่จบ -> ไปขั้นต่อไป return { nextStepSequence: currentSequence + 1, shouldUpdateStatus: false, }; case WorkflowAction.REJECT: - // จบ Workflow ทันทีแบบไม่สวย + case 'REJECT': return { nextStepSequence: null, shouldUpdateStatus: true, @@ -47,7 +181,7 @@ export class WorkflowEngineService { }; case WorkflowAction.RETURN: - // ย้อนกลับไปขั้นตอนก่อนหน้า (หรือที่ระบุ) + case 'RETURN': const targetStep = returnToSequence || currentSequence - 1; if (targetStep < 1) { throw new BadRequestException('Cannot return beyond the first step'); @@ -55,38 +189,25 @@ export class WorkflowEngineService { return { nextStepSequence: targetStep, shouldUpdateStatus: true, - documentStatus: 'REVISE_REQUIRED', // สถานะเอกสารเป็น "รอแก้ไข" + documentStatus: 'REVISE_REQUIRED', }; default: - throw new BadRequestException(`Invalid action: ${action}`); + // กรณีส่ง Action อื่นมา ให้ถือว่าเป็น Approve (หรือจะ Throw Error ก็ได้) + this.logger.warn( + `Unknown legacy action: ${action}, treating as next step.`, + ); + if (currentSequence >= totalSteps) { + return { + nextStepSequence: null, + shouldUpdateStatus: true, + documentStatus: 'COMPLETED', + }; + } + return { + nextStepSequence: currentSequence + 1, + shouldUpdateStatus: false, + }; } } - - /** - * ตรวจสอบว่า User คนนี้ มีสิทธิ์กด Action ในขั้นตอนนี้ไหม - * (Logic เบื้องต้น - เดี๋ยวเราจะเชื่อมกับ RBAC จริงๆ ใน Service หลัก) - */ - validateAccess( - step: WorkflowStep, - userOrgId: number, - userId: number, - ): boolean { - // ถ้าขั้นตอนนี้ยังไม่ Active (เช่น PENDING หรือ SKIPPED) -> ห้ามยุ่ง - if (step.status !== StepStatus.IN_PROGRESS) { - return false; - } - - // เช็คว่าตรงกับ Organization ที่กำหนดไหม - if (step.organizationId && step.organizationId !== userOrgId) { - return false; - } - - // เช็คว่าตรงกับ User ที่กำหนดไหม (ถ้าระบุ) - if (step.assigneeId && step.assigneeId !== userId) { - return false; - } - - return true; - } } diff --git a/temp.md b/temp.md index 033bdae..0ee4424 100644 --- a/temp.md +++ b/temp.md @@ -1,52 +1,40 @@ -import { - Controller, - Get, - Post, - Body, - Param, - ParseIntPipe, - UseGuards, -} from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +## บทบาท: คุณคือ Programmer ที่เชี่ยวชาญ การจัดการฐานข้อมูล (Database Management), การวิเคราะห์ฐานข้อมูล (Database Analysis), การจัดการฐานข้อมูลเชิงสัมพันธ์ (Relational Databases), ภาษา SQL, RBAC, ABAC, การเขียนโค๊ด NodeJS NestJS NextJS, การ debug โค้ด และ แก้ไข error ภายในโค้ด -import { RfaService } from './rfa.service'; -import { CreateRfaDto } from './dto/create-rfa.dto'; -import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto'; // Reuse DTO -import { User } from '../user/entities/user.entity'; +## Basic data: + 1. Application Requirements file: 0_Requirements_V1_4_3.md + 2. Full Stack JS file: 1_FullStackJS_V1_4_3.md + 3. Backend Development Plan: 2_Backend_Plan_V1_4_3.md + 4. Frontend Development Plan: 3_Frontend_Plan_V1_4_3.md + 5. Data Dictionary file: 4_Data_Dictionary_V1_4_3.md, 01_lcbp3_v1_4_3.sql + 6. Backend Development Plan Phase 6A: 2_Backend_Plan_Phase6A_V1_4_3.md + 7. Backend File & Folder: 5_Backend_Folder_V1_4_3.md -import { JwtAuthGuard } from '../../common/auth/guards/jwt-auth.guard'; -import { RbacGuard } from '../../common/auth/guards/rbac.guard'; -import { RequirePermission } from '../../common/decorators/require-permission.decorator'; -import { CurrentUser } from '../../common/decorators/current-user.decorator'; +## rules: + - ใช้ภาษาไทยใน comments + - เขียนโค้ดให้อ่านง่าย, ใส่ path/filename ในบรรทัดแรก โค้ด + - การอัพเดทโค้ด ให้แก้ไขจากต้นฉบับเป็น โค้ดที่สมบูรณ์ + - เขียน documentation สำหรับ function สำคัญ -@ApiTags('RFA (Request for Approval)') -@ApiBearerAuth() -@UseGuards(JwtAuthGuard, RbacGuard) -@Controller('rfas') -export class RfaController { - constructor(private readonly rfaService: RfaService) {} +## เป้าหมายและจุดประสงค์: +* ให้ความช่วยเหลือผู้ใช้ในงานที่เกี่ยวข้องกับการพัฒนาซอฟต์แวร์ โดยเฉพาะอย่างยิ่งในส่วนของ JavaScript (NodeJS, NestJS, NextJS) และฐานข้อมูล (SQL, Relational Databases) +* ให้คำปรึกษาเกี่ยวกับการจัดการข้อมูล, การออกแบบฐานข้อมูลเชิงสัมพันธ์, และการใช้โมเดลการควบคุมการเข้าถึง (RBAC, ABAC) +* ช่วยเหลือในการวิเคราะห์และแก้ไขข้อผิดพลาด (debug และ error) ในโค้ดตามที่ผู้ใช้ระบุ +* ใช้ข้อมูลพื้นฐานที่ให้มา (Basic data) เพื่อให้คำแนะนำและโค้ดที่สอดคล้องกับเอกสารโครงการ (เช่น Requirements, Plans, Data Dictionary) - // ... (Create, FindOne endpoints) ... +## พฤติกรรมและกฎเพิ่มเติม: +1) การเริ่มต้นและการโต้ตอบ: + a) ทักทายผู้ใช้ด้วยภาษาไทยอย่างเป็นมิตร และสอบถามเกี่ยวกับปัญหาหรือความช่วยเหลือที่ต้องการในด้านการเขียนโปรแกรมหรือฐานข้อมูล + b) ตอบคำถามทางเทคนิคอย่างแม่นยำและเป็นมืออาชีพ โดยใช้ศัพท์เฉพาะทางที่ถูกต้อง + c) จำกัดจำนวนประโยคในการตอบกลับแต่ละครั้งให้กระชับและตรงประเด็นเพื่อความรวดเร็วในการสื่อสาร - @Post(':id/submit') - @ApiOperation({ summary: 'Submit RFA to Workflow' }) - @RequirePermission('rfa.create') // ผู้สร้างมีสิทธิ์ส่ง - submit( - @Param('id', ParseIntPipe) id: number, - @Body('templateId', ParseIntPipe) templateId: number, // รับ Template ID - @CurrentUser() user: User, - ) { - return this.rfaService.submit(id, templateId, user); - } +2) การจัดการโค้ดและข้อมูล: + a) เมื่อผู้ใช้ขอให้อัพเดทโค้ด ให้ทำการแสดงโค้ดฉบับเต็มที่สมบูรณ์และได้รับการแก้ไขแล้ว (ไม่ใช่แค่ส่วนที่แก้ไข) + b) ต้องแน่ใจว่าโค้ดที่สร้างขึ้นมานั้นอ่านง่ายและมี comments เป็นภาษาไทยตามที่ระบุใน rules + c) สำหรับฟังก์ชันที่มีความซับซ้อนหรือมีความสำคัญต่อระบบ ต้องเขียน documentation อธิบายวัตถุประสงค์, พารามิเตอร์, และผลลัพธ์ของฟังก์ชันนั้นๆ ด้วยภาษาไทย + d) หากต้องอ้างอิงถึงโครงสร้างข้อมูลหรือข้อกำหนดใดๆ ให้ตรวจสอบจากไฟล์ Basic data ที่ผู้ใช้ให้มาก่อนเสมอ ถ้าไม่พบ ให้แจ้งผู้ใช้ทราบ + e) ถ้ามีการอ้างอิงถึงโค้ดที่อยู่ใน Phase หรือ Task ก่อนหน้า ให้สอบถามผู้ใช้เพื่อให้ upload ไฟล์โค้ดที่อ้างอิง (ไม่เดาหรือสร้างใหม่ เพิ่อประหยัดเวลา) - @Post(':id/action') - @ApiOperation({ summary: 'Process Workflow Action (Approve/Reject)' }) - @RequirePermission('workflow.action_review') // สิทธิ์ในการ Approve/Review - processAction( - @Param('id', ParseIntPipe) id: number, - @Body() actionDto: WorkflowActionDto, - @CurrentUser() user: User, - ) { - return this.rfaService.processAction(id, actionDto, user); - } -} \ No newline at end of file +1) โทนโดยรวม: + * ใช้ภาษาไทยในการสื่อสารเป็นหลัก ยกเว้นศัพท์เทคนิค + * มีความมั่นใจและแสดงออกถึงความเชี่ยวชาญในฐานะโปรแกรมเมอร์ผู้เชี่ยวชาญ + * มีความเป็นระเบียบและให้คำแนะนำที่เป็นขั้นตอน \ No newline at end of file