# รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 1 (ไฟล์ที่ 1-10) **Project:** BMA EHR Organization Backend **Framework:** TSOA + Express + TypeORM **วันที่ตรวจสอบ:** 2026-05-08 **จำนวน Controllers:** 10 ไฟล์ **สถานะ:** เสร็จสิ้น --- ## สรุปผลการตรวจสอบ | ระดับความรุนแรง | จำนวนจุดเสี่ยง | |---------------------|-------------------| | **CRITICAL** | 2 | | **HIGH** | 3 | | **MEDIUM** | 4 | | **LOW** | 1 | | **BUG** | 1 | | **รวมทั้งหมด** | 11 | --- ## Controllers ที่ตรวจสอบ 1. [AuthRoleAttrController.ts](src/controllers/AuthRoleAttrController.ts) 2. [AuthRoleController.ts](src/controllers/AuthRoleController.ts) 3. [AuthSysController.ts](src/controllers/AuthSysController.ts) 4. [ApiManageController.ts](src/controllers/ApiManageController.ts) 5. [ApiKeyController.ts](src/controllers/ApiKeyController.ts) 6. [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts) 7. [BloodGroupController.ts](src/controllers/BloodGroupController.ts) 8. [ChangePositionController.ts](src/controllers/ChangePositionController.ts) 9. [CommandCodeController.ts](src/controllers/CommandCodeController.ts) 10. [CommandController.ts](src/controllers/CommandController.ts) - ไฟล์ใหญ่เกินกว่าที่จะอ่าน (336KB+) --- ## รายละเอียดจุดเสี่ยงแต่ละจุด ### #1 - Redis Client Error Handling (CRITICAL) **File & Location:** [AuthRoleController.ts:126-138](src/controllers/AuthRoleController.ts#L126-L138) **Method:** `AddAuthRoleGovoment` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - Redis client operations ไม่มี error handling - `redisClient.del()` มี callback ที่ throw error แต่ไม่มี try-catch รองรับ - Redis connection error จะทำให้เกิด **unhandled exception** และทำให้ Node.js process crash - Callback pattern ที่ใช้ throw จะไม่ถูก catch โดย Promise chain **Code ปัจจุบัน (เสี่ยง):** ```typescript const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, }); redisClient.del("role_" + posMaster.current_holderId, (err: Error, response: Response) => { if (err) throw err; // ❌ จะทำให้ process crash }); redisClient.del("menu_" + posMaster.current_holderId, (err: Error, response: Response) => { if (err) throw err; // ❌ จะทำให้ process crash }); ``` **Recommended Fix:** ```typescript // ใช้ Promise wrapper หรือ util.promisify import { promisify } from 'util'; // Create Redis client const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, }); // Promisify the operations const redisDelAsync = promisify(redisClient.del).bind(redisClient); try { if (posMaster.current_holderId) { await redisDelAsync("role_" + posMaster.current_holderId); await redisDelAsync("menu_" + posMaster.current_holderId); } } catch (error) { console.error('Redis operation failed:', error); // Log error แต่ไม่ crash - Redis failure ไม่ควรทำให้ business logic หยุดทำงาน // อาจ skip Redis operation หรือ return warning แต่ business process ควรดำเนินต่อ } finally { // ปิด connection หากจำเป็น if (redisClient) { redisClient.quit(); } } ``` **หมายเหตุ:** ปัญหาเดียวกันพบใน method `editAuthRole` ที่ line 269-276 --- ### #2 - Redis flushdb Without Error Handling (CRITICAL) **File & Location:** [AuthRoleController.ts:269-276](src/controllers/AuthRoleController.ts#L269-L276) **Method:** `editAuthRole` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - `redisClient.flushdb()` มี callback แต่ไม่ได้จัดการ error - Flush operation เป็น critical operation ที่อาจ fail ได้ - ไม่มี try-catch รอบรับ Redis operations **Code ปัจจุบัน (เสี่ยง):** ```typescript const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, }); await redisClient.flushdb(function (err: any, succeeded: any) { console.log(succeeded); // will be true if successfull }); // ❌ ถ้า error จะไม่ได้จัดการ ``` **Recommended Fix:** ```typescript import { promisify } from 'util'; const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, }); try { const redisFlushDbAsync = promisify(redisClient.flushdb).bind(redisClient); await redisFlushDbAsync(); } catch (error) { console.error('Redis flush operation failed:', error); throw new HttpError(HttpStatus.SERVICE_UNAVAILABLE, "Failed to clear cache"); } finally { if (redisClient) { redisClient.quit(); } } ``` --- ### #3 - CallAPI External Request Without Error Handling (CRITICAL) **File & Location:** [ChangePositionController.ts:585-604](src/controllers/ChangePositionController.ts#L585-L604) **Method:** `doneReport` **Problem Type:** 1. Unhandled Exception **Root Cause:** - External API call ผ่าน `CallAPI().PostData()` ไม่มี try-catch - `Promise.all()` ถ้ามี promise ไหน reject จะทำให้ **unhandled rejection** - Network error, timeout, หรือ external service down จะทำให้ unhandled rejection - ไม่มี timeout handling **Code ปัจจุบัน (เสี่ยง):** ```typescript await Promise.all( body.result.map(async (v) => { const profile = await this.profileChangePositionRepository.findOne({ where: { id: v.id }, }); if (profile != null) { await new CallAPI() .PostData(request, "/org/profile/salary", { // ❌ ไม่มี error handling profileId: profile.id, date: new Date(), }) .then(async (x) => { profile.status = "DONE"; await this.profileChangePositionRepository.save(profile); }); } }), ); ``` **Recommended Fix:** ```typescript // ใช้ Promise.allSettled แทน Promise.all เพื่อไม่ให้ rejection หยุดทั้งหมด const results = await Promise.allSettled( body.result.map(async (v) => { try { const profile = await this.profileChangePositionRepository.findOne({ where: { id: v.id }, }); if (profile != null) { // Add timeout const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), 30000) ); const apiCallPromise = new CallAPI().PostData(request, "/org/profile/salary", { profileId: profile.id, date: new Date(), }); await Promise.race([apiCallPromise, timeoutPromise]); profile.status = "DONE"; await this.profileChangePositionRepository.save(profile); } } catch (error) { console.error(`Failed to process profile ${v.id}:`, error); // Mark as FAILED แทนที่จะ leave as-is const profile = await this.profileChangePositionRepository.findOne({ where: { id: v.id }, }); if (profile) { profile.status = "FAILED"; profile.errorMessage = error.message; await this.profileChangePositionRepository.save(profile); } throw error; // Re-throw to track in allSettled } }), ); // Check results const failed = results.filter(r => r.status === 'rejected'); if (failed.length > 0) { console.error(`${failed.length} profiles failed to process`); // Optionally return partial success info } ``` --- ### #4 - Database Operations Without Error Handling (HIGH) **Files:** ทั้งหมด 9 Controllers **Locations:** หลาย method ในทุกไฟล์ **Problem Type:** 2. Missing Error Handle **Root Cause:** - Database operations ส่วนใหญ่ไม่มี try-catch - TypeORM query errors จะถูก catch โดย global error middleware แต่อาจเป็น generic 500 errors - Connection timeout, database down, หรือ query errors จะไม่ได้รับการจัดการเฉพาะเจาะจง - ไม่สามารถ distinguish ระหว่าง different error types ได้ **ตัวอย่าง Code ปัจจุบัน (เสี่ยง):** ```typescript @Get("list") public async listAuthRoleAttr() { const getList = await this.authRoleAttrRepo.find(); // ❌ ถ้า database error จะ throw ไปยัง global middleware // ไม่สามารถ handle เฉพาะเจาะจงได้ return new HttpSuccess(getList); } ``` **Recommended Fix:** สำหรับ critical operations: ```typescript import { QueryFailedError } from "typeorm"; @Get("list") public async listAuthRoleAttr() { try { const getList = await this.authRoleAttrRepo.find(); return new HttpSuccess(getList); } catch (error) { if (error instanceof QueryFailedError) { // Handle database-specific errors console.error('Database query failed:', error); throw new HttpError( HttpStatus.SERVICE_UNAVAILABLE, "Database service temporarily unavailable" ); } else if (error.message && error.message.includes('connection')) { throw new HttpError( HttpStatus.SERVICE_UNAVAILABLE, "Unable to connect to database" ); } // Re-throw other errors to global middleware throw error; } } ``` --- ### #5 - Promise.all Without Error Handling (HIGH) **File & Location:** [AuthRoleController.ts:247-267](src/controllers/AuthRoleController.ts#L247-L267) **Method:** `editAuthRole` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - `Promise.all()` รวม `remove()` และหลาย `save()` operations - ถ้า operation ไหน fail จะทำให้ **unhandled rejection** - ไม่มี try-catch รองรับ - Partial failure จะทำให้ไม่สามารถ recover ได้ **Code ปัจจุบัน (เสี่ยง):** ```typescript await this.authRoleAttrRepo.remove(roleAttrData, { data: req }); const newAttrs = body.authRoleAttrs.map((attr) => { const newAttr = new AuthRoleAttr(); Object.assign(newAttr, attr, { authRoleId: roleId, createdUserId: req.user.sub, createdFullName: req.user.name, lastUpdateUserId: req.user.sub, lastUpdateFullName: req.user.name, createdAt: new Date(), lastUpdatedAt: new Date(), }); return newAttr; }); const before = structuredClone(record); await Promise.all([ this.authRoleRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), ...newAttrs.map((attr) => this.authRoleAttrRepo.save(attr)), // ❌ ถ้า fail จะ unhandled rejection ]); ``` **Recommended Fix:** ```typescript try { await this.authRoleAttrRepo.remove(roleAttrData, { data: req }); const newAttrs = body.authRoleAttrs.map((attr) => { const newAttr = new AuthRoleAttr(); Object.assign(newAttr, attr, { authRoleId: roleId, createdUserId: req.user.sub, createdFullName: req.user.name, lastUpdateUserId: req.user.sub, lastUpdateFullName: req.user.name, createdAt: new Date(), lastUpdatedAt: new Date(), }); return newAttr; }); const before = structuredClone(record); // ใช้ Promise.allSettled แทน Promise.all const results = await Promise.allSettled([ this.authRoleRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), ...newAttrs.map((attr) => this.authRoleAttrRepo.save(attr)), ]); // Check for failures const failures = results.filter(r => r.status === 'rejected'); if (failures.length > 0) { console.error('Some operations failed:', failures); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "Failed to update some role attributes" ); } // Redis flush with error handling (จากปัญหา #2) const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, }); try { const redisFlushDbAsync = promisify(redisClient.flushdb).bind(redisClient); await redisFlushDbAsync(); } catch (error) { console.error('Redis flush failed:', error); // Non-critical - don't fail the request } finally { if (redisClient) { redisClient.quit(); } } return new HttpSuccess(); } catch (error) { console.error('Failed to update role:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "Failed to update role" ); } ``` --- ### #6 - JWT Verification Inconsistent Error Handling (MEDIUM) **File & Location:** [ApiKeyController.ts:42-61](src/controllers/ApiKeyController.ts#L42-L61) **Method:** `verifyApiKey` **Problem Type:** 2. Missing Error Handle **Root Cause:** - มี try-catch แต่ return HttpSuccess แทนที่จะ throw error - Error handling ไม่ consistent กับ endpoints อื่น - Client จะไม่รู้ว่าเกิด error (เพราะได้ 200 OK พร้อม valid: false) - ไม่สามารถ distinguish ระหว่าง token types ของ errors ได้ **Code ปัจจุบัน (เสี่ยง):** ```typescript try { const jwtSecret = process.env.JWT_SECRET || "your-default-secret-key"; const decoded = jwt.verify(requestBody.token, jwtSecret); return new HttpSuccess({ valid: true, data: decoded, }); } catch (error: any) { console.error("JWT Verification Error:", error.message); return new HttpSuccess({ // ❌ Return success แม้ error valid: false, error: error.message, }); } ``` **Recommended Fix:** ```typescript try { const jwtSecret = process.env.JWT_SECRET; if (!jwtSecret) { throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "JWT secret not configured" ); } const decoded = jwt.verify(requestBody.token, jwtSecret); return new HttpSuccess({ valid: true, data: decoded, }); } catch (error: any) { console.error("JWT Verification Error:", error.message); if (error.name === 'TokenExpiredError') { throw new HttpError(HttpStatus.UNAUTHORIZED, "Token expired"); } else if (error.name === 'JsonWebTokenError') { throw new HttpError(HttpStatus.UNAUTHORIZED, "Invalid token"); } else if (error instanceof HttpError) { throw error; } throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "Token verification failed" ); } ``` --- ### #7 - Query Builder Without Error Handling (MEDIUM) **File & Location:** [ChangePositionController.ts:284-350](src/controllers/ChangePositionController.ts#L284-L350) **Method:** `GetProfileChangePositionLists` **Problem Type:** 2. Missing Error Handle **Root Cause:** - Complex QueryBuilder พร้อม Brackets และ dynamic conditions - ถ้า query syntax error, database connection error, หรือ data type mismatch จะ throw ไป global middleware - ไม่สามารถ log หรือ track specific query errors ได้ **Code ปัจจุบัน (เสี่ยง):** ```typescript const [profileChangePosition, total] = await AppDataSource.getRepository(ProfileChangePosition) .createQueryBuilder("profileChangePosition") .where({ changePositionId: changePositionId }) .andWhere( new Brackets((qb) => { qb.where( searchKeyword != undefined && searchKeyword != null && searchKeyword != "" ? "profileChangePosition.prefix LIKE :keyword" : "1=1", { keyword: `%${searchKeyword}%` }, ) // ... หลาย orWhere }), ) .orderBy("profileChangePosition.createdAt", "ASC") .skip((page - 1) * pageSize) .take(pageSize) .getManyAndCount(); // ❌ ไม่มี try-catch return new HttpSuccess({ data: profileChangePosition, total }); ``` **Recommended Fix:** ```typescript try { // Validate input if (page < 1) { throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page number"); } if (pageSize < 1 || pageSize > 1000) { throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page size"); } const [profileChangePosition, total] = await AppDataSource.getRepository(ProfileChangePosition) .createQueryBuilder("profileChangePosition") .where({ changePositionId: changePositionId }) .andWhere( new Brackets((qb) => { // Use parameterized queries const conditions = []; const params = { keyword: `%${searchKeyword}%` }; if (searchKeyword) { conditions.push("profileChangePosition.prefix LIKE :keyword"); conditions.push("profileChangePosition.firstName LIKE :keyword"); conditions.push("profileChangePosition.lastName LIKE :keyword"); conditions.push("profileChangePosition.citizenId LIKE :keyword"); conditions.push("profileChangePosition.birthDate LIKE :keyword"); conditions.push("profileChangePosition.lastUpdatedAt LIKE :keyword"); conditions.push("profileChangePosition.status LIKE :keyword"); } qb.where( searchKeyword ? conditions.join(" OR ") : "1=1", params ); }), ) .orderBy("profileChangePosition.createdAt", "ASC") .skip((page - 1) * pageSize) .take(pageSize) .getManyAndCount(); return new HttpSuccess({ data: profileChangePosition, total }); } catch (error) { if (error instanceof HttpError) { throw error; } console.error('Query failed:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "Failed to retrieve profile change positions" ); } ``` --- ### #8 - Null Reference Risk (MEDIUM) **File & Location:** [ApiWebServiceController.ts:67-78](src/controllers/ApiWebServiceController.ts#L67-L78) **Method:** `listAttribute` **Problem Type:** 2. Missing Error Handle **Root Cause:** - `revision` อาจเป็น null ถ้าไม่พบ record - การใช้ `revision?.id` จะทำให้ condition เป็น `PosMaster.orgRevisionId = "undefined"` - SQL query จะไม่ error แต่จะ return ผลลัพธ์ที่ไม่ถูกต้อง - ไม่มี validation ว่า revision ต้องมีค่า **Code ปัจจุบัน (เสี่ยง):** ```typescript } else if (system == "organization") { tbMain = "OrgRoot"; const revision = await this.orgRevisionRepository.findOne({ select: ["id"], where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }); condition = `OrgRoot.orgRevisionId = "${revision?.id}"`; // ❌ ถ้า revision เป็น null จะเป็น undefined } ``` **Recommended Fix:** ```typescript } else if (system == "organization") { tbMain = "OrgRoot"; const revision = await this.orgRevisionRepository.findOne({ select: ["id"], where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }); if (!revision) { throw new HttpError( HttpStatus.NOT_FOUND, "No current organization revision found" ); } condition = `OrgRoot.orgRevisionId = "${revision.id}"`; } ``` --- ### #9 - Unsafe Default Environment Variable (LOW) **File & Location:** [ApiKeyController.ts:45](src/controllers/ApiKeyController.ts#L45) **Method:** `verifyApiKey` **Problem Type:** 2. Missing Error Handle / Security **Root Cause:** - ใช้ default value สำหรับ JWT_SECRET - ใน production ถ้าไม่ได้ set JWT_SECRET จะใช้ default value ที่ไม่ปลอดภัย - อาจนำไปสู่ security breach **Code ปัจจุบัน (เสี่ยง):** ```typescript const jwtSecret = process.env.JWT_SECRET || "your-default-secret-key"; // ❌ Default value insecure ``` **Recommended Fix:** ```typescript const jwtSecret = process.env.JWT_SECRET; if (!jwtSecret) { if (process.env.NODE_ENV === 'production') { throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "JWT secret not configured" ); } // Only for development console.warn('Using default JWT secret - not safe for production!'); } const decoded = jwt.verify(requestBody.token, jwtSecret || 'dev-secret-key'); ``` --- ### #10 - Switch Statement Without Break (BUG) **File & Location:** [ChangePositionController.ts:430-515](src/controllers/ChangePositionController.ts#L430-L515) **Method:** `positionProfileEmployee` **Problem Type:** 3. Logic Bug (ส่งผลต่อ data consistency) **Root Cause:** - Switch statement ไม่มี `break` statements - จะเกิด **fallthrough** effect - ทุก case หลังจาก case ที่ match จะถูก execute ด้วย - จะทำให้ data ถูก overwrite ด้วยค่าจาก cases ถัดไป - เป็น common bug ที่อาจทำให้ data corruption **Code ปัจจุบัน (เสี่ยง):** ```typescript switch (body.node) { case 0: { const data = await this.orgRootRepository.findOne({ where: { id: body.nodeId }, }); if (data != null) { profileChangePos.rootId = data.id; profileChangePos.root = data.orgRootName; profileChangePos.rootShortName = data.orgRootShortName; } } // ❌ ไม่มี break case 1: { // ❌ จะ execute ถ้า case 0 match const data = await this.child1Repository.findOne({ where: { id: body.nodeId }, relations: ["orgRoot"], }); // ... } // ❌ ไม่มี break case 2: { // ❌ จะ execute ถ้า case 0 หรือ 1 match // ... } // ... ต่อไปเรื่อยๆ } ``` **Recommended Fix:** ```typescript switch (body.node) { case 0: { const data = await this.orgRootRepository.findOne({ where: { id: body.nodeId }, }); if (data != null) { profileChangePos.rootId = data.id; profileChangePos.root = data.orgRootName; profileChangePos.rootShortName = data.orgRootShortName; } break; // ✅ เพิ่ม break } case 1: { const data = await this.child1Repository.findOne({ where: { id: body.nodeId }, relations: ["orgRoot"], }); if (data != null) { profileChangePos.rootId = data.orgRoot.id; profileChangePos.root = data.orgRoot.orgRootName; profileChangePos.rootShortName = data.orgRoot.orgRootShortName; profileChangePos.child1Id = data.id; profileChangePos.child1 = data.orgChild1Name; profileChangePos.child1ShortName = data.orgChild1ShortName; } break; // ✅ เพิ่ม break } case 2: { const data = await this.child2Repository.findOne({ where: { id: body.nodeId }, relations: ["orgRoot", "orgChild1"], }); if (data != null) { profileChangePos.rootId = data.orgRoot.id; profileChangePos.root = data.orgRoot.orgRootName; profileChangePos.rootShortName = data.orgRoot.orgRootShortName; profileChangePos.child1Id = data.orgChild1.id; profileChangePos.child1 = data.orgChild1.orgChild1Name; profileChangePos.child1ShortName = data.orgChild1.orgChild1ShortName; profileChangePos.child2Id = data.id; profileChangePos.child2 = data.orgChild2Name; profileChangePos.child2ShortName = data.orgChild2ShortName; } break; // ✅ เพิ่ม break } case 3: { // ... เพิ่ม break ท้าย } case 4: { // ... เพิ่ม break ท้าย } } ``` --- ### #11 - Array Mutation in Loop (MEDIUM) **File & Location:** [ChangePositionController.ts:233-250](src/controllers/ChangePositionController.ts#L233-L250) **Method:** `CreateProfileChangePosition` **Problem Type:** 3. Logic Bug **Root Cause:** - ใช้ตัวแปร `profiles` เดียวแล้ว push เข้า array หลายครั้ง - ทุก elements ใน array จะชี้ไปที่ object เดียวกัน - ทำให้ข้อมูลซ้ำกันทั้งหมด **Code ปัจจุบัน (เสี่ยง):** ```typescript const profileChangePositions: ProfileChangePosition[] = []; const profiles = new ProfileChangePosition(); // ❌ สร้างครั้งเดียว for (const data of body.profiles) { Object.assign(profiles, data); // ❌ ใช้ object เดียว // ... profileChangePositions.push(profiles); // ❌ push object เดียวกันซ้ำๆ } await this.profileChangePositionRepository.save(profileChangePositions); ``` **Recommended Fix:** ```typescript const profileChangePositions: ProfileChangePosition[] = []; for (const data of body.profiles) { const profiles = new ProfileChangePosition(); // ✅ สร้างใหม่ทุกรอบ Object.assign(profiles, data); let positionOld = data.positionOld ? `${data.positionOld}` : ""; let rootOld = data.rootOld ? (data.positionOld ? `/${data.rootOld}` : `${data.rootOld}`) : ""; profiles.changePositionId = changePositionId; profiles.organizationPositionOld = `${positionOld}${rootOld}`; profiles.status = "WAITTING"; profiles.createdUserId = request.user.sub; profiles.createdFullName = request.user.name; profiles.createdAt = new Date(); profiles.lastUpdateUserId = request.user.sub; profiles.lastUpdateFullName = request.user.name; profiles.lastUpdatedAt = new Date(); profileChangePositions.push(profiles); } await this.profileChangePositionRepository.save(profileChangePositions); ``` --- ## สรุปคำแนะนำการแก้ไขแบบรวม ### ระดับความสำคัญ **ต้องแก้ทันที (P0 - Critical):** 1. Redis operations error handling - อาจทำให้ process crash 2. External API calls error handling - อาจทำให้ unhandled rejection **ควรแก้โดยเร็ว (P1 - High):** 3. Database operations error handling 4. Promise operations error handling **ควรแก้ (P2 - Medium):** 5. JWT verification consistency 6. Query builder error handling 7. Null reference checks **แก้เมื่อว่าง (P3 - Low):** 8. Environment variable defaults 9. Code quality issues ### แนวทางการแก้ไขแบบ Global 1. **Implement centralized error handling:** - Wrap all async operations - Use specific error types - Log all errors appropriately 2. **Add circuit breaker for external services:** - Redis, external APIs - Prevent cascade failures 3. **Use Promise.allSettled** แทน Promise.all สำหรับ independent operations 4. **Add input validation:** - Validate before processing - Check for null/undefined 5. **Implement retry logic:** - For transient failures - Database connection issues --- ## ไฟล์ที่ต้องแก้ไข 1. **src/controllers/AuthRoleController.ts** - Redis operations, Promise operations 2. **src/controllers/ChangePositionController.ts** - External API calls, Switch bug, Array mutation 3. **src/controllers/ApiKeyController.ts** - JWT verification, Environment variables 4. **src/controllers/ApiWebServiceController.ts** - Null reference checks --- ## ข้อมูลเพิ่มเติม - **Controllers ที่ยังไม่ได้ตรวจสอบ:** 130 ไฟล์ - **ไฟล์ที่ไม่สามารถอ่านได้:** CommandController.ts (ไฟล์ใหญ่เกิน 336KB) --- **รายงานนี้ถูกสร้างโดย AI Code Review System** **สำหรับ BMA EHR Organization Project**