Skip to content

11.5 · cu-central-api GraphQL + REST

บทนี้รวบรวม API surface ของ cu-central-api — 83 GraphQL operations + 5 REST auth endpoints — พร้อมเน้น SourceDB sharding ที่เป็นเอกลักษณ์ของ service นี้


ภาพรวม

ตัวเลขค่า
TypeDefs files32
Resolver files27
GraphQL operations~83 (70 Query + 13 Mutation)
Subscription0
REST endpoints5 (auth + conicle)
Custom scalarsเหมือน vtrc-api (Date, JSON, ...)
Custom directives1 (@auth(requires, accessLevel, ignoreAuth))

Root schema

4:19:typeDefs/root.js
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

DomainTypeDefs fileQMTotal@auth
HRMI Leave/Medicalhrmi.js19726ไม่มี (public)
Registration/Accountregistration.js1781
Two-way (admin)twoWay.js606ไม่มี
Personal profilepersonal.js5052
Employeeemployee.js5053
Fund/Providentfund.js4150 (commented)
User verificationuserVerification.js303ไม่มี
Reference datareferenceData.js303ไม่มี
Master datamasterData.js224ไม่มี
Child tuitionchildTuition.js404ไม่มี
Payroll slippayrollSlip.js2022
Authenticationauthentication.js2021
Study leave requeststudyLeaveRequest.js202ไม่มี
Bank bookbank.js202ไม่มี
HR enumhrEnum.js101
HR Mutation: Taxeshr/hrTaxes.js0111
HR Mutation: Phonehr/hrPhoneNumber.js0111
HR Mutation: Addresshr/hrAddress.js0111
Salary certsalaryCert.js101ไม่มี
Compensationcompensation.js1011
Finance typefinance.js1011
Familyfamily.js1011
Time attendancetimeAttendance.js1011
ProbationhrPerProbation.js101ไม่มี
Search profile (admin)searchProfile.js101bypass
SSO non-employeesso.js101ไม่มี
Stat logstatLog.js101ไม่มี
Notification (SMS)notification.js011ไม่มี
Organizationorganization.js000(types only)
Taxtax.js000(types only)
รวม32 files~70~13~8314 fields

Operations เด่น — signature ตัวอย่าง

employeeProfile — ข้อมูลพนักงาน

1:10:typeDefs/employee.js
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 — สลิปเงินเดือน

5:16:typeDefs/payrollSlip.js
payrollSlipDetail(
  profileKey: String!
  "Required yyyy (A.D.) format. Should be >= 2020"
  year: Int!
  "Number 1 - 12"
  month: Int!
): PayrollSlipResponse! @auth

createAccountByVerifyCode — สมัครสมาชิก (public)

18:32:typeDefs/registration.js
createAccountByVerifyCode(
  verificationCode: String!
  citizenId: String!
  employeeCode: String!
  org: String!
  birthDate: Date!
  password: String!
  confirmPassword: String!
  phoneNumber: String
  otp: OtpInput
): AuthResponse!

createLeaveDocument — สร้างเอกสารลา (public — ไม่มี @auth)

111:126:typeDefs/hrmi.js
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
): LeaveDocumentCreatedResponse

searchUser — admin bypass

5:12:typeDefs/searchProfile.js
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:

สถานะจำนวน
มี @auth14
ไม่มี @auth (public)69

Public operations ที่น่าตกใจ

  • ทั้ง hrmi.js (26 ops) — leave/medical/approver — เป็น public ทั้งหมด
  • fund.js@auth ถูก comment ออก → fetchEmployeeProvidentFund, fetchEmployeeSalary กลายเป็น public
  • registration.jscreateAccountByVerifyCode, requestOtp, isAccountExists — public

ผล: ใครมี apiKey ของ cu-central-api สามารถสร้าง/ลบเอกสารลาของ empCode ใด ๆ ได้


REST endpoints — 5 ตัว

MethodPathบรรทัดAuthความหมาย
POST/auth/signin21apiKeyLogin (AD bind)
POST/auth/signinbypass31apiKeyLogin ไม่ต้อง password (dev/admin)
POST/auth/token39apiKey + tokensRefresh JWT
POST/auth/signout59apiKey + accessTokenLogout (ลบ Redis session)
POST/conicle/longterm11ไม่มี (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 ตรง ๆ

graphql
fetchEmployeeProvidentFund(empId: String, sourceDb: String): EmployeeProvidentFund

ตัวอย่าง resolver ที่ใช้ SourceDB

28:55:resolvers/payrollSlip.js
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

35:38:lib/payrollSlip.js
if (sourceDb !== null) {
  clause.where.SourceDB = sourceDb
}
return Models.viewUvi3PayslipDt.findAll(clause).then(…)

SourceDB-aware ranking

ถ้า user มีหลาย profile (หลายหน่วยงาน) — server ต้องเลือก "main" profile:

181:211:lib/employeeProfile.js
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.SourceDB filter
  • 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

ConnectionEnv varหน้าที่
dbDB_NAMEcore RC data
dbHrDB_HR_NAMEHRMI leave/payroll
dbHrTempDB_HR_DATA_LAKE_NAMEstaging lake
dbMasDbDB_MASTER_DATA_NAMEplaces, org
dbHRMI_Center_HISDB_HRMI_CENTER_HISHIS heal card

4. HRMI HTTP API

  • lib/hrmi/token.js:38-59getToken(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-40ssoLogin POST ${SSO_END_POINT}/ssoLogin (AES-encrypted + DigiCert pinning)
  • ถูกเรียกใน /auth/signin flow

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/longterm REST 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 bind

DB 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

js
// 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-105 import ถูก comment ออก
  • เป็น dead code ที่ควรลบ

เปรียบเทียบกับ vtrc-api

ด้านcu-central-apivtrc-api
Operations83265
TypeDefs files3244
@auth coverage17% (14/83)~72% (~190/265)
Subscription0 (typo Subscribtion)0
REST endpoints5 (auth)30 (file/PDF/export)
ORMSequelize (MSSQL)Sequelize (MariaDB)
AuthJWT + apiKey + LDAPJWT + apiKey
ShardingSourceDB (per-row)ไม่มี
Docsไม่มีไม่มี

ขั้นตอนถัดไป

ไป บท 10.6 centralize-api (NestJS) REST + Swagger