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

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) - รายงานการตรวจสอบด้านความปลอดภัย