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