17 KiB
รายงานการตรวจสอบ Unhandled Exception และ Crash Loop
Batch 12: Controllers 111-120
วันที่ตรวจสอบ: 2026-05-08
จำนวน Controllers ที่ตรวจสอบ: 10 Controllers
Controllers ที่ตรวจสอบในชุดนี้
- ProfileInsigniaController.ts
- ProfileInsigniaEmployeeController.ts
- ProfileInsigniaEmployeeTempController.ts
- ProfileLeaveController.ts
- ProfileLeaveEmployeeController.ts
- ProfileLeaveEmployeeTempController.ts
- ProfileNopaidController.ts
- ProfileNopaidEmployeeController.ts
- ProfileNopaidEmployeeTempController.ts
- ProfileOtherController.ts
รายการปัญหาที่พบ
1. 🔴 CRITICAL - ProfileInsigniaController.ts - Unhandled Promise in editInsignia
File & Location: ProfileInsigniaController.ts - editInsignia() method
Problem Type: 1. Unhandled Exception
Root Cause:
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:
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 - editInsignia() method
Problem Type: 1. Unhandled Exception
Root Cause:
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:
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 - editInsignia() method
Problem Type: 1. Unhandled Exception
Root Cause:
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:
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 - updateCancel() method
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause:
@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:
@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 - newLeave() method
Problem Type: 1. Unhandled Exception
Root Cause:
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:
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 - editNopaid() method
Problem Type: 1. Unhandled Exception
Root Cause:
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:
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 - editNopaid() method
Problem Type: 1. Unhandled Exception
Root Cause:
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:
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 - editNopaid() method
Problem Type: 1. Unhandled Exception
Root Cause:
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:
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 - updateCancel() method
Problem Type: 2. Missing Error Handle
Root Cause:
@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:
@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,
'เกิดข้อผิดพลาดในการยกเลิกการลา'
);
}
}
สรุปประเด็นสำคัญ
ปัญหาที่พบเป็นพื้นฐานซ้ำๆ:
- Unhandled Promise Rejections - การเรียก database save methods โดยไม่มี
awaitใน methods แก้ไขข้อมูล (edit/update) - Missing Try-Catch Blocks - การขาด error handling รอบ database operations
- Data Consistency Risks - การบันทึก history โดยไม่รู้ว่า main record บันทึกสำเร็จหรือไม่
- Bug in updateCancel - การใช้
leaveIdแทนidใน findOneBy
คำแนะนำในการแก้ไข:
- เพิ่ม try-catch ครอบทุก database operations ที่เสี่ยงต่อการเกิด error
- ใช้
awaitกับทุก promise ที่เกี่ยวกับ database save/update - เพิ่ม permission check ใน method
updateCancel - แก้ไข bug การใช้
leaveIdใน findOneBy ให้เป็นid - พิจารณาใช้ Transaction สำหรับการบันทึกข้อมูลที่ต้องการความสอดคล้องกัน (main record + history)
การประเมินความเสี่ยง:
- 🔴 CRITICAL: 4 จุด - อาจทำให้เกิด Unhandled Exception และ Crash Loop
- 🟡 HIGH: 4 จุด - อาจทำให้เกิด Unhandled Exception
- 🟢 MEDIUM: 1 จุด - ปัญหาความปลอดภัยและความสอดคล้องของระบบ