1070 lines
37 KiB
Markdown
1070 lines
37 KiB
Markdown
# รายงานการตรวจสอบ Unhandled Exception และ Crash Loop
|
|
## Batch 10: Controllers 91-100
|
|
|
|
**วันที่ตรวจสอบ:** 2026-05-08
|
|
**จำนวน Controllers ที่ตรวจสอบ:** 10 Controllers
|
|
|
|
---
|
|
|
|
## Controllers ที่ตรวจสอบในชุดนี้
|
|
|
|
1. [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts)
|
|
2. [SocketController.ts](src/controllers/SocketController.ts)
|
|
3. [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts)
|
|
4. [ApiManageController.ts](src/controllers/ApiManageController.ts)
|
|
5. [ImportDataController.ts](src/controllers/ImportDataController.ts)
|
|
6. [ExRetirementController.ts](src/controllers/ExRetirementController.ts)
|
|
7. [IssuesController.ts](src/controllers/IssuesController.ts)
|
|
8. [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts)
|
|
9. [MyController.ts](src/controllers/MyController.ts)
|
|
10. [MainController.ts](src/controllers/MainController.ts)
|
|
|
|
---
|
|
|
|
## รายการปัญหาที่พบ
|
|
|
|
### 1. 🔴 CRITICAL - KeycloakSyncController.ts - Unhandled Promise in Loop
|
|
|
|
**File & Location:** [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts:159-182) - `syncByProfileIds()` method
|
|
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
for (const profileId of profileIds) {
|
|
try {
|
|
const success = await this.keycloakAttributeService.syncOnOrganizationChange(
|
|
profileId,
|
|
profileType,
|
|
);
|
|
// ...
|
|
} catch (error: any) {
|
|
result.failed++;
|
|
result.details.push({ profileId, status: "failed", error: error.message });
|
|
}
|
|
}
|
|
```
|
|
|
|
แม้ว่าจะมี `try-catch` ภายใน loop แต่การ catch error แล้วเพียงแค่บันทึกผลลัพธ์ อาจไม่เพียงพอสำหรับบางกรณี:
|
|
- หาก `syncOnOrganizationChange` มี Promise rejection ที่ไม่ถูก handle อย่างถูกต้องภายใน service
|
|
- หากเกิด error ระหว่างการทำงานของ loop ที่ไม่ใช่จาก `syncOnOrganizationChange` เช่น จาก `result.details.push()`
|
|
- Error ที่เกิดขึ้นอาจเป็น unhandled rejection หาก service ไม่ return Promise อย่างถูกต้อง
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
@Post("sync-profiles-batch")
|
|
async syncByProfileIds(
|
|
@Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" },
|
|
) {
|
|
const { profileIds, profileType } = request;
|
|
|
|
if (!profileIds || profileIds.length === 0) {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า");
|
|
}
|
|
|
|
if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
|
|
throw new HttpError(
|
|
HttpStatus.BAD_REQUEST,
|
|
"profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
|
|
);
|
|
}
|
|
|
|
const result = {
|
|
total: profileIds.length,
|
|
success: 0,
|
|
failed: 0,
|
|
details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>,
|
|
};
|
|
|
|
// เพิ่ม timeout protection และ error handling ที่ดีขึ้น
|
|
const SYNC_TIMEOUT = 30000; // 30 วินาทีต่อ profile
|
|
|
|
for (const profileId of profileIds) {
|
|
try {
|
|
// เพิ่ม Promise.race เพื่อป้องกันการ hang
|
|
const syncPromise = this.keycloakAttributeService.syncOnOrganizationChange(
|
|
profileId,
|
|
profileType,
|
|
);
|
|
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Sync timeout')), SYNC_TIMEOUT)
|
|
);
|
|
|
|
const success = await Promise.race([syncPromise, timeoutPromise]) as boolean;
|
|
|
|
if (success) {
|
|
result.success++;
|
|
result.details.push({ profileId, status: "success" });
|
|
} else {
|
|
result.failed++;
|
|
result.details.push({
|
|
profileId,
|
|
status: "failed",
|
|
error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID",
|
|
});
|
|
}
|
|
} catch (error: any) {
|
|
result.failed++;
|
|
// เพิ่ม validation ก่อน push เพื่อป้องกัน crash จาก invalid data
|
|
const errorMessage = error?.message || String(error);
|
|
result.details.push({
|
|
profileId,
|
|
status: "failed",
|
|
error: errorMessage.substring(0, 500) // จำกัดความยาว
|
|
});
|
|
|
|
// Log error สำหรับ monitoring
|
|
console.error(`[KeycloakSync] Failed to sync profile ${profileId}:`, error);
|
|
}
|
|
}
|
|
|
|
return new HttpSuccess({
|
|
message: "Batch sync เสร็จสิ้น",
|
|
...result,
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Large Loop
|
|
|
|
**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:219-364) - `UploadFileSqlOfficer()` method
|
|
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
@Post("uploadProfile-Officer")
|
|
async UploadFileSqlOfficer(@Request() request: { user: Record<string, any> }) {
|
|
const OFFICER = await this.OFFICERRepo.find();
|
|
let rowCount = 0;
|
|
// ...
|
|
for await (const item of OFFICER) {
|
|
rowCount++;
|
|
// ... การประมวลผลข้อมูล ...
|
|
await this.profileRepo.save(profile);
|
|
}
|
|
return new HttpSuccess();
|
|
}
|
|
```
|
|
|
|
**ปัญหาที่พบ:**
|
|
1. **ไม่มี try-catch รอบ loop** - หากเกิด error ระหว่างการประมวลผล เช่น:
|
|
- Database connection lost
|
|
- Invalid data format
|
|
- Constraint violation
|
|
- Memory overflow
|
|
|
|
จะทำให้เกิด Unhandled Exception และ **Process Crash**
|
|
|
|
2. **ไม่มี Error Recovery** - หากเกิด error ที่ record ใด record หนึ่ง ทั้งกระบวนการจะหยุดทันที และไม่มีการ rollback หรือ cleanup
|
|
|
|
3. **Loading all data at once** - `await this.OFFICERRepo.find()` โหลดข้อมูลทั้งหมดเข้า memory อาจทำให้เกิด Out of Memory
|
|
|
|
4. **No transaction management** - แต่ละรอบบันทึกแยกกัน หากเกิด error ข้อมูลบางส่วนอาจถูกบันทึกแล้วบางส่วนไม่ได้
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
@Post("uploadProfile-Officer")
|
|
async UploadFileSqlOfficer(@Request() request: { user: Record<string, any> }) {
|
|
const queryRunner = AppDataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
let rowCount = 0;
|
|
let successCount = 0;
|
|
let failedCount = 0;
|
|
const errors: Array<{row: number, citizenId: string, error: string}> = [];
|
|
|
|
try {
|
|
// ใช้ pagination แทนการโหลดทั้งหมด
|
|
const BATCH_SIZE = 500;
|
|
let offset = 0;
|
|
let hasMore = true;
|
|
|
|
while (hasMore) {
|
|
const OFFICER = await queryRunner.manager.find(OFFICER, {
|
|
take: BATCH_SIZE,
|
|
skip: offset,
|
|
order: { id: 'ASC' }
|
|
});
|
|
|
|
if (OFFICER.length === 0) {
|
|
hasMore = false;
|
|
break;
|
|
}
|
|
|
|
for (const item of OFFICER) {
|
|
rowCount++;
|
|
|
|
try {
|
|
let type_: any = null;
|
|
let level_: any = null;
|
|
const profile = new Profile();
|
|
|
|
const existingProfile = await queryRunner.manager.findOne(Profile, {
|
|
where: { citizenId: item.CIT.toString() },
|
|
});
|
|
|
|
if (existingProfile) {
|
|
// ข้ามกรณีมีข้อมูลอยู่แล้ว
|
|
continue;
|
|
}
|
|
|
|
// ... การประมวลผลข้อมูลเดิม ...
|
|
|
|
// ใช้ queryRunner.manager.save แทน this.profileRepo.save
|
|
await queryRunner.manager.save(profile);
|
|
successCount++;
|
|
|
|
} catch (itemError: any) {
|
|
failedCount++;
|
|
errors.push({
|
|
row: rowCount,
|
|
citizenId: item.CIT?.toString() || 'unknown',
|
|
error: itemError?.message || String(itemError)
|
|
});
|
|
// Log แต่ไม่หยุดการทำงาน
|
|
console.error(`[UploadOfficer] Error at row ${rowCount}:`, itemError);
|
|
}
|
|
}
|
|
|
|
offset += BATCH_SIZE;
|
|
|
|
// Commit ทุกๆ batch เพื่อป้องกัน transaction ใหญ่เกินไป
|
|
await queryRunner.commitTransaction();
|
|
await queryRunner.startTransaction();
|
|
}
|
|
|
|
// Commit transaction สุดท้าย
|
|
await queryRunner.commitTransaction();
|
|
|
|
return new HttpSuccess({
|
|
message: "อัปโหลดข้อมูลเสร็จสิ้น",
|
|
total: rowCount,
|
|
success: successCount,
|
|
failed: failedCount,
|
|
errors: errors.slice(0, 100) // ส่งเฉพาะ 100 errors แรก
|
|
});
|
|
|
|
} catch (error: any) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw new HttpError(
|
|
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
`ไม่สามารถอัปโหลดข้อมูลได้: ${error?.message || 'Unknown error'}`
|
|
);
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Employee Upload
|
|
|
|
**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:369-496) - `UploadFileSQL()` method
|
|
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
เหมือนกับปัญหาข้างต้น แต่สำหรับการอัปโหลดข้อมูลลูกจ้างประจำ มีความเสี่ยงเช่นเดียวกัน:
|
|
- ไม่มี try-catch ใน loop
|
|
- ไม่มี transaction management
|
|
- ไม่มี error recovery
|
|
|
|
**Recommended Fix:**
|
|
ใช้ pattern เดียวกับข้อ 2 โดยใช้ QueryRunner สำหรับ transaction management
|
|
|
|
---
|
|
|
|
### 4. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Temp Employee Upload
|
|
|
|
**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:501-633) - `UploadFileSQLTemp()` method
|
|
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
if (item.CIT.toString() == "1101801164891") {
|
|
continue;
|
|
}
|
|
const existingProfile = await this.profileEmpRepo.findOne({
|
|
where: { employeeClass: "TEMP", citizenId: item.CIT.toString() },
|
|
});
|
|
if (existingProfile) {
|
|
profile.id = existingProfile.id;
|
|
} else {
|
|
continue;
|
|
}
|
|
```
|
|
|
|
**ปัญหาเพิ่มเติม:**
|
|
1. **Hardcoded citizenId check** - มีการ hardcode เงื่อนไข `item.CIT.toString() == "1101801164891"` ซึ่งอาจเป็น bug หรือ test code ที่ลืมลบ
|
|
2. **การ skip ที่ไม่ชัดเจน** - หากไม่พบ existingProfile จะ continue ทันที ทำให้ไม่สร้าง profile ใหม่
|
|
3. **ไม่มี error handling** เหมือนปัญหาก่อนหน้า
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
@Post("uploadProfile-EmployeeTemp")
|
|
async UploadFileSQLTemp(@Request() request: { user: Record<string, any> }) {
|
|
const queryRunner = AppDataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
let rowCount = 0;
|
|
let successCount = 0;
|
|
let failedCount = 0;
|
|
const errors: Array<{row: number, citizenId: string, error: string}> = [];
|
|
|
|
try {
|
|
const BATCH_SIZE = 500;
|
|
let offset = 0;
|
|
let hasMore = true;
|
|
|
|
while (hasMore) {
|
|
const EMPLOYEE = await queryRunner.manager.find(EMPLOYEETEMP, {
|
|
take: BATCH_SIZE,
|
|
skip: offset,
|
|
order: { id: 'ASC' }
|
|
});
|
|
|
|
if (EMPLOYEE.length === 0) {
|
|
hasMore = false;
|
|
break;
|
|
}
|
|
|
|
for (const item of EMPLOYEE) {
|
|
rowCount++;
|
|
|
|
try {
|
|
// เอา hardcode check ออก หรือเปลี่ยนเป็น configurable
|
|
// if (item.CIT.toString() === "1101801164891") {
|
|
// continue;
|
|
// }
|
|
|
|
const existingProfile = await queryRunner.manager.findOne(ProfileEmployee, {
|
|
where: {
|
|
employeeClass: "TEMP",
|
|
citizenId: item.CIT.toString()
|
|
},
|
|
});
|
|
|
|
let profile: ProfileEmployee;
|
|
|
|
if (existingProfile) {
|
|
profile = existingProfile;
|
|
} else {
|
|
// สร้าง profile ใหม่ถ้าไม่พบ
|
|
profile = new ProfileEmployee();
|
|
profile.employeeClass = "TEMP";
|
|
}
|
|
|
|
// ... การประมวลผลข้อมูลเดิม ...
|
|
|
|
await queryRunner.manager.save(profile);
|
|
successCount++;
|
|
|
|
} catch (itemError: any) {
|
|
failedCount++;
|
|
errors.push({
|
|
row: rowCount,
|
|
citizenId: item.CIT?.toString() || 'unknown',
|
|
error: itemError?.message || String(itemError)
|
|
});
|
|
console.error(`[UploadEmployeeTemp] Error at row ${rowCount}:`, itemError);
|
|
}
|
|
}
|
|
|
|
offset += BATCH_SIZE;
|
|
await queryRunner.commitTransaction();
|
|
await queryRunner.startTransaction();
|
|
}
|
|
|
|
await queryRunner.commitTransaction();
|
|
|
|
return new HttpSuccess({
|
|
message: "อัปโหลดข้อมูลลูกจ้างชั่วคราวเสร็จสิ้น",
|
|
total: rowCount,
|
|
success: successCount,
|
|
failed: failedCount,
|
|
errors: errors.slice(0, 100)
|
|
});
|
|
|
|
} catch (error: any) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw new HttpError(
|
|
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
`ไม่สามารถอัปโหลดข้อมูลลูกจ้างชั่วคราวได้: ${error?.message || 'Unknown error'}`
|
|
);
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5. 🟡 HIGH - ExRetirementController.ts - Unhandled External API Error
|
|
|
|
**File & Location:** [ExRetirementController.ts](src/controllers/ExRetirementController.ts:148-173) - `getToken()` function
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
|
|
// ...
|
|
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 });
|
|
}
|
|
}
|
|
```
|
|
|
|
**ปัญหา:**
|
|
1. **Generic error handling** - Error ที่ return มาเป็น object ธรรมดา ไม่ใช่ Error instance ทำให้การ stack trace หายไป
|
|
2. **ไม่มี retry logic** - หาก external API ล้ม ชั่วคราว จะไม่มีการ retry อัตโนมัติ
|
|
3. **No timeout** - หาก external API ไม่ตอบสนอง จะทำให้ request ค้างไปตลอด
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
|
|
const cacheKey = `${ClientID}:${ClientSecret}`;
|
|
|
|
const cachedToken = TokenCache.get(cacheKey);
|
|
if (cachedToken) {
|
|
return cachedToken;
|
|
}
|
|
|
|
const MAX_RETRIES = 3;
|
|
const TIMEOUT = 10000; // 10 วินาที
|
|
let lastError: any;
|
|
|
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
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: TIMEOUT,
|
|
});
|
|
|
|
const token = res.data.token;
|
|
if (!token) {
|
|
throw new Error('Token not found in response');
|
|
}
|
|
|
|
TokenCache.set(cacheKey, token);
|
|
return token;
|
|
|
|
} catch (error: any) {
|
|
lastError = error;
|
|
|
|
// ไม่ retry หากเป็น client error (4xx)
|
|
if (error.response?.status >= 400 && error.response?.status < 500) {
|
|
break;
|
|
}
|
|
|
|
// Retry หากเป็น server error หรือ network error
|
|
if (attempt < MAX_RETRIES) {
|
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log error สำหรับ monitoring
|
|
console.error(`[ExRetirement] Failed to get token after ${MAX_RETRIES} attempts:`, lastError);
|
|
|
|
throw new Error(`ไม่สามารถขอ Token ได้: ${lastError?.message || 'Unknown error'}`);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6. 🟡 HIGH - ApiWebServiceController.ts - Potential Null Reference
|
|
|
|
**File & Location:** [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts:67-78) - `listAttribute()` method
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
if (system == "organization") {
|
|
tbMain = "OrgRoot";
|
|
const revision = await this.orgRevisionRepository.findOne({
|
|
select: ["id"],
|
|
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
|
});
|
|
condition = `OrgRoot.orgRevisionId = "${revision?.id}"`;
|
|
}
|
|
```
|
|
|
|
**ปัญหา:**
|
|
1. **revision อาจเป็น null** - หากไม่พบ revision ที่ตรงตามเงื่อนไข `revision?.id` จะเป็น `undefined`
|
|
2. **SQL Injection vulnerability** - การใส่ค่าโดยตรงเข้าไปใน condition string อาจทำให้เกิด SQL injection หรือ syntax error
|
|
3. **ไม่มี error handling** - หาก query ล้มเพราะ invalid condition จะทำให้เกิด unhandled exception
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
@Get("/:system/:code")
|
|
async listAttribute(
|
|
@Request() request: RequestWithUserWebService,
|
|
@Path("system")
|
|
system: SystemCode,
|
|
@Path("code") code: string,
|
|
@Query("page") page: number = 1,
|
|
@Query("pageSize") pageSize: number = 100,
|
|
): Promise<HttpSuccess | HttpError> {
|
|
try {
|
|
const apiName = await this.apiNameRepository.findOne({
|
|
where: { code },
|
|
select: ["id", "code", "methodApi", "system", "isActive"],
|
|
relations: ["apiAttributes"],
|
|
order: {
|
|
apiAttributes: {
|
|
ordering: "ASC",
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!apiName || apiName.system != system || !apiName.isActive || apiName.methodApi != "GET") {
|
|
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบ API ที่ระบุ");
|
|
}
|
|
|
|
await isPermissionRequest(request, apiName.id);
|
|
|
|
const offset = (page - 1) * pageSize;
|
|
const propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`);
|
|
|
|
let tbMain: string = "";
|
|
let condition: string = "1=1";
|
|
let revisionId: string | null = null;
|
|
|
|
if (system == "registry") {
|
|
tbMain = "Profile";
|
|
} else if (system == "registry_emp") {
|
|
tbMain = "ProfileEmployee";
|
|
condition = `ProfileEmployee.employeeClass = "PERM"`;
|
|
} else if (system == "registry_temp") {
|
|
tbMain = "ProfileEmployee";
|
|
condition = `ProfileEmployee.employeeClass = "TEMP"`;
|
|
} else if (system == "organization") {
|
|
tbMain = "OrgRoot";
|
|
const revision = await this.orgRevisionRepository.findOne({
|
|
select: ["id"],
|
|
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
|
});
|
|
|
|
if (!revision) {
|
|
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน");
|
|
}
|
|
|
|
revisionId = revision.id;
|
|
condition = `OrgRoot.orgRevisionId = :revisionId`;
|
|
} else if (system == "position") {
|
|
tbMain = "PosMaster";
|
|
const revision = await this.orgRevisionRepository.findOne({
|
|
select: ["id"],
|
|
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
|
});
|
|
|
|
if (!revision) {
|
|
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน");
|
|
}
|
|
|
|
revisionId = revision.id;
|
|
condition = `PosMaster.orgRevisionId = :revisionId`;
|
|
}
|
|
|
|
const repo = AppDataSource.getRepository(tbMain);
|
|
const metadata = repo.metadata;
|
|
|
|
const relationMap: Record<string, string> = {};
|
|
metadata.relations.forEach((rel) => {
|
|
relationMap[rel.inverseEntityMetadata.name] = rel.propertyName;
|
|
});
|
|
|
|
let propertyOtherKey: any[] = [];
|
|
propertyOtherKey = [
|
|
...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)),
|
|
];
|
|
|
|
const queryBuilder = repo.createQueryBuilder(tbMain);
|
|
|
|
if (propertyOtherKey.length > 0) {
|
|
propertyOtherKey.forEach((tb) => {
|
|
const relationName = relationMap[tb];
|
|
if (relationName) {
|
|
queryBuilder.leftJoin(
|
|
`${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`,
|
|
tb,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
let pk: string = "";
|
|
const primaryColumns = metadata.primaryColumns;
|
|
primaryColumns.forEach((col) => {
|
|
pk = col.propertyName;
|
|
if (!propertyKey.includes(`${tbMain}.${pk}`)) {
|
|
propertyKey.push(`${tbMain}.${pk}`);
|
|
}
|
|
});
|
|
|
|
// ใช้ parameterized query แทน string interpolation
|
|
const queryParams: any = {};
|
|
if (revisionId) {
|
|
queryParams.revisionId = revisionId;
|
|
}
|
|
|
|
const [items, total] = await queryBuilder
|
|
.select(propertyKey)
|
|
.where(condition, queryParams)
|
|
.orderBy(propertyKey[0], "ASC")
|
|
.skip(offset)
|
|
.take(pageSize)
|
|
.getManyAndCount();
|
|
|
|
const data = items.map((item) => {
|
|
const { [pk]: removedPk, ...x } = item;
|
|
return x;
|
|
});
|
|
|
|
// save api history after query success
|
|
const history = {
|
|
headerApi: JSON.stringify({
|
|
host: request.headers.host,
|
|
"x-api-key": request.headers["x-api-key"],
|
|
connection: request.headers.connection,
|
|
accept: request.headers.accept,
|
|
}),
|
|
tokenApi: Array.isArray(request.headers["x-api-key"])
|
|
? request.headers["x-api-key"][0] || ""
|
|
: request.headers["x-api-key"] || "",
|
|
requestApi: `${request.method} ${request.protocol}://${request.headers.host}${request.originalUrl || request.url}`,
|
|
responseApi: "OK",
|
|
ipApi: request.ip,
|
|
codeApi: code,
|
|
apiKeyId: request.user.id,
|
|
apiNameId: apiName.id,
|
|
createdFullName: request.user.name,
|
|
lastUpdateFullName: request.user.name,
|
|
};
|
|
|
|
try {
|
|
await this.apiHistoryRepository.save(history);
|
|
} catch (historyError) {
|
|
// Log แต่ไม่ให้กระทบต่อ response
|
|
console.error('[ApiWebService] Failed to save history:', historyError);
|
|
}
|
|
|
|
return new HttpSuccess({ data: data, total });
|
|
|
|
} catch (error: any) {
|
|
if (error instanceof HttpError) {
|
|
throw error;
|
|
}
|
|
throw new HttpError(
|
|
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
`เกิดข้อผิดพลาด: ${error?.message || 'Unknown error'}`
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 7. 🟡 MEDIUM - ApiManageController.ts - Missing Transaction Error Handling
|
|
|
|
**File & Location:** [ApiManageController.ts](src/controllers/ApiManageController.ts:464-518) - `createApi()` method
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
@Post("")
|
|
async createApi(
|
|
@Request() req: RequestWithUser,
|
|
@Body() apiData: CreateApi,
|
|
): Promise<HttpSuccess | HttpError> {
|
|
const queryRunner = AppDataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
this.validateSuperAdminRole(req.user);
|
|
// ...
|
|
await queryRunner.commitTransaction();
|
|
return new HttpSuccess(apiName.id);
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw new HttpError(...);
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
```
|
|
|
|
**ปัญหา:**
|
|
1. **validateSuperAdminRole อยู่นอก try-catch** - หาก function นี้ throw error จะทำให้ queryRunner ไม่ถูก release และเกิด connection leak
|
|
2. **ไม่ validate req.user** ก่อนเรียก `validateSuperAdminRole` - หาก `req.user` เป็น null หรือ undefined จะเกิด error
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
@Post("")
|
|
async createApi(
|
|
@Request() req: RequestWithUser,
|
|
@Body() apiData: CreateApi,
|
|
): Promise<HttpSuccess | HttpError> {
|
|
const queryRunner = AppDataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
// Validate request ก่อน
|
|
if (!req.user) {
|
|
throw new HttpError(HttpStatusCode.UNAUTHORIZED, "ไม่พบข้อมูลผู้ใช้");
|
|
}
|
|
|
|
this.validateSuperAdminRole(req.user);
|
|
|
|
const code = this.generateApiCode();
|
|
const postData = {
|
|
name: apiData.name,
|
|
code,
|
|
pathApi: this.createApiPath(apiData.system as SystemCode, code),
|
|
methodApi: apiData.methodApi || "GET",
|
|
system: apiData.system || "registry",
|
|
isActive: apiData.isActive || false,
|
|
createdUserId: req.user?.sub,
|
|
createdFullName: req.user?.name || "",
|
|
};
|
|
|
|
const apiName = await queryRunner.manager.getRepository(ApiName).save(postData);
|
|
|
|
if (apiData.apiAttributes?.length) {
|
|
let orderingCounter = 0;
|
|
const attributesToSave = apiData.apiAttributes.flatMap((attr) =>
|
|
attr.propertyKey.map((propertyKey) => ({
|
|
apiNameId: apiName.id,
|
|
tbName: attr.tbName,
|
|
propertyKey,
|
|
ordering: orderingCounter++,
|
|
createdUserId: req.user?.sub,
|
|
createdFullName: req.user?.name || "",
|
|
})),
|
|
);
|
|
|
|
await queryRunner.manager.getRepository(ApiAttribute).save(attributesToSave);
|
|
}
|
|
|
|
await queryRunner.commitTransaction();
|
|
return new HttpSuccess(apiName.id);
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
|
|
if (error instanceof HttpError) {
|
|
throw error;
|
|
}
|
|
|
|
throw new HttpError(
|
|
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
error instanceof Error ? error.message : "เกิดข้อผิดพลาด ไม่สามารถบันทึกข้อมูลได้ กรุณาลองใหม่ในภายหลัง",
|
|
);
|
|
} finally {
|
|
// Ensure release is called even if rollback fails
|
|
try {
|
|
await queryRunner.release();
|
|
} catch (releaseError) {
|
|
console.error('[ApiManage] Failed to release queryRunner:', releaseError);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 8. 🟡 MEDIUM - DevelopmentRequestController.ts - Unhandled Promise in Parallel Operations
|
|
|
|
**File & Location:** [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts:349-365) - `newDevelopmentRequest()` method
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
if (body.developmentProjects != null) {
|
|
await Promise.all(
|
|
body.developmentProjects.map(async (x) => {
|
|
let developmentProject = new DevelopmentProject();
|
|
developmentProject.name = x;
|
|
developmentProject.createdUserId = req.user.sub;
|
|
developmentProject.createdFullName = req.user.name;
|
|
developmentProject.lastUpdateUserId = req.user.sub;
|
|
developmentProject.lastUpdateFullName = req.user.name;
|
|
developmentProject.createdAt = new Date();
|
|
developmentProject.lastUpdateUpdatedAt = new Date();
|
|
developmentProject.developmentRequestId = data.id;
|
|
await this.developmentProjectRepository.save(developmentProject, { data: req });
|
|
setLogDataDiff(req, { before, after: developmentProject });
|
|
}),
|
|
);
|
|
}
|
|
```
|
|
|
|
**ปัญหา:**
|
|
1. **Unhandled Promise rejection** - หาก `save` หรือ `setLogDataDiff` ล้ม จะเกิด unhandled rejection
|
|
2. **ไม่มี error handling รายตัว** - หาก project หนึ่งล้ม ทั้ง batch จะล้ม
|
|
3. **No cleanup on partial failure** - หาก save บางส่วนสำเร็จแล้วล้ม จะมีข้อมูล partial อยู่ใน database
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
if (body.developmentProjects != null) {
|
|
const savedProjects: DevelopmentProject[] = [];
|
|
|
|
try {
|
|
for (const projectName of body.developmentProjects) {
|
|
try {
|
|
let developmentProject = new DevelopmentProject();
|
|
developmentProject.name = projectName;
|
|
developmentProject.createdUserId = req.user.sub;
|
|
developmentProject.createdFullName = req.user.name;
|
|
developmentProject.lastUpdateUserId = req.user.sub;
|
|
developmentProject.lastUpdateFullName = req.user.name;
|
|
developmentProject.createdAt = new Date();
|
|
developmentProject.lastUpdateUpdatedAt = new Date();
|
|
developmentProject.developmentRequestId = data.id;
|
|
|
|
const saved = await this.developmentProjectRepository.save(developmentProject, { data: req });
|
|
savedProjects.push(saved);
|
|
|
|
setLogDataDiff(req, { before: null, after: saved });
|
|
} catch (projectError: any) {
|
|
console.error(`[DevelopmentRequest] Failed to save project ${projectName}:`, projectError);
|
|
// Continue with next project instead of failing entire request
|
|
}
|
|
}
|
|
} catch (batchError: any) {
|
|
console.error('[DevelopmentRequest] Error in projects batch:', batchError);
|
|
// Clean up any successfully saved projects if needed
|
|
if (savedProjects.length > 0) {
|
|
try {
|
|
await this.developmentProjectRepository.delete({
|
|
developmentRequestId: data.id
|
|
});
|
|
} catch (cleanupError) {
|
|
console.error('[DevelopmentRequest] Failed to cleanup projects:', cleanupError);
|
|
}
|
|
}
|
|
throw batchError;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 9. 🟢 LOW - SocketController.ts - No Error Handling
|
|
|
|
**File & Location:** [SocketController.ts](src/controllers/SocketController.ts:6-24) - `notify()` method
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
@Post("notify")
|
|
async notify(
|
|
@Body()
|
|
payload: {
|
|
message: string;
|
|
userId?: string | string[];
|
|
roles?: string | string[];
|
|
error?: boolean;
|
|
},
|
|
) {
|
|
sendWebSocket(
|
|
"socket-notification",
|
|
{ success: !payload.error, message: payload.message },
|
|
{
|
|
roles: payload.roles || [],
|
|
userId: payload.userId || [],
|
|
},
|
|
);
|
|
}
|
|
```
|
|
|
|
**ปัญหา:**
|
|
1. **ไม่มี try-catch** - หาก `sendWebSocket` throw error จะเป็น unhandled exception
|
|
2. **ไม่ return ค่า** - ไม่มีการ return HttpSuccess หรือ error response
|
|
3. **No validation** - ไม่ validate payload ก่อนใช้งาน
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
@Post("notify")
|
|
async notify(
|
|
@Body()
|
|
payload: {
|
|
message: string;
|
|
userId?: string | string[];
|
|
roles?: string | string[];
|
|
error?: boolean;
|
|
},
|
|
) {
|
|
try {
|
|
// Validate payload
|
|
if (!payload.message || typeof payload.message !== 'string') {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "message ต้องเป็น string ที่ไม่ว่างเปล่า");
|
|
}
|
|
|
|
// Validate userId and roles
|
|
if (payload.userId && !Array.isArray(payload.userId) && typeof payload.userId !== 'string') {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "userId ต้องเป็น string หรือ array of strings");
|
|
}
|
|
|
|
if (payload.roles && !Array.isArray(payload.roles) && typeof payload.roles !== 'string') {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "roles ต้องเป็น string หรือ array of strings");
|
|
}
|
|
|
|
sendWebSocket(
|
|
"socket-notification",
|
|
{ success: !payload.error, message: payload.message },
|
|
{
|
|
roles: payload.roles || [],
|
|
userId: payload.userId || [],
|
|
},
|
|
);
|
|
|
|
return new HttpSuccess({
|
|
message: "ส่งการแจ้งเตือนสำเร็จ",
|
|
notification: {
|
|
type: "socket-notification",
|
|
success: !payload.error,
|
|
message: payload.message,
|
|
roles: payload.roles || [],
|
|
userId: payload.userId || [],
|
|
}
|
|
});
|
|
} catch (error: any) {
|
|
if (error instanceof HttpError) {
|
|
throw error;
|
|
}
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
`ไม่สามารถส่งการแจ้งเตือนได้: ${error?.message || 'Unknown error'}`
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 10. 🟢 LOW - IssuesController.ts - Missing Try-Catch
|
|
|
|
**File & Location:** [IssuesController.ts](src/controllers/IssuesController.ts:31-39) - `getIssues()` method
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
```typescript
|
|
@Get("lists")
|
|
async getIssues() {
|
|
const issues = await this.issuesRepository.find({
|
|
order: {
|
|
createdAt: "DESC",
|
|
},
|
|
});
|
|
return new HttpSuccess(issues);
|
|
}
|
|
```
|
|
|
|
**ปัญหา:**
|
|
- ไม่มี try-catch หาก database connection ล้มหรือ query มีปัญหา จะเกิด unhandled exception
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
@Get("lists")
|
|
async getIssues() {
|
|
try {
|
|
const issues = await this.issuesRepository.find({
|
|
order: {
|
|
createdAt: "DESC",
|
|
},
|
|
});
|
|
return new HttpSuccess(issues);
|
|
} catch (error: any) {
|
|
throw new HttpError(
|
|
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
`ไม่สามารถดึงรายการปัญหาได้: ${error?.message || 'Unknown error'}`
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## สรุปสถิติ
|
|
|
|
| ระดับความรุนแรง | จำนวน | รายการ |
|
|
|---|---|---|
|
|
| 🔴 CRITICAL | 4 | 1, 2, 3, 4 |
|
|
| 🟡 HIGH | 2 | 5, 6 |
|
|
| 🟡 MEDIUM | 2 | 7, 8 |
|
|
| 🟢 LOW | 2 | 9, 10 |
|
|
|
|
---
|
|
|
|
## คำแนะนำการจัดลำดับการแก้ไข
|
|
|
|
### แก้ไขทันที (P0 - Critical)
|
|
1. **ImportDataController.ts** - ทั้ง 3 methods (`UploadFileSqlOfficer`, `UploadFileSQL`, `UploadFileSQLTemp`)
|
|
- เพิ่ม transaction management
|
|
- เพิ่ม try-catch ใน loops
|
|
- เพิ่ม pagination แทนการโหลดทั้งหมด
|
|
|
|
### แก้ไขเร็วๆ นี้ (P1 - High)
|
|
2. **KeycloakSyncController.ts** - เพิ่ม timeout protection
|
|
3. **ExRetirementController.ts** - ปรับปรุง error handling และ retry logic
|
|
4. **ApiWebServiceController.ts** - แก้ null reference issue
|
|
|
|
### แก้ไขในภายหลัง (P2 - Medium)
|
|
5. **ApiManageController.ts** - ปรับปรุง transaction error handling
|
|
6. **DevelopmentRequestController.ts** - เพิ่ม error handling สำหรับ parallel operations
|
|
|
|
### แก้ไขเมื่อว่าง (P3 - Low)
|
|
7. **SocketController.ts** - เพิ่ม validation และ error handling
|
|
8. **IssuesController.ts** - เพิ่ม try-catch
|
|
|
|
---
|
|
|
|
## ข้อเสนอแนะเพิ่มเติม
|
|
|
|
1. **ใช้ Global Error Handler** - ให้พิจารณาใช้ TSOA's middleware หรือ NestJS interceptor สำหรับ centralized error handling
|
|
2. **เพิ่ม Health Check** - สำหรับ endpoints ที่เชื่อมต่อกับ external services (Keycloak, ExProfile API)
|
|
3. **Circuit Breaker Pattern** - สำหรับการเรียก external API เพื่อป้องกัน cascade failures
|
|
4. **Graceful Shutdown** - ให้แน่ใจว่า long-running operations สามารถยกเลิกได้อย่างปลอดภัยเมื่อ server shutdown
|
|
5. **Logging Strategy** - เพิ่ม structured logging สำหรับ monitoring และ debugging
|
|
|
|
---
|
|
|
|
## ไฟล์รายงานที่เกี่ยวข้อง
|
|
|
|
- [Batch 1-10 Analysis](../reports/) - รายงานการตรวจสอบ Controllers ชุดก่อนหน้า
|
|
- [Security Audit Report](../reports/security-audit.md) - รายงานการตรวจสอบด้านความปลอดภัย
|