report: Controllers
This commit is contained in:
parent
7104ce4f34
commit
85e9be08f6
15 changed files with 10752 additions and 0 deletions
848
reports/batch-01-controllers-1-10-analysis.md
Normal file
848
reports/batch-01-controllers-1-10-analysis.md
Normal file
|
|
@ -0,0 +1,848 @@
|
|||
# รายงานการตรวจสอบ 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**
|
||||
Loading…
Add table
Add a link
Reference in a new issue