830 lines
27 KiB
Markdown
830 lines
27 KiB
Markdown
|
|
# รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 2 (ไฟล์ที่ 11-20)
|
||
|
|
|
||
|
|
**Project:** BMA EHR Organization Backend
|
||
|
|
**Framework:** TSOA + Express + TypeORM
|
||
|
|
**วันที่ตรวจสอบ:** 2026-05-08
|
||
|
|
**จำนวน Controllers:** 10 ไฟล์
|
||
|
|
**สถานะ:** เสร็จสิ้น
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## สรุปผลการตรวจสอบ
|
||
|
|
|
||
|
|
| ระดับความรุนแรง | จำนวนจุดเสี่ยง |
|
||
|
|
|---------------------|-------------------|
|
||
|
|
| **CRITICAL** | 1 |
|
||
|
|
| **HIGH** | 3 |
|
||
|
|
| **MEDIUM** | 4 |
|
||
|
|
| **LOW** | 2 |
|
||
|
|
| **BUG** | 2 |
|
||
|
|
| **รวมทั้งหมด** | 12 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Controllers ที่ตรวจสอบ
|
||
|
|
|
||
|
|
11. [CommandOperatorController.ts](src/controllers/CommandOperatorController.ts)
|
||
|
|
12. [CommandSalaryController.ts](src/controllers/CommandSalaryController.ts)
|
||
|
|
13. [CommandSysController.ts](src/controllers/CommandSysController.ts)
|
||
|
|
14. [CommandTypeController.ts](src/controllers/CommandTypeController.ts)
|
||
|
|
15. [DPISController.ts](src/controllers/DPISController.ts)
|
||
|
|
16. [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts)
|
||
|
|
17. [DistrictController.ts](src/controllers/DistrictController.ts)
|
||
|
|
18. [EducationLevelController.ts](src/controllers/EducationLevelController.ts)
|
||
|
|
19. [EmployeePosLevelController.ts](src/controllers/EmployeePosLevelController.ts)
|
||
|
|
20. [EmployeePosTypeController.ts](src/controllers/EmployeePosTypeController.ts)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## รายละเอียดจุดเสี่ยงแต่ละจุด
|
||
|
|
|
||
|
|
### #1 - Transaction QueryRunner Not Released on Error (CRITICAL)
|
||
|
|
|
||
|
|
**File & Location:** [CommandOperatorController.ts:169-222](src/controllers/CommandOperatorController.ts#L169-L222)
|
||
|
|
**Method:** `deleteCommandOperator`
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- ใช้ QueryRunner และ Transaction แต่มี error handling ที่ไม่ปลอดภัย
|
||
|
|
- ถ้าเกิด error หลังจาก `throw error` ใน catch block แล้ว จะไม่ถึง `finally` block
|
||
|
|
- QueryRunner จะไม่ถูก release ทำให้ connection leak
|
||
|
|
- ในกรณีที่ HttpError ถูก throw ภายใน catch จะไม่มีการ release queryRunner
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
||
|
|
```typescript
|
||
|
|
const queryRunner = AppDataSource.createQueryRunner();
|
||
|
|
await queryRunner.connect();
|
||
|
|
await queryRunner.startTransaction();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// ... operations
|
||
|
|
await queryRunner.commitTransaction();
|
||
|
|
return new HttpSuccess(true);
|
||
|
|
} catch (error) {
|
||
|
|
await queryRunner.rollbackTransaction();
|
||
|
|
throw error; // ❌ ถ้า throw HttpError จะไม่ถึง finally
|
||
|
|
} finally {
|
||
|
|
await queryRunner.release(); // ❌ จะไม่ถูกเรียกถ้า throw error ใน catch
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
const queryRunner = AppDataSource.createQueryRunner();
|
||
|
|
try {
|
||
|
|
await queryRunner.connect();
|
||
|
|
await queryRunner.startTransaction();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 1. หา operator
|
||
|
|
const operator = await queryRunner.manager.findOne(CommandOperator, {
|
||
|
|
where: {
|
||
|
|
id: operatorId,
|
||
|
|
commandId: commandId,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!operator) {
|
||
|
|
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบเจ้าหน้าที่ดำเนินการ");
|
||
|
|
}
|
||
|
|
|
||
|
|
const removedOrderNo = operator.orderNo;
|
||
|
|
|
||
|
|
// 3. ลบ
|
||
|
|
await queryRunner.manager.remove(operator);
|
||
|
|
|
||
|
|
// 4. re orderNumber ตัวที่เหลือ
|
||
|
|
await queryRunner.manager
|
||
|
|
.createQueryBuilder()
|
||
|
|
.update(CommandOperator)
|
||
|
|
.set({
|
||
|
|
orderNo: () => "orderNo - 1",
|
||
|
|
})
|
||
|
|
.where("commandId = :commandId", { commandId })
|
||
|
|
.andWhere("orderNo > :removedOrderNo", { removedOrderNo })
|
||
|
|
.execute();
|
||
|
|
|
||
|
|
await queryRunner.commitTransaction();
|
||
|
|
return new HttpSuccess(true);
|
||
|
|
} catch (error) {
|
||
|
|
await queryRunner.rollbackTransaction();
|
||
|
|
// Re-throw after rollback
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
// ✅ ใช้ finally block ระดับนอกสุดเพื่อให้แน่ใจว่าจะถูกเรียกเสมอ
|
||
|
|
if (queryRunner.isReleased) {
|
||
|
|
// Already released, skip
|
||
|
|
} else {
|
||
|
|
await queryRunner.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #2 - Promise.all Without Error Handling in Development Request (HIGH)
|
||
|
|
|
||
|
|
**File & Location:** [DevelopmentRequestController.ts:349-364](src/controllers/DevelopmentRequestController.ts#L349-L364)
|
||
|
|
**Method:** `newDevelopmentRequest`
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- `Promise.all()` กับการบันทึก development projects หลายรายการ
|
||
|
|
- ถ้ามี project ไหน save ไม่สำเร็จ จะทำให้ unhandled rejection
|
||
|
|
- ไม่มี try-catch รองรับ
|
||
|
|
- External API call ใช้ `.catch()` แต่ไม่ได้ throw error ต่อ
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
||
|
|
```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.lastUpdatedAt = new Date();
|
||
|
|
developmentProject.developmentRequestId = data.id;
|
||
|
|
await this.developmentProjectRepository.save(developmentProject, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: developmentProject });
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
await new CallAPI()
|
||
|
|
.PostData(req, "/org/workflow/add-workflow", {
|
||
|
|
refId: data.id,
|
||
|
|
sysName: "REGISTRY_IDP",
|
||
|
|
posLevelName: profile.posLevel.posLevelName,
|
||
|
|
posTypeName: profile.posType.posTypeName,
|
||
|
|
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
|
||
|
|
isDeputy: orgRoot?.isDeputy ?? false,
|
||
|
|
orgRootId: orgRoot?.id ?? null
|
||
|
|
})
|
||
|
|
.catch((error) => {
|
||
|
|
console.error("Error calling API:", error);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
if (body.developmentProjects != null) {
|
||
|
|
try {
|
||
|
|
await Promise.all(
|
||
|
|
body.developmentProjects.map(async (x) => {
|
||
|
|
try {
|
||
|
|
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.lastUpdatedAt = new Date();
|
||
|
|
developmentProject.developmentRequestId = data.id;
|
||
|
|
await this.developmentProjectRepository.save(developmentProject, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: developmentProject });
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Failed to save development project "${x}":`, error);
|
||
|
|
throw error; // Re-throw to be caught by Promise.all
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to save some development projects:", error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
"Failed to save development projects"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Call external API with proper error handling
|
||
|
|
try {
|
||
|
|
await new CallAPI().PostData(req, "/org/workflow/add-workflow", {
|
||
|
|
refId: data.id,
|
||
|
|
sysName: "REGISTRY_IDP",
|
||
|
|
posLevelName: profile.posLevel.posLevelName,
|
||
|
|
posTypeName: profile.posType.posTypeName,
|
||
|
|
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
|
||
|
|
isDeputy: orgRoot?.isDeputy ?? false,
|
||
|
|
orgRootId: orgRoot?.id ?? null
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to call workflow API:", error);
|
||
|
|
// Optionally mark the request as having workflow issues
|
||
|
|
// But don't fail the entire request
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #3 - QueryBuilder with Dynamic Conditions Without Error Handling (HIGH)
|
||
|
|
|
||
|
|
**File & Location:** [DevelopmentRequestController.ts:122-265](src/controllers/DevelopmentRequestController.ts#L122-L265)
|
||
|
|
**Method:** `getDevelopmentRequestAdmin`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- Complex QueryBuilder พร้อม dynamic conditions หลายอย่าง
|
||
|
|
- ไม่มี try-catch รองรับ
|
||
|
|
- Permission check อาจ throw error
|
||
|
|
- Null reference risks หลายจุด (`orgRevisionPublish?.id`, `data.root`, etc.)
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
@Get("admin")
|
||
|
|
public async getDevelopmentRequestAdmin(
|
||
|
|
@Request() request: RequestWithUser,
|
||
|
|
@Query("status") status: string,
|
||
|
|
@Query("keyword") keyword: string = "",
|
||
|
|
@Query("page") page: number = 1,
|
||
|
|
@Query("pageSize") pageSize: number = 10,
|
||
|
|
@Query("sortBy") sortBy?: string,
|
||
|
|
@Query("descending") descending?: boolean,
|
||
|
|
) {
|
||
|
|
try {
|
||
|
|
// Validate inputs
|
||
|
|
if (page < 1) throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page number");
|
||
|
|
if (pageSize < 1 || pageSize > 1000) {
|
||
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page size");
|
||
|
|
}
|
||
|
|
|
||
|
|
let data = await new permission().PermissionOrgList(request, "SYS_REGISTRY_OFFICER");
|
||
|
|
|
||
|
|
const orgRevisionPublish = await this.orgRevisionRepository
|
||
|
|
.createQueryBuilder("orgRevision")
|
||
|
|
.where("orgRevision.orgRevisionIsDraft = false")
|
||
|
|
.andWhere("orgRevision.orgRevisionIsCurrent = true")
|
||
|
|
.getOne();
|
||
|
|
|
||
|
|
let query = await AppDataSource.getRepository(DevelopmentRequest)
|
||
|
|
.createQueryBuilder("developmentRequest")
|
||
|
|
.leftJoinAndSelect("developmentRequest.profile", "profile")
|
||
|
|
.leftJoinAndSelect("profile.current_holders", "current_holders")
|
||
|
|
.leftJoinAndSelect("current_holders.orgRevision", "orgRevision")
|
||
|
|
.andWhere(
|
||
|
|
status == undefined || status.trim().toUpperCase() == "ALL" || status == ""
|
||
|
|
? "1=1"
|
||
|
|
: "developmentRequest.status = :status",
|
||
|
|
{
|
||
|
|
status: status == undefined || status == null ? "" : status.trim().toUpperCase(),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
.andWhere(
|
||
|
|
orgRevisionPublish ? `current_holders.orgRevisionId = :revisionId` : "1=1",
|
||
|
|
{
|
||
|
|
revisionId: orgRevisionPublish?.id,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
// ... rest of the query
|
||
|
|
|
||
|
|
const [lists, total] = await query
|
||
|
|
.skip((page - 1) * pageSize)
|
||
|
|
.take(pageSize)
|
||
|
|
.getManyAndCount();
|
||
|
|
|
||
|
|
const _data = lists.map((item) => ({ ...item, profile: null }));
|
||
|
|
return new HttpSuccess({ data: _data, total });
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof HttpError) {
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
console.error('Failed to get development requests:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
"Failed to retrieve development requests"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #4 - Promise.all in Edit Development Request Without Error Handling (HIGH)
|
||
|
|
|
||
|
|
**File & Location:** [DevelopmentRequestController.ts:402-417](src/controllers/DevelopmentRequestController.ts#L402-L417)
|
||
|
|
**Method:** `editUserDevelopmentRequest`
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- ใช้ `Promise.all()` โดยไม่มี error handling
|
||
|
|
- Similar to #2 but in edit operation
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
||
|
|
```typescript
|
||
|
|
await this.developmentProjectRepository.delete({ developmentRequestId: record.id });
|
||
|
|
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.lastUpdatedAt = new Date();
|
||
|
|
developmentProject.developmentRequestId = record.id;
|
||
|
|
await this.developmentProjectRepository.save(developmentProject, { data: req });
|
||
|
|
setLogDataDiff(req, { before: null, after: record });
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
await this.developmentProjectRepository.delete({ developmentRequestId: record.id });
|
||
|
|
if (body.developmentProjects != null) {
|
||
|
|
try {
|
||
|
|
await Promise.all(
|
||
|
|
body.developmentProjects.map(async (x) => {
|
||
|
|
try {
|
||
|
|
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.lastUpdatedAt = new Date();
|
||
|
|
developmentProject.developmentRequestId = record.id;
|
||
|
|
await this.developmentProjectRepository.save(developmentProject, { data: req });
|
||
|
|
setLogDataDiff(req, { before: null, after: developmentProject });
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Failed to update development project "${x}":`, error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to update development projects:", error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
"Failed to update development projects"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #5 - Null Reference Risk in Profile Query (MEDIUM)
|
||
|
|
|
||
|
|
**File & Location:** [DPISController.ts:272-275](src/controllers/DPISController.ts#L272-L275)
|
||
|
|
**Method:** `GetProfileCitizenIdAsync`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- `findRevision` อาจเป็น null ถ้าไม่พบ current revision
|
||
|
|
- การใช้ `findRevision?.id` ใน `find()` operation จะเป็น undefined
|
||
|
|
- `current_holders?.find()` อาจ return undefined
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
||
|
|
```typescript
|
||
|
|
const findRevision = await this.orgRevisionRepo.findOne({
|
||
|
|
where: { orgRevisionIsCurrent: true },
|
||
|
|
});
|
||
|
|
var current_holder = profile.current_holders?.find((x) => x.orgRevisionId == findRevision?.id);
|
||
|
|
|
||
|
|
const mapProfile: DPISResult = {
|
||
|
|
// ...
|
||
|
|
organization: {
|
||
|
|
orgRootName: current_holder?.orgRoot?.orgRootName || "", // ❌ multiple levels of null checks
|
||
|
|
orgChild1Name: current_holder?.orgChild1?.orgChild1Name || "",
|
||
|
|
// ...
|
||
|
|
},
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
const findRevision = await this.orgRevisionRepo.findOne({
|
||
|
|
where: { orgRevisionIsCurrent: true },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!findRevision) {
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.NOT_FOUND,
|
||
|
|
"No current organization revision found"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
var current_holder = profile.current_holders?.find((x) => x.orgRevisionId == findRevision.id);
|
||
|
|
|
||
|
|
if (!current_holder) {
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.NOT_FOUND,
|
||
|
|
"No current organization assignment found for this profile"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const mapProfile: DPISResult = {
|
||
|
|
// ...
|
||
|
|
organization: {
|
||
|
|
orgRootName: current_holder.orgRoot?.orgRootName || "",
|
||
|
|
orgChild1Name: current_holder.orgChild1?.orgChild1Name || "",
|
||
|
|
orgChild2Name: current_holder.orgChild2?.orgChild2Name || "",
|
||
|
|
orgChild3Name: current_holder.orgChild3?.orgChild3Name || "",
|
||
|
|
orgChild4Name: current_holder.orgChild4?.orgChild4Name || "",
|
||
|
|
},
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #6 - Unsafe Optional Chain in OrgRoot Query (MEDIUM)
|
||
|
|
|
||
|
|
**File & Location:** [DevelopmentRequestController.ts:322-330](src/controllers/DevelopmentRequestController.ts#L322-L330)
|
||
|
|
**Method:** `newDevelopmentRequest`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- ใช้ optional chaining (`?.`) และ nullish coalescing ใน query
|
||
|
|
- `find()` อาจ return undefined และการใช้ `!` (non-null assertion) อาจทำให้ runtime error
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
||
|
|
```typescript
|
||
|
|
const orgRoot = await this.orgRootRepo.findOne({
|
||
|
|
select: {
|
||
|
|
id: true,
|
||
|
|
isDeputy: true
|
||
|
|
},
|
||
|
|
where: {
|
||
|
|
id: profile.current_holders.find(x => x.orgRootId)!.orgRootId ?? "" // ❌ unsafe
|
||
|
|
}
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
const currentHolder = profile.current_holders.find(x => x.orgRootId);
|
||
|
|
if (!currentHolder || !currentHolder.orgRootId) {
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.BAD_REQUEST,
|
||
|
|
"Profile must have a current organization assignment"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const orgRoot = await this.orgRootRepo.findOne({
|
||
|
|
select: {
|
||
|
|
id: true,
|
||
|
|
isDeputy: true
|
||
|
|
},
|
||
|
|
where: {
|
||
|
|
id: currentHolder.orgRootId
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!orgRoot) {
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.NOT_FOUND,
|
||
|
|
"Organization root not found"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #7 - Promise.all in Admin Edit Without Error Handling (MEDIUM)
|
||
|
|
|
||
|
|
**File & Location:** [DevelopmentRequestController.ts:467-490](src/controllers/DevelopmentRequestController.ts#L467-L490)
|
||
|
|
**Method:** `editAdminDevelopmentRequest`
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- `Promise.all()` กับ nested save operations
|
||
|
|
- ไม่มี error handling สำหรับ individual promises
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
if (record.developmentProjects != null) {
|
||
|
|
try {
|
||
|
|
await Promise.all(
|
||
|
|
record.developmentProjects.map(async (x) => {
|
||
|
|
try {
|
||
|
|
let developmentProject = new DevelopmentProject();
|
||
|
|
let developmentProjectHistory = new DevelopmentProject();
|
||
|
|
Object.assign(developmentProject, {
|
||
|
|
...meta,
|
||
|
|
id: undefined,
|
||
|
|
name: record.name,
|
||
|
|
profileDevelopmentId: profileDevelopment.id,
|
||
|
|
});
|
||
|
|
Object.assign(developmentProject, {
|
||
|
|
...meta,
|
||
|
|
id: undefined,
|
||
|
|
name: record.name,
|
||
|
|
profileDevelopmentHistoryId: history.id,
|
||
|
|
});
|
||
|
|
await Promise.all([
|
||
|
|
this.developmentProjectRepository.save(developmentProject, { data: req }),
|
||
|
|
setLogDataDiff(req, { before: null, after: developmentProject }),
|
||
|
|
this.developmentProjectRepository.save(developmentProjectHistory, { data: req }),
|
||
|
|
]);
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Failed to save development project for "${record.name}":`, error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to save development projects:", error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
"Failed to save development projects"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #8 - QueryBuilder Parameters Without Validation (MEDIUM)
|
||
|
|
|
||
|
|
**File & Location:** [CommandSalaryController.ts:73-108](src/controllers/CommandSalaryController.ts#L73-L108)
|
||
|
|
**Method:** `GetAdmin`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- QueryBuilder พร้อม dynamic conditions
|
||
|
|
- ไม่มี input validation
|
||
|
|
- Page number validation เป็น optional (มี default value แต่ไม่ validate range)
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
@Get("admin")
|
||
|
|
async GetAdmin(
|
||
|
|
@Query("page") page: number = 1,
|
||
|
|
@Query("pageSize") pageSize: number = 10,
|
||
|
|
@Query() commandSysId?: string | null,
|
||
|
|
@Query() isActive?: boolean | null,
|
||
|
|
@Query() searchKeyword?: string | null,
|
||
|
|
) {
|
||
|
|
try {
|
||
|
|
// Validate inputs
|
||
|
|
if (page < 1 || page > 10000) {
|
||
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page number");
|
||
|
|
}
|
||
|
|
if (pageSize < 1 || pageSize > 1000) {
|
||
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page size");
|
||
|
|
}
|
||
|
|
|
||
|
|
const [commandSalarys, total] = await this.commandSalaryRepository
|
||
|
|
.createQueryBuilder("commandSalary")
|
||
|
|
.andWhere(
|
||
|
|
isActive != null && isActive != undefined ? "commandSalary.isActive = :isActive" : "1=1",
|
||
|
|
{
|
||
|
|
isActive:
|
||
|
|
isActive == null || isActive == undefined ? null : `${isActive == true ? 1 : 0}`,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
// ... rest of query
|
||
|
|
.getManyAndCount();
|
||
|
|
return new HttpSuccess({ commandSalarys, total });
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof HttpError) {
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
console.error('Failed to get command salaries:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
"Failed to retrieve command salaries"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #9 - Hardcoded Response Data (LOW)
|
||
|
|
|
||
|
|
**File & Location:** [CommandTypeController.ts:140-199](src/controllers/CommandTypeController.ts#L140-L199)
|
||
|
|
**Method:** `GetById`
|
||
|
|
|
||
|
|
**Problem Type:** 3. Code Quality
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- Hardcoded template data ใน code
|
||
|
|
- ไม่ flexible และยากต่อการ maintain
|
||
|
|
- ควรเก็บใน database หรือ configuration
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
||
|
|
```typescript
|
||
|
|
if (_commandType.code == "C-PM-10") {
|
||
|
|
let _commandType10: any;
|
||
|
|
_commandType10 = {
|
||
|
|
// ... hardcoded fields
|
||
|
|
name1: "๑. ..........................ประธาน",
|
||
|
|
name2: "๒. ..........................กรรมการ",
|
||
|
|
// ...
|
||
|
|
};
|
||
|
|
_commandType = _commandType10;
|
||
|
|
} else if (["C-PM-21", "C-PM-23"].includes(_commandType.code)) {
|
||
|
|
let _commandType21and23: any;
|
||
|
|
_commandType21and23 = {
|
||
|
|
// ... hardcoded fields
|
||
|
|
persons: [
|
||
|
|
{
|
||
|
|
no: "",
|
||
|
|
org: "",
|
||
|
|
// ...
|
||
|
|
},
|
||
|
|
],
|
||
|
|
};
|
||
|
|
_commandType = _commandType21and23;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
// Move these templates to database or configuration
|
||
|
|
const commandTemplates = await this.commandTemplateRepository.find({
|
||
|
|
where: { commandTypeCode: _commandType.code }
|
||
|
|
});
|
||
|
|
|
||
|
|
if (commandTemplates.length > 0) {
|
||
|
|
const template = commandTemplates[0];
|
||
|
|
return new HttpSuccess({
|
||
|
|
..._commandType,
|
||
|
|
...template.templateData
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return new HttpSuccess(_commandType);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #10 - Missing Transaction for Related Operations (LOW)
|
||
|
|
|
||
|
|
**File & Location:** [CommandOperatorController.ts:109-112](src/controllers/CommandOperatorController.ts#L109-L112)
|
||
|
|
**Method:** `swapCommandOperator`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- มีการ swap orderNo ระหว่าง 2 records
|
||
|
|
- ไม่ได้ใช้ transaction
|
||
|
|
- ถ้า save ตัวแรกสำเร็จ แต่ตัวที่สอง fail จะเกิด data inconsistency
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
||
|
|
```typescript
|
||
|
|
// swap
|
||
|
|
const temp = source.orderNo;
|
||
|
|
source.orderNo = dest.orderNo;
|
||
|
|
dest.orderNo = temp;
|
||
|
|
|
||
|
|
await Promise.all([
|
||
|
|
this.commandOperatorRepo.save(source),
|
||
|
|
this.commandOperatorRepo.save(dest),
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
const queryRunner = AppDataSource.createQueryRunner();
|
||
|
|
try {
|
||
|
|
await queryRunner.connect();
|
||
|
|
await queryRunner.startTransaction();
|
||
|
|
|
||
|
|
// swap
|
||
|
|
const temp = source.orderNo;
|
||
|
|
source.orderNo = dest.orderNo;
|
||
|
|
dest.orderNo = temp;
|
||
|
|
|
||
|
|
await Promise.all([
|
||
|
|
queryRunner.manager.save(CommandOperator, source),
|
||
|
|
queryRunner.manager.save(CommandOperator, dest),
|
||
|
|
]);
|
||
|
|
|
||
|
|
await queryRunner.commitTransaction();
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
await queryRunner.rollbackTransaction();
|
||
|
|
console.error('Failed to swap command operators:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
"Failed to swap operator order"
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
await queryRunner.release();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #11 - Wrong Error Status Code (BUG)
|
||
|
|
|
||
|
|
**File & Location:** [Multiple Files - CommandSysController.ts:127](src/controllers/CommandSysController.ts#L127), [CommandTypeController.ts:216](src/controllers/CommandTypeController.ts#L216), etc.
|
||
|
|
**Methods:** `Post` in various controllers
|
||
|
|
|
||
|
|
**Problem Type:** 3. Logic Bug
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- Throw `HttpError(HttpStatusCode.NOT_FOUND, ...)` สำหรับ duplicate name errors
|
||
|
|
- ควรใช้ `BAD_REQUEST` หรือ `CONFLICT` แทน
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (ผิด):**
|
||
|
|
```typescript
|
||
|
|
if (checkName) {
|
||
|
|
throw new HttpError(HttpStatusCode.NOT_FOUND, "ชื่อนี้มีอยู่ในระบบแล้ว"); // ❌ Wrong status code
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
if (checkName) {
|
||
|
|
throw new HttpError(HttpStatusCode.CONFLICT, "ชื่อนี้มีอยู่ในระบบแล้ว"); // ✅ Correct status code
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### #12 - Typos in Status Field (BUG)
|
||
|
|
|
||
|
|
**File & Location:** [ChangePositionController.ts:79](src/controllers/ChangePositionController.ts#L79)
|
||
|
|
**Method:** `CreateChangePosition`
|
||
|
|
|
||
|
|
**Problem Type:** 3. Logic Bug
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
- Status เป็น "WAITTING" (ตัว T เกิน)
|
||
|
|
- ควรเป็น "WAITING"
|
||
|
|
|
||
|
|
**Code ปัจจุบัน (ผิด):**
|
||
|
|
```typescript
|
||
|
|
changePosition.status = "WAITTING"; // ❌ typo
|
||
|
|
```
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
changePosition.status = "WAITING"; // ✅ correct spelling
|
||
|
|
```
|
||
|
|
|
||
|
|
**หมายเหตุ:** ปัญหาเดียวกันนี้พบใน [ChangePositionController.ts:241](src/controllers/ChangePositionController.ts#L241)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## สรุปคำแนะนำการแก้ไขแบบรวม
|
||
|
|
|
||
|
|
### ระดับความสำคัญ
|
||
|
|
|
||
|
|
**ต้องแก้ทันที (P0 - Critical):**
|
||
|
|
1. QueryRunner transaction not released on error - อาจทำให้ connection leak
|
||
|
|
|
||
|
|
**ควรแก้โดยเร็ว (P1 - High):**
|
||
|
|
2. Promise.all operations without error handling - unhandled rejections
|
||
|
|
3. QueryBuilder without validation and error handling
|
||
|
|
|
||
|
|
**ควรแก้ (P2 - Medium):**
|
||
|
|
4. Null reference checks
|
||
|
|
5. Unsafe optional chain usage
|
||
|
|
6. Promise operations in edit methods
|
||
|
|
|
||
|
|
**แก้เมื่อว่าง (P3 - Low):**
|
||
|
|
7. Hardcoded data
|
||
|
|
8. Missing transaction for related operations
|
||
|
|
|
||
|
|
### แนวทางการแก้ไขแบบ Global
|
||
|
|
|
||
|
|
1. **Use try-finally pattern** for all QueryRunner operations
|
||
|
|
2. **Add input validation** for all query parameters
|
||
|
|
3. **Use Promise.allSettled** หรือ wrap promises in try-catch
|
||
|
|
4. **Implement proper null checks** ก่อน accessing nested properties
|
||
|
|
5. **Use transactions** สำหรับ operations ที่ต้องการ consistency
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ไฟล์ที่ต้องแก้ไข
|
||
|
|
|
||
|
|
1. **src/controllers/CommandOperatorController.ts** - Transaction handling, Promise operations
|
||
|
|
2. **src/controllers/DevelopmentRequestController.ts** - Promise.all, QueryBuilder validation
|
||
|
|
3. **src/controllers/DPISController.ts** - Null reference checks
|
||
|
|
4. **src/controllers/CommandSalaryController.ts** - Input validation
|
||
|
|
5. **src/controllers/CommandTypeController.ts** - Error status codes, hardcoded data
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ข้อมูลเพิ่มเติม
|
||
|
|
|
||
|
|
- **Controllers ที่ยังไม่ได้ตรวจสอบ:** 120 ไฟล์
|
||
|
|
- **จุดเสี่ยงที่พบซ้ำจากชุดที่ 1:** Promise.all without error handling, QueryBuilder without error handling
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**รายงานนี้ถูกสร้างโดย AI Code Review System**
|
||
|
|
**สำหรับ BMA EHR Organization Project**
|