report: Controllers
This commit is contained in:
parent
7104ce4f34
commit
85e9be08f6
15 changed files with 10752 additions and 0 deletions
874
reports/batch-03-controllers-21-30-analysis.md
Normal file
874
reports/batch-03-controllers-21-30-analysis.md
Normal file
|
|
@ -0,0 +1,874 @@
|
|||
# รายงานการตรวจสอบ 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<string> {
|
||||
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<string> {
|
||||
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<UpdateIssueRequest>,
|
||||
@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<UpdateIssueRequest>,
|
||||
@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<T>(
|
||||
items: T[],
|
||||
executor: (item: T, index: number) => Promise<any>,
|
||||
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**
|
||||
Loading…
Add table
Add a link
Reference in a new issue