hrms-api-org/reports/batch-12-controllers-111-120-analysis.md
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 85e9be08f6 report: Controllers
2026-05-08 18:15:03 +07:00

17 KiB

รายงานการตรวจสอบ Unhandled Exception และ Crash Loop

Batch 12: Controllers 111-120

วันที่ตรวจสอบ: 2026-05-08
จำนวน Controllers ที่ตรวจสอบ: 10 Controllers


Controllers ที่ตรวจสอบในชุดนี้

  1. ProfileInsigniaController.ts
  2. ProfileInsigniaEmployeeController.ts
  3. ProfileInsigniaEmployeeTempController.ts
  4. ProfileLeaveController.ts
  5. ProfileLeaveEmployeeController.ts
  6. ProfileLeaveEmployeeTempController.ts
  7. ProfileNopaidController.ts
  8. ProfileNopaidEmployeeController.ts
  9. ProfileNopaidEmployeeTempController.ts
  10. 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,
      'เกิดข้อผิดพลาดในการยกเลิกการลา'
    );
  }
}

สรุปประเด็นสำคัญ

ปัญหาที่พบเป็นพื้นฐานซ้ำๆ:

  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 จุด - ปัญหาความปลอดภัยและความสอดคล้องของระบบ