11.5 · cu-central-api GraphQL + REST
บทนี้รวบรวม API surface ของ cu-central-api — 83 GraphQL operations + 5 REST auth endpoints — พร้อมเน้น SourceDB sharding ที่เป็นเอกลักษณ์ของ service นี้
ภาพรวม
| ตัวเลข | ค่า |
|---|---|
| TypeDefs files | 32 |
| Resolver files | 27 |
| GraphQL operations | ~83 (70 Query + 13 Mutation) |
| Subscription | 0 |
| REST endpoints | 5 (auth + conicle) |
| Custom scalars | เหมือน vtrc-api (Date, JSON, ...) |
| Custom directives | 1 (@auth(requires, accessLevel, ignoreAuth)) |
Root schema
directive @auth(requires: [String], accessLevel: Int, ignoreAuth: Boolean) on FIELD_DEFINITION
type Query { _: String }
type Mutation { _: String }
type Subscribtion { _: String } # typo — Subscribtion (ไม่ใช่ Subscription)ข้อระวัง: typo Subscribtion อยู่ใน root schema — ไม่ได้ใช้จริง แต่ถ้าจะ refactor ต้องระวัง
GraphQL operations ต่อ domain
| Domain | TypeDefs file | Q | M | Total | @auth |
|---|---|---|---|---|---|
| HRMI Leave/Medical | hrmi.js | 19 | 7 | 26 | ไม่มี (public) |
| Registration/Account | registration.js | 1 | 7 | 8 | 1 |
| Two-way (admin) | twoWay.js | 6 | 0 | 6 | ไม่มี |
| Personal profile | personal.js | 5 | 0 | 5 | 2 |
| Employee | employee.js | 5 | 0 | 5 | 3 |
| Fund/Provident | fund.js | 4 | 1 | 5 | 0 (commented) |
| User verification | userVerification.js | 3 | 0 | 3 | ไม่มี |
| Reference data | referenceData.js | 3 | 0 | 3 | ไม่มี |
| Master data | masterData.js | 2 | 2 | 4 | ไม่มี |
| Child tuition | childTuition.js | 4 | 0 | 4 | ไม่มี |
| Payroll slip | payrollSlip.js | 2 | 0 | 2 | 2 |
| Authentication | authentication.js | 2 | 0 | 2 | 1 |
| Study leave request | studyLeaveRequest.js | 2 | 0 | 2 | ไม่มี |
| Bank book | bank.js | 2 | 0 | 2 | ไม่มี |
| HR enum | hrEnum.js | 1 | 0 | 1 | — |
| HR Mutation: Taxes | hr/hrTaxes.js | 0 | 1 | 1 | 1 |
| HR Mutation: Phone | hr/hrPhoneNumber.js | 0 | 1 | 1 | 1 |
| HR Mutation: Address | hr/hrAddress.js | 0 | 1 | 1 | 1 |
| Salary cert | salaryCert.js | 1 | 0 | 1 | ไม่มี |
| Compensation | compensation.js | 1 | 0 | 1 | 1 |
| Finance type | finance.js | 1 | 0 | 1 | 1 |
| Family | family.js | 1 | 0 | 1 | 1 |
| Time attendance | timeAttendance.js | 1 | 0 | 1 | 1 |
| Probation | hrPerProbation.js | 1 | 0 | 1 | ไม่มี |
| Search profile (admin) | searchProfile.js | 1 | 0 | 1 | bypass |
| SSO non-employee | sso.js | 1 | 0 | 1 | ไม่มี |
| Stat log | statLog.js | 1 | 0 | 1 | ไม่มี |
| Notification (SMS) | notification.js | 0 | 1 | 1 | ไม่มี |
| Organization | organization.js | 0 | 0 | 0 | (types only) |
| Tax | tax.js | 0 | 0 | 0 | (types only) |
| รวม | 32 files | ~70 | ~13 | ~83 | 14 fields |
Operations เด่น — signature ตัวอย่าง
employeeProfile — ข้อมูลพนักงาน
extend type Query {
employeeProfile(profileKey: String!): EmployeeProfile! @auth
accordingProfile: [AccordingProfile!]! @auth
employeeFinancial(profileKey: String!): EmployeeFinancial! @auth
employeeManagement(orgCode: String, fullName: String, limit: Int): [EmployeeManagement]
checkEmployeeManagement(empCode: String): ResponseStatus
}payrollSlipDetail — สลิปเงินเดือน
payrollSlipDetail(
profileKey: String!
"Required yyyy (A.D.) format. Should be >= 2020"
year: Int!
"Number 1 - 12"
month: Int!
): PayrollSlipResponse! @authcreateAccountByVerifyCode — สมัครสมาชิก (public)
createAccountByVerifyCode(
verificationCode: String!
citizenId: String!
employeeCode: String!
org: String!
birthDate: Date!
password: String!
confirmPassword: String!
phoneNumber: String
otp: OtpInput
): AuthResponse!createLeaveDocument — สร้างเอกสารลา (public — ไม่มี @auth)
createLeaveDocument(
empCode: String
documentDate: Date!
leaveTypeId: ID!
leaveStartDate: Date!
leaveEndDate: Date!
durationType: DurationType!
leaveDay: Float!
description: String!
approveStatus: ApproveStatus!
shiftId: String
leaveStartTime: Int
leaveEndTime: Int
leaveHour: Int
documentNo: String
): LeaveDocumentCreatedResponsesearchUser — admin bypass
searchUser(
empCode: String!
firstName: String!
lastName: String!
identityCard: String!
userIds: [ID!]
): SearchUserResponse! @auth(accessLevel: 4, ignoreAuth: true)ignoreAuth: true ข้าม JWT check — ตรวจแค่ apiKey level 4 (admin)
@auth coverage — เบาบางมาก
จาก 83 operations:
| สถานะ | จำนวน |
|---|---|
มี @auth | 14 |
ไม่มี @auth (public) | 69 |
Public operations ที่น่าตกใจ
- ทั้ง hrmi.js (26 ops) — leave/medical/approver — เป็น public ทั้งหมด
- fund.js —
@authถูก comment ออก →fetchEmployeeProvidentFund,fetchEmployeeSalaryกลายเป็น public - registration.js —
createAccountByVerifyCode,requestOtp,isAccountExists— public
ผล: ใครมี apiKey ของ cu-central-api สามารถสร้าง/ลบเอกสารลาของ empCode ใด ๆ ได้
REST endpoints — 5 ตัว
| Method | Path | บรรทัด | Auth | ความหมาย |
|---|---|---|---|---|
| POST | /auth/signin | 21 | apiKey | Login (AD bind) |
| POST | /auth/signinbypass | 31 | apiKey | Login ไม่ต้อง password (dev/admin) |
| POST | /auth/token | 39 | apiKey + tokens | Refresh JWT |
| POST | /auth/signout | 59 | apiKey + accessToken | Logout (ลบ Redis session) |
| POST | /conicle/longterm | 11 | ไม่มี (magic param runLong=20k) | SFTP file transfer ไป Conicle |
POST /auth/signin — flow
1. req.body: { username, password, org, apiKey }
↓
2. checkApiKey(apiKey) → { appName, level }
↓
3. formatOrgId(org) → split เป็น org + orgUnit
↓
4. getEmployeeByEmpCode(username)
→ Sequelize query + buildOrderBySourceDb() ranking
↓
5. checkEmployeeAbleLogin
→ single ID-card rule + org-unit blacklist
↓
6. internalUsername = sha256(idCard)
adUsername = crc64(idCard:HASH_KEY)
↓
7. bindAdWithUserAndPassword(adUsername, password)
→ LDAP bind (service account + user bind)
↓
8. generateSignInToken (JWT)
+ generateUserSessionAndStore (Redis)
↓
9. Response: {
accessToken,
expiresIn: '20m',
refreshToken,
tokenType: 'Bearer',
userData: { uid, profileKey, sourceDb },
error: null,
responsedAt
}ที่มา: lib/auth/auth.js:11-98
POST /auth/token — refresh
1. req.body: { accessToken, refreshToken, apiKey }
↓
2. verifyTokenAndSession
→ decode JWT (ไม่เช็ค exp)
→ ดึง session จาก Redis
→ re-verify ด้วย audience
↓
3. เปรียบเทียบ refreshToken ที่เก็บไว้
↓
4. issue JWT ใหม่ (claims เดิม)ที่มา: lib/auth/auth.js:130-149
SourceDB sharding — เอกลักษณ์ของ cu-central-api
SourceDB คืออะไร
SourceDB เป็น column ในทุก HRMI table (emEmployee.SourceDB, emEmpWorkProfile.SourceDB, ...) บอกว่า record นั้นอยู่หน่วยงานใด:
| SourceDB value | หน่วยงาน |
|---|---|
dbHRMI_RC_B_rep | สภากาชาดไทย (สำนักงานใหญ่) |
dbHRMI_RC_C_rep | สภากาชาด — สาขา/ศูนย์ |
dbHRMI_CU_H_rep | โรงพยาบาลจุฬาลงกรณ์ |
dbHRMI_CU_SC_rep | คลินิกพิเศษ รพ.จุฬา |
dbHRMI_CU_F_rep | คณะแพทย์ (excluded) |
NO_ANY_DB | ไม่มี workflow / pre-employee |
สำคัญ — SourceDB ไม่ใช่ DB คนละตัว
SourceDB ทั้ง 5 ค่า อยู่ใน MSSQL เดียวกัน (dbHr) — SourceDB เป็น tenant discriminator column ไม่ใช่ physical sharding
SourceDB มาจากไหนในแต่ละ request
วิธีที่ 1 — จาก token → userData.sourceDb
login → formatWorkprofileFromEmployee → ส่ง sourceDb กลับ client
client → echo profileKey = "{empId}:{sourceDb}" ในทุก request
resolver → splitProfileKey(profileKey) → { empId, sourceDb }วิธีที่ 2 — จาก args ตรง ๆ
fetchEmployeeProvidentFund(empId: String, sourceDb: String): EmployeeProvidentFundตัวอย่าง resolver ที่ใช้ SourceDB
const profile = await personAndEmployeeIsMatch(userData.identityCard, profileKey)
if (profile !== null) {
const empId = profile.empId
const sourceDb = profile.sourceDb
let payrollSlip = await getPayrollSlipDetail(empId, year, month, sourceDb)
…
let employee = await getEmployeeProfile(empId, 'EmpID', sourceDb)SourceDB → where clause
if (sourceDb !== null) {
clause.where.SourceDB = sourceDb
}
return Models.viewUvi3PayslipDt.findAll(clause).then(…)SourceDB-aware ranking
ถ้า user มีหลาย profile (หลายหน่วยงาน) — server ต้องเลือก "main" profile:
export const buildOrderBySourceDb = (workProfileName = '[workingProfile]') => {
const sources = [
['dbHRMI_RC_B_rep', `${workProfileName}.[PosManageID] IS NOT NULL`],
['dbHRMI_CU_H_rep', `${workProfileName}.[PosManageID] IS NOT NULL`],
// ... ranking rules
]
return `CASE WHEN ... THEN 1 ...`
}Fallback — SourceDB ไม่ระบุ
- lib function ส่วนใหญ่รับ
sourceDb = null→ ข้ามwhere.SourceDBfilter - query จะ scan ทุก SourceDB แล้วใช้ ranking เลือกอันแรก
personAndEmployeeIsMatchเป็น trust boundary — ยืนยันว่า profileKey ที่ขอตรงกับ identityCard ของ user ที่ login
External integrations (outbound)
cu-central-api มี external dependencies หลายตัว:
1. LDAP / Active Directory
- Library:
ldapjs - Config:
AD_URL,AD_BASE_DN,AD_USERNAME,AD_PASSWORD - ใช้สำหรับ auth (bind), search user, add user, change password
2. Redis
- Library:
redis+node-redis-scan - TTL:
- Session: 1 วัน
- Profile cache: 30 นาที
- Org tree: 5 ชั่วโมง
- Search profile: 5 ชั่วโมง
3. MSSQL — 5 connections
| Connection | Env var | หน้าที่ |
|---|---|---|
db | DB_NAME | core RC data |
dbHr | DB_HR_NAME | HRMI leave/payroll |
dbHrTemp | DB_HR_DATA_LAKE_NAME | staging lake |
dbMasDb | DB_MASTER_DATA_NAME | places, org |
dbHRMI_Center_HIS | DB_HRMI_CENTER_HIS | HIS heal card |
4. HRMI HTTP API
lib/hrmi/token.js:38-59—getToken(username, password)POST${HRMI_END_POINT}/api/Authenticate/RequestToken- proxy leave/medical query ไปยัง HRMI REST service ภายนอก
5. SSO HTTP API
lib/sso/sso.js:13-40—ssoLoginPOST${SSO_END_POINT}/ssoLogin(AES-encrypted + DigiCert pinning)- ถูกเรียกใน
/auth/signinflow
6. SMTP / SMS
- SMTP:
nodemailerสำหรับ verify-code-by-email - SMS: POST
${SMS_GATEWAY_URL}สำหรับ OTP
7. Conicle SFTP
lib/conicle/conicleConverter.js+ssh2-sftp-client- trigger ผ่าน
/conicle/longtermREST endpoint
Resolver pattern
GraphQL operation (typeDefs/*.js)
↓
Resolver (resolvers/*.js)
↓
Lib function (lib/*.js)
↓
Sequelize Model (Models.<ModelName>)
↓
MSSQL (one of 5 connections)หรือ สำหรับ external service:
Resolver → lib function → HTTP call (HRMI/SSO) / LDAP bindDB access pattern
| Pattern | ใช้ตอนไหน |
|---|---|
Sequelize Model (findAll, findOne) | ส่วนใหญ่ — ผ่าน Models.<ModelName> |
Raw SQL (sequelize.query) | บาง resolver — เช่น personal.js:57,60 |
| Raw SQL fragment (ORDER BY) | buildOrderBySourceDb สำหรับ ranking |
ข้อระวังทั้งหมด
1. Auth coverage น้อย
- 69 จาก 83 operations เป็น public
- HRMI ทั้ง domain (26 ops) ไม่มี
@auth - ถ้า apiKey รั่ว — ผู้โจมตีทำอะไรได้เยอะ
2. signinbypass endpoint
- อนุญาตให้ login ไม่ต้องใช้ password
- ถ้าเปิดใน production — เป็นช่องโหว่ร้ายแรง
- ตรวจว่า env block ไว้หรือไม่
3. Hardcoded apiKey
// lib/auth/auth.controller.js:257-271
const allowedApiKeys = [
{ apiKey: 'OGJlYTEzOTBjMzk3...', level: 1 },
{ apiKey: '562B132B1E87695A...', level: 4 }
]apiKey ฝังใน source — เป็น secret ที่ commit ลง git
4. Schema typo
Subscribtion(ควรเป็นSubscription) ใน root type- ไม่ได้ใช้จริง แต่ถ้าจะ refactor ต้องระวัง
5. dbHRMI_test dead code
connector.js:117สร้าง connection นี้models/index.js:96-105import ถูก comment ออก- เป็น dead code ที่ควรลบ
เปรียบเทียบกับ vtrc-api
| ด้าน | cu-central-api | vtrc-api |
|---|---|---|
| Operations | 83 | 265 |
| TypeDefs files | 32 | 44 |
@auth coverage | 17% (14/83) | ~72% (~190/265) |
| Subscription | 0 (typo Subscribtion) | 0 |
| REST endpoints | 5 (auth) | 30 (file/PDF/export) |
| ORM | Sequelize (MSSQL) | Sequelize (MariaDB) |
| Auth | JWT + apiKey + LDAP | JWT + apiKey |
| Sharding | SourceDB (per-row) | ไม่มี |
| Docs | ไม่มี | ไม่มี |