Skip to content

9.5 · ERD — Identity & Access

โดเมน Identity คือส่วนที่จัดการว่า "ใคร login เข้ามา, มี session อะไรบ้าง, มี role อะไร" เป็น foundation ของทุก domain อื่น — ถ้า data model ตรงนี้ผิด ทุก flow พังตาม

บทนี้ map ตารางทั้งหมด, relation, และจุดที่ design ไม่ relational จริง (RBAC เก็บใน JSON)


Tables ใน domain

ตารางPKschemaหน้าที่
UsersuserId UUIDvtrc-centralize (db2)identity หลัก + refId map ไป HRMI
UserDivisionuserDivisionId UUIDvtrc-centralize (db2)division/level ของ user (multi-profile)
SessionssessionId UUIDvtrc (db)refresh token + current session state
LoginSessionUUIDvtrcpre-login / login-flow session (เก็บ empCode ใน JSON — ใช้ตอน sign-in)
CaptchaSessionUUIDvtrccaptcha challenge state
AuthSessionUUIDvtrcmobile bypass session (publicKey + userId — ไม่ใช่ emp-code verify)
RolesroleId UUIDvtrcrole definition
RoleAccessroleAccessId UUIDvtrcmenu access per role

ERD — ASCII vertical flow

                    vtrc-centralize schema (db2)
   ┌─────────────────────────────────────────────────────────────┐
   │                                                              │
   │  Users                                                       │
   │  ├── userId (PK, UUID)                                       │
   │  ├── personId (UUID)                                         │
   │  ├── param (STRING — empCode/link key)                       │
   │  ├── role (TEXT, JSON-encoded array via getter/setter)       │
   │  ├── token (TEXT — current JWT)                              │
   │  ├── refreshToken (TEXT)                                     │
   │  ├── clientInfo (TEXT)                                       │
   │  ├── refId (TEXT — link to HRMI EmpCode)                    │
   │  └── sourceDb (STRING — HRMI SourceDB id เช่น `dbHRMI_RC_B_rep`, `dbHRMI_CU_SC_rep`; ไม่ใช่ enum `'vtrc'\|'nbc'`) │
   │         │                                                    │
   │         │ hasMany                                            │
   │         ▼                                                    │
   │  UserDivision                                                │
   │  ├── userDivisionId (PK, UUID)                               │
   │  ├── userId (FK → Users.userId)                              │
   │  ├── level (STRING)                                          │
   │  ├── idLevel (STRING — UUID ของ division ฝั่ง HRMI)           │
   │  ├── profileKey (STRING)                                     │
   │  ├── appId (STRING)                                          │
   │  └── profileLevel (ENUM 'MAIN' | 'SUB')                      │
   │                                                              │
   └─────────────────────────────────────────────────────────────┘

                              │ (no FK — cross-schema, join ผ่าน refId/EmpCode)

                    vtrc schema (db)
   ┌─────────────────────────────────────────────────────────────┐
   │                                                              │
   │  Sessions                                                    │
   │  ├── sessionId (PK, UUID)                                    │
   │  ├── userId (UUID — logical FK, ไม่ใช่ DB constraint)         │
   │  ├── refreshToken (STRING)                                   │
   │  ├── userSession (TEXT, JSON-encoded)                        │
   │  ├── role (STRING)                                           │
   │  ├── expireDate (DATE)                                       │
   │  ├── notificationExpireDate (DATE)                           │
   │  ├── client (ENUM 'WEB' | 'MOBILE' | 'WEB_ADMIN')            │
   │  ├── currentProfile (TEXT)                                   │
   │  ├── tokenCD (TEXT — Chula SSO token cached)                 │
   │  └── refreshTokenCD (STRING)                                 │
   │                                                              │
   │  Roles                                                       │
   │  ├── roleId (PK, UUID)                                       │
   │  ├── roleLabel (STRING)                                      │
   │  ├── roleName (STRING)                                       │
   │  ├── description (TEXT)                                      │
   │  ├── isActive (BOOLEAN)                                      │
   │  └── updateBy (UUID)                                         │
   │         │                                                    │
   │         │ hasMany                                            │
   │         ▼                                                    │
   │  RoleAccess                                                  │
   │  ├── roleAccessId (PK, UUID)                                 │
   │  ├── roleId (FK → Roles.roleId)                              │
   │  ├── nameTH (STRING)                                         │
   │  ├── nameEN (STRING)                                         │
   │  ├── menuKey (STRING — เชื่อมกับ menu definition)              │
   │  └── orderNo (STRING)                                        │
   │                                                              │
   └─────────────────────────────────────────────────────────────┘

Relation definitions (จาก code)

Users → UserDivision

63:65:vtrc-api/api/src/models/centralize/user/user.js
  User.associate = (models) => {
    models.User.hasMany(models.UserDivision, { as: 'userUserDivision', foreignKey: 'userId', onDelete: 'RESTRICT', onUpdate: 'RESTRICT' })
  }

one User → many UserDivision (multi-profile user — พนักงานที่มีหลาย position)

Role → RoleAccess

44:46:vtrc-api/api/src/models/core/role/role.js
  Role.associate = (models) => {
    models.Role.hasMany(models.RoleAccess, { as: 'roleAccess', foreignKey: 'roleId', onDelete: 'RESTRICT', onUpdate: 'RESTRICT', hooks: true })
  }
47:49:vtrc-api/api/src/models/core/role/roleAccess.js
  RoleAccess.associate = (models) => {
    models.RoleAccess.belongsTo(models.Role, { as: 'role', foreignKey: 'roleId' })
  }

one Role → many RoleAccess (แต่ละ role เข้าถึง menu หลายอัน)

Session → User — ไม่มี relation จริง

66:69:vtrc-api/api/src/models/core/session/session.js
  // associate
  Session.associate = (models) => {

  }

Session.associate ว่างเปล่า — Session ไม่ได้ declare belongsTo User เพราะ Session อยู่ใน vtrc schema ส่วน User อยู่ใน vtrc-centralize schema ที่ Sequelize ไม่ cross-database relation ได้

ผลกระทบ — join Session กับ User ต้องทำสอง query แล้ว merge ใน memory, หรือใช้ raw query cross-schema


RBAC — JSON-encoded ไม่ใช่ relation

19:28:vtrc-api/api/src/models/centralize/user/user.js
    role: {
      type: Sequelize.TEXT,
      allowNull: false,
      get: function () {
        return JSON.parse(this.getDataValue('role'))
      },
      set: function (value) {
        this.setDataValue('role', JSON.stringify(value))
      },
    },

Users.role เก็บเป็น JSON string ใน TEXT column — ไม่ใช่ FK ไปที่ Roles

ตัวอย่างค่าใน DB:

json
["ADMIN", "STAFF"]

ผลกระทบของการเก็บแบบนี้

ปัญหารายละเอียด
audit "ใครมี role X" ยากต้อง SELECT * FROM Users แล้ว parse JSON ทุก row ในแอป
join กับ Roles table ไม่ได้ไม่มี FK ทำให้หาชื่อ role ต้อง query สองครั้ง
ลบ role แล้วข้อมูลค้างถ้าลบ role STAFF จาก Roles table, Users ที่มี ["STAFF"] ใน JSON ยังคงมีค่าเดิม
ไม่ enforce ค่าที่ validใส่ ["ANYTHING"] ได้ — DB ไม่ validate
index ไม่ได้ไม่สามารถ WHERE role = 'STAFF' ได้โดยตรง

ทำไมถึงเป็นแบบนี้

เดาว่าทีมอยากให้ user หนึ่งคนมีได้หลาย role (multi-role) โดยไม่ต้องสร้าง join table แยก — เป็น shortcut ที่ทำงานได้แต่ trade-off คือ audit / migration ยาก

Roles กับ RoleAccess table มีอยู่จริง — แต่ใช้แค่ฝั่ง backoffice สำหรับจัดการ menu access ไม่ใช่การ map user → role


ทำไมมี Session table แยก

Session เก็บ state ที่ runtime ต้องการ:

  • refreshToken — เพื่อ rotate โดยไม่ต้องให้ user login ใหม่
  • userSession (JSON) — cache data ที่ fetch บ่อย เช่น profile, division, เพื่อลด query ไป HRMI
  • client (ENUM) — แยก WEB / MOBILE / WEB_ADMIN เพื่อ limit จำนวน device per user
  • expireDate — TTL ของ session
  • tokenCD / refreshTokenCD — cache Chula SSO token เพื่อไม่ต้อง re-auth กับ AD ทุกครั้ง

Session เป็น สะพานระหว่าง vtrc กับ Chula — เพราะ SSO token ของ Chula ถูก cache ไว้ที่นี่


Multi-profile user

พนักงานบางคนมีหลาย position (เช่น ทำงานที่กอง A และกอง B) → มีหลาย UserDivision row

javascript
// User u123 อาจจะมี:
UserDivision { userDivisionId: 'd1', userId: 'u123', level: '2', idLevel: 'div-A', profileKey: 'p1', profileLevel: 'MAIN' }
UserDivision { userDivisionId: 'd2', userId: 'u123', level: '3', idLevel: 'div-B', profileKey: 'p2', profileLevel: 'SUB' }

profileLevel: 'MAIN' คือ profile หลักที่ user จะเห็นตอน login, 'SUB' คือ profile รองที่ user เลือก switch ได้

ผลกระทบ — resolver ที่ query ข้อมูล user ต้องรับ profileKey parameter เพื่อเลือก division context


Cross-schema bridge — ไม่มี FK

vtrc-centralize.Users.userId  ←──→  vtrc.Sessions.userId
                                       (logical FK, no DB constraint)
                                       
vtrc-centralize.Users.refId   ←──→  dbHRMI_Center.Employee.EmpCode
                                       (string mapping, no constraint)

Sequelize ไม่ cross-database relation ได้ → ทุก reference ข้าม schema เป็น "logical FK" ที่แอป enforce เอง

ผลกระทบ:

  • DB ไม่บังคับ referential integrity → foreign row หาย ข้อมูลนี้ orphan ได้
  • ORM ไม่ช่วย → ทุก query ที่ join ข้าม schema ต้อง manual
  • migration / cleanup ยาก — ไม่รู้ว่า Session.userId ไหนยัง valid

เคล็ดลับตอนทำงานจริง

ถ้าจะ query "user คนนี้มี role อะไรบ้าง"

อย่า query Users.role โดยตรง — ใช้ Session ปัจจุบันแทน เพราะ Session.role เป็นค่าที่ resolved แล้วจาก user's role JSON + current profile

ถ้าเจอ user ที่หน้า backoffice ไม่แสดง menu

  • เช็ก Roles.isActive — ถ้า role ถูกปิด menu จะหาย
  • เช็ก RoleAccess.menuKey — ถ้า menu key ไม่ตรงกับ config.js#API_MENUS ฝั่ง backoffice → menu ไม่ขึ้น

ถ้าเจอ session ที่ userId ไม่ตรงกับ user จริง

Session.userId เป็น logical FK → ถ้า user ถูกลบจาก vtrc-centralize.Users session ยังค้างอยู่ (orphan) ต้อง manual cleanup

อย่า trust Users.role JSON

ค่าใน JSON อาจจะมี role ที่ลบจาก Roles table ไปแล้ว → enforce RBAC ฝั่ง controller ทุกครั้ง อย่าเชื่อ JSON เป็น source of truth


ไป 8.6 ERD — Leave & Approvals