251124:1700 Ready to Phase 7
This commit is contained in:
16
2_2_phase_6a_workflow_dsl.sql
Normal file
16
2_2_phase_6a_workflow_dsl.sql
Normal file
@@ -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);
|
||||||
@@ -8,12 +8,14 @@
|
|||||||
|
|
||||||
* [x] `.env` (สำหรับ Local Dev เท่านั้น ห้าม commit)
|
* [x] `.env` (สำหรับ Local Dev เท่านั้น ห้าม commit)
|
||||||
* [x] `.gitignore`
|
* [x] `.gitignore`
|
||||||
* [x] `docker-compose.yml` (Configuration หลักสำหรับ Deploy)
|
* [x] `.prettierrc`
|
||||||
* [x] `docker-compose.override.yml` (สำหรับ Inject Secrets ตอน Dev)
|
* [x] `docker-compose.override.yml`
|
||||||
|
* [x] `docker-compose.yml`
|
||||||
|
* [x] `nest-cli.json`
|
||||||
|
* [x] `tsconfig.build.json`
|
||||||
|
* [x] `tsconfig.json`
|
||||||
* [x] `package.json`
|
* [x] `package.json`
|
||||||
* [x] `pnpm-lock.yaml`
|
* [x] `pnpm-lock.yaml`
|
||||||
* [x] `tsconfig.json`
|
|
||||||
* [x] `nest-cli.json`
|
|
||||||
* [x] `README.md`
|
* [x] `README.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -24,6 +26,10 @@
|
|||||||
|
|
||||||
* [x] `main.ts` (Application Bootstrap, Swagger, Global Pipes)
|
* [x] `main.ts` (Application Bootstrap, Swagger, Global Pipes)
|
||||||
* [x] `app.module.ts` (Root Module ที่รวมทุก Modules เข้าด้วยกัน)
|
* [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)
|
### **📁 src/common/** (Shared Resources)
|
||||||
|
|
||||||
@@ -32,6 +38,9 @@
|
|||||||
* **dto/**
|
* **dto/**
|
||||||
* [x] **login.dto.ts**
|
* [x] **login.dto.ts**
|
||||||
* [x] **register.dto.ts**
|
* [x] **register.dto.ts**
|
||||||
|
* **strategies/**
|
||||||
|
* [x] **local.strategy.ts**
|
||||||
|
* [x] **jwt.strategy.ts**
|
||||||
* [x] **auth.controller.spec.ts**
|
* [x] **auth.controller.spec.ts**
|
||||||
* [x] **auth.controller.ts**
|
* [x] **auth.controller.ts**
|
||||||
* [x] **auth.module.ts**
|
* [x] **auth.module.ts**
|
||||||
@@ -39,10 +48,15 @@
|
|||||||
* [x] **auth.service.ts**
|
* [x] **auth.service.ts**
|
||||||
* **config/** (Configuration Service)
|
* **config/** (Configuration Service)
|
||||||
* [x] **env.validation.ts**
|
* [x] **env.validation.ts**
|
||||||
|
* [x] **redis.config.ts**
|
||||||
* **decorators/**
|
* **decorators/**
|
||||||
* [x] **audit.decorator.ts**
|
* [x] **audit.decorator.ts**
|
||||||
|
* [x] **bypass-maintenance.decorator.ts**
|
||||||
* [x] `current-user.decorator.ts`
|
* [x] `current-user.decorator.ts`
|
||||||
|
* [x] **idempotency.decorator.ts**
|
||||||
* [x] `require-permission.decorator.ts`
|
* [x] `require-permission.decorator.ts`
|
||||||
|
* [x] **retry.decorator.ts**
|
||||||
|
* [x] **circuit-breaker.decorator.ts**
|
||||||
* **entities/**
|
* **entities/**
|
||||||
* [x] **audit-log.entity.ts**
|
* [x] **audit-log.entity.ts**
|
||||||
* [x] **base.entity.ts**
|
* [x] **base.entity.ts**
|
||||||
@@ -56,14 +70,21 @@
|
|||||||
* [x] **file-storage.module.ts**
|
* [x] **file-storage.module.ts**
|
||||||
* [x] **file-storage.service.spec.ts**
|
* [x] **file-storage.service.spec.ts**
|
||||||
* [x] `file-storage.service.ts` (Upload, Scan Virus, Commit)
|
* [x] `file-storage.service.ts` (Upload, Scan Virus, Commit)
|
||||||
|
* [x] **file-cleanup.service.ts** (Cleanup Temporary Files)
|
||||||
* [x] `guards/`
|
* [x] `guards/`
|
||||||
* [x] `jwt-auth.guard.ts`
|
* [x] `jwt-auth.guard.ts`
|
||||||
* [x] **jwt.strategy.ts**
|
* [x] **jwt-refresh.guard.ts**
|
||||||
|
* [x] **maintenance-mode.guard.ts**
|
||||||
* [x] `rbac.guard.ts` (ตรวจสอบสิทธิ์ 4 ระดับ)
|
* [x] `rbac.guard.ts` (ตรวจสอบสิทธิ์ 4 ระดับ)
|
||||||
* **interceptors/**
|
* **interceptors/**
|
||||||
* [x] `audit-log.interceptor.ts` (เก็บ Log ลง DB)
|
* [x] `audit-log.interceptor.ts` (เก็บ Log ลง DB)
|
||||||
|
* [x] **idempotency.interceptor.ts** (Idempotency Interceptor)
|
||||||
* [x] `transform.interceptor.ts` (Standard Response Format)
|
* [x] `transform.interceptor.ts` (Standard Response Format)
|
||||||
* **resilience/** (Circuit Breaker & Retry)
|
* **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)
|
### **📁 src/modules/** (Feature Modules)
|
||||||
|
|
||||||
@@ -71,13 +92,17 @@
|
|||||||
* [x] `dto/`
|
* [x] `dto/`
|
||||||
* [x] **assign-user-role.dto.ts**
|
* [x] **assign-user-role.dto.ts**
|
||||||
* [x] `create-user.dto.ts`
|
* [x] `create-user.dto.ts`
|
||||||
|
* [ ] **search-user.dto.ts**
|
||||||
* [x] `update-user.dto.ts`
|
* [x] `update-user.dto.ts`
|
||||||
|
* [x] `update-user-preference.dto.ts`
|
||||||
* [x] `entities/`
|
* [x] `entities/`
|
||||||
* [x] `user.entity.ts`
|
* [x] `user.entity.ts`
|
||||||
* [x] `role.entity.ts`
|
* [x] `role.entity.ts`
|
||||||
* [x] `permission.entity.ts`
|
* [x] `permission.entity.ts`
|
||||||
|
* [x] **user-assignment.entity.ts**
|
||||||
* [x] `user-preference.entity.ts`
|
* [x] `user-preference.entity.ts`
|
||||||
* [x] **user-assignment.service.ts**
|
* [x] **user-assignment.service.ts**
|
||||||
|
* [x] **user-preference.service.ts**
|
||||||
* [x] `user.controller.ts`
|
* [x] `user.controller.ts`
|
||||||
* [x] `user.module.ts`
|
* [x] `user.module.ts`
|
||||||
* [x] `user.service.ts`
|
* [x] `user.service.ts`
|
||||||
@@ -146,7 +171,7 @@
|
|||||||
* [x] `shop-drawing.controller.ts`
|
* [x] `shop-drawing.controller.ts`
|
||||||
* [x] `shop-drawing.service.ts`
|
* [x] `shop-drawing.service.ts`
|
||||||
|
|
||||||
4. **rfa/** (Request for Approval & Advanced Workflow)
|
5. **rfa/** (Request for Approval & Advanced Workflow)
|
||||||
* [x] `dto/`
|
* [x] `dto/`
|
||||||
* [x] `create-rfa.dto.ts`
|
* [x] `create-rfa.dto.ts`
|
||||||
* [x] `search-rfa.dto.ts`
|
* [x] `search-rfa.dto.ts`
|
||||||
@@ -165,7 +190,7 @@
|
|||||||
* [x] `rfa.module.ts`
|
* [x] `rfa.module.ts`
|
||||||
* [x] `rfa.service.ts` (Unified Workflow Integration)
|
* [x] `rfa.service.ts` (Unified Workflow Integration)
|
||||||
|
|
||||||
5. **circulation/** (Internal Routing)
|
6. **circulation/** (Internal Routing)
|
||||||
* [x] `dto/`
|
* [x] `dto/`
|
||||||
* [x] `create-circulation.dto.ts`
|
* [x] `create-circulation.dto.ts`
|
||||||
* [x] `update-circulation-routing.dto.ts`
|
* [x] `update-circulation-routing.dto.ts`
|
||||||
@@ -178,7 +203,7 @@
|
|||||||
* [x] `circulation.module.ts`
|
* [x] `circulation.module.ts`
|
||||||
* [x] `circulation.service.ts`
|
* [x] `circulation.service.ts`
|
||||||
|
|
||||||
6. **transmittal/** (Document Forwarding)
|
7. **transmittal/** (Document Forwarding)
|
||||||
* [x] `dto/`
|
* [x] `dto/`
|
||||||
* [x] `create-transmittal.dto.ts`
|
* [x] `create-transmittal.dto.ts`
|
||||||
* [x] `search-transmittal.dto.ts`
|
* [x] `search-transmittal.dto.ts`
|
||||||
@@ -190,7 +215,7 @@
|
|||||||
* [x] `transmittal.module.ts`
|
* [x] `transmittal.module.ts`
|
||||||
* [x] `transmittal.service.ts`
|
* [x] `transmittal.service.ts`
|
||||||
|
|
||||||
7. **notification/** (System Alerts)
|
8. **notification/** (System Alerts)
|
||||||
* [x] `dto/`
|
* [x] `dto/`
|
||||||
* [x] `create-notification.dto.ts`
|
* [x] `create-notification.dto.ts`
|
||||||
* [x] `search-notification.dto.ts`
|
* [x] `search-notification.dto.ts`
|
||||||
@@ -203,13 +228,13 @@
|
|||||||
* [x] `notification.processor.ts` (Consumer/Worker for Email & Line)
|
* [x] `notification.processor.ts` (Consumer/Worker for Email & Line)
|
||||||
* [x] `notification.service.ts` (Producer)
|
* [x] `notification.service.ts` (Producer)
|
||||||
|
|
||||||
8. **search/** (Elasticsearch)
|
9. **search/** (Elasticsearch)
|
||||||
* [x] `dto/search-query.dto.ts`
|
* [x] `dto/search-query.dto.ts`
|
||||||
* [x] `search.controller.ts`
|
* [x] `search.controller.ts`
|
||||||
* [x] `search.module.ts`
|
* [x] `search.module.ts`
|
||||||
* [x] `search.service.ts` (Indexing & Searching)
|
* [x] `search.service.ts` (Indexing & Searching)
|
||||||
|
|
||||||
9. **document-numbering/** (Internal Service)
|
10. **document-numbering/** (Internal Service)
|
||||||
* [x] `entities/`
|
* [x] `entities/`
|
||||||
* [x] `document-number-format.entity.ts`
|
* [x] `document-number-format.entity.ts`
|
||||||
* [x] `document-number-counter.entity.ts`
|
* [x] `document-number-counter.entity.ts`
|
||||||
@@ -217,13 +242,19 @@
|
|||||||
* [x] **document-numbering.service.spec.ts**
|
* [x] **document-numbering.service.spec.ts**
|
||||||
* [x] `document-numbering.service.ts` (Double-Lock Mechanism)
|
* [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] `interfaces/workflow.interface.ts`
|
||||||
|
* [x] **workflow-dsl.service.ts**
|
||||||
* [x] `workflow-engine.module.ts`
|
* [x] `workflow-engine.module.ts`
|
||||||
* [x] **workflow-engine.service.spec.ts**
|
* [x] **workflow-engine.service.spec.ts**
|
||||||
* [x] `workflow-engine.service.ts` (State Machine Logic)
|
* [x] `workflow-engine.service.ts` (State Machine Logic)
|
||||||
|
|
||||||
11. **json-schema/** (Validation)
|
12. **json-schema/** (Validation)
|
||||||
* [x] `dto/`
|
* [x] `dto/`
|
||||||
* [x] `create-json-schema.dto.ts`+
|
* [x] `create-json-schema.dto.ts`+
|
||||||
* [x] `search-json-schema.dto.ts`
|
* [x] `search-json-schema.dto.ts`
|
||||||
@@ -236,6 +267,16 @@
|
|||||||
* [x] **json-schema.service.spec.ts**
|
* [x] **json-schema.service.spec.ts**
|
||||||
* [x] `json-schema.service.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)** ที่
|
## **Folder Structure ของ Backend (NestJS)** ที่
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -246,17 +287,22 @@
|
|||||||
backend/
|
backend/
|
||||||
├── .env # Environment variables for local development only (not committed)
|
├── .env # Environment variables for local development only (not committed)
|
||||||
├── .gitignore # Git ignore rules
|
├── .gitignore # Git ignore rules
|
||||||
|
├── .prettierrc # Prettier configuration
|
||||||
├── docker-compose.yml # Main deployment container configuration
|
├── docker-compose.yml # Main deployment container configuration
|
||||||
├── docker-compose.override.yml # Dev-time secret/environment injection
|
├── docker-compose.override.yml # Dev-time secret/environment injection
|
||||||
├── package.json # Node dependencies and NPM scripts
|
├── package.json # Node dependencies and NPM scripts
|
||||||
├── pnpm-lock.yaml # Dependency lock file for pnpm
|
├── pnpm-lock.yaml # Dependency lock file for pnpm
|
||||||
├── tsconfig.json # TypeScript compiler configuration
|
├── tsconfig.json # TypeScript compiler configuration
|
||||||
|
├── tsconfig.build.json # TypeScript compiler configuration for production
|
||||||
├── nest-cli.json # NestJS project configuration
|
├── nest-cli.json # NestJS project configuration
|
||||||
├── README.md # Project documentation
|
├── README.md # Project documentation
|
||||||
└── src/
|
└── src/
|
||||||
├── main.ts # Application bootstrap and initialization
|
├── main.ts # Application bootstrap and initialization
|
||||||
├── app.module.ts # Root application module
|
├── 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/ # 🛠️ Shared framework resources used across modules
|
||||||
│ ├── common.module.ts # Registers shared providers
|
│ ├── common.module.ts # Registers shared providers
|
||||||
@@ -265,6 +311,9 @@ backend/
|
|||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── login.dto.ts # Login request payload
|
│ │ │ ├── login.dto.ts # Login request payload
|
||||||
│ │ │ └── register.dto.ts # Registration 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.module.ts # Auth DI module
|
||||||
│ │ ├── auth.controller.ts # Auth REST endpoints
|
│ │ ├── auth.controller.ts # Auth REST endpoints
|
||||||
│ │ ├── auth.controller.spec.ts # Unit tests for controller
|
│ │ ├── auth.controller.spec.ts # Unit tests for controller
|
||||||
@@ -272,12 +321,17 @@ backend/
|
|||||||
│ │ └── auth.service.spec.ts # Unit test for service
|
│ │ └── auth.service.spec.ts # Unit test for service
|
||||||
│ │
|
│ │
|
||||||
│ ├── config/ # 📄 Configuration
|
│ ├── config/ # 📄 Configuration
|
||||||
│ │ └── env.validation.ts # Zod/Joi validation for environment variables
|
│ │ ├── 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
|
│ │ ├── 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
|
│ │ ├── 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
|
│ ├── entities/ # 📚 Database entities
|
||||||
│ │ ├── audit-log.entity.ts # Audit log database entity
|
│ │ ├── audit-log.entity.ts # Audit log database entity
|
||||||
@@ -293,186 +347,222 @@ backend/
|
|||||||
│ │ ├── file-storage.controller.spec.ts # Unit tests
|
│ │ ├── file-storage.controller.spec.ts # Unit tests
|
||||||
│ │ ├── file-storage.module.ts # Module DI bindings
|
│ │ ├── file-storage.module.ts # Module DI bindings
|
||||||
│ │ ├── file-storage.service.ts # File handling logic
|
│ │ ├── 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
|
│ ├── guards/ # 🛡️ JWT authentication guard
|
||||||
│ │ ├── jwt-auth.guard.ts # 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
|
│ │ └── 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
|
│ │ ├── audit-log.interceptor.ts # Automatically logs certain operations
|
||||||
|
│ │ ├── idempotency.interceptor.ts # Idempotency interceptor
|
||||||
│ │ └── transform.interceptor.ts # Standardized response formatting
|
│ │ └── 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
|
├── modules/ # 📦 Module-specific resources
|
||||||
│ ├── user/ # 👤 User + RBAC module
|
│ ├── user/ # 👤 User + RBAC module
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── assign-user-role.dto.ts # Assign roles to users
|
│ │ │ ├── assign-user-role.dto.ts # Assign roles to users
|
||||||
│ │ │ ├── create-user.dto.ts
|
│ │ │ ├── create-user.dto.ts # Create new user
|
||||||
│ │ │ └── update-user.dto.ts
|
│ │ │ ├── search-user.dto.ts # Search users
|
||||||
|
│ │ │ ├── update-user.dto.ts # Update user details
|
||||||
|
│ │ │ └── update-user-preference.dto.ts # Update user preferences
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── user.entity.ts # User table definition
|
│ │ │ ├── user.entity.ts # User table definition
|
||||||
│ │ │ ├── role.entity.ts # Role definition
|
│ │ │ ├── role.entity.ts # Role definition
|
||||||
│ │ │ ├── permission.entity.ts # Permission entity
|
│ │ │ ├── permission.entity.ts # Permission entity
|
||||||
|
│ │ │ ├── user-assignment.entity.ts # User assignment entity
|
||||||
│ │ │ └── user-preference.entity.ts # User preference settings
|
│ │ │ └── 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.controller.ts # REST endpoints
|
||||||
|
│ │ ├── user.module.ts # Module DI container
|
||||||
│ │ ├── user.service.ts # Business logic
|
│ │ ├── user.service.ts # Business logic
|
||||||
│ │ ├── user.service.spec.ts # Unit tests
|
│ │ └── user.service.spec.ts # Unit tests
|
||||||
│ │ └── user.module.ts # Module DI container
|
|
||||||
│ │
|
│ │
|
||||||
│ ├── project/ # 🏢 Project/Organization/Contract structure
|
│ ├── project/ # 🏢 Project/Organization/Contract structure
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── create-project.dto.ts
|
│ │ │ ├── create-project.dto.ts # Create new project
|
||||||
│ │ │ ├── search-project.dto.ts
|
│ │ │ ├── search-project.dto.ts # Search projects
|
||||||
│ │ │ └── update-project.dto.ts
|
│ │ │ └── update-project.dto.ts # Update project
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── project.entity.ts
|
│ │ │ ├── project.entity.ts # Project table definition
|
||||||
│ │ │ ├── contract.entity.ts
|
│ │ │ ├── contract.entity.ts # Contract table definition
|
||||||
│ │ │ ├── organization.entity.ts
|
│ │ │ ├── organization.entity.ts # Organization table definition
|
||||||
│ │ │ ├── project-organization.entity.ts
|
│ │ │ ├── project-organization.entity.ts # Project organization entity
|
||||||
│ │ │ └── contract-organization.entity.ts
|
│ │ │ └── contract-organization.entity.ts # Contract organization entity
|
||||||
│ │ ├── project.controller.ts
|
│ │ ├── project.controller.ts # REST endpoints
|
||||||
│ │ ├── project.controller.spec.ts
|
│ │ ├── project.controller.spec.ts # Unit tests
|
||||||
│ │ └── project.service.ts
|
│ │ ├── project.module.ts # Module DI container
|
||||||
│
|
│ │ ├── project.service.ts # Business logic
|
||||||
|
│ │ └── project.service.spec.ts # Unit tests
|
||||||
|
│ │
|
||||||
│ ├── correspondence/ # ✉️ Formal letters with routing workflow
|
│ ├── correspondence/ # ✉️ Formal letters with routing workflow
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── add-reference.dto.ts
|
│ │ │ ├── add-reference.dto.ts # Add reference to correspondence
|
||||||
│ │ │ ├── create-correspondence.dto.ts
|
│ │ │ ├── create-correspondence.dto.ts # Create new correspondence
|
||||||
│ │ │ ├── search-correspondence.dto.ts
|
│ │ │ ├── search-correspondence.dto.ts # Search correspondences
|
||||||
│ │ │ ├── submit-correspondence.dto.ts
|
│ │ │ ├── submit-correspondence.dto.ts # Submit correspondence
|
||||||
│ │ │ └── workflow-action.dto.ts
|
│ │ │ └── workflow-action.dto.ts # Workflow action
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── correspondence.entity.ts
|
│ │ │ ├── correspondence.entity.ts # Correspondence table definition
|
||||||
│ │ │ ├── correspondence-revision.entity.ts
|
│ │ │ ├── correspondence-revision.entity.ts # Correspondence revision entity
|
||||||
│ │ │ ├── correspondence-routing.entity.ts
|
│ │ │ ├── correspondence-routing.entity.ts # Correspondence routing entity
|
||||||
│ │ │ ├── correspondence-status.entity.ts
|
│ │ │ ├── correspondence-status.entity.ts # Correspondence status entity
|
||||||
│ │ │ ├── correspondence-type.entity.ts
|
│ │ │ ├── correspondence-type.entity.ts # Correspondence type entity
|
||||||
│ │ │ ├── correspondence-reference.entity.ts
|
│ │ │ ├── correspondence-reference.entity.ts # Correspondence reference entity
|
||||||
│ │ │ ├── routing-template.entity.ts
|
│ │ │ ├── routing-template.entity.ts # Routing template entity
|
||||||
│ │ │ └── routing-template-step.entity.ts
|
│ │ │ └── routing-template-step.entity.ts # Routing template step entity
|
||||||
│ │ ├── correspondence.controller.ts
|
│ │ ├── correspondence.controller.ts # REST endpoints
|
||||||
│ │ ├── correspondence.controller.spec.ts
|
│ │ ├── correspondence.controller.spec.ts # Unit tests
|
||||||
│ │ └── correspondence.service.ts
|
│ │ ├── correspondence.module.ts # Module DI container
|
||||||
│
|
│ │ ├── correspondence.service.ts # Business logic
|
||||||
|
│ │ └── correspondence.service.spec.ts # Unit tests
|
||||||
|
│ │
|
||||||
│ ├── drawing/ # 📐Contract & Shop drawing tracking
|
│ ├── drawing/ # 📐Contract & Shop drawing tracking
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── create-contract-drawing.dto.ts
|
│ │ │ ├── create-contract-drawing.dto.ts # Create new contract drawing
|
||||||
│ │ │ ├── create-shop-drawing.dto.ts
|
│ │ │ ├── create-shop-drawing.dto.ts # Create new shop drawing
|
||||||
│ │ │ ├── create-shop-drawing-revision.dto.ts
|
│ │ │ ├── create-shop-drawing-revision.dto.ts # Create new shop drawing revision
|
||||||
│ │ │ ├── search-contract-drawing.dto.ts
|
│ │ │ ├── search-contract-drawing.dto.ts # Search contract drawings
|
||||||
│ │ │ ├── search-shop-drawing.dto.ts
|
│ │ │ ├── search-shop-drawing.dto.ts # Search shop drawings
|
||||||
│ │ │ └── update-contract-drawing.dto.ts
|
│ │ │ └── update-contract-drawing.dto.ts # Update contract drawing
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── contract-drawing.entity.ts
|
│ │ │ ├── contract-drawing.entity.ts # Contract drawing entity
|
||||||
│ │ │ ├── contract-drawing-volume.entity.ts
|
│ │ │ ├── contract-drawing-volume.entity.ts # Contract drawing volume entity
|
||||||
│ │ │ ├── contract-drawing-sub-category.entity.ts
|
│ │ │ ├── contract-drawing-sub-category.entity.ts # Contract drawing sub category entity
|
||||||
│ │ │ ├── shop-drawing.entity.ts
|
│ │ │ ├── shop-drawing.entity.ts # Shop drawing entity
|
||||||
│ │ │ ├── shop-drawing-revision.entity.ts
|
│ │ │ ├── shop-drawing-revision.entity.ts # Shop drawing revision entity
|
||||||
│ │ │ ├── shop-drawing-main-category.entity.ts
|
│ │ │ ├── shop-drawing-main-category.entity.ts # Shop drawing main category entity
|
||||||
│ │ │ └── shop-drawing-sub-category.entity.ts
|
│ │ │ └── shop-drawing-sub-category.entity.ts # Shop drawing sub category entity
|
||||||
│ │ ├── drawing.module.ts
|
│ │ ├── drawing.module.ts # Module DI container
|
||||||
│ │ ├── contract-drawing.controller.ts
|
│ │ ├── contract-drawing.controller.ts # REST endpoints
|
||||||
│ │ ├── contract-drawing.service.ts
|
│ │ ├── contract-drawing.service.ts # Business logic
|
||||||
│ │ ├── drawing-master-data.controller.ts
|
│ │ ├── drawing-master-data.controller.ts # REST endpoints
|
||||||
│ │ ├── drawing-master-data.service.ts
|
│ │ ├── drawing-master-data.service.ts # Business logic
|
||||||
│ │ ├── shop-drawing.controller.ts
|
│ │ ├── shop-drawing.controller.ts # REST endpoints
|
||||||
│ │ └── shop-drawing.service.ts
|
│ │ └── shop-drawing.service.ts # Business logic
|
||||||
│
|
│ │
|
||||||
│ ├── rfa/ # ✅ Request for Approval (multi-step workflow)
|
│ ├── rfa/ # ✅ Request for Approval (multi-step workflow)
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── create-rfa.dto.ts
|
│ │ │ ├── create-rfa.dto.ts # Create new RFA
|
||||||
│ │ │ ├── search-rfa.dto.ts
|
│ │ │ ├── search-rfa.dto.ts # Search RFAs
|
||||||
│ │ │ └── update-rfa.dto.ts
|
│ │ │ └── update-rfa.dto.ts # Update RFA
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── rfa.entity.ts
|
│ │ │ ├── rfa.entity.ts # RFA entity
|
||||||
│ │ │ ├── rfa-revision.entity.ts
|
│ │ │ ├── rfa-revision.entity.ts # RFA revision entity
|
||||||
│ │ │ ├── rfa-item.entity.ts
|
│ │ │ ├── rfa-item.entity.ts # RFA item entity
|
||||||
│ │ │ ├── rfa-type.entity.ts
|
│ │ │ ├── rfa-type.entity.ts # RFA type entity
|
||||||
│ │ │ ├── rfa-status-code.entity.ts
|
│ │ │ ├── rfa-status-code.entity.ts # RFA status code entity
|
||||||
│ │ │ ├── rfa-approve-code.entity.ts
|
│ │ │ ├── rfa-approve-code.entity.ts # RFA approve code entity
|
||||||
│ │ │ ├── rfa-workflow.entity.ts
|
│ │ │ ├── rfa-workflow.entity.ts # RFA workflow entity
|
||||||
│ │ │ ├── rfa-workflow-template.entity.ts
|
│ │ │ ├── rfa-workflow-template.entity.ts # RFA workflow template entity
|
||||||
│ │ │ └── rfa-workflow-template-step.entity.ts
|
│ │ │ └── rfa-workflow-template-step.entity.ts # RFA workflow template step entity
|
||||||
│ │ ├── rfa.controller.ts
|
│ │ ├── rfa.controller.ts # REST endpoints
|
||||||
│ │ ├── rfa.module.ts
|
│ │ ├── rfa.module.ts # Module DI container
|
||||||
│ │ └── rfa.service.ts
|
│ │ └── rfa.service.ts # Business logic
|
||||||
│
|
│ │
|
||||||
│ ├── circulation/ # 🔄 Internal routing workflow
|
│ ├── circulation/ # 🔄 Internal routing workflow
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── create-circulation.dto.ts
|
│ │ │ ├── create-circulation.dto.ts # Create new circulation
|
||||||
│ │ │ ├── update-circulation-routing.dto.ts
|
│ │ │ ├── update-circulation-routing.dto.ts # Update circulation routing
|
||||||
│ │ │ └── search-circulation.dto.ts
|
│ │ │ └── search-circulation.dto.ts # Search circulation
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── circulation.entity.ts
|
│ │ │ ├── circulation.entity.ts # Circulation entity
|
||||||
│ │ │ ├── circulation-routing.entity.ts
|
│ │ │ ├── circulation-routing.entity.ts # Circulation routing entity
|
||||||
│ │ │ └── circulation-status-code.entity.ts
|
│ │ │ └── circulation-status-code.entity.ts # Circulation status code entity
|
||||||
│ │ ├── circulation.controller.ts
|
│ │ ├── circulation.controller.ts # REST endpoints
|
||||||
│ │ ├── circulation.module.ts
|
│ │ ├── circulation.module.ts # Module DI container
|
||||||
│ │ └── circulation.service.ts
|
│ │ └── circulation.service.ts # Business logic
|
||||||
│
|
│ │
|
||||||
│ ├── transmittal/ # 📤 Document forwarding
|
│ ├── transmittal/ # 📤 Document forwarding
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── create-transmittal.dto.ts
|
│ │ │ ├── create-transmittal.dto.ts # Create new transmittal
|
||||||
│ │ │ ├── search-transmittal.dto.ts
|
│ │ │ ├── search-transmittal.dto.ts # Search transmittal
|
||||||
│ │ │ └── update-transmittal.dto.ts
|
│ │ │ └── update-transmittal.dto.ts # Update transmittal
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── transmittal.entity.ts
|
│ │ │ ├── transmittal.entity.ts # Transmittal entity
|
||||||
│ │ │ └── transmittal-item.entity.ts
|
│ │ │ └── transmittal-item.entity.ts # Transmittal item entity
|
||||||
│ │ ├── transmittal.controller.ts
|
│ │ ├── transmittal.controller.ts # REST endpoints
|
||||||
│ │ ├── transmittal.module.ts
|
│ │ ├── transmittal.module.ts # Module DI container
|
||||||
│ │ └── transmittal.service.ts
|
│ │ └── transmittal.service.ts # Business logic
|
||||||
│
|
│ │
|
||||||
│ ├── notification/ # 🔔 Real-Time notification system
|
│ ├── notification/ # 🔔 Real-Time notification system
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ ├── create-notification.dto.ts
|
│ │ │ ├── create-notification.dto.ts # Create new notification
|
||||||
│ │ │ └── search-notification.dto.ts
|
│ │ │ └── search-notification.dto.ts # Search notification
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ └── notification.entity.ts
|
│ │ │ └── notification.entity.ts # Notification entity
|
||||||
│ │ ├── notification.module.ts # WebSocket + Processor registration
|
│ │ ├── notification.module.ts # WebSocket + Processor registration
|
||||||
│ │ ├── notification.controller.ts
|
│ │ ├── notification.controller.ts # REST endpoints
|
||||||
│ │ ├── notification.gateway.ts # WebSocket gateway
|
│ │ ├── notification.gateway.ts # WebSocket gateway
|
||||||
│ │ ├── notification.processor.ts # Message consumer (e.g. mail worker)
|
│ │ ├── notification.processor.ts # Message consumer (e.g. mail worker)
|
||||||
│ │ ├── notification.service.ts
|
│ │ ├── notification.service.ts # Business logic
|
||||||
│ │ └── notification-cleanup.service.ts # Cron-based cleanup job
|
│ │ └── notification-cleanup.service.ts # Cron-based cleanup job
|
||||||
│
|
│ │
|
||||||
│ ├── search/ # 🔍 Elasticsearch integration
|
│ ├── search/ # 🔍 Elasticsearch integration
|
||||||
│ │ ├── dto/
|
│ │ ├── dto/
|
||||||
│ │ │ └── search-query.dto.ts
|
│ │ │ └── search-query.dto.ts # Search query
|
||||||
│ │ ├── search.module.ts
|
│ │ ├── search.module.ts # Module DI container
|
||||||
│ │ ├── search.controller.ts
|
│ │ ├── search.controller.ts # REST endpoints
|
||||||
│ │ └── search.service.ts # Indexing/search logic
|
│ │ └── search.service.ts # Indexing/search logic
|
||||||
│
|
│ │
|
||||||
│ ├── document-numbering/ # 🔢 Auto-increment controlled ID generation
|
│ ├── document-numbering/ # 🔢 Auto-increment controlled ID generation
|
||||||
│ │ ├── entities/
|
│ │ ├── entities/
|
||||||
│ │ │ ├── document-number-format.entity.ts
|
│ │ │ ├── document-number-format.entity.ts # Document number format entity
|
||||||
│ │ │ └── document-number-counter.entity.ts
|
│ │ │ └── document-number-counter.entity.ts # Document number counter entity
|
||||||
│ │ ├── document-numbering.module.ts
|
│ │ ├── document-numbering.module.ts # Module DI container
|
||||||
│ │ ├── document-numbering.service.ts
|
│ │ ├── document-numbering.service.ts # Business logic
|
||||||
│ │ └── document-numbering.service.spec.ts
|
│ │ └── document-numbering.service.spec.ts # Unit tests
|
||||||
│
|
│ │
|
||||||
│ ├── workflow-engine/ # ⚙️ Unified state-machine workflow engine
|
│ ├── 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/
|
│ │ ├── interfaces/
|
||||||
│ │ │ └── workflow.interface.ts
|
│ │ │ └── workflow.interface.ts # Workflow interface
|
||||||
│ │ ├── workflow-engine.module.ts
|
│ │ ├── workflow-engine.controller.ts # REST endpoints
|
||||||
│ │ ├── workflow-engine.service.ts
|
│ │ ├── workflow-engine.module.ts # Module DI container
|
||||||
│ │ └── workflow-engine.service.spec.ts
|
│ │ ├── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.7.3",
|
"@casl/ability": "^6.7.3",
|
||||||
"@elastic/elasticsearch": "^8.11.1",
|
"@elastic/elasticsearch": "^8.11.1",
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/bullmq": "^11.0.4",
|
"@nestjs/bullmq": "^11.0.4",
|
||||||
"@nestjs/cache-manager": "^3.0.1",
|
"@nestjs/cache-manager": "^3.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
@@ -35,12 +36,14 @@
|
|||||||
"@nestjs/platform-socket.io": "^11.1.9",
|
"@nestjs/platform-socket.io": "^11.1.9",
|
||||||
"@nestjs/schedule": "^6.0.1",
|
"@nestjs/schedule": "^6.0.1",
|
||||||
"@nestjs/swagger": "^11.2.3",
|
"@nestjs/swagger": "^11.2.3",
|
||||||
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/throttler": "^6.4.0",
|
"@nestjs/throttler": "^6.4.0",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"@nestjs/websockets": "^11.1.9",
|
"@nestjs/websockets": "^11.1.9",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
|
"async-retry": "^1.3.3",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bullmq": "^5.63.2",
|
"bullmq": "^5.63.2",
|
||||||
@@ -54,16 +57,20 @@
|
|||||||
"joi": "^18.0.1",
|
"joi": "^18.0.1",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.15.3",
|
"mysql2": "^3.15.3",
|
||||||
|
"nest-winston": "^1.10.2",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
|
"opossum": "^9.0.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
"prom-client": "^15.1.3",
|
||||||
"redlock": "5.0.0-beta.2",
|
"redlock": "5.0.0-beta.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0",
|
||||||
|
"winston": "^3.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
@@ -71,6 +78,7 @@
|
|||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/async-retry": "^1.4.9",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/cache-manager": "^5.0.0",
|
"@types/cache-manager": "^5.0.0",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
@@ -79,6 +87,7 @@
|
|||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/opossum": "^8.1.9",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// File: src/app.module.ts
|
// File: src/app.module.ts
|
||||||
// บันทึกการแก้ไข: เพิ่ม CacheModule (Redis), Config สำหรับ Idempotency และ Maintenance Mode (T1.1)
|
// บันทึกการแก้ไข: เพิ่ม CacheModule (Redis), Config สำหรับ Idempotency และ Maintenance Mode (T1.1)
|
||||||
|
// บันทึกการแก้ไข: เพิ่ม MonitoringModule และ WinstonModule (T6.3)
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
@@ -8,18 +9,19 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
import { CacheModule } from '@nestjs/cache-manager';
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
|
import { WinstonModule } from 'nest-winston'; // ✅ Import WinstonModule
|
||||||
import { redisStore } from 'cache-manager-redis-yet';
|
import { redisStore } from 'cache-manager-redis-yet';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { envValidationSchema } from './common/config/env.validation.js';
|
import { envValidationSchema } from './common/config/env.validation.js';
|
||||||
import redisConfig from './common/config/redis.config';
|
import redisConfig from './common/config/redis.config';
|
||||||
|
import { winstonConfig } from './modules/monitoring/logger/winston.config'; // ✅ Import Config
|
||||||
|
|
||||||
// Entities & Interceptors
|
// Entities & Interceptors
|
||||||
import { AuditLog } from './common/entities/audit-log.entity';
|
import { AuditLog } from './common/entities/audit-log.entity';
|
||||||
import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor';
|
import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor';
|
||||||
// ✅ Import Guard ใหม่สำหรับ Maintenance Mode
|
|
||||||
import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard';
|
import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard';
|
||||||
// import { IdempotencyInterceptor } from './common/interceptors/idempotency.interceptor'; // ✅ เตรียมไว้ใช้ (ถ้าต้องการ Global)
|
// import { IdempotencyInterceptor } from './common/interceptors/idempotency.interceptor';
|
||||||
|
|
||||||
// Modules
|
// Modules
|
||||||
import { UserModule } from './modules/user/user.module';
|
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 { JsonSchemaModule } from './modules/json-schema/json-schema.module.js';
|
||||||
import { WorkflowEngineModule } from './modules/workflow-engine/workflow-engine.module';
|
import { WorkflowEngineModule } from './modules/workflow-engine/workflow-engine.module';
|
||||||
import { CorrespondenceModule } from './modules/correspondence/correspondence.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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
// 1. Setup Config Module พร้อม Validation
|
// 1. Setup Config Module พร้อม Validation
|
||||||
@@ -68,6 +79,9 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo
|
|||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 📝 Setup Winston Logger (Structured Logging) [Req 6.10]
|
||||||
|
WinstonModule.forRoot(winstonConfig),
|
||||||
|
|
||||||
// 2. Setup TypeORM (MariaDB)
|
// 2. Setup TypeORM (MariaDB)
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
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]),
|
TypeOrmModule.forFeature([AuditLog]),
|
||||||
|
|
||||||
// 3. BullMQ (Redis) Setup
|
// 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
|
// Feature Modules
|
||||||
AuthModule,
|
AuthModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
@@ -109,16 +126,23 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo
|
|||||||
JsonSchemaModule,
|
JsonSchemaModule,
|
||||||
WorkflowEngineModule,
|
WorkflowEngineModule,
|
||||||
CorrespondenceModule,
|
CorrespondenceModule,
|
||||||
|
RfaModule, // 👈 ต้องมี
|
||||||
|
DrawingModule, // 👈 ต้องมี
|
||||||
|
TransmittalModule, // 👈 ต้องมี
|
||||||
|
CirculationModule, // 👈 ต้องมี
|
||||||
|
SearchModule, // ✅ Register Module
|
||||||
|
NotificationModule, // 👈 ต้องมี
|
||||||
|
ResilienceModule, // ✅ Register Module
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
AppService,
|
AppService,
|
||||||
// 🛡️ 1. Register Global Guard (Rate Limit) - ทำงานก่อนเพื่อน
|
// 🛡️ 1. Register Global Guard (Rate Limit)
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: ThrottlerGuard,
|
useClass: ThrottlerGuard,
|
||||||
},
|
},
|
||||||
// 🚧 2. Maintenance Mode Guard - ทำงานต่อมา เพื่อ Block การเข้าถึงถ้าระบบปิดอยู่
|
// 🚧 2. Maintenance Mode Guard
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: MaintenanceModeGuard,
|
useClass: MaintenanceModeGuard,
|
||||||
@@ -128,7 +152,7 @@ import { CorrespondenceModule } from './modules/correspondence/correspondence.mo
|
|||||||
provide: APP_INTERCEPTOR,
|
provide: APP_INTERCEPTOR,
|
||||||
useClass: AuditLogInterceptor,
|
useClass: AuditLogInterceptor,
|
||||||
},
|
},
|
||||||
// 🔄 4. Register Idempotency (Uncomment เมื่อต้องการบังคับใช้ Global)
|
// 🔄 4. Register Idempotency (ถ้าต้องการ Global)
|
||||||
// {
|
// {
|
||||||
// provide: APP_INTERCEPTOR,
|
// provide: APP_INTERCEPTOR,
|
||||||
// useClass: IdempotencyInterceptor,
|
// useClass: IdempotencyInterceptor,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// File: src/common/auth/auth.controller.ts
|
// File: src/common/auth/auth.controller.ts
|
||||||
// บันทึกการแก้ไข: เพิ่ม Endpoints ให้ครบตามแผน T1.2 (Refresh, Logout, Profile)
|
// บันทึกการแก้ไข: เพิ่ม Type ให้ req และแก้ไข Import (Fix TS7006)
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
Request,
|
Req,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@@ -16,9 +16,15 @@ import { Throttle } from '@nestjs/throttler';
|
|||||||
import { AuthService } from './auth.service.js';
|
import { AuthService } from './auth.service.js';
|
||||||
import { LoginDto } from './dto/login.dto.js';
|
import { LoginDto } from './dto/login.dto.js';
|
||||||
import { RegisterDto } from './dto/register.dto.js';
|
import { RegisterDto } from './dto/register.dto.js';
|
||||||
import { JwtAuthGuard } from './guards/jwt-auth.guard.js';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
||||||
import { JwtRefreshGuard } from './guards/jwt-refresh.guard.js'; // ต้องสร้าง Guard นี้
|
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard.js';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; // (ถ้าใช้ Swagger)
|
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')
|
@ApiTags('Authentication')
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@@ -26,7 +32,7 @@ export class AuthController {
|
|||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@Throttle({ default: { limit: 5, ttl: 60000 } }) // เข้มงวด: 5 ครั้ง/นาที
|
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'เข้าสู่ระบบเพื่อรับ Access & Refresh Token' })
|
@ApiOperation({ summary: 'เข้าสู่ระบบเพื่อรับ Access & Refresh Token' })
|
||||||
async login(@Body() loginDto: LoginDto) {
|
async login(@Body() loginDto: LoginDto) {
|
||||||
@@ -43,7 +49,7 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('register-admin')
|
@Post('register-admin')
|
||||||
@UseGuards(JwtAuthGuard) // ควรป้องกัน Route นี้ให้เฉพาะ Superadmin
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'สร้างบัญชีผู้ใช้ใหม่ (Admin Only)' })
|
@ApiOperation({ summary: 'สร้างบัญชีผู้ใช้ใหม่ (Admin Only)' })
|
||||||
async register(@Body() registerDto: RegisterDto) {
|
async register(@Body() registerDto: RegisterDto) {
|
||||||
@@ -54,8 +60,8 @@ export class AuthController {
|
|||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'ขอ Access Token ใหม่ด้วย Refresh Token' })
|
@ApiOperation({ summary: 'ขอ Access Token ใหม่ด้วย Refresh Token' })
|
||||||
async refresh(@Request() req) {
|
async refresh(@Req() req: RequestWithUser) {
|
||||||
// req.user จะมาจาก JwtRefreshStrategy
|
// ✅ ระบุ Type ชัดเจน
|
||||||
return this.authService.refreshToken(req.user.sub, req.user.refreshToken);
|
return this.authService.refreshToken(req.user.sub, req.user.refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,9 +70,13 @@ export class AuthController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'ออกจากระบบ (Revoke Token)' })
|
@ApiOperation({ summary: 'ออกจากระบบ (Revoke Token)' })
|
||||||
async logout(@Request() req) {
|
async logout(@Req() req: RequestWithUser) {
|
||||||
// ดึง Token จาก Header Authorization: Bearer <token>
|
// ✅ ระบุ Type ชัดเจน
|
||||||
const token = req.headers.authorization?.split(' ')[1];
|
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);
|
return this.authService.logout(req.user.sub, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +84,8 @@ export class AuthController {
|
|||||||
@Get('profile')
|
@Get('profile')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'ดึงข้อมูลผู้ใช้ปัจจุบัน' })
|
@ApiOperation({ summary: 'ดึงข้อมูลผู้ใช้ปัจจุบัน' })
|
||||||
getProfile(@Request() req) {
|
getProfile(@Req() req: RequestWithUser) {
|
||||||
|
// ✅ ระบุ Type ชัดเจน
|
||||||
return req.user;
|
return req.user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// File: src/common/auth/auth.module.ts
|
// File: src/common/auth/auth.module.ts
|
||||||
// บันทึกการแก้ไข: ลงทะเบียน Refresh Strategy และแก้ไข Config
|
// บันทึกการแก้ไข: แก้ไข Type Mismatch ของ expiresIn (Fix TS2322)
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
@@ -21,17 +21,14 @@ import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy.js';
|
|||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
secret: configService.get<string>('JWT_SECRET'),
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
signOptions: {
|
signOptions: {
|
||||||
// ใช้ Template String หรือค่า Default ที่ปลอดภัย
|
// ✅ Fix: Cast เป็น any เพื่อแก้ปัญหา Type ไม่ตรงกับ Library (StringValue vs string)
|
||||||
expiresIn: configService.get<string>('JWT_EXPIRATION') || '15m',
|
expiresIn: (configService.get<string>('JWT_EXPIRATION') ||
|
||||||
|
'15m') as any,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [AuthService, JwtStrategy, JwtRefreshStrategy],
|
||||||
AuthService,
|
|
||||||
JwtStrategy,
|
|
||||||
JwtRefreshStrategy, // ✅ เพิ่ม Strategy สำหรับ Refresh Token
|
|
||||||
],
|
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// File: src/common/auth/auth.service.ts
|
// File: src/common/auth/auth.service.ts
|
||||||
// บันทึกการแก้ไข: เพิ่ม Refresh Token, Logout (Redis Blacklist) และ Profile ตาม T1.2
|
// บันทึกการแก้ไข: แก้ไข Type Mismatch ใน signAsync (Fix TS2769)
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
@@ -10,11 +10,10 @@ import {
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { Cache } from 'cache-manager';
|
import type { Cache } from 'cache-manager';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { UserService } from '../../modules/user/user.service.js';
|
import { UserService } from '../../modules/user/user.service.js';
|
||||||
import { RegisterDto } from './dto/register.dto.js';
|
import { RegisterDto } from './dto/register.dto.js';
|
||||||
import { User } from '../../modules/user/entities/user.entity.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -22,7 +21,7 @@ export class AuthService {
|
|||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
@Inject(CACHE_MANAGER) private cacheManager: Cache, // ใช้ Redis สำหรับ Blacklist
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// 1. ตรวจสอบ Username/Password
|
// 1. ตรวจสอบ Username/Password
|
||||||
@@ -41,31 +40,33 @@ export class AuthService {
|
|||||||
const payload = {
|
const payload = {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
sub: user.user_id,
|
sub: user.user_id,
|
||||||
scope: 'Global', // ตัวอย่าง: ใส่ Scope เริ่มต้น หรือดึงจาก Role
|
scope: 'Global',
|
||||||
};
|
};
|
||||||
|
|
||||||
const [accessToken, refreshToken] = await Promise.all([
|
const [accessToken, refreshToken] = await Promise.all([
|
||||||
this.jwtService.signAsync(payload, {
|
this.jwtService.signAsync(payload, {
|
||||||
secret: this.configService.get<string>('JWT_SECRET'),
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
expiresIn: this.configService.get<string>('JWT_EXPIRATION') || '15m',
|
// ✅ Fix: Cast as any
|
||||||
|
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||||
|
'15m') as any,
|
||||||
}),
|
}),
|
||||||
this.jwtService.signAsync(payload, {
|
this.jwtService.signAsync(payload, {
|
||||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||||
expiresIn:
|
// ✅ Fix: Cast as any
|
||||||
this.configService.get<string>('JWT_REFRESH_EXPIRATION') || '7d',
|
expiresIn: (this.configService.get<string>('JWT_REFRESH_EXPIRATION') ||
|
||||||
|
'7d') as any,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
user: user, // ส่งข้อมูล user กลับไปให้ Frontend ใช้แสดงผลเบื้องต้น
|
user: user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Register (สำหรับ Admin)
|
// 3. Register (สำหรับ Admin)
|
||||||
async register(userDto: RegisterDto) {
|
async register(userDto: RegisterDto) {
|
||||||
// ตรวจสอบว่ามี user อยู่แล้วหรือไม่
|
|
||||||
const existingUser = await this.userService.findOneByUsername(
|
const existingUser = await this.userService.findOneByUsername(
|
||||||
userDto.username,
|
userDto.username,
|
||||||
);
|
);
|
||||||
@@ -84,33 +85,30 @@ export class AuthService {
|
|||||||
|
|
||||||
// 4. Refresh Token: ออก Token ใหม่
|
// 4. Refresh Token: ออก Token ใหม่
|
||||||
async refreshToken(userId: number, refreshToken: string) {
|
async refreshToken(userId: number, refreshToken: string) {
|
||||||
// ตรวจสอบความถูกต้องของ Refresh Token (ถ้าใช้ DB เก็บ Refresh Token ก็เช็คตรงนี้)
|
|
||||||
// ในที่นี้เราเชื่อใจ Signature ของ JWT Refresh Secret
|
|
||||||
const user = await this.userService.findOne(userId);
|
const user = await this.userService.findOne(userId);
|
||||||
if (!user) throw new UnauthorizedException('User not found');
|
if (!user) throw new UnauthorizedException('User not found');
|
||||||
|
|
||||||
// สร้าง Access Token ใหม่
|
|
||||||
const payload = { username: user.username, sub: user.user_id };
|
const payload = { username: user.username, sub: user.user_id };
|
||||||
|
|
||||||
const accessToken = await this.jwtService.signAsync(payload, {
|
const accessToken = await this.jwtService.signAsync(payload, {
|
||||||
secret: this.configService.get<string>('JWT_SECRET'),
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
expiresIn: this.configService.get<string>('JWT_EXPIRATION') || '15m',
|
// ✅ Fix: Cast as any
|
||||||
|
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||||
|
'15m') as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
// refresh_token: refreshToken, // จะส่งเดิมกลับ หรือ Rotate ใหม่ก็ได้ (แนะนำ Rotate เพื่อความปลอดภัยสูงสุด)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Logout: นำ Token เข้า Blacklist ใน Redis
|
// 5. Logout: นำ Token เข้า Blacklist ใน Redis
|
||||||
async logout(userId: number, accessToken: string) {
|
async logout(userId: number, accessToken: string) {
|
||||||
// หาเวลาที่เหลือของ Token เพื่อตั้ง TTL ใน Redis
|
|
||||||
try {
|
try {
|
||||||
const decoded = this.jwtService.decode(accessToken);
|
const decoded = this.jwtService.decode(accessToken);
|
||||||
if (decoded && decoded.exp) {
|
if (decoded && decoded.exp) {
|
||||||
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
|
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
|
||||||
if (ttl > 0) {
|
if (ttl > 0) {
|
||||||
// Key pattern: blacklist:token:{token_string}
|
|
||||||
await this.cacheManager.set(
|
await this.cacheManager.set(
|
||||||
`blacklist:token:${accessToken}`,
|
`blacklist:token:${accessToken}`,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// File: src/common/auth/strategies/jwt-refresh.strategy.ts
|
// File: src/common/auth/strategies/jwt-refresh.strategy.ts
|
||||||
// บันทึกการแก้ไข: Strategy สำหรับ Refresh Token (T1.2)
|
// บันทึกการแก้ไข: Strategy สำหรับ Refresh Token (T1.2)
|
||||||
|
// บันทึกการแก้ไข: แก้ไข TS2345 โดยยืนยันค่า secretOrKey ด้วย ! (Non-null assertion)
|
||||||
|
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
@@ -16,8 +17,8 @@ export class JwtRefreshStrategy extends PassportStrategy(
|
|||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
// ใช้ Secret แยกต่างหากสำหรับ Refresh Token
|
// ✅ Fix: ใส่ ! เพื่อบอก TS ว่าค่านี้มีอยู่จริง (จาก env validation)
|
||||||
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
|
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET')!,
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
// File: src/common/auth/strategies/jwt.strategy.ts
|
// บันทึกการแก้ไข: แก้ไข TS2345 (secretOrKey type) และ TS2551 (user.isActive property name)
|
||||||
// บันทึกการแก้ไข: ปรับปรุง JwtStrategy ให้ตรวจสอบ Blacklist (Redis) และสถานะ User (T1.2)
|
|
||||||
|
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Injectable, UnauthorizedException, Inject } from '@nestjs/common';
|
import { Injectable, UnauthorizedException, Inject } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager'; // ✅ ใช้สำหรับ Blacklist
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { Cache } from 'cache-manager';
|
import type { Cache } from 'cache-manager';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { UserService } from '../../../modules/user/user.service.js';
|
import { UserService } from '../../../modules/user/user.service.js';
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ import { UserService } from '../../../modules/user/user.service.js';
|
|||||||
export interface JwtPayload {
|
export interface JwtPayload {
|
||||||
sub: number;
|
sub: number;
|
||||||
username: string;
|
username: string;
|
||||||
scope?: string; // เพิ่ม Scope ถ้ามีการใช้
|
scope?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -22,13 +21,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
constructor(
|
constructor(
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
@Inject(CACHE_MANAGER) private cacheManager: Cache, // ✅ Inject Redis Cache
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: configService.get<string>('JWT_SECRET'),
|
// ✅ Fix TS2345: ใส่ ! เพื่อยืนยันว่า Secret Key มีค่าแน่นอน
|
||||||
passReqToCallback: true, // ✅ จำเป็นต้องใช้ เพื่อดึง Raw Token มาเช็ค Blacklist
|
secretOrKey: configService.get<string>('JWT_SECRET')!,
|
||||||
|
passReqToCallback: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
// 1. ดึง Token ออกมาเพื่อตรวจสอบใน Blacklist
|
// 1. ดึง Token ออกมาเพื่อตรวจสอบใน Blacklist
|
||||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
|
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
|
||||||
|
|
||||||
// 2. ตรวจสอบว่า Token นี้อยู่ใน Redis Blacklist หรือไม่ (กรณี Logout ไปแล้ว)
|
// 2. ตรวจสอบว่า Token นี้อยู่ใน Redis Blacklist หรือไม่
|
||||||
const isBlacklisted = await this.cacheManager.get(
|
const isBlacklisted = await this.cacheManager.get(
|
||||||
`blacklist:token:${token}`,
|
`blacklist:token:${token}`,
|
||||||
);
|
);
|
||||||
@@ -53,11 +53,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. (Optional) ตรวจสอบว่า User ยัง Active อยู่หรือไม่
|
// 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');
|
throw new UnauthorizedException('User account is inactive');
|
||||||
}
|
}
|
||||||
|
|
||||||
// คืนค่า User เพื่อนำไปใส่ใน req.user
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
// File: src/common/config/redis.config.ts
|
// File: src/common/config/redis.config.ts
|
||||||
// บันทึกการแก้ไข: สร้าง Config สำหรับ Redis (T0.2)
|
// บันทึกการแก้ไข: สร้าง Config สำหรับ Redis (T0.2)
|
||||||
|
// บันทึกการแก้ไข: แก้ไข TS2345 โดยการจัดการค่า undefined ของ process.env ก่อน parseInt
|
||||||
|
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
export default registerAs('redis', () => ({
|
export default registerAs('redis', () => ({
|
||||||
host: process.env.REDIS_HOST || 'cache', // Default เป็นชื่อ Service ใน Docker
|
// ใช้ค่า Default 'cache' ถ้าหาไม่เจอ
|
||||||
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
host: process.env.REDIS_HOST || 'cache',
|
||||||
ttl: parseInt(process.env.REDIS_TTL, 10) || 3600, // Default TTL 1 ชั่วโมง
|
// ✅ Fix: ใช้ || '6379' เพื่อให้มั่นใจว่าเป็น string ก่อนเข้า parseInt
|
||||||
// password: process.env.REDIS_PASSWORD, // เปิดใช้ถ้ามี Password
|
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
||||||
|
// ✅ Fix: ใช้ || '3600' เพื่อให้มั่นใจว่าเป็น string
|
||||||
|
ttl: parseInt(process.env.REDIS_TTL || '3600', 10),
|
||||||
|
// password: process.env.REDIS_PASSWORD,
|
||||||
}));
|
}));
|
||||||
|
|||||||
49
backend/src/common/decorators/circuit-breaker.decorator.ts
Normal file
49
backend/src/common/decorators/circuit-breaker.decorator.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
60
backend/src/common/decorators/retry.decorator.ts
Normal file
60
backend/src/common/decorators/retry.decorator.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
70
backend/src/common/file-storage/file-cleanup.service.ts
Normal file
70
backend/src/common/file-storage/file-cleanup.service.ts
Normal file
@@ -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<Attachment>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* รันทุกวันเวลาเที่ยงคืน (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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
|
// File: src/common/file-storage/file-storage.controller.ts
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Post,
|
Post,
|
||||||
|
Get,
|
||||||
|
Delete, // ✅ Import Delete
|
||||||
|
Param,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -8,12 +12,16 @@ import {
|
|||||||
ParseFilePipe,
|
ParseFilePipe,
|
||||||
MaxFileSizeValidator,
|
MaxFileSizeValidator,
|
||||||
FileTypeValidator,
|
FileTypeValidator,
|
||||||
|
Res,
|
||||||
|
StreamableFile,
|
||||||
|
ParseIntPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import type { Response } from 'express';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { FileStorageService } from './file-storage.service.js';
|
import { FileStorageService } from './file-storage.service.js';
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
||||||
|
|
||||||
// ✅ 1. สร้าง Interface เพื่อระบุ Type ของ Request
|
// Interface เพื่อระบุ Type ของ Request ที่ผ่าน JwtAuthGuard มาแล้ว
|
||||||
interface RequestWithUser {
|
interface RequestWithUser {
|
||||||
user: {
|
user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -33,17 +41,56 @@ export class FileStorageController {
|
|||||||
new ParseFilePipe({
|
new ParseFilePipe({
|
||||||
validators: [
|
validators: [
|
||||||
new MaxFileSizeValidator({ maxSize: 50 * 1024 * 1024 }), // 50MB
|
new MaxFileSizeValidator({ maxSize: 50 * 1024 * 1024 }), // 50MB
|
||||||
// ตรวจสอบประเภทไฟล์ (Regex)
|
// ตรวจสอบประเภทไฟล์ (Regex) - รวม image, pdf, docs, zip
|
||||||
new FileTypeValidator({
|
new FileTypeValidator({
|
||||||
fileType: /(pdf|msword|openxmlformats|zip|octet-stream)/,
|
fileType:
|
||||||
|
/(pdf|msword|openxmlformats|zip|octet-stream|image|jpeg|png)/,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
@Request() req: RequestWithUser, // ✅ 2. ระบุ Type ตรงนี้แทน any
|
@Request() req: RequestWithUser,
|
||||||
) {
|
) {
|
||||||
// ส่ง userId จาก Token ไปด้วย
|
// ส่ง userId จาก Token ไปด้วย
|
||||||
return this.fileStorageService.upload(file, req.user.userId);
|
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<StreamableFile> {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule'; // ✅ Import
|
||||||
import { FileStorageService } from './file-storage.service.js';
|
import { FileStorageService } from './file-storage.service.js';
|
||||||
import { FileStorageController } from './file-storage.controller.js';
|
import { FileStorageController } from './file-storage.controller.js';
|
||||||
|
import { FileCleanupService } from './file-cleanup.service.js'; // ✅ Import
|
||||||
import { Attachment } from './entities/attachment.entity.js';
|
import { Attachment } from './entities/attachment.entity.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Attachment])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Attachment]),
|
||||||
|
ScheduleModule.forRoot(), // ✅ เปิดใช้งาน Cron Job],
|
||||||
|
],
|
||||||
controllers: [FileStorageController],
|
controllers: [FileStorageController],
|
||||||
providers: [FileStorageService],
|
providers: [
|
||||||
|
FileStorageService,
|
||||||
|
FileCleanupService, // ✅ Register Provider
|
||||||
|
],
|
||||||
exports: [FileStorageService], // Export ให้ Module อื่น (เช่น Correspondence) เรียกใช้ตอน Commit
|
exports: [FileStorageService], // Export ให้ Module อื่น (เช่น Correspondence) เรียกใช้ตอน Commit
|
||||||
})
|
})
|
||||||
export class FileStorageModule {}
|
export class FileStorageModule {}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// File: src/common/file-storage/file-storage.service.ts
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
@@ -12,6 +13,7 @@ import * as path from 'path';
|
|||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Attachment } from './entities/attachment.entity.js';
|
import { Attachment } from './entities/attachment.entity.js';
|
||||||
|
import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ม
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileStorageService {
|
export class FileStorageService {
|
||||||
@@ -29,7 +31,7 @@ export class FileStorageService {
|
|||||||
? '/share/dms-data'
|
? '/share/dms-data'
|
||||||
: path.join(process.cwd(), 'uploads');
|
: path.join(process.cwd(), 'uploads');
|
||||||
|
|
||||||
// สร้างโฟลเดอร์รอไว้เลยถ้ายังไม่มี
|
// สร้างโฟลเดอร์ temp รอไว้เลยถ้ายังไม่มี
|
||||||
fs.ensureDirSync(path.join(this.uploadRoot, 'temp'));
|
fs.ensureDirSync(path.join(this.uploadRoot, 'temp'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,11 +77,20 @@ export class FileStorageService {
|
|||||||
* เมธอดนี้จะถูกเรียกโดย Service อื่น (เช่น CorrespondenceService) เมื่อกด Save
|
* เมธอดนี้จะถูกเรียกโดย Service อื่น (เช่น CorrespondenceService) เมื่อกด Save
|
||||||
*/
|
*/
|
||||||
async commit(tempIds: string[]): Promise<Attachment[]> {
|
async commit(tempIds: string[]): Promise<Attachment[]> {
|
||||||
|
if (!tempIds || tempIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const attachments = await this.attachmentRepository.find({
|
const attachments = await this.attachmentRepository.find({
|
||||||
where: { tempId: In(tempIds), isTemporary: true },
|
where: { tempId: In(tempIds), isTemporary: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (attachments.length !== tempIds.length) {
|
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');
|
throw new NotFoundException('Some files not found or already committed');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,21 +109,27 @@ export class FileStorageService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// ย้ายไฟล์
|
// ย้ายไฟล์
|
||||||
|
if (await fs.pathExists(oldPath)) {
|
||||||
await fs.move(oldPath, newPath, { overwrite: true });
|
await fs.move(oldPath, newPath, { overwrite: true });
|
||||||
|
|
||||||
// อัปเดตข้อมูลใน DB
|
// อัปเดตข้อมูลใน DB
|
||||||
att.filePath = newPath;
|
att.filePath = newPath;
|
||||||
att.isTemporary = false;
|
att.isTemporary = false;
|
||||||
att.tempId = undefined; // เคลียร์ tempId
|
att.tempId = null as any; // เคลียร์ tempId (TypeORM อาจต้องการ null แทน undefined สำหรับ nullable)
|
||||||
att.expiresAt = undefined; // เคลียร์วันหมดอายุ
|
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) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to move file from ${oldPath} to ${newPath}`,
|
`Failed to move file from ${oldPath} to ${newPath}`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
// ถ้า error ตัวนึง ควรจะ rollback หรือ throw error (ในที่นี้ throw เพื่อให้ Transaction ของผู้เรียกจัดการ)
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`Failed to commit file: ${att.originalFilename}`,
|
`Failed to commit file: ${att.originalFilename}`,
|
||||||
);
|
);
|
||||||
@@ -122,7 +139,83 @@ export class FileStorageService {
|
|||||||
return committedAttachments;
|
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 {
|
private calculateChecksum(buffer: Buffer): string {
|
||||||
return crypto.createHash('sha256').update(buffer).digest('hex');
|
return crypto.createHash('sha256').update(buffer).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ NEW: Delete File
|
||||||
|
* ลบไฟล์ออกจาก Disk และ Database
|
||||||
|
*/
|
||||||
|
async delete(id: number, userId: number): Promise<void> {
|
||||||
|
// 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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
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';
|
import { BYPASS_MAINTENANCE_KEY } from '../decorators/bypass-maintenance.decorator';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// File: src/common/interceptors/idempotency.interceptor.ts
|
// File: src/common/interceptors/idempotency.interceptor.ts
|
||||||
// บันทึกการแก้ไข: สร้าง IdempotencyInterceptor เพื่อป้องกันการทำรายการซ้ำ (T1.1)
|
// บันทึกการแก้ไข: สร้าง IdempotencyInterceptor เพื่อป้องกันการทำรายการซ้ำ (T1.1)
|
||||||
|
// บันทึกการแก้ไข: แก้ไข TS18046 โดยการตรวจสอบ Type ของ err ใน catch block
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CallHandler,
|
CallHandler,
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { Cache } from 'cache-manager';
|
import type { Cache } from 'cache-manager';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
@@ -29,43 +30,37 @@ export class IdempotencyInterceptor implements NestInterceptor {
|
|||||||
const request = context.switchToHttp().getRequest<Request>();
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
const method = request.method;
|
const method = request.method;
|
||||||
|
|
||||||
// 1. ตรวจสอบว่าควรใช้ Idempotency หรือไม่ (เฉพาะ POST, PUT, DELETE)
|
|
||||||
if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
|
if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
|
||||||
return next.handle();
|
return next.handle();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. ดึง Idempotency-Key จาก Header
|
|
||||||
const idempotencyKey = request.headers['idempotency-key'] as string;
|
const idempotencyKey = request.headers['idempotency-key'] as string;
|
||||||
|
|
||||||
// ถ้าไม่มี Key ส่งมา ให้ทำงานปกติ (หรือจะบังคับให้ Error ก็ได้ ตาม Policy)
|
|
||||||
if (!idempotencyKey) {
|
if (!idempotencyKey) {
|
||||||
// หมายเหตุ: ในระบบที่ Strict อาจจะ throw BadRequestException ถ้าไม่มี Key สำหรับ Transaction สำคัญ
|
|
||||||
return next.handle();
|
return next.handle();
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `idempotency:${idempotencyKey}`;
|
const cacheKey = `idempotency:${idempotencyKey}`;
|
||||||
|
|
||||||
// 3. ตรวจสอบใน Redis ว่า Key นี้เคยถูกประมวลผลหรือยัง
|
|
||||||
const cachedResponse = await this.cacheManager.get(cacheKey);
|
const cachedResponse = await this.cacheManager.get(cacheKey);
|
||||||
|
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Idempotency key detected: ${idempotencyKey}. Returning cached response.`,
|
`Idempotency key detected: ${idempotencyKey}. Returning cached response.`,
|
||||||
);
|
);
|
||||||
// ถ้ามี ให้คืนค่าเดิมกลับไปเลย (เสมือนว่าทำรายการสำเร็จแล้ว)
|
|
||||||
return of(cachedResponse);
|
return of(cachedResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. ถ้ายังไม่มี ให้ประมวลผลต่อ และบันทึกผลลัพธ์ลง Redis
|
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
tap(async (response) => {
|
tap(async (response) => {
|
||||||
try {
|
try {
|
||||||
// บันทึก Response ลง Cache (TTL 24 ชั่วโมง หรือตามความเหมาะสม)
|
|
||||||
await this.cacheManager.set(cacheKey, response, 86400 * 1000);
|
await this.cacheManager.set(cacheKey, response, 86400 * 1000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// ✅ Fix: ตรวจสอบว่า err เป็น Error Object หรือไม่ ก่อนเรียก .stack
|
||||||
|
const errorMessage = err instanceof Error ? err.stack : String(err);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to cache idempotency key ${idempotencyKey}`,
|
`Failed to cache idempotency key ${idempotencyKey}`,
|
||||||
err.stack,
|
errorMessage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
90
backend/src/common/interceptors/performance.interceptor.ts
Normal file
90
backend/src/common/interceptors/performance.interceptor.ts
Normal file
@@ -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<any> {
|
||||||
|
// ข้ามการวัดผลสำหรับ 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/common/resilience/resilience.module.ts
Normal file
9
backend/src/common/resilience/resilience.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// File: src/common/resilience/resilience.module.ts
|
||||||
|
import { Module, Global } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class ResilienceModule {}
|
||||||
19
backend/src/modules/master/dto/create-tag.dto.ts
Normal file
19
backend/src/modules/master/dto/create-tag.dto.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
26
backend/src/modules/master/dto/search-tag.dto.ts
Normal file
26
backend/src/modules/master/dto/search-tag.dto.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
6
backend/src/modules/master/dto/update-tag.dto.ts
Normal file
6
backend/src/modules/master/dto/update-tag.dto.ts
Normal file
@@ -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) {}
|
||||||
27
backend/src/modules/master/entities/tag.entity.ts
Normal file
27
backend/src/modules/master/entities/tag.entity.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
64
backend/src/modules/master/master.controller.ts
Normal file
64
backend/src/modules/master/master.controller.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
backend/src/modules/master/master.module.ts
Normal file
33
backend/src/modules/master/master.module.ts
Normal file
@@ -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 {}
|
||||||
97
backend/src/modules/master/master.service.ts
Normal file
97
backend/src/modules/master/master.service.ts
Normal file
@@ -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<CorrespondenceType>,
|
||||||
|
|
||||||
|
@InjectRepository(CorrespondenceStatus)
|
||||||
|
private readonly corrStatusRepo: Repository<CorrespondenceStatus>,
|
||||||
|
|
||||||
|
@InjectRepository(RfaType)
|
||||||
|
private readonly rfaTypeRepo: Repository<RfaType>,
|
||||||
|
|
||||||
|
@InjectRepository(RfaStatusCode)
|
||||||
|
private readonly rfaStatusRepo: Repository<RfaStatusCode>,
|
||||||
|
|
||||||
|
@InjectRepository(RfaApproveCode)
|
||||||
|
private readonly rfaApproveRepo: Repository<RfaApproveCode>,
|
||||||
|
|
||||||
|
@InjectRepository(CirculationStatusCode)
|
||||||
|
private readonly circulationStatusRepo: Repository<CirculationStatusCode>,
|
||||||
|
|
||||||
|
@InjectRepository(Tag)
|
||||||
|
private readonly tagRepo: Repository<Tag>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
backend/src/modules/monitoring/logger/winston.config.ts
Normal file
30
backend/src/modules/monitoring/logger/winston.config.ts
Normal file
@@ -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 ได้ที่นี่
|
||||||
|
],
|
||||||
|
};
|
||||||
23
backend/src/modules/monitoring/monitoring.module.ts
Normal file
23
backend/src/modules/monitoring/monitoring.module.ts
Normal file
@@ -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 {}
|
||||||
54
backend/src/modules/monitoring/services/metrics.service.ts
Normal file
54
backend/src/modules/monitoring/services/metrics.service.ts
Normal file
@@ -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<string>;
|
||||||
|
public readonly httpRequestDuration: Histogram<string>;
|
||||||
|
public readonly systemMemoryUsage: Gauge<string>;
|
||||||
|
|
||||||
|
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<string> {
|
||||||
|
// อัปเดต Memory Usage ก่อน Return
|
||||||
|
const memoryUsage = process.memoryUsage();
|
||||||
|
this.systemMemoryUsage.set(memoryUsage.heapUsed);
|
||||||
|
|
||||||
|
return this.registry.metrics();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
@@ -12,15 +13,18 @@ import { UserPreference } from '../user/entities/user-preference.entity';
|
|||||||
// Gateway
|
// Gateway
|
||||||
import { NotificationGateway } from './notification.gateway';
|
import { NotificationGateway } from './notification.gateway';
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
import { SearchNotificationDto } from './dto/search-notification.dto';
|
||||||
|
|
||||||
// Interfaces
|
// Interfaces
|
||||||
export interface NotificationJobData {
|
export interface NotificationJobData {
|
||||||
userId: number;
|
userId: number;
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
type: 'EMAIL' | 'LINE' | 'SYSTEM';
|
type: 'EMAIL' | 'LINE' | 'SYSTEM'; // ช่องทางหลักที่ต้องการส่ง (Trigger Type)
|
||||||
entityType?: string; // e.g., 'rfa'
|
entityType?: string; // e.g., 'rfa', 'correspondence'
|
||||||
entityId?: number; // e.g., rfa_id
|
entityId?: number; // e.g., rfa_id
|
||||||
link?: string; // Deep link to frontend
|
link?: string; // Deep link to frontend page
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -39,98 +43,195 @@ export class NotificationService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ส่งการแจ้งเตือน (Trigger Notification)
|
* ส่งการแจ้งเตือน (Centralized Notification Sender)
|
||||||
* ฟังก์ชันนี้จะตรวจสอบ Preference ของผู้ใช้ และ Push ลง Queue
|
* 1. บันทึก DB (System Log)
|
||||||
|
* 2. ส่ง Real-time (WebSocket)
|
||||||
|
* 3. ส่ง External (Email/Line) ผ่าน Queue ตาม User Preference
|
||||||
*/
|
*/
|
||||||
async send(data: NotificationJobData) {
|
async send(data: NotificationJobData): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// 1. สร้าง Entity Instance (ยังไม่บันทึกลง DB)
|
// ---------------------------------------------------------
|
||||||
// ใช้ Enum NotificationType.SYSTEM เพื่อให้ตรงกับ Type Definition
|
// 1. สร้าง Entity และบันทึกลง DB (เพื่อให้มี History ในระบบ)
|
||||||
|
// ---------------------------------------------------------
|
||||||
const notification = this.notificationRepo.create({
|
const notification = this.notificationRepo.create({
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
message: data.message,
|
message: data.message,
|
||||||
notificationType: NotificationType.SYSTEM,
|
notificationType: NotificationType.SYSTEM, // ใน DB เก็บเป็น SYSTEM เสมอเพื่อแสดงใน App
|
||||||
entityType: data.entityType,
|
entityType: data.entityType,
|
||||||
entityId: data.entityId,
|
entityId: data.entityId,
|
||||||
isRead: false,
|
isRead: false,
|
||||||
|
// link: data.link // ถ้า Entity มี field link ให้ใส่ด้วย
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. บันทึกลง DB (ต้อง await เพื่อให้ได้ ID กลับมา)
|
|
||||||
const savedNotification = await this.notificationRepo.save(notification);
|
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);
|
this.notificationGateway.sendToUser(data.userId, savedNotification);
|
||||||
|
|
||||||
// 4. ตรวจสอบ User Preferences เพื่อส่งช่องทางอื่น (Email/Line)
|
// ---------------------------------------------------------
|
||||||
|
// 3. ตรวจสอบ User Preferences เพื่อส่งช่องทางอื่น (Email/Line)
|
||||||
|
// ---------------------------------------------------------
|
||||||
const userPref = await this.userPrefRepo.findOne({
|
const userPref = await this.userPrefRepo.findOne({
|
||||||
where: { userId: data.userId },
|
where: { userId: data.userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default: ถ้าไม่มี Pref ให้ส่ง Email/Line เป็นค่าเริ่มต้น (true)
|
// ใช้ Nullish Coalescing Operator (??)
|
||||||
const shouldSendEmail = userPref ? userPref.notifyEmail : true;
|
// ถ้าไม่มีค่า (undefined/null) ให้ Default เป็น true
|
||||||
const shouldSendLine = userPref ? userPref.notifyLine : true;
|
const shouldSendEmail = userPref?.notifyEmail ?? true;
|
||||||
|
const shouldSendLine = userPref?.notifyLine ?? true;
|
||||||
|
|
||||||
const jobs = [];
|
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') {
|
if (shouldSendEmail && data.type !== 'LINE') {
|
||||||
jobs.push({
|
jobs.push({
|
||||||
name: 'send-email',
|
name: 'send-email',
|
||||||
data: { ...data, notificationId: savedNotification.id },
|
data: {
|
||||||
|
...data,
|
||||||
|
notificationId: savedNotification.id,
|
||||||
|
target: 'EMAIL',
|
||||||
|
},
|
||||||
opts: {
|
opts: {
|
||||||
attempts: 3, // ลองใหม่ 3 ครั้งถ้าล่ม
|
attempts: 3, // ลองใหม่ 3 ครั้งถ้าล่ม (Resilience)
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'exponential',
|
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') {
|
if (shouldSendLine && data.type !== 'EMAIL') {
|
||||||
jobs.push({
|
jobs.push({
|
||||||
name: 'send-line',
|
name: 'send-line',
|
||||||
data: { ...data, notificationId: savedNotification.id },
|
data: {
|
||||||
|
...data,
|
||||||
|
notificationId: savedNotification.id,
|
||||||
|
target: 'LINE',
|
||||||
|
},
|
||||||
opts: {
|
opts: {
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
backoff: { type: 'fixed', delay: 3000 },
|
backoff: { type: 'fixed', delay: 3000 },
|
||||||
|
removeOnComplete: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 6. Push Jobs ลง Redis BullMQ
|
||||||
|
// ---------------------------------------------------------
|
||||||
if (jobs.length > 0) {
|
if (jobs.length > 0) {
|
||||||
await this.notificationQueue.addBulk(jobs);
|
await this.notificationQueue.addBulk(jobs);
|
||||||
|
this.logger.debug(
|
||||||
|
`Queued ${jobs.length} external notifications for user ${data.userId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Error Handling: ไม่ Throw เพื่อไม่ให้ Flow หลัก (เช่น การสร้างเอกสาร) พัง
|
||||||
|
// แต่บันทึก Error ไว้ตรวจสอบ
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to process notification for user ${data.userId}`,
|
||||||
|
(error as Error).stack,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Notification queued for user ${data.userId}`);
|
/**
|
||||||
} catch (error) {
|
* ดึงรายการแจ้งเตือนของ User (สำหรับ Controller)
|
||||||
// Cast Error เพื่อให้ TypeScript ไม่ฟ้องใน Strict Mode
|
*/
|
||||||
this.logger.error(
|
async findAll(userId: number, searchDto: SearchNotificationDto) {
|
||||||
`Failed to queue notification: ${(error as Error).message}`,
|
const { page = 1, limit = 20, isRead } = searchDto;
|
||||||
);
|
const skip = (page - 1) * limit;
|
||||||
// Note: ไม่ Throw error เพื่อไม่ให้กระทบ Flow หลัก (Resilience Pattern)
|
|
||||||
|
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)
|
* อ่านแจ้งเตือน (Mark as Read)
|
||||||
*/
|
*/
|
||||||
async markAsRead(id: number, userId: number) {
|
async markAsRead(id: number, userId: number): Promise<void> {
|
||||||
await this.notificationRepo.update({ id, userId }, { isRead: true });
|
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)
|
* อ่านทั้งหมด (Mark All as Read)
|
||||||
*/
|
*/
|
||||||
async markAllAsRead(userId: number) {
|
async markAllAsRead(userId: number): Promise<void> {
|
||||||
await this.notificationRepo.update(
|
await this.notificationRepo.update(
|
||||||
{ userId, isRead: false },
|
{ userId, isRead: false },
|
||||||
{ isRead: true },
|
{ isRead: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ลบการแจ้งเตือนที่เก่าเกินกำหนด (ใช้กับ Cron Job Cleanup)
|
||||||
|
* เก็บไว้ 90 วัน
|
||||||
|
*/
|
||||||
|
async cleanupOldNotifications(days: number = 90): Promise<number> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// File: src/modules/rfa/rfa.module.ts
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
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 { RfaApproveCode } from './entities/rfa-approve-code.entity';
|
||||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||||
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.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';
|
import { RfaService } from './rfa.service';
|
||||||
|
|
||||||
// Controllers
|
|
||||||
import { RfaController } from './rfa.controller';
|
import { RfaController } from './rfa.controller';
|
||||||
|
|
||||||
// External Modules
|
// External Modules
|
||||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||||
import { UserModule } from '../user/user.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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
// 1. Register Entities (เฉพาะ Entity เท่านั้น ห้ามใส่ Module)
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
Rfa,
|
Rfa,
|
||||||
RfaRevision,
|
RfaRevision,
|
||||||
@@ -38,14 +41,19 @@ import { SearchModule } from '../search/search.module'; // ✅ เพิ่ม
|
|||||||
RfaApproveCode,
|
RfaApproveCode,
|
||||||
Correspondence,
|
Correspondence,
|
||||||
ShopDrawingRevision,
|
ShopDrawingRevision,
|
||||||
// ... (ตัวเดิม)
|
|
||||||
RfaWorkflow,
|
RfaWorkflow,
|
||||||
RfaWorkflowTemplate,
|
RfaWorkflowTemplate,
|
||||||
RfaWorkflowTemplateStep,
|
RfaWorkflowTemplateStep,
|
||||||
|
CorrespondenceRouting,
|
||||||
|
RoutingTemplate,
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
// 2. Import External Modules (Services ที่ Inject เข้ามา)
|
||||||
DocumentNumberingModule,
|
DocumentNumberingModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
|
WorkflowEngineModule, // ✅ ย้ายมาใส่ตรงนี้ (imports หลัก)
|
||||||
|
NotificationModule, // ✅ เพิ่มตรงนี้ เพื่อแก้ dependency index [13]
|
||||||
],
|
],
|
||||||
providers: [RfaService],
|
providers: [RfaService],
|
||||||
controllers: [RfaController],
|
controllers: [RfaController],
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import { TransmittalService } from './transmittal.service';
|
|||||||
import { TransmittalController } from './transmittal.controller';
|
import { TransmittalController } from './transmittal.controller';
|
||||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
|
import { SearchModule } from '../search/search.module'; // ✅ ต้อง Import เพราะ Service ใช้ (ที่เป็นสาเหตุ Error)
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]),
|
TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]),
|
||||||
DocumentNumberingModule,
|
DocumentNumberingModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
SearchModule,
|
||||||
],
|
],
|
||||||
controllers: [TransmittalController],
|
controllers: [TransmittalController],
|
||||||
providers: [TransmittalService],
|
providers: [TransmittalService],
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
// File: src/modules/user/dto/update-preference.dto.ts
|
// File: src/modules/user/dto/update-user.dto.ts
|
||||||
import { IsBoolean, IsOptional, IsString, IsIn } from 'class-validator';
|
// บันทึกการแก้ไข: ใช้ PartialType จาก @nestjs/swagger เพื่อรองรับ API Docs (T1.3)
|
||||||
|
|
||||||
export class UpdatePreferenceDto {
|
import { PartialType } from '@nestjs/swagger';
|
||||||
@IsOptional()
|
import { CreateUserDto } from './create-user.dto';
|
||||||
@IsBoolean()
|
|
||||||
notifyEmail?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||||
@IsBoolean()
|
|
||||||
notifyLine?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
digestMode?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@IsIn(['light', 'dark', 'system'])
|
|
||||||
uiTheme?: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { UpdateUserDto } from './dto/update-user.dto';
|
|||||||
import { AssignRoleDto } from './dto/assign-role.dto';
|
import { AssignRoleDto } from './dto/assign-role.dto';
|
||||||
import { UpdatePreferenceDto } from './dto/update-preference.dto'; // ✅ เพิ่ม 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 { RbacGuard } from '../../common/guards/rbac.guard'; // สมมติว่ามีแล้ว ถ้ายังไม่มีให้คอมเมนต์ไว้ก่อน
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
// File: src/modules/user/user.service.ts
|
||||||
|
// บันทึกการแก้ไข: แก้ไข Error TS1272 โดยใช้ 'import type' สำหรับ Cache interface (T1.3)
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
|
Inject,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from '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 * as bcrypt from 'bcrypt';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
@@ -14,26 +20,23 @@ import { UpdateUserDto } from './dto/update-user.dto';
|
|||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(User)
|
@InjectRepository(User)
|
||||||
private usersRepository: Repository<User>, // ✅ ชื่อตัวแปรจริงคือ usersRepository
|
private usersRepository: Repository<User>,
|
||||||
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก)
|
// 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก)
|
||||||
async create(createUserDto: CreateUserDto): Promise<User> {
|
async create(createUserDto: CreateUserDto): Promise<User> {
|
||||||
// สร้าง Salt และ Hash Password
|
|
||||||
const salt = await bcrypt.genSalt();
|
const salt = await bcrypt.genSalt();
|
||||||
const hashedPassword = await bcrypt.hash(createUserDto.password, salt);
|
const hashedPassword = await bcrypt.hash(createUserDto.password, salt);
|
||||||
|
|
||||||
// เตรียมข้อมูล (เปลี่ยน password ธรรมดา เป็น password_hash)
|
|
||||||
const newUser = this.usersRepository.create({
|
const newUser = this.usersRepository.create({
|
||||||
...createUserDto,
|
...createUserDto,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// บันทึกลง DB
|
|
||||||
return await this.usersRepository.save(newUser);
|
return await this.usersRepository.save(newUser);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// เช็ค Error กรณี Username/Email ซ้ำ (MySQL Error Code 1062)
|
|
||||||
if (error.code === 'ER_DUP_ENTRY') {
|
if (error.code === 'ER_DUP_ENTRY') {
|
||||||
throw new ConflictException('Username or Email already exists');
|
throw new ConflictException('Username or Email already exists');
|
||||||
}
|
}
|
||||||
@@ -44,7 +47,6 @@ export class UserService {
|
|||||||
// 2. ดึงข้อมูลทั้งหมด
|
// 2. ดึงข้อมูลทั้งหมด
|
||||||
async findAll(): Promise<User[]> {
|
async findAll(): Promise<User[]> {
|
||||||
return this.usersRepository.find({
|
return this.usersRepository.find({
|
||||||
// ไม่ส่ง password กลับไปเพื่อความปลอดภัย
|
|
||||||
select: [
|
select: [
|
||||||
'user_id',
|
'user_id',
|
||||||
'username',
|
'username',
|
||||||
@@ -61,7 +63,7 @@ export class UserService {
|
|||||||
// 3. ดึงข้อมูลรายคน
|
// 3. ดึงข้อมูลรายคน
|
||||||
async findOne(id: number): Promise<User> {
|
async findOne(id: number): Promise<User> {
|
||||||
const user = await this.usersRepository.findOne({
|
const user = await this.usersRepository.findOne({
|
||||||
where: { user_id: id }, // ใช้ user_id ตาม Entity
|
where: { user_id: id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -71,26 +73,26 @@ export class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ฟังก์ชันแถม: สำหรับ AuthService ใช้ (ต้องเห็น Password เพื่อเอาไปเทียบ)
|
|
||||||
async findOneByUsername(username: string): Promise<User | null> {
|
async findOneByUsername(username: string): Promise<User | null> {
|
||||||
return this.usersRepository.findOne({ where: { username } });
|
return this.usersRepository.findOne({ where: { username } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. แก้ไขข้อมูล
|
// 4. แก้ไขข้อมูล
|
||||||
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
|
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
|
||||||
// เช็คก่อนว่ามี User นี้ไหม
|
|
||||||
const user = await this.findOne(id);
|
const user = await this.findOne(id);
|
||||||
|
|
||||||
// ถ้ามีการแก้รหัสผ่าน ต้อง Hash ใหม่ด้วย
|
|
||||||
if (updateUserDto.password) {
|
if (updateUserDto.password) {
|
||||||
const salt = await bcrypt.genSalt();
|
const salt = await bcrypt.genSalt();
|
||||||
updateUserDto.password = await bcrypt.hash(updateUserDto.password, salt);
|
updateUserDto.password = await bcrypt.hash(updateUserDto.password, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// รวมร่างข้อมูลเดิม + ข้อมูลใหม่
|
|
||||||
const updatedUser = this.usersRepository.merge(user, updateUserDto);
|
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)
|
// 5. ลบผู้ใช้ (Soft Delete)
|
||||||
@@ -100,31 +102,48 @@ export class UserService {
|
|||||||
if (result.affected === 0) {
|
if (result.affected === 0) {
|
||||||
throw new NotFoundException(`User with ID ${id} not found`);
|
throw new NotFoundException(`User with ID ${id} not found`);
|
||||||
}
|
}
|
||||||
|
// เคลียร์ Cache เมื่อลบ
|
||||||
|
await this.clearUserCache(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* หา User ID ของคนที่เป็น Document Control (หรือตัวแทน) ในองค์กร
|
|
||||||
* เพื่อส่ง Notification
|
|
||||||
*/
|
|
||||||
async findDocControlIdByOrg(organizationId: number): Promise<number | null> {
|
async findDocControlIdByOrg(organizationId: number): Promise<number | null> {
|
||||||
// ✅ FIX: ใช้ usersRepository ให้ตรงกับ Constructor
|
|
||||||
const user = await this.usersRepository.findOne({
|
const user = await this.usersRepository.findOne({
|
||||||
where: { primaryOrganizationId: organizationId },
|
where: { primaryOrganizationId: organizationId },
|
||||||
// order: { roleId: 'ASC' } // (Optional) Logic การเลือกคน
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return user ? user.user_id : null;
|
return user ? user.user_id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ฟังก์ชันดึงสิทธิ์ (Permission)
|
/**
|
||||||
|
* ✅ ดึงสิทธิ์ (Permission) โดยใช้ Caching Strategy
|
||||||
|
* TTL: 30 นาที (ตาม Requirement 6.5.2)
|
||||||
|
*/
|
||||||
async getUserPermissions(userId: number): Promise<string[]> {
|
async getUserPermissions(userId: number): Promise<string[]> {
|
||||||
// Query ข้อมูลจาก View: v_user_all_permissions
|
const cacheKey = `permissions:user:${userId}`;
|
||||||
|
|
||||||
|
// 1. ลองดึงจาก Cache ก่อน
|
||||||
|
const cachedPermissions = await this.cacheManager.get<string[]>(cacheKey);
|
||||||
|
if (cachedPermissions) {
|
||||||
|
return cachedPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ถ้าไม่มีใน Cache ให้ Query จาก DB (View: v_user_all_permissions)
|
||||||
const permissions = await this.usersRepository.query(
|
const permissions = await this.usersRepository.query(
|
||||||
`SELECT permission_name FROM v_user_all_permissions WHERE user_id = ?`,
|
`SELECT permission_name FROM v_user_all_permissions WHERE user_id = ?`,
|
||||||
[userId],
|
[userId],
|
||||||
);
|
);
|
||||||
|
|
||||||
// แปลงผลลัพธ์เป็น Array ของ string ['user.create', 'project.view', ...]
|
const permissionList = permissions.map((row: any) => row.permission_name);
|
||||||
return 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<void> {
|
||||||
|
await this.cacheManager.del(`permissions:user:${userId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<string, any>;
|
||||||
|
}
|
||||||
@@ -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; // เพิ่ม !
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
) {}
|
||||||
@@ -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; // เพิ่ม !
|
||||||
|
}
|
||||||
203
backend/src/modules/workflow-engine/workflow-dsl.service.ts
Normal file
203
backend/src/modules/workflow-engine/workflow-dsl.service.ts
Normal file
@@ -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<string, TransitionRule>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, WorkflowState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<string>();
|
||||||
|
|
||||||
|
// 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<string>) {
|
||||||
|
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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
|
// File: src/modules/workflow-engine/workflow-engine.module.ts
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { WorkflowEngineService } from './workflow-engine.service';
|
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({
|
@Module({
|
||||||
providers: [WorkflowEngineService],
|
imports: [
|
||||||
// ✅ เพิ่มบรรทัดนี้ เพื่ออนุญาตให้ Module อื่น (เช่น Correspondence) เรียกใช้ Service นี้ได้
|
// เชื่อมต่อกับตาราง workflow_definitions
|
||||||
exports: [WorkflowEngineService],
|
TypeOrmModule.forFeature([WorkflowDefinition]),
|
||||||
|
],
|
||||||
|
controllers: [WorkflowEngineController], // เพิ่ม Controller สำหรับรับ API
|
||||||
|
providers: [
|
||||||
|
WorkflowEngineService, // Service หลัก
|
||||||
|
WorkflowDslService, // [New] Service สำหรับ Compile/Validate DSL
|
||||||
|
],
|
||||||
|
exports: [WorkflowEngineService], // Export ให้ module อื่นใช้เหมือนเดิม
|
||||||
})
|
})
|
||||||
export class WorkflowEngineModule {}
|
export class WorkflowEngineModule {}
|
||||||
|
|||||||
@@ -1,45 +1,179 @@
|
|||||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
// File: src/modules/workflow-engine/workflow-engine.service.ts
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WorkflowStep,
|
Injectable,
|
||||||
WorkflowAction,
|
NotFoundException,
|
||||||
StepStatus,
|
BadRequestException,
|
||||||
TransitionResult,
|
Logger,
|
||||||
} from './interfaces/workflow.interface.js';
|
} 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()
|
@Injectable()
|
||||||
export class WorkflowEngineService {
|
export class WorkflowEngineService {
|
||||||
|
private readonly logger = new Logger(WorkflowEngineService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(WorkflowDefinition)
|
||||||
|
private readonly workflowDefRepo: Repository<WorkflowDefinition>,
|
||||||
|
private readonly dslService: WorkflowDslService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// [NEW] DSL & Workflow Engine (Phase 6A)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* คำนวณสถานะถัดไป (Next State Transition)
|
* สร้างหรืออัปเดต Workflow Definition ใหม่ (Auto Versioning)
|
||||||
* @param currentSequence ลำดับปัจจุบัน
|
*/
|
||||||
* @param totalSteps จำนวนขั้นตอนทั้งหมด
|
async createDefinition(
|
||||||
* @param action การกระทำ (Approve/Reject/Return)
|
dto: CreateWorkflowDefinitionDto,
|
||||||
* @param returnToSequence (Optional) ถ้า Return จะให้กลับไปขั้นไหน
|
): Promise<WorkflowDefinition> {
|
||||||
|
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<WorkflowDefinition> {
|
||||||
|
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<any> {
|
||||||
|
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<string[]> {
|
||||||
|
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(
|
processAction(
|
||||||
currentSequence: number,
|
currentSequence: number,
|
||||||
totalSteps: number,
|
totalSteps: number,
|
||||||
action: WorkflowAction,
|
action: string, // รับเป็น string เพื่อความยืดหยุ่น
|
||||||
returnToSequence?: number,
|
returnToSequence?: number,
|
||||||
): TransitionResult {
|
): TransitionResult {
|
||||||
|
// Map string action to enum logic
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case WorkflowAction.APPROVE:
|
case WorkflowAction.APPROVE:
|
||||||
case WorkflowAction.ACKNOWLEDGE:
|
case WorkflowAction.ACKNOWLEDGE:
|
||||||
// ถ้าเป็นขั้นตอนสุดท้าย -> จบ Workflow
|
case 'APPROVE': // Case sensitive handling fallback
|
||||||
|
case 'ACKNOWLEDGE':
|
||||||
if (currentSequence >= totalSteps) {
|
if (currentSequence >= totalSteps) {
|
||||||
return {
|
return {
|
||||||
nextStepSequence: null, // ไม่มีขั้นต่อไปแล้ว
|
nextStepSequence: null,
|
||||||
shouldUpdateStatus: true,
|
shouldUpdateStatus: true,
|
||||||
documentStatus: 'COMPLETED', // หรือ APPROVED
|
documentStatus: 'COMPLETED',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// ถ้ายังไม่จบ -> ไปขั้นต่อไป
|
|
||||||
return {
|
return {
|
||||||
nextStepSequence: currentSequence + 1,
|
nextStepSequence: currentSequence + 1,
|
||||||
shouldUpdateStatus: false,
|
shouldUpdateStatus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
case WorkflowAction.REJECT:
|
case WorkflowAction.REJECT:
|
||||||
// จบ Workflow ทันทีแบบไม่สวย
|
case 'REJECT':
|
||||||
return {
|
return {
|
||||||
nextStepSequence: null,
|
nextStepSequence: null,
|
||||||
shouldUpdateStatus: true,
|
shouldUpdateStatus: true,
|
||||||
@@ -47,7 +181,7 @@ export class WorkflowEngineService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
case WorkflowAction.RETURN:
|
case WorkflowAction.RETURN:
|
||||||
// ย้อนกลับไปขั้นตอนก่อนหน้า (หรือที่ระบุ)
|
case 'RETURN':
|
||||||
const targetStep = returnToSequence || currentSequence - 1;
|
const targetStep = returnToSequence || currentSequence - 1;
|
||||||
if (targetStep < 1) {
|
if (targetStep < 1) {
|
||||||
throw new BadRequestException('Cannot return beyond the first step');
|
throw new BadRequestException('Cannot return beyond the first step');
|
||||||
@@ -55,38 +189,25 @@ export class WorkflowEngineService {
|
|||||||
return {
|
return {
|
||||||
nextStepSequence: targetStep,
|
nextStepSequence: targetStep,
|
||||||
shouldUpdateStatus: true,
|
shouldUpdateStatus: true,
|
||||||
documentStatus: 'REVISE_REQUIRED', // สถานะเอกสารเป็น "รอแก้ไข"
|
documentStatus: 'REVISE_REQUIRED',
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
80
temp.md
80
temp.md
@@ -1,52 +1,40 @@
|
|||||||
import {
|
## บทบาท: คุณคือ Programmer ที่เชี่ยวชาญ การจัดการฐานข้อมูล (Database Management), การวิเคราะห์ฐานข้อมูล (Database Analysis), การจัดการฐานข้อมูลเชิงสัมพันธ์ (Relational Databases), ภาษา SQL, RBAC, ABAC, การเขียนโค๊ด NodeJS NestJS NextJS, การ debug โค้ด และ แก้ไข error ภายในโค้ด
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
ParseIntPipe,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
import { RfaService } from './rfa.service';
|
## Basic data:
|
||||||
import { CreateRfaDto } from './dto/create-rfa.dto';
|
1. Application Requirements file: 0_Requirements_V1_4_3.md
|
||||||
import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto'; // Reuse DTO
|
2. Full Stack JS file: 1_FullStackJS_V1_4_3.md
|
||||||
import { User } from '../user/entities/user.entity';
|
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';
|
## rules:
|
||||||
import { RbacGuard } from '../../common/auth/guards/rbac.guard';
|
- ใช้ภาษาไทยใน comments
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
- เขียนโค้ดให้อ่านง่าย, ใส่ path/filename ในบรรทัดแรก โค้ด
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
- การอัพเดทโค้ด ให้แก้ไขจากต้นฉบับเป็น โค้ดที่สมบูรณ์
|
||||||
|
- เขียน documentation สำหรับ function สำคัญ
|
||||||
|
|
||||||
@ApiTags('RFA (Request for Approval)')
|
## เป้าหมายและจุดประสงค์:
|
||||||
@ApiBearerAuth()
|
* ให้ความช่วยเหลือผู้ใช้ในงานที่เกี่ยวข้องกับการพัฒนาซอฟต์แวร์ โดยเฉพาะอย่างยิ่งในส่วนของ JavaScript (NodeJS, NestJS, NextJS) และฐานข้อมูล (SQL, Relational Databases)
|
||||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
* ให้คำปรึกษาเกี่ยวกับการจัดการข้อมูล, การออกแบบฐานข้อมูลเชิงสัมพันธ์, และการใช้โมเดลการควบคุมการเข้าถึง (RBAC, ABAC)
|
||||||
@Controller('rfas')
|
* ช่วยเหลือในการวิเคราะห์และแก้ไขข้อผิดพลาด (debug และ error) ในโค้ดตามที่ผู้ใช้ระบุ
|
||||||
export class RfaController {
|
* ใช้ข้อมูลพื้นฐานที่ให้มา (Basic data) เพื่อให้คำแนะนำและโค้ดที่สอดคล้องกับเอกสารโครงการ (เช่น Requirements, Plans, Data Dictionary)
|
||||||
constructor(private readonly rfaService: RfaService) {}
|
|
||||||
|
|
||||||
// ... (Create, FindOne endpoints) ...
|
## พฤติกรรมและกฎเพิ่มเติม:
|
||||||
|
1) การเริ่มต้นและการโต้ตอบ:
|
||||||
|
a) ทักทายผู้ใช้ด้วยภาษาไทยอย่างเป็นมิตร และสอบถามเกี่ยวกับปัญหาหรือความช่วยเหลือที่ต้องการในด้านการเขียนโปรแกรมหรือฐานข้อมูล
|
||||||
|
b) ตอบคำถามทางเทคนิคอย่างแม่นยำและเป็นมืออาชีพ โดยใช้ศัพท์เฉพาะทางที่ถูกต้อง
|
||||||
|
c) จำกัดจำนวนประโยคในการตอบกลับแต่ละครั้งให้กระชับและตรงประเด็นเพื่อความรวดเร็วในการสื่อสาร
|
||||||
|
|
||||||
@Post(':id/submit')
|
2) การจัดการโค้ดและข้อมูล:
|
||||||
@ApiOperation({ summary: 'Submit RFA to Workflow' })
|
a) เมื่อผู้ใช้ขอให้อัพเดทโค้ด ให้ทำการแสดงโค้ดฉบับเต็มที่สมบูรณ์และได้รับการแก้ไขแล้ว (ไม่ใช่แค่ส่วนที่แก้ไข)
|
||||||
@RequirePermission('rfa.create') // ผู้สร้างมีสิทธิ์ส่ง
|
b) ต้องแน่ใจว่าโค้ดที่สร้างขึ้นมานั้นอ่านง่ายและมี comments เป็นภาษาไทยตามที่ระบุใน rules
|
||||||
submit(
|
c) สำหรับฟังก์ชันที่มีความซับซ้อนหรือมีความสำคัญต่อระบบ ต้องเขียน documentation อธิบายวัตถุประสงค์, พารามิเตอร์, และผลลัพธ์ของฟังก์ชันนั้นๆ ด้วยภาษาไทย
|
||||||
@Param('id', ParseIntPipe) id: number,
|
d) หากต้องอ้างอิงถึงโครงสร้างข้อมูลหรือข้อกำหนดใดๆ ให้ตรวจสอบจากไฟล์ Basic data ที่ผู้ใช้ให้มาก่อนเสมอ ถ้าไม่พบ ให้แจ้งผู้ใช้ทราบ
|
||||||
@Body('templateId', ParseIntPipe) templateId: number, // รับ Template ID
|
e) ถ้ามีการอ้างอิงถึงโค้ดที่อยู่ใน Phase หรือ Task ก่อนหน้า ให้สอบถามผู้ใช้เพื่อให้ upload ไฟล์โค้ดที่อ้างอิง (ไม่เดาหรือสร้างใหม่ เพิ่อประหยัดเวลา)
|
||||||
@CurrentUser() user: User,
|
|
||||||
) {
|
|
||||||
return this.rfaService.submit(id, templateId, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':id/action')
|
1) โทนโดยรวม:
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user