# รายงานการตรวจสอบ Unhandled Exception และ Crash Loop ## Batch 12: Controllers 111-120 **วันที่ตรวจสอบ:** 2026-05-08 **จำนวน Controllers ที่ตรวจสอบ:** 10 Controllers --- ## Controllers ที่ตรวจสอบในชุดนี้ 1. [ProfileInsigniaController.ts](src/controllers/ProfileInsigniaController.ts) 2. [ProfileInsigniaEmployeeController.ts](src/controllers/ProfileInsigniaEmployeeController.ts) 3. [ProfileInsigniaEmployeeTempController.ts](src/controllers/ProfileInsigniaEmployeeTempController.ts) 4. [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts) 5. [ProfileLeaveEmployeeController.ts](src/controllers/ProfileLeaveEmployeeController.ts) 6. [ProfileLeaveEmployeeTempController.ts](src/controllers/ProfileLeaveEmployeeTempController.ts) 7. [ProfileNopaidController.ts](src/controllers/ProfileNopaidController.ts) 8. [ProfileNopaidEmployeeController.ts](src/controllers/ProfileNopaidEmployeeController.ts) 9. [ProfileNopaidEmployeeTempController.ts](src/controllers/ProfileNopaidEmployeeTempController.ts) 10. [ProfileOtherController.ts](src/controllers/ProfileOtherController.ts) --- ## รายการปัญหาที่พบ ### 1. 🔴 CRITICAL - ProfileInsigniaController.ts - Unhandled Promise in editInsignia **File & Location:** [ProfileInsigniaController.ts](src/controllers/ProfileInsigniaController.ts:192-197) - `editInsignia()` method **Problem Type:** 1. Unhandled Exception **Root Cause:** ```typescript this.insigniaRepo.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { this.insigniaHistoryRepo.save(history, { data: req }); } ``` - มีการเรียก `this.insigniaRepo.save()` และ `this.insigniaHistoryRepo.save()` โดยไม่มี `await` หรือการจัดการ error - ถ้าเกิด error จากการ save database จะทำให้เกิด **Unhandled Promise Rejection** - ไม่มี try-catch รองรับ **Recommended Fix:** ```typescript try { await this.insigniaRepo.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { await this.insigniaHistoryRepo.save(history, { data: req }); } return new HttpSuccess(); } catch (error) { console.error('Error updating insignia:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์' ); } ``` --- ### 2. 🔴 CRITICAL - ProfileInsigniaEmployeeController.ts - Unhandled Promise in editInsignia **File & Location:** [ProfileInsigniaEmployeeController.ts](src/controllers/ProfileInsigniaEmployeeController.ts:200-205) - `editInsignia()` method **Problem Type:** 1. Unhandled Exception **Root Cause:** ```typescript this.insigniaRepo.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { this.insigniaHistoryRepo.save(history, { data: req }); } ``` - มีการเรียก `this.insigniaRepo.save()` และ `this.insigniaHistoryRepo.save()` โดยไม่มี `await` - ถ้า database save ล้มเหลวจะเกิด **Unhandled Promise Rejection** - Data inconsistency อาจเกิดขึ้นถ้า history save ไม่สำเร็จ **Recommended Fix:** ```typescript try { await this.insigniaRepo.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { await this.insigniaHistoryRepo.save(history, { data: req }); } return new HttpSuccess(); } catch (error) { console.error('Error updating employee insignia:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์' ); } ``` --- ### 3. 🔴 CRITICAL - ProfileInsigniaEmployeeTempController.ts - Unhandled Promise in editInsignia **File & Location:** [ProfileInsigniaEmployeeTempController.ts](src/controllers/ProfileInsigniaEmployeeTempController.ts:189-194) - `editInsignia()` method **Problem Type:** 1. Unhandled Exception **Root Cause:** ```typescript this.insigniaRepo.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { this.insigniaHistoryRepo.save(history, { data: req }); } ``` - ไม่มีการ await หรือจัดการ error สำหรับ database operations - ถ้าเกิด error จะทำให้เกิด **Unhandled Promise Rejection** และอาจ crash service **Recommended Fix:** ```typescript try { await this.insigniaRepo.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { await this.insigniaHistoryRepo.save(history, { data: req }); } return new HttpSuccess(); } catch (error) { console.error('Error updating temp employee insignia:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์' ); } ``` --- ### 4. 🔴 CRITICAL - ProfileLeaveController.ts - Unhandled Promise in editLeave **File & Location:** [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts:312) - `updateCancel()` method **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** ```typescript @Patch("cancel/{leaveId}") public async updateCancel( @Request() req: RequestWithUser, @Path() leaveId: string, ) { const record = await this.leaveRepo.findOneBy({ leaveId: leaveId }); // ❌ ใช้ leaveId แทน id if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา"); // ... ``` - **BUG**: ใช้ `leaveId` ใน `findOneBy({ leaveId: leaveId })` แต่ column ที่ถูกต้องควรเป็น `id` - ถ้าไม่พบข้อมูลจะ throw HttpError แต่ถ้า database error จะเกิด unhandled exception - ไม่มี try-catch ครอบ database operations **Recommended Fix:** ```typescript @Patch("cancel/{leaveId}") public async updateCancel( @Request() req: RequestWithUser, @Path() leaveId: string, ) { try { const record = await this.leaveRepo.findOneBy({ id: leaveId }); // ✅ ใช้ id แทน leaveId if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา"); const before = structuredClone(record); record.status = "cancel"; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; record.lastUpdatedAt = new Date(); await Promise.all([ this.leaveRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), ]); return new HttpSuccess(); } catch (error) { if (error instanceof HttpError) throw error; console.error('Error canceling leave:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการยกเลิกการลา' ); } } ``` --- ### 5. 🔴 CRITICAL - ProfileLeaveEmployeeTempController.ts - Unhandled Promises **File & Location:** [ProfileLeaveEmployeeTempController.ts](src/controllers/ProfileLeaveEmployeeTempController.ts:132-134) - `newLeave()` method **Problem Type:** 1. Unhandled Exception **Root Cause:** ```typescript await this.leaveRepo.save(data); // ❌ ไม่มี { data: req } context history.profileLeaveId = data.id; // ❌ ใช้ data.id ที่อาจยังไม่ถูกต้องถ้า save ไม่สำเร็จ await this.leaveHistoryRepo.save(history); // ❌ ไม่มี { data: req } context ``` - ไม่มี error handling รอบ database operations - การไม่ใส่ `{ data: req }` อาจทำให้ audit trail ไม่สมบูรณ์ - ถ้า `leaveRepo.save()` ล้มเหลว จะเกิด unhandled rejection **Recommended Fix:** ```typescript try { await this.leaveRepo.save(data, { data: req }); setLogDataDiff(req, { before, after: data }); history.profileLeaveId = data.id; await this.leaveHistoryRepo.save(history, { data: req }); return new HttpSuccess(data.id); } catch (error) { console.error('Error creating employee temp leave:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการบันทึกข้อมูลการลา' ); } ``` --- ### 6. 🟡 HIGH - ProfileNopaidController.ts - Unhandled Promise in editNopaid **File & Location:** [ProfileNopaidController.ts](src/controllers/ProfileNopaidController.ts:133-137) - `editNopaid()` method **Problem Type:** 1. Unhandled Exception **Root Cause:** ```typescript this.nopaidRepository.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { this.nopaidHistoryRepository.save(history, { data: req }); } ``` - ไม่มี `await` สำหรับ database save operations - ถ้าเกิด error จะเป็น **Unhandled Promise Rejection** - ไม่มี try-catch ครอบ **Recommended Fix:** ```typescript try { await this.nopaidRepository.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { await this.nopaidHistoryRepository.save(history, { data: req }); } return new HttpSuccess(); } catch (error) { console.error('Error updating nopaid:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน' ); } ``` --- ### 7. 🟡 HIGH - ProfileNopaidEmployeeController.ts - Unhandled Promise in editNopaid **File & Location:** [ProfileNopaidEmployeeController.ts](src/controllers/ProfileNopaidEmployeeController.ts:140-144) - `editNopaid()` method **Problem Type:** 1. Unhandled Exception **Root Cause:** ```typescript this.nopaidRepository.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { this.nopaidHistoryRepository.save(history, { data: req }); } ``` - ไม่มีการ await database save operations - ถ้าเกิด error จะทำให้เกิด unhandled promise rejection **Recommended Fix:** ```typescript try { await this.nopaidRepository.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { await this.nopaidHistoryRepository.save(history, { data: req }); } return new HttpSuccess(); } catch (error) { console.error('Error updating employee nopaid:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน' ); } ``` --- ### 8. 🟡 HIGH - ProfileNopaidEmployeeTempController.ts - Unhandled Promise in editNopaid **File & Location:** [ProfileNopaidEmployeeTempController.ts](src/controllers/ProfileNopaidEmployeeTempController.ts:137-141) - `editNopaid()` method **Problem Type:** 1. Unhandled Exception **Root Cause:** ```typescript this.nopaidRepository.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { this.nopaidHistoryRepository.save(history, { data: req }); } ``` - ไม่มี `await` สำหรับ database operations - Unhandled promise rejection อาจเกิดขึ้น **Recommended Fix:** ```typescript try { await this.nopaidRepository.save(record, { data: req }); setLogDataDiff(req, { before, after: record }); if (!(Object.keys(body).length === 1 && body.isUpload)) { await this.nopaidHistoryRepository.save(history, { data: req }); } return new HttpSuccess(); } catch (error) { console.error('Error updating temp employee nopaid:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน' ); } ``` --- ### 9. 🟢 MEDIUM - ProfileLeaveController.ts - Missing Permission Check in updateCancel **File & Location:** [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts:308-328) - `updateCancel()` method **Problem Type:** 2. Missing Error Handle **Root Cause:** ```typescript @Patch("cancel/{leaveId}") public async updateCancel( @Request() req: RequestWithUser, @Path() leaveId: string, ) { const record = await this.leaveRepo.findOneBy({ leaveId: leaveId }); if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา"); const before = structuredClone(record); record.status = "cancel"; // ... ❌ ไม่มี permission check ``` - Method `updateCancel` ไม่มีการ check permission ก่อนทำการ cancel - ผู้ใช้ที่ไม่มีสิทธิ์อาจสามารถ cancel การลาของคนอื่นได้ - เมื่อเทียบกับ methods อื่นๆ ที่มี permission check ถือว่าเป็นความไม่สอดคล้อง **Recommended Fix:** ```typescript @Patch("cancel/{leaveId}") public async updateCancel( @Request() req: RequestWithUser, @Path() leaveId: string, ) { try { const record = await this.leaveRepo.findOneBy({ id: leaveId }); if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา"); // ✅ เพิ่ม permission check await new permission().PermissionOrgUserUpdate( req, "SYS_REGISTRY_OFFICER", record.profileId ); const before = structuredClone(record); record.status = "cancel"; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; record.lastUpdatedAt = new Date(); await Promise.all([ this.leaveRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), ]); return new HttpSuccess(); } catch (error) { if (error instanceof HttpError) throw error; console.error('Error canceling leave:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'เกิดข้อผิดพลาดในการยกเลิกการลา' ); } } ``` --- ## สรุปประเด็นสำคัญ ### ปัญหาที่พบเป็นพื้นฐานซ้ำๆ: 1. **Unhandled Promise Rejections** - การเรียก database save methods โดยไม่มี `await` ใน methods แก้ไขข้อมูล (edit/update) 2. **Missing Try-Catch Blocks** - การขาด error handling รอบ database operations 3. **Data Consistency Risks** - การบันทึก history โดยไม่รู้ว่า main record บันทึกสำเร็จหรือไม่ 4. **Bug in updateCancel** - การใช้ `leaveId` แทน `id` ใน findOneBy ### คำแนะนำในการแก้ไข: 1. เพิ่ม try-catch ครอบทุก database operations ที่เสี่ยงต่อการเกิด error 2. ใช้ `await` กับทุก promise ที่เกี่ยวกับ database save/update 3. เพิ่ม permission check ใน method `updateCancel` 4. แก้ไข bug การใช้ `leaveId` ใน findOneBy ให้เป็น `id` 5. พิจารณาใช้ Transaction สำหรับการบันทึกข้อมูลที่ต้องการความสอดคล้องกัน (main record + history) ### การประเมินความเสี่ยง: - 🔴 **CRITICAL**: 4 จุด - อาจทำให้เกิด Unhandled Exception และ Crash Loop - 🟡 **HIGH**: 4 จุด - อาจทำให้เกิด Unhandled Exception - 🟢 **MEDIUM**: 1 จุด - ปัญหาความปลอดภัยและความสอดคล้องของระบบ