# รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 3 (ไฟล์ที่ 21-30) **Project:** BMA EHR Organization Backend **Framework:** TSOA + Express + TypeORM **วันที่ตรวจสอบ:** 2026-05-08 **จำนวน Controllers:** 10 ไฟล์ **สถานะ:** เสร็จสิ้น --- ## สรุปผลการตรวจสอบ | ระดับความรุนแรง | จำนวนจุดเสี่ยง | |---------------------|-------------------| | **CRITICAL** | 0 | | **HIGH** | 4 | | **MEDIUM** | 5 | | **LOW** | 2 | | **BUG** | 2 | | **รวมทั้งหมด** | 13 | --- ## Controllers ที่ตรวจสอบ 21. [EmployeePositionController.ts](src/controllers/EmployeePositionController.ts) 22. [EmployeeTempPositionController.ts](src/controllers/EmployeeTempPositionController.ts) 23. [ExRetirementController.ts](src/controllers/ExRetirementController.ts) 24. [GenderController.ts](src/controllers/GenderController.ts) 25. [ImportDataController.ts](src/controllers/ImportDataController.ts) 26. [InsigniaController.ts](src/controllers/InsigniaController.ts) 27. [InsigniaTypeController.ts](src/controllers/InsigniaTypeController.ts) 28. [IssuesController.ts](src/controllers/IssuesController.ts) 29. [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts) 30. [LoginController.ts](src/controllers/LoginController.ts) --- ## รายละเอียดจุดเสี่ยงแต่ละจุด ### #1 - Promise.all Without Error Handling in Position Creation (HIGH) **File & Location:** [EmployeePositionController.ts:690-707](src/controllers/EmployeePositionController.ts#L690-L707) **Method:** `createEmpMaster` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - ใช้ `Promise.all()` สำหรับบันทึก positions หลายรายการพร้อมกัน - ถ้ามี position ไหน save ไม่สำเร็จ จะเกิด unhandled rejection - ไม่มี try-catch รองรับ ทำให้ไม่สามารถควบคุม error ได้ - ส่งผลให้อาจเกิด data inconsistency ถ้า save บางส่วนสำเร็จ แต่บางส่วนล้มเหลว **Code ปัจจุบัน (เสี่ยง):** ```typescript await Promise.all( requestBody.positions.map(async (x: any) => { const position = Object.assign(new EmployeePosition()); position.positionName = x.posDictName; position.posTypeId = x.posTypeId == "" ? null : x.posTypeId; position.posLevelId = x.posLevelId == "" ? null : x.posLevelId; position.positionIsSelected = false; position.posMasterId = posMaster.id; position.createdUserId = request.user.sub; position.createdFullName = request.user.name; position.createdAt = new Date(); position.lastUpdateUserId = request.user.sub; position.lastUpdateFullName = request.user.name; position.lastUpdatedAt = new Date(); await this.employeePositionRepository.save(position, { data: request }); }), ); return new HttpSuccess(posMaster.id); ``` **Recommended Fix:** ```typescript try { await Promise.all( requestBody.positions.map(async (x: any) => { try { const position = Object.assign(new EmployeePosition()); position.positionName = x.posDictName; position.posTypeId = x.posTypeId == "" ? null : x.posTypeId; position.posLevelId = x.posLevelId == "" ? null : x.posLevelId; position.positionIsSelected = false; position.posMasterId = posMaster.id; position.createdUserId = request.user.sub; position.createdFullName = request.user.name; position.createdAt = new Date(); position.lastUpdateUserId = request.user.sub; position.lastUpdateFullName = request.user.name; position.lastUpdatedAt = new Date(); await this.employeePositionRepository.save(position, { data: request }); } catch (error) { console.error(`Failed to save position "${x.posDictName}":`, error); throw error; // Re-throw to be caught by Promise.all } }), ); return new HttpSuccess(posMaster.id); } catch (error) { console.error("Failed to save positions:", error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "Failed to save positions" ); } ``` --- ### #2 - Promise.all Without Error Handling in Position Update (HIGH) **File & Location:** [EmployeePositionController.ts:905-921](src/controllers/EmployeePositionController.ts#L905-L921) **Method:** `updateEmpMaster` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - Similar to #1 แต่เกิดใน update operation - `Promise.all()` โดยไม่มี error handling - เกิดการลบ positions เก่า ก่อน แล้วค่อยสร้างใหม่ ถ้าสร้างใหม่ fail จะเกิด data loss **Code ปัจจุบัน (เสี่ยง):** ```typescript await this.employeePositionRepository.delete({ posMasterId: posMaster.id }); await Promise.all( requestBody.positions.map(async (x: any) => { const position = Object.assign(new EmployeePosition()); position.positionName = x.posDictName; position.posTypeId = x.posTypeId == "" ? null : x.posTypeId; position.posLevelId = x.posLevelId == "" ? null : x.posLevelId; position.positionIsSelected = false; position.posMasterId = posMaster.id; position.createdUserId = request.user.sub; position.createdFullName = request.user.name; position.createdAt = new Date(); position.lastUpdateUserId = request.user.sub; position.lastUpdateFullName = request.user.name; position.lastUpdatedAt = new Date(); await this.employeePositionRepository.save(position, { data: request }); }), ); ``` **Recommended Fix:** ```typescript // Get existing positions as backup before deletion const existingPositions = await this.employeePositionRepository.find({ where: { posMasterId: posMaster.id } }); try { await this.employeePositionRepository.delete({ posMasterId: posMaster.id }); await Promise.all( requestBody.positions.map(async (x: any) => { try { const position = Object.assign(new EmployeePosition()); position.positionName = x.posDictName; position.posTypeId = x.posTypeId == "" ? null : x.posTypeId; position.posLevelId = x.posLevelId == "" ? null : x.posLevelId; position.positionIsSelected = false; position.posMasterId = posMaster.id; position.createdUserId = request.user.sub; position.createdFullName = request.user.name; position.createdAt = new Date(); position.lastUpdateUserId = request.user.sub; position.lastUpdateFullName = request.user.name; position.lastUpdatedAt = new Date(); await this.employeePositionRepository.save(position, { data: request }); } catch (error) { console.error(`Failed to update position "${x.posDictName}":`, error); throw error; } }), ); } catch (error) { console.error("Failed to update positions, restoring backup:", error); // Restore backup positions await this.employeePositionRepository.save(existingPositions); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "Failed to update positions" ); } ``` --- ### #3 - Async forEach Without Proper Error Handling (HIGH) **File & Location:** [EmployeePositionController.ts:2378-2395](src/controllers/EmployeePositionController.ts#L2378-L2395) **Method:** `createEmpHolder` **Problem Type:** 1. Unhandled Exception **Root Cause:** - ใช้ `forEach` กับ async function ซึ่งไม่รอให้ทุก operation สำเร็จ - การใช้ `forEach` กับ async จะไม่ catch error ที่เกิดใน loop - ถ้ามีการ save ที่ fail จะไม่ทราบ และ data อาจไม่ถูกต้อง **Code ปัจจุบัน (เสี่ยง):** ```typescript dataMaster.positions.forEach(async (position) => { if (position.id === requestBody.position) { position.positionIsSelected = true; const profile = await this.profileRepository.findOne({ where: { id: requestBody.profileId }, }); if (profile != null) { const _null: any = null; profile.posLevelId = position?.posLevelId ?? _null; profile.posTypeId = position?.posTypeId ?? _null; profile.position = position?.positionName ?? _null; await this.profileRepository.save(profile); } } else { position.positionIsSelected = false; } await this.employeePositionRepository.save(position); }); ``` **Recommended Fix:** ```typescript // Use Promise.all instead of forEach with async await Promise.all( dataMaster.positions.map(async (position) => { try { if (position.id === requestBody.position) { position.positionIsSelected = true; const profile = await this.profileRepository.findOne({ where: { id: requestBody.profileId }, }); if (profile != null) { const _null: any = null; profile.posLevelId = position?.posLevelId ?? _null; profile.posTypeId = position?.posTypeId ?? _null; profile.position = position?.positionName ?? _null; await this.profileRepository.save(profile); } } else { position.positionIsSelected = false; } await this.employeePositionRepository.save(position); } catch (error) { console.error(`Failed to update position ${position.id}:`, error); throw error; } }) ); ``` --- ### #4 - Promise.all in EmployeeTempPositionController Without Error Handling (HIGH) **File & Location:** [EmployeeTempPositionController.ts:557-574](src/controllers/EmployeeTempPositionController.ts#L557-L574) **Method:** `createEmpMaster` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - เหมือนกับ #1 แต่เกิดใน EmployeeTempPositionController - ใช้ `Promise.all()` โดยไม่มี error handling **Code ปัจจุบัน (เสี่ยง):** ```typescript await Promise.all( requestBody.positions.map(async (x: any) => { const position = Object.assign(new EmployeePosition()); position.positionName = x.posDictName; position.posTypeId = x.posTypeId == "" ? null : x.posTypeId; position.posLevelId = x.posLevelId == "" ? null : x.posLevelId; position.positionIsSelected = false; position.posMasterTempId = posMaster.id; position.createdUserId = request.user.sub; position.createdFullName = request.user.name; position.createdAt = new Date(); position.lastUpdateUserId = request.user.sub; position.lastUpdateFullName = request.user.name; position.lastUpdatedAt = new Date(); await this.employeePositionRepository.save(position, { data: request }); }), ); ``` **Recommended Fix:** ใช้การแก้ไขเดียวกันกับ #1 --- ### #5 - Unsafe Token Fetch in ExRetirementController (MEDIUM) **File & Location:** [ExRetirementController.ts:148-173](src/controllers/ExRetirementController.ts#L148-L173) **Method:** `getToken` **Problem Type:** 2. Missing Error Handle **Root Cause:** - ฟังก์ชัน `getToken` มีการ throw error แต่ใช้ `Promise.reject` - ไม่มีการระบุประเภทของ error ที่ชัดเจน - Error ที่เกิดขึ้นอาจไม่ถูก handle อย่างเหมาะสมในบางกรณี - Token cache อาจเก็บ token ที่หมดอายุ **Code ปัจจุบัน (เสี่ยง):** ```typescript async function getToken(ClientID: string, ClientSecret: string): Promise { const cacheKey = `${ClientID}:${ClientSecret}`; // ลองหา token ใน cache ก่อน const cachedToken = TokenCache.get(cacheKey); if (cachedToken) { return cachedToken; } // ถ้าไม่มีใน cache ให้ขอใหม่ try { const formData = new FormData(); formData.append("ClientID", ClientID); formData.append("ClientSecret", ClientSecret); const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, { headers: { "Content-Type": "application/json", }, }); const token = res.data.token; TokenCache.set(cacheKey, token); return token; } catch (error) { return Promise.reject({ message: "Error occurred", error }); } } ``` **Recommended Fix:** ```typescript async function getToken(ClientID: string, ClientSecret: string): Promise { const cacheKey = `${ClientID}:${ClientSecret}`; // ลองหา token ใน cache ก่อน const cachedToken = TokenCache.get(cacheKey); if (cachedToken) { return cachedToken; } // ถ้าไม่มีใน cache ให้ขอใหม่ try { const formData = new FormData(); formData.append("ClientID", ClientID); formData.append("ClientSecret", ClientSecret); const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, { headers: { "Content-Type": "application/json", }, timeout: 10000, // Add timeout }); if (!res.data || !res.data.token) { throw new Error("Invalid token response from exprofile API"); } const token = res.data.token; TokenCache.set(cacheKey, token); return token; } catch (error: any) { console.error("Failed to get exprofile token:", error); // More specific error handling if (error.response?.status === 401) { throw new Error("Invalid credentials for exprofile API"); } else if (error.code === 'ECONNABORTED') { throw new Error("Request timeout while fetching exprofile token"); } else { throw new Error(`Failed to fetch exprofile token: ${error.message}`); } } } ``` --- ### #6 - Promise.all Without Error Handling in ImportDataController (MEDIUM) **File & Location:** [ImportDataController.ts:2425-2443](src/controllers/ImportDataController.ts#L2425-L2443) **Method:** Import Education Mis Data **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - ใช้ `Promise.all()` สำหรับ batch insert ข้อมูลลง database - ไม่มี error handling ถ้า insert บางรายการ fail - ไม่สามารถรู้ได้ว่ามีกี่รายการที่สำเร็จ/ล้มเหลว **Code ปัจจุบัน (เสี่ยง):** ```typescript await Promise.all( getExcel.map(async (item: any) => { const educationMis = new EducationMis(); educationMis.EDUCATION_CODE = item.EDUCATION_CODE; educationMis.EDUCATION_NAME = item.EDUCATION_NAME; // ... set other properties await this.educationMisRepository.save(educationMis); }), ); ``` **Recommended Fix:** ```typescript const results = await Promise.allSettled( getExcel.map(async (item: any) => { try { const educationMis = new EducationMis(); educationMis.EDUCATION_CODE = item.EDUCATION_CODE; educationMis.EDUCATION_NAME = item.EDUCATION_NAME; // ... set other properties await this.educationMisRepository.save(educationMis); return { status: 'success', code: item.EDUCATION_CODE }; } catch (error) { console.error(`Failed to save education ${item.EDUCATION_CODE}:`, error); return { status: 'failed', code: item.EDUCATION_CODE, error: error.message }; } }), ); const failed = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status === 'failed')); if (failed.length > 0) { console.warn(`Failed to import ${failed.length} education records`); // Optionally notify user about partial failure } ``` --- ### #7 - External API Call Without Proper Error Handling (MEDIUM) **File & Location:** [ExRetirementController.ts:50-103](src/controllers/ExRetirementController.ts#L50-L103) **Method:** `getExRetirement` **Problem Type:** 2. Missing Error Handle **Root Cause:** - มีการเรียก external API แต่ error handling ยังไม่ครอบคลุม - ใช้ retry mechanism แต่ไม่มี exponential backoff - ไม่มี logging ที่ชัดเจนสำหรับการ debug **Code ปัจจุบัน (เสี่ยง):** ```typescript let retryCount = 0; const maxRetries = 2; while (retryCount < maxRetries) { try { const token = await getToken(clientId, clientSecret); if (!token) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่สามารถขอ Token ได้"); } const scope = "getOfficerRetireData"; const startRecord = requestBody.page !== 1 ? (requestBody.page - 1) * 25 : 0; const formData = new FormData(); formData.append("scope", scope); formData.append("startRecord", startRecord.toString()); formData.append("retireYear", requestBody.retireYear); formData.append("citizenID", requestBody.citizenID); formData.append("firstNameTH", requestBody.firstNameTH); formData.append("lastNameTH", requestBody.lastNameTH); formData.append("officerTypeID", requestBody.type === "officer" ? "1" : "2"); const res = await axios.post(API_URL_BANGKOK + "/getData", formData, { headers: { Authorization: `Bearer ${token}`, }, }); return new HttpSuccess(res.data.data); } catch (error: any) { if (error.response?.status === 500 && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); retryCount++; continue; } throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้"); } } ``` **Recommended Fix:** ```typescript let retryCount = 0; const maxRetries = 2; const baseDelay = 1000; // 1 second while (retryCount < maxRetries) { try { const token = await getToken(clientId, clientSecret); if (!token) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่สามารถขอ Token ได้"); } const scope = "getOfficerRetireData"; const startRecord = requestBody.page !== 1 ? (requestBody.page - 1) * 25 : 0; const formData = new FormData(); formData.append("scope", scope); formData.append("startRecord", startRecord.toString()); formData.append("retireYear", requestBody.retireYear); formData.append("citizenID", requestBody.citizenID); formData.append("firstNameTH", requestBody.firstNameTH); formData.append("lastNameTH", requestBody.lastNameTH); formData.append("officerTypeID", requestBody.type === "officer" ? "1" : "2"); const res = await axios.post(API_URL_BANGKOK + "/getData", formData, { headers: { Authorization: `Bearer ${token}`, }, timeout: 30000, // 30 second timeout }); return new HttpSuccess(res.data.data); } catch (error: any) { retryCount++; // Log error for debugging console.error(`Error fetching retirement data (attempt ${retryCount}/${maxRetries}):`, { message: error.message, status: error.response?.status, code: error.code }); // Check if we should retry const shouldRetry = (error.response?.status === 500 || error.response?.status === 503 || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') && retryCount < maxRetries; if (shouldRetry) { TokenCache.delete(`${clientId}:${clientSecret}`); // Exponential backoff const delay = baseDelay * Math.pow(2, retryCount - 1); await new Promise(resolve => setTimeout(resolve, delay)); continue; } // Don't retry on client errors (4xx) or after max retries throw new HttpError( HttpStatusCode.INTERNAL_SERVER_ERROR, `ไม่สามารถติดต่อ API ได้: ${error.message}` ); } } ``` --- ### #8 - Missing Error Handling in IssuesController (MEDIUM) **File & Location:** [IssuesController.ts:54-71](src/controllers/IssuesController.ts#L54-L71) **Method:** `updateIssue` **Problem Type:** 2. Missing Error Handle **Root Cause:** - ไม่มี try-catch รองรับ operation ที่อาจ fail - ไม่มีการตรวจสอบว่า request body ถูกต้องหรือไม่ - การใช้ `Object.assign` โดยไม่ validate อาจทำให้เกิด invalid data **Code ปัจจุบัน (เสี่ยง):** ```typescript @Put("{id}") async updateIssue( @Path("id") id: string, @Body() requestBody: Partial, @Request() request: RequestWithUser, ) { let issue = await this.issuesRepository.findOneBy({ id }); if (!issue) { this.setStatus(HttpStatusCode.NOT_FOUND); return { message: "ไม่พบข้อมูลที่ต้องการแก้ไข" }; } Object.assign(issue, requestBody); issue.lastUpdateUserId = request.user.sub; issue.lastUpdateFullName = request.user.name; issue.lastUpdatedAt = new Date(); await this.issuesRepository.save(issue); return new HttpSuccess(issue); } ``` **Recommended Fix:** ```typescript @Put("{id}") async updateIssue( @Path("id") id: string, @Body() requestBody: Partial, @Request() request: RequestWithUser, ) { try { let issue = await this.issuesRepository.findOneBy({ id }); if (!issue) { this.setStatus(HttpStatusCode.NOT_FOUND); return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลที่ต้องการแก้ไข"); } // Validate request body if needed if (requestBody.status !== undefined) { // Validate status enum values } Object.assign(issue, requestBody); issue.lastUpdateUserId = request.user.sub; issue.lastUpdateFullName = request.user.name; issue.lastUpdatedAt = new Date(); try { await this.issuesRepository.save(issue); } catch (saveError: any) { console.error("Failed to save issue:", saveError); throw new HttpError( HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถบันทึกข้อมูลได้" ); } return new HttpSuccess(issue); } catch (error) { if (error instanceof HttpError) { throw error; } console.error("Error updating issue:", error); throw new HttpError( HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการอัปเดตข้อมูล" ); } } ``` --- ### #9 - Promise.all Without Error Handling in Province Import (MEDIUM) **File & Location:** [ImportDataController.ts:2856-2874](src/controllers/ImportDataController.ts#L2856-L2874) **Method:** Import Province Data **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - Similar to #6 แต่เกิดใน province import - ใช้ `Promise.all()` โดยไม่มี error handling **Recommended Fix:** ใช้การแก้ไขเดียวกันกับ #6 และ #11 --- ### #10 - Wrong Error Status Code (BUG) **File & Location:** [InsigniaController.ts:62-64](src/controllers/InsigniaController.ts#L62-L64), [InsigniaTypeController.ts:58-60](src/controllers/InsigniaTypeController.ts#L58-L60) **Methods:** `CreateInsignia`, `CreateInsigniaType` **Problem Type:** 3. Logic Bug **Root Cause:** - Throw `HttpError(HttpStatusCode.NOT_FOUND, ...)` สำหรับ duplicate data errors - ควรใช้ `CONFLICT` (409) หรือ `BAD_REQUEST` (400) แทน **Code ปัจจุบัน (ผิด):** ```typescript if (rowRepeated) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ข้อมูล Row นี้มีอยู่ในระบบแล้ว"); } ``` **Recommended Fix:** ```typescript if (rowRepeated) { throw new HttpError(HttpStatusCode.CONFLICT, "ข้อมูล Row นี้มีอยู่ในระบบแล้ว"); } ``` --- ### #11 - Promise.all Without Error Handling in Amphur Import (LOW) **File & Location:** [ImportDataController.ts:2889-2908](src/controllers/ImportDataController.ts#L2889-L2908) **Method:** Import Amphur Data **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** - Similar to #6 แต่เกิดใน amphur import - ใช้ `Promise.all()` โดยไม่มี error handling **Recommended Fix:** ใช้การแก้ไขเดียวกันกับ #6 --- ### #12 - Redundant Promise.all in LoginController (LOW) **File & Location:** [LoginController.ts:38-47](src/controllers/LoginController.ts#L38-L47) **Method:** `login` **Problem Type:** 3. Code Quality **Root Cause:** - ใช้ `Promise.all` กับ array ที่มีแค่ 1 promise - การใช้งานไม่มีประโยชน์เพราะไม่ได้ parallelize อะไรเลย - Code อ่านยากและสับสน **Code ปัจจุบัน (ผิด):** ```typescript let _data: any = null; await Promise.all([ await new CallAPI() .PostDataKeycloak(`/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, data) .then(async (x) => { _data = x; }) .catch(async (x) => { throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง"); }), ]); ``` **Recommended Fix:** ```typescript try { const _data = await new CallAPI().PostDataKeycloak( `/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, data ); if (!_data) { throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง"); } return new HttpSuccess(_data); } catch (error: any) { if (error instanceof HttpError) { throw error; } throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง"); } ``` --- ### #13 - Error Message from External API Not Handled (BUG) **File & Location:** [LoginController.ts:44-46](src/controllers/LoginController.ts#L44-L46), [LoginController.ts:85-87](src/controllers/LoginController.ts#L85-L87) **Methods:** `login`, `loginCheckin` **Problem Type:** 3. Logic Bug **Root Cause:** - Catch error แล้ว throw HttpError ใหม่ แต่ไม่ได้ preserve error message ต้นทาง - ทำให้ user ไม่รู้สาเหตุที่แท้จริงของการ login ล้มเหลว **Code ปัจจุบัน (ผิด):** ```typescript .catch(async (x) => { throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง"); }), ``` **Recommended Fix:** ```typescript .catch(async (error: any) => { const errorMessage = error?.response?.data?.error_description || error?.response?.data?.error || error?.message || "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง"; console.error("Login failed:", error); throw new HttpError(HttpStatus.UNAUTHORIZED, errorMessage); }), ``` --- ## สรุปคำแนะนำการแก้ไขแบบรวม ### ระดับความสำคัญ **ต้องแก้โดยเร็ว (P1 - High):** 1. Promise.all operations without error handling - unhandled rejections อาจทำให้ service ไม่เสถียร 2. Async forEach ที่ไม่รอ completion - อาจทำให้ data ไม่ถูกต้อง **ควรแก้ (P2 - Medium):** 3. External API call error handling - ควรมี retry mechanism ที่ดีขึ้น 4. Missing error handling in IssuesController 5. Promise operations in import controllers **แก้เมื่อว่าง (P3 - Low):** 6. Redundant Promise.all in LoginController 7. Error status code issues ### แนวทางการแก้ไขแบบ Global 1. **สร้าง utility function** สำหรับ Promise.all ที่มี error handling: ```typescript async function safePromiseAll( items: T[], executor: (item: T, index: number) => Promise, options: { continueOnError?: boolean; throwOnError?: boolean; } = {} ) { const { continueOnError = false, throwOnError = true } = options; if (continueOnError) { const results = await Promise.allSettled( items.map((item, index) => executor(item, index)) ); const failures = results.filter(r => r.status === 'rejected'); if (failures.length > 0 && throwOnError) { console.warn(`${failures.length} operations failed`); } return results; } else { return Promise.all( items.map((item, index) => executor(item, index)) ); } } ``` 2. **ใช้ try-catch** รอบทุก database operation และ external API call 3. **Implement logging** ที่สมบูรณ์สำหรับ debugging 4. **Use proper HTTP status codes** ตามมาตรฐาน REST API --- ## ไฟล์ที่ต้องแก้ไข 1. **src/controllers/EmployeePositionController.ts** - Promise.all handling, forEach with async 2. **src/controllers/EmployeeTempPositionController.ts** - Promise.all handling 3. **src/controllers/ExRetirementController.ts** - External API error handling, token management 4. **src/controllers/ImportDataController.ts** - Promise.all in import operations 5. **src/controllers/IssuesController.ts** - Error handling 6. **src/controllers/LoginController.ts** - Redundant Promise.all, error messages 7. **src/controllers/InsigniaController.ts** - Error status codes 8. **src/controllers/InsigniaTypeController.ts** - Error status codes --- ## ข้อมูลเพิ่มเติม - **Controllers ที่ยังไม่ได้ตรวจสอบ:** 110 ไฟล์ - **จุดเสี่ยงที่พบซ้ำจากชุดที่ 1-2:** Promise.all without error handling, wrong HTTP status codes --- **รายงานนี้ถูกสร้างโดย AI Code Review System** **สำหรับ BMA EHR Organization Project**