848 lines
28 KiB
Markdown
848 lines
28 KiB
Markdown
# รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 1 (ไฟล์ที่ 1-10)
|
|
|
|
**Project:** BMA EHR Organization Backend
|
|
**Framework:** TSOA + Express + TypeORM
|
|
**วันที่ตรวจสอบ:** 2026-05-08
|
|
**จำนวน Controllers:** 10 ไฟล์
|
|
**สถานะ:** เสร็จสิ้น
|
|
|
|
---
|
|
|
|
## สรุปผลการตรวจสอบ
|
|
|
|
| ระดับความรุนแรง | จำนวนจุดเสี่ยง |
|
|
|---------------------|-------------------|
|
|
| **CRITICAL** | 2 |
|
|
| **HIGH** | 3 |
|
|
| **MEDIUM** | 4 |
|
|
| **LOW** | 1 |
|
|
| **BUG** | 1 |
|
|
| **รวมทั้งหมด** | 11 |
|
|
|
|
---
|
|
|
|
## Controllers ที่ตรวจสอบ
|
|
|
|
1. [AuthRoleAttrController.ts](src/controllers/AuthRoleAttrController.ts)
|
|
2. [AuthRoleController.ts](src/controllers/AuthRoleController.ts)
|
|
3. [AuthSysController.ts](src/controllers/AuthSysController.ts)
|
|
4. [ApiManageController.ts](src/controllers/ApiManageController.ts)
|
|
5. [ApiKeyController.ts](src/controllers/ApiKeyController.ts)
|
|
6. [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts)
|
|
7. [BloodGroupController.ts](src/controllers/BloodGroupController.ts)
|
|
8. [ChangePositionController.ts](src/controllers/ChangePositionController.ts)
|
|
9. [CommandCodeController.ts](src/controllers/CommandCodeController.ts)
|
|
10. [CommandController.ts](src/controllers/CommandController.ts) - ไฟล์ใหญ่เกินกว่าที่จะอ่าน (336KB+)
|
|
|
|
---
|
|
|
|
## รายละเอียดจุดเสี่ยงแต่ละจุด
|
|
|
|
### #1 - Redis Client Error Handling (CRITICAL)
|
|
|
|
**File & Location:** [AuthRoleController.ts:126-138](src/controllers/AuthRoleController.ts#L126-L138)
|
|
**Method:** `AddAuthRoleGovoment`
|
|
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
- Redis client operations ไม่มี error handling
|
|
- `redisClient.del()` มี callback ที่ throw error แต่ไม่มี try-catch รองรับ
|
|
- Redis connection error จะทำให้เกิด **unhandled exception** และทำให้ Node.js process crash
|
|
- Callback pattern ที่ใช้ throw จะไม่ถูก catch โดย Promise chain
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
const redisClient = await this.redis.createClient({
|
|
host: REDIS_HOST,
|
|
port: REDIS_PORT,
|
|
});
|
|
|
|
redisClient.del("role_" + posMaster.current_holderId, (err: Error, response: Response) => {
|
|
if (err) throw err; // ❌ จะทำให้ process crash
|
|
});
|
|
|
|
redisClient.del("menu_" + posMaster.current_holderId, (err: Error, response: Response) => {
|
|
if (err) throw err; // ❌ จะทำให้ process crash
|
|
});
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
// ใช้ Promise wrapper หรือ util.promisify
|
|
import { promisify } from 'util';
|
|
|
|
// Create Redis client
|
|
const redisClient = await this.redis.createClient({
|
|
host: REDIS_HOST,
|
|
port: REDIS_PORT,
|
|
});
|
|
|
|
// Promisify the operations
|
|
const redisDelAsync = promisify(redisClient.del).bind(redisClient);
|
|
|
|
try {
|
|
if (posMaster.current_holderId) {
|
|
await redisDelAsync("role_" + posMaster.current_holderId);
|
|
await redisDelAsync("menu_" + posMaster.current_holderId);
|
|
}
|
|
} catch (error) {
|
|
console.error('Redis operation failed:', error);
|
|
// Log error แต่ไม่ crash - Redis failure ไม่ควรทำให้ business logic หยุดทำงาน
|
|
// อาจ skip Redis operation หรือ return warning แต่ business process ควรดำเนินต่อ
|
|
} finally {
|
|
// ปิด connection หากจำเป็น
|
|
if (redisClient) {
|
|
redisClient.quit();
|
|
}
|
|
}
|
|
```
|
|
|
|
**หมายเหตุ:** ปัญหาเดียวกันพบใน method `editAuthRole` ที่ line 269-276
|
|
|
|
---
|
|
|
|
### #2 - Redis flushdb Without Error Handling (CRITICAL)
|
|
|
|
**File & Location:** [AuthRoleController.ts:269-276](src/controllers/AuthRoleController.ts#L269-L276)
|
|
**Method:** `editAuthRole`
|
|
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
- `redisClient.flushdb()` มี callback แต่ไม่ได้จัดการ error
|
|
- Flush operation เป็น critical operation ที่อาจ fail ได้
|
|
- ไม่มี try-catch รอบรับ Redis operations
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
const redisClient = await this.redis.createClient({
|
|
host: REDIS_HOST,
|
|
port: REDIS_PORT,
|
|
});
|
|
|
|
await redisClient.flushdb(function (err: any, succeeded: any) {
|
|
console.log(succeeded); // will be true if successfull
|
|
}); // ❌ ถ้า error จะไม่ได้จัดการ
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
import { promisify } from 'util';
|
|
|
|
const redisClient = await this.redis.createClient({
|
|
host: REDIS_HOST,
|
|
port: REDIS_PORT,
|
|
});
|
|
|
|
try {
|
|
const redisFlushDbAsync = promisify(redisClient.flushdb).bind(redisClient);
|
|
await redisFlushDbAsync();
|
|
} catch (error) {
|
|
console.error('Redis flush operation failed:', error);
|
|
throw new HttpError(HttpStatus.SERVICE_UNAVAILABLE, "Failed to clear cache");
|
|
} finally {
|
|
if (redisClient) {
|
|
redisClient.quit();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### #3 - CallAPI External Request Without Error Handling (CRITICAL)
|
|
|
|
**File & Location:** [ChangePositionController.ts:585-604](src/controllers/ChangePositionController.ts#L585-L604)
|
|
**Method:** `doneReport`
|
|
|
|
**Problem Type:** 1. Unhandled Exception
|
|
|
|
**Root Cause:**
|
|
- External API call ผ่าน `CallAPI().PostData()` ไม่มี try-catch
|
|
- `Promise.all()` ถ้ามี promise ไหน reject จะทำให้ **unhandled rejection**
|
|
- Network error, timeout, หรือ external service down จะทำให้ unhandled rejection
|
|
- ไม่มี timeout handling
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
await Promise.all(
|
|
body.result.map(async (v) => {
|
|
const profile = await this.profileChangePositionRepository.findOne({
|
|
where: { id: v.id },
|
|
});
|
|
if (profile != null) {
|
|
await new CallAPI()
|
|
.PostData(request, "/org/profile/salary", { // ❌ ไม่มี error handling
|
|
profileId: profile.id,
|
|
date: new Date(),
|
|
})
|
|
.then(async (x) => {
|
|
profile.status = "DONE";
|
|
await this.profileChangePositionRepository.save(profile);
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
// ใช้ Promise.allSettled แทน Promise.all เพื่อไม่ให้ rejection หยุดทั้งหมด
|
|
const results = await Promise.allSettled(
|
|
body.result.map(async (v) => {
|
|
try {
|
|
const profile = await this.profileChangePositionRepository.findOne({
|
|
where: { id: v.id },
|
|
});
|
|
if (profile != null) {
|
|
// Add timeout
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Request timeout')), 30000)
|
|
);
|
|
|
|
const apiCallPromise = new CallAPI().PostData(request, "/org/profile/salary", {
|
|
profileId: profile.id,
|
|
date: new Date(),
|
|
});
|
|
|
|
await Promise.race([apiCallPromise, timeoutPromise]);
|
|
|
|
profile.status = "DONE";
|
|
await this.profileChangePositionRepository.save(profile);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to process profile ${v.id}:`, error);
|
|
// Mark as FAILED แทนที่จะ leave as-is
|
|
const profile = await this.profileChangePositionRepository.findOne({
|
|
where: { id: v.id },
|
|
});
|
|
if (profile) {
|
|
profile.status = "FAILED";
|
|
profile.errorMessage = error.message;
|
|
await this.profileChangePositionRepository.save(profile);
|
|
}
|
|
throw error; // Re-throw to track in allSettled
|
|
}
|
|
}),
|
|
);
|
|
|
|
// Check results
|
|
const failed = results.filter(r => r.status === 'rejected');
|
|
if (failed.length > 0) {
|
|
console.error(`${failed.length} profiles failed to process`);
|
|
// Optionally return partial success info
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### #4 - Database Operations Without Error Handling (HIGH)
|
|
|
|
**Files:** ทั้งหมด 9 Controllers
|
|
**Locations:** หลาย method ในทุกไฟล์
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
- Database operations ส่วนใหญ่ไม่มี try-catch
|
|
- TypeORM query errors จะถูก catch โดย global error middleware แต่อาจเป็น generic 500 errors
|
|
- Connection timeout, database down, หรือ query errors จะไม่ได้รับการจัดการเฉพาะเจาะจง
|
|
- ไม่สามารถ distinguish ระหว่าง different error types ได้
|
|
|
|
**ตัวอย่าง Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
@Get("list")
|
|
public async listAuthRoleAttr() {
|
|
const getList = await this.authRoleAttrRepo.find();
|
|
// ❌ ถ้า database error จะ throw ไปยัง global middleware
|
|
// ไม่สามารถ handle เฉพาะเจาะจงได้
|
|
return new HttpSuccess(getList);
|
|
}
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
สำหรับ critical operations:
|
|
```typescript
|
|
import { QueryFailedError } from "typeorm";
|
|
|
|
@Get("list")
|
|
public async listAuthRoleAttr() {
|
|
try {
|
|
const getList = await this.authRoleAttrRepo.find();
|
|
return new HttpSuccess(getList);
|
|
} catch (error) {
|
|
if (error instanceof QueryFailedError) {
|
|
// Handle database-specific errors
|
|
console.error('Database query failed:', error);
|
|
throw new HttpError(
|
|
HttpStatus.SERVICE_UNAVAILABLE,
|
|
"Database service temporarily unavailable"
|
|
);
|
|
} else if (error.message && error.message.includes('connection')) {
|
|
throw new HttpError(
|
|
HttpStatus.SERVICE_UNAVAILABLE,
|
|
"Unable to connect to database"
|
|
);
|
|
}
|
|
// Re-throw other errors to global middleware
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### #5 - Promise.all Without Error Handling (HIGH)
|
|
|
|
**File & Location:** [AuthRoleController.ts:247-267](src/controllers/AuthRoleController.ts#L247-L267)
|
|
**Method:** `editAuthRole`
|
|
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
- `Promise.all()` รวม `remove()` และหลาย `save()` operations
|
|
- ถ้า operation ไหน fail จะทำให้ **unhandled rejection**
|
|
- ไม่มี try-catch รองรับ
|
|
- Partial failure จะทำให้ไม่สามารถ recover ได้
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
await this.authRoleAttrRepo.remove(roleAttrData, { data: req });
|
|
|
|
const newAttrs = body.authRoleAttrs.map((attr) => {
|
|
const newAttr = new AuthRoleAttr();
|
|
Object.assign(newAttr, attr, {
|
|
authRoleId: roleId,
|
|
createdUserId: req.user.sub,
|
|
createdFullName: req.user.name,
|
|
lastUpdateUserId: req.user.sub,
|
|
lastUpdateFullName: req.user.name,
|
|
createdAt: new Date(),
|
|
lastUpdatedAt: new Date(),
|
|
});
|
|
return newAttr;
|
|
});
|
|
const before = structuredClone(record);
|
|
await Promise.all([
|
|
this.authRoleRepo.save(record, { data: req }),
|
|
setLogDataDiff(req, { before, after: record }),
|
|
...newAttrs.map((attr) => this.authRoleAttrRepo.save(attr)), // ❌ ถ้า fail จะ unhandled rejection
|
|
]);
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
try {
|
|
await this.authRoleAttrRepo.remove(roleAttrData, { data: req });
|
|
|
|
const newAttrs = body.authRoleAttrs.map((attr) => {
|
|
const newAttr = new AuthRoleAttr();
|
|
Object.assign(newAttr, attr, {
|
|
authRoleId: roleId,
|
|
createdUserId: req.user.sub,
|
|
createdFullName: req.user.name,
|
|
lastUpdateUserId: req.user.sub,
|
|
lastUpdateFullName: req.user.name,
|
|
createdAt: new Date(),
|
|
lastUpdatedAt: new Date(),
|
|
});
|
|
return newAttr;
|
|
});
|
|
const before = structuredClone(record);
|
|
|
|
// ใช้ Promise.allSettled แทน Promise.all
|
|
const results = await Promise.allSettled([
|
|
this.authRoleRepo.save(record, { data: req }),
|
|
setLogDataDiff(req, { before, after: record }),
|
|
...newAttrs.map((attr) => this.authRoleAttrRepo.save(attr)),
|
|
]);
|
|
|
|
// Check for failures
|
|
const failures = results.filter(r => r.status === 'rejected');
|
|
if (failures.length > 0) {
|
|
console.error('Some operations failed:', failures);
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"Failed to update some role attributes"
|
|
);
|
|
}
|
|
|
|
// Redis flush with error handling (จากปัญหา #2)
|
|
const redisClient = await this.redis.createClient({
|
|
host: REDIS_HOST,
|
|
port: REDIS_PORT,
|
|
});
|
|
|
|
try {
|
|
const redisFlushDbAsync = promisify(redisClient.flushdb).bind(redisClient);
|
|
await redisFlushDbAsync();
|
|
} catch (error) {
|
|
console.error('Redis flush failed:', error);
|
|
// Non-critical - don't fail the request
|
|
} finally {
|
|
if (redisClient) {
|
|
redisClient.quit();
|
|
}
|
|
}
|
|
|
|
return new HttpSuccess();
|
|
} catch (error) {
|
|
console.error('Failed to update role:', error);
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"Failed to update role"
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### #6 - JWT Verification Inconsistent Error Handling (MEDIUM)
|
|
|
|
**File & Location:** [ApiKeyController.ts:42-61](src/controllers/ApiKeyController.ts#L42-L61)
|
|
**Method:** `verifyApiKey`
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
- มี try-catch แต่ return HttpSuccess แทนที่จะ throw error
|
|
- Error handling ไม่ consistent กับ endpoints อื่น
|
|
- Client จะไม่รู้ว่าเกิด error (เพราะได้ 200 OK พร้อม valid: false)
|
|
- ไม่สามารถ distinguish ระหว่าง token types ของ errors ได้
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
try {
|
|
const jwtSecret = process.env.JWT_SECRET || "your-default-secret-key";
|
|
const decoded = jwt.verify(requestBody.token, jwtSecret);
|
|
return new HttpSuccess({
|
|
valid: true,
|
|
data: decoded,
|
|
});
|
|
} catch (error: any) {
|
|
console.error("JWT Verification Error:", error.message);
|
|
return new HttpSuccess({ // ❌ Return success แม้ error
|
|
valid: false,
|
|
error: error.message,
|
|
});
|
|
}
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
try {
|
|
const jwtSecret = process.env.JWT_SECRET;
|
|
if (!jwtSecret) {
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"JWT secret not configured"
|
|
);
|
|
}
|
|
|
|
const decoded = jwt.verify(requestBody.token, jwtSecret);
|
|
return new HttpSuccess({
|
|
valid: true,
|
|
data: decoded,
|
|
});
|
|
} catch (error: any) {
|
|
console.error("JWT Verification Error:", error.message);
|
|
|
|
if (error.name === 'TokenExpiredError') {
|
|
throw new HttpError(HttpStatus.UNAUTHORIZED, "Token expired");
|
|
} else if (error.name === 'JsonWebTokenError') {
|
|
throw new HttpError(HttpStatus.UNAUTHORIZED, "Invalid token");
|
|
} else if (error instanceof HttpError) {
|
|
throw error;
|
|
}
|
|
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"Token verification failed"
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### #7 - Query Builder Without Error Handling (MEDIUM)
|
|
|
|
**File & Location:** [ChangePositionController.ts:284-350](src/controllers/ChangePositionController.ts#L284-L350)
|
|
**Method:** `GetProfileChangePositionLists`
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
- Complex QueryBuilder พร้อม Brackets และ dynamic conditions
|
|
- ถ้า query syntax error, database connection error, หรือ data type mismatch จะ throw ไป global middleware
|
|
- ไม่สามารถ log หรือ track specific query errors ได้
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
const [profileChangePosition, total] = await AppDataSource.getRepository(ProfileChangePosition)
|
|
.createQueryBuilder("profileChangePosition")
|
|
.where({ changePositionId: changePositionId })
|
|
.andWhere(
|
|
new Brackets((qb) => {
|
|
qb.where(
|
|
searchKeyword != undefined && searchKeyword != null && searchKeyword != ""
|
|
? "profileChangePosition.prefix LIKE :keyword"
|
|
: "1=1",
|
|
{ keyword: `%${searchKeyword}%` },
|
|
)
|
|
// ... หลาย orWhere
|
|
}),
|
|
)
|
|
.orderBy("profileChangePosition.createdAt", "ASC")
|
|
.skip((page - 1) * pageSize)
|
|
.take(pageSize)
|
|
.getManyAndCount(); // ❌ ไม่มี try-catch
|
|
|
|
return new HttpSuccess({ data: profileChangePosition, total });
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
try {
|
|
// Validate input
|
|
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");
|
|
}
|
|
|
|
const [profileChangePosition, total] = await AppDataSource.getRepository(ProfileChangePosition)
|
|
.createQueryBuilder("profileChangePosition")
|
|
.where({ changePositionId: changePositionId })
|
|
.andWhere(
|
|
new Brackets((qb) => {
|
|
// Use parameterized queries
|
|
const conditions = [];
|
|
const params = { keyword: `%${searchKeyword}%` };
|
|
|
|
if (searchKeyword) {
|
|
conditions.push("profileChangePosition.prefix LIKE :keyword");
|
|
conditions.push("profileChangePosition.firstName LIKE :keyword");
|
|
conditions.push("profileChangePosition.lastName LIKE :keyword");
|
|
conditions.push("profileChangePosition.citizenId LIKE :keyword");
|
|
conditions.push("profileChangePosition.birthDate LIKE :keyword");
|
|
conditions.push("profileChangePosition.lastUpdatedAt LIKE :keyword");
|
|
conditions.push("profileChangePosition.status LIKE :keyword");
|
|
}
|
|
|
|
qb.where(
|
|
searchKeyword ? conditions.join(" OR ") : "1=1",
|
|
params
|
|
);
|
|
}),
|
|
)
|
|
.orderBy("profileChangePosition.createdAt", "ASC")
|
|
.skip((page - 1) * pageSize)
|
|
.take(pageSize)
|
|
.getManyAndCount();
|
|
|
|
return new HttpSuccess({ data: profileChangePosition, total });
|
|
} catch (error) {
|
|
if (error instanceof HttpError) {
|
|
throw error;
|
|
}
|
|
console.error('Query failed:', error);
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"Failed to retrieve profile change positions"
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### #8 - Null Reference Risk (MEDIUM)
|
|
|
|
**File & Location:** [ApiWebServiceController.ts:67-78](src/controllers/ApiWebServiceController.ts#L67-L78)
|
|
**Method:** `listAttribute`
|
|
|
|
**Problem Type:** 2. Missing Error Handle
|
|
|
|
**Root Cause:**
|
|
- `revision` อาจเป็น null ถ้าไม่พบ record
|
|
- การใช้ `revision?.id` จะทำให้ condition เป็น `PosMaster.orgRevisionId = "undefined"`
|
|
- SQL query จะไม่ error แต่จะ return ผลลัพธ์ที่ไม่ถูกต้อง
|
|
- ไม่มี validation ว่า revision ต้องมีค่า
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
} else if (system == "organization") {
|
|
tbMain = "OrgRoot";
|
|
const revision = await this.orgRevisionRepository.findOne({
|
|
select: ["id"],
|
|
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
|
});
|
|
condition = `OrgRoot.orgRevisionId = "${revision?.id}"`; // ❌ ถ้า revision เป็น null จะเป็น undefined
|
|
}
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
} else if (system == "organization") {
|
|
tbMain = "OrgRoot";
|
|
const revision = await this.orgRevisionRepository.findOne({
|
|
select: ["id"],
|
|
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
|
});
|
|
|
|
if (!revision) {
|
|
throw new HttpError(
|
|
HttpStatus.NOT_FOUND,
|
|
"No current organization revision found"
|
|
);
|
|
}
|
|
condition = `OrgRoot.orgRevisionId = "${revision.id}"`;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### #9 - Unsafe Default Environment Variable (LOW)
|
|
|
|
**File & Location:** [ApiKeyController.ts:45](src/controllers/ApiKeyController.ts#L45)
|
|
**Method:** `verifyApiKey`
|
|
|
|
**Problem Type:** 2. Missing Error Handle / Security
|
|
|
|
**Root Cause:**
|
|
- ใช้ default value สำหรับ JWT_SECRET
|
|
- ใน production ถ้าไม่ได้ set JWT_SECRET จะใช้ default value ที่ไม่ปลอดภัย
|
|
- อาจนำไปสู่ security breach
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
const jwtSecret = process.env.JWT_SECRET || "your-default-secret-key"; // ❌ Default value insecure
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
const jwtSecret = process.env.JWT_SECRET;
|
|
if (!jwtSecret) {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"JWT secret not configured"
|
|
);
|
|
}
|
|
// Only for development
|
|
console.warn('Using default JWT secret - not safe for production!');
|
|
}
|
|
|
|
const decoded = jwt.verify(requestBody.token, jwtSecret || 'dev-secret-key');
|
|
```
|
|
|
|
---
|
|
|
|
### #10 - Switch Statement Without Break (BUG)
|
|
|
|
**File & Location:** [ChangePositionController.ts:430-515](src/controllers/ChangePositionController.ts#L430-L515)
|
|
**Method:** `positionProfileEmployee`
|
|
|
|
**Problem Type:** 3. Logic Bug (ส่งผลต่อ data consistency)
|
|
|
|
**Root Cause:**
|
|
- Switch statement ไม่มี `break` statements
|
|
- จะเกิด **fallthrough** effect - ทุก case หลังจาก case ที่ match จะถูก execute ด้วย
|
|
- จะทำให้ data ถูก overwrite ด้วยค่าจาก cases ถัดไป
|
|
- เป็น common bug ที่อาจทำให้ data corruption
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
switch (body.node) {
|
|
case 0: {
|
|
const data = await this.orgRootRepository.findOne({
|
|
where: { id: body.nodeId },
|
|
});
|
|
if (data != null) {
|
|
profileChangePos.rootId = data.id;
|
|
profileChangePos.root = data.orgRootName;
|
|
profileChangePos.rootShortName = data.orgRootShortName;
|
|
}
|
|
} // ❌ ไม่มี break
|
|
case 1: { // ❌ จะ execute ถ้า case 0 match
|
|
const data = await this.child1Repository.findOne({
|
|
where: { id: body.nodeId },
|
|
relations: ["orgRoot"],
|
|
});
|
|
// ...
|
|
} // ❌ ไม่มี break
|
|
case 2: { // ❌ จะ execute ถ้า case 0 หรือ 1 match
|
|
// ...
|
|
}
|
|
// ... ต่อไปเรื่อยๆ
|
|
}
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
switch (body.node) {
|
|
case 0: {
|
|
const data = await this.orgRootRepository.findOne({
|
|
where: { id: body.nodeId },
|
|
});
|
|
if (data != null) {
|
|
profileChangePos.rootId = data.id;
|
|
profileChangePos.root = data.orgRootName;
|
|
profileChangePos.rootShortName = data.orgRootShortName;
|
|
}
|
|
break; // ✅ เพิ่ม break
|
|
}
|
|
case 1: {
|
|
const data = await this.child1Repository.findOne({
|
|
where: { id: body.nodeId },
|
|
relations: ["orgRoot"],
|
|
});
|
|
if (data != null) {
|
|
profileChangePos.rootId = data.orgRoot.id;
|
|
profileChangePos.root = data.orgRoot.orgRootName;
|
|
profileChangePos.rootShortName = data.orgRoot.orgRootShortName;
|
|
profileChangePos.child1Id = data.id;
|
|
profileChangePos.child1 = data.orgChild1Name;
|
|
profileChangePos.child1ShortName = data.orgChild1ShortName;
|
|
}
|
|
break; // ✅ เพิ่ม break
|
|
}
|
|
case 2: {
|
|
const data = await this.child2Repository.findOne({
|
|
where: { id: body.nodeId },
|
|
relations: ["orgRoot", "orgChild1"],
|
|
});
|
|
if (data != null) {
|
|
profileChangePos.rootId = data.orgRoot.id;
|
|
profileChangePos.root = data.orgRoot.orgRootName;
|
|
profileChangePos.rootShortName = data.orgRoot.orgRootShortName;
|
|
profileChangePos.child1Id = data.orgChild1.id;
|
|
profileChangePos.child1 = data.orgChild1.orgChild1Name;
|
|
profileChangePos.child1ShortName = data.orgChild1.orgChild1ShortName;
|
|
profileChangePos.child2Id = data.id;
|
|
profileChangePos.child2 = data.orgChild2Name;
|
|
profileChangePos.child2ShortName = data.orgChild2ShortName;
|
|
}
|
|
break; // ✅ เพิ่ม break
|
|
}
|
|
case 3: {
|
|
// ... เพิ่ม break ท้าย
|
|
}
|
|
case 4: {
|
|
// ... เพิ่ม break ท้าย
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### #11 - Array Mutation in Loop (MEDIUM)
|
|
|
|
**File & Location:** [ChangePositionController.ts:233-250](src/controllers/ChangePositionController.ts#L233-L250)
|
|
**Method:** `CreateProfileChangePosition`
|
|
|
|
**Problem Type:** 3. Logic Bug
|
|
|
|
**Root Cause:**
|
|
- ใช้ตัวแปร `profiles` เดียวแล้ว push เข้า array หลายครั้ง
|
|
- ทุก elements ใน array จะชี้ไปที่ object เดียวกัน
|
|
- ทำให้ข้อมูลซ้ำกันทั้งหมด
|
|
|
|
**Code ปัจจุบัน (เสี่ยง):**
|
|
```typescript
|
|
const profileChangePositions: ProfileChangePosition[] = [];
|
|
const profiles = new ProfileChangePosition(); // ❌ สร้างครั้งเดียว
|
|
for (const data of body.profiles) {
|
|
Object.assign(profiles, data); // ❌ ใช้ object เดียว
|
|
// ...
|
|
profileChangePositions.push(profiles); // ❌ push object เดียวกันซ้ำๆ
|
|
}
|
|
await this.profileChangePositionRepository.save(profileChangePositions);
|
|
```
|
|
|
|
**Recommended Fix:**
|
|
```typescript
|
|
const profileChangePositions: ProfileChangePosition[] = [];
|
|
for (const data of body.profiles) {
|
|
const profiles = new ProfileChangePosition(); // ✅ สร้างใหม่ทุกรอบ
|
|
Object.assign(profiles, data);
|
|
let positionOld = data.positionOld ? `${data.positionOld}` : "";
|
|
let rootOld = data.rootOld ? (data.positionOld ? `/${data.rootOld}` : `${data.rootOld}`) : "";
|
|
profiles.changePositionId = changePositionId;
|
|
profiles.organizationPositionOld = `${positionOld}${rootOld}`;
|
|
profiles.status = "WAITTING";
|
|
profiles.createdUserId = request.user.sub;
|
|
profiles.createdFullName = request.user.name;
|
|
profiles.createdAt = new Date();
|
|
profiles.lastUpdateUserId = request.user.sub;
|
|
profiles.lastUpdateFullName = request.user.name;
|
|
profiles.lastUpdatedAt = new Date();
|
|
profileChangePositions.push(profiles);
|
|
}
|
|
await this.profileChangePositionRepository.save(profileChangePositions);
|
|
```
|
|
|
|
---
|
|
|
|
## สรุปคำแนะนำการแก้ไขแบบรวม
|
|
|
|
### ระดับความสำคัญ
|
|
|
|
**ต้องแก้ทันที (P0 - Critical):**
|
|
1. Redis operations error handling - อาจทำให้ process crash
|
|
2. External API calls error handling - อาจทำให้ unhandled rejection
|
|
|
|
**ควรแก้โดยเร็ว (P1 - High):**
|
|
3. Database operations error handling
|
|
4. Promise operations error handling
|
|
|
|
**ควรแก้ (P2 - Medium):**
|
|
5. JWT verification consistency
|
|
6. Query builder error handling
|
|
7. Null reference checks
|
|
|
|
**แก้เมื่อว่าง (P3 - Low):**
|
|
8. Environment variable defaults
|
|
9. Code quality issues
|
|
|
|
### แนวทางการแก้ไขแบบ Global
|
|
|
|
1. **Implement centralized error handling:**
|
|
- Wrap all async operations
|
|
- Use specific error types
|
|
- Log all errors appropriately
|
|
|
|
2. **Add circuit breaker for external services:**
|
|
- Redis, external APIs
|
|
- Prevent cascade failures
|
|
|
|
3. **Use Promise.allSettled** แทน Promise.all สำหรับ independent operations
|
|
|
|
4. **Add input validation:**
|
|
- Validate before processing
|
|
- Check for null/undefined
|
|
|
|
5. **Implement retry logic:**
|
|
- For transient failures
|
|
- Database connection issues
|
|
|
|
---
|
|
|
|
## ไฟล์ที่ต้องแก้ไข
|
|
|
|
1. **src/controllers/AuthRoleController.ts** - Redis operations, Promise operations
|
|
2. **src/controllers/ChangePositionController.ts** - External API calls, Switch bug, Array mutation
|
|
3. **src/controllers/ApiKeyController.ts** - JWT verification, Environment variables
|
|
4. **src/controllers/ApiWebServiceController.ts** - Null reference checks
|
|
|
|
---
|
|
|
|
## ข้อมูลเพิ่มเติม
|
|
|
|
- **Controllers ที่ยังไม่ได้ตรวจสอบ:** 130 ไฟล์
|
|
- **ไฟล์ที่ไม่สามารถอ่านได้:** CommandController.ts (ไฟล์ใหญ่เกิน 336KB)
|
|
|
|
---
|
|
|
|
**รายงานนี้ถูกสร้างโดย AI Code Review System**
|
|
**สำหรับ BMA EHR Organization Project**
|