Skip to content

14.7 User & session management

บทนี้รวบรวมการจัดการ user และ session — revoke JWT, บังคับ logout, reset password, จัดการ device key

โครงสร้าง session

มีสองชั้นที่ต้องแยก:

A) vtrc-api — MariaDB Sessions

vtrc-api เก็บ session ในตาราง Session (vtrc-api/api/src/models/core/session/session.js:2):

FieldTypeคำอธิบาย
sessionIdUUID (PK)รหัสเฉพาะของ session
refreshTokenSTRINGrefresh token ที่ใช้ขอ JWT ใหม่
userSessionTEXT (JSON)เก็บข้อมูล user ที่ login รวม jwtId
userIdUUIDรหัส user
roleSTRINGrole ปัจจุบัน
expireDateDATEวันหมดอายุ
clientENUMWEB / MOBILE / WEB_ADMIN
currentProfileTEXTprofileKey ปัจจุบัน ([empId]:[sourceDb])
tokenCDTEXTJWT ของ cu-central-api

การ lookup session ปัจจุบันใช้ userSession LIKE '%jwtId%' (vtrc-api/api/src/lib/repositories/session/session.js:31-33) — เป็น full table scan (PERF-3 ใน audit)

B) cu-central-api — Redis key sess:{userUid}_{sessionId}

cu-central เก็บ session ใน Redis ด้วย prefix sess (main-api/src/lib/redis.controller.js) และรูปแบบ:

sess:{userUid}_{sessionId}

สร้างจาก buildSessionPattern ใน main-api/src/lib/session/session.js — TTL ตาม prefix 1d

การ force logout ฝั่ง cu-central ต้องลบ key sess:* ที่ Redis ของ cu-central (Volume 13.6) ไม่ใช่แค่แถวใน vtrc Sessions

การดู session ที่ active

ดู session ของ user เฉพาะ

sql
USE vtrc;

-- หา userId จาก empcode หรือ username
SELECT id, username, empCode FROM Users WHERE empCode = 'EMP001';

-- ดู session ทั้งหมดของ user
SELECT 
    sessionId, 
    client, 
    role, 
    expireDate, 
    createdAt, 
    updatedAt,
    JSON_EXTRACT(userSession, '$.jwtId') AS jwtId
FROM Sessions 
WHERE userId = '<user-uuid>'
ORDER BY createdAt DESC;

ดูจำนวน session ที่ active

sql
SELECT 
    client,
    COUNT(*) AS active_sessions
FROM Sessions 
WHERE expireDate > NOW()
GROUP BY client;

ดู session ที่หมดอายุแล้วแต่ยังไม่ถูกลบ

sql
SELECT COUNT(*) FROM Sessions WHERE expireDate < NOW();

การ revoke JWT (force logout เฉพาะ session)

sql
-- ลบ session เฉพาะ sessionId
DELETE FROM Sessions WHERE sessionId = '<session-uuid>';

หลังลบ — JWT ของ session นั้นจะไม่สามารถ refresh ได้อีก แต่ JWT ปัจจุบันยังใช้ได้จนกว่าจะหมดอายุ (15 นาที default)

การ force logout ทุก session ของ user

sql
-- ลบ session ทั้งหมดของ user
DELETE FROM Sessions WHERE userId = '<user-uuid>';

ผู้ใช้จะถูก logout จากทุกอุปกรณ์ — ทั้งเว็บและมือถือ

การ force logout ทุกคน (emergency)

sql
-- เก็บ backup ก่อน
CREATE TABLE Sessions_backup_20250711 AS SELECT * FROM Sessions;

-- ลบ session ทั้งหมด
TRUNCATE TABLE Sessions;

ผลกระทบ: ทุกผู้ใช้ถูก logout ทันที รวม admin ที่กำลังใช้งาน — ใช้เฉพาะในกรณีวิกฤตจริง ๆ เช่น suspected credential leak

การดู login history

ตาราง loginSession (vtrc-api/api/src/models/core/session/loginSession.js) เก็บประวัติการ login:

sql
SELECT * FROM loginSessions 
WHERE userId = '<user-uuid>'
ORDER BY createdAt DESC
LIMIT 20;

การ reset password

ผ่านหน้า forget password (ปกติ)

ผู้ใช้ทำเองผ่าน https://vtrc.redcross.or.th/forget-password:

  1. กรอก identityCard + email
  2. รับ OTP (เก็บใน MariaDB ตาราง OTP ของ cu-central, TTL 3 นาที — ไม่ใช่ Redis; vtrc-api proxy requestOtpCD)
  3. กรอก OTP + password ใหม่
  4. cu-central-api เขียน password ใหม่ไปยัง LDAP/AD

ผ่านการ intervention admin (กรณีผู้ใช้ไม่สามารถทำเอง)

ต้องประสานงานกับผู้ดูแล LDAP/AD ที่ on-prem เพราะ vtrc-api ไม่ได้เก็บ password โดยตรง

ขั้นตอน:

  1. ผู้ดูแล LDAP รีเซ็ต password ใน AD ให้ผู้ใช้
  2. ผู้ใช้ login ด้วย password ใหม่ที่ได้รับ
  3. ระบบจะบังคับให้เปลี่ยน password ในการ login ครั้งแรก

กรณี bypass — mobile PIN

มือถือมี PIN bypass สำหรับ login บางกรณี — รายละเอียดใน Volume 9 บทที่ 2

ถ้าต้องการลบ PIN ของมือถือ:

sql
-- ดูว่ามี field ที่เก็บ PIN ใน Users หรือไม่
DESCRIBE Users;

field ที่เกี่ยวข้องต้องตรวจสอบจาก model จริง

การจัดการ role

role ของ user เก็บในตาราง RoleAccess ที่อ้างถึงตาราง Role:

sql
-- ดู role ทั้งหมดของ user
SELECT r.roleName, ra.menuCode, ra.accessLevel
FROM RoleAccesses ra
JOIN Users u ON ra.userId = u.id
JOIN Roles r ON ra.roleId = r.id
WHERE u.empCode = 'EMP001';

เพิ่ม role ให้ user

sql
-- หา roleId ที่ต้องการ
SELECT id, roleName FROM Roles WHERE roleName = 'APPROVER';

-- เพิ่ม
INSERT INTO RoleAccesses (userId, roleId, menuCode, accessLevel, createdAt, updatedAt)
VALUES ('<user-uuid>', '<role-uuid>', 'LEAVE', 'WRITE', NOW(), NOW());

ลบ role

sql
DELETE FROM RoleAccesses 
WHERE userId = '<user-uuid>' AND roleId = '<role-uuid>';

การจัดการ device key

device key เก็บใน vtrc-api/api/src/config.js:42-148 — เป็น array ของ object:

javascript
{
  id: '93361f06-6f55-11eb-9439-0242ac130002',
  key: 'fd77b56f-1489-4fb1-a094-a614f0d16198',
  APP_TYPE: 'WEB',
  APP_NAME: 'VTRC-WEB'
}

ดู device key ทั้งหมด

bash
# ใน source code ของ vtrc-api
grep -A 5 'API_DEVICE_KEY' /home/dev/vtrc-api/api/src/config.js | head -50

เพิ่ม device key ใหม่

แก้ config.js โดยตรง — เพิ่ม entry ใน array prod หรือ uat:

javascript
{
  id: '<new-uuid>',
  key: '<new-uuid>',
  APP_TYPE: 'WEB',
  APP_NAME: 'NEW-APP'
}

จากนั้น restart vtrc-api ทุก replica

ลบ device key

ลบ entry ออกจาก array — restart service

คำเตือน: ถ้าลบ device key ที่มี client ใช้อยู่ client นั้นจะถูกปฏิเสธทันที

การดูแล user profile

profile ของ user ฝั่ง vtrc-api เก็บใน schema vtrc-centralize (db2):

sql
USE vtrc-centralize;

-- ดู profile ของ user
SELECT * FROM Users WHERE empCode = 'EMP001';

-- ดู organization
SELECT * FROM Organizations WHERE code = '<org-code>';

profile ที่แท้จริง (เงินเดือน, ตำแหน่ง) อยู่ใน MSSQL HRMI ที่ cu-central-api ดูแล

การ deactivate user

ไม่มี field isActive ที่ใช้งานจริงในทุก flow — ต้องตรวจสอบ model

วิธีที่ใช้บ่อย:

  1. ลบ session ทั้งหมดของ user (force logout)
  2. เปลี่ยน password ใน LDAP (ถ้าต้องการกันการ login ใหม่)
  3. ลบ role ทั้งหมดของ user

การตรวจสอบ suspicious activity

ดู login จาก IP ที่ผิดปกติ

sql
SELECT 
    userId, 
    client, 
    createdAt, 
    JSON_EXTRACT(userSession, '$.ip') AS ip
FROM loginSessions
WHERE createdAt > DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY userId, ip
HAVING COUNT(DISTINCT ip) > 3;

ดู refresh token ที่ผิดปกติ

sql
-- refresh token มากเกินไปในเวลาน้อย (ทำนาย token theft)
SELECT userId, COUNT(*) AS refresh_count
FROM Sessions
WHERE updatedAt > DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY userId
HAVING refresh_count > 20;

ปัญหาเฉพาะตัว

1. JWT_SECRET เป็นค่า default 'secret'

vtrc-api/api/src/config.js:12 — ถ้า env var ไม่ตั้ง, JWT ทุกตัวเซ็นด้วย 'secret'

ตรวจสอบ:

bash
docker exec vtrc-api-nodejs printenv JWT_SECRET

ถ้าเป็นค่าว่างหรือ 'secret' ต้องแก้ทันที

2. Session lookup เป็น full table scan

vtrc-api/api/src/lib/repositories/session/session.js:31-33 ใช้ userSession LIKE '%jwtId%' — ช้าเมื่อมี session เยอะ

ถ้าระบบช้าตอน authenticate, ลองเพิ่ม index หรือ migrate ไปเก็บ jwtId เป็น column แยก

3. LDAP bind ล้มเหลวบางครั้ง

cu-central-api เชื่อม LDAP ผ่าน extra_hosts — ถ้า network ระหว่าง GCP กับ on-prem ไม่เสถียร, login จะล้มเหลวเป็นบางครั้ง

4. JWT expire ไม่ตรงกับ session expire

JWT default expire 15 นาที (JWT_EXP = '15m') แต่ session expire อาจตั้งไว้นานกว่า — refresh token ทำงานในช่วงนี้

ถ้า refresh ล้มเหลว ผู้ใช้จะถูก logout

แนะนำ

  1. เพิ่ม audit log สำหรับการ revoke session, reset password, เปลี่ยน role
  2. สร้าง admin panel สำหรับการกระทำเหล่านี้ (ปัจจุบันทำด้วย SQL manual)
  3. เปลี่ยน device key ไปเก็บใน database แทน config.js เพื่อให้แก้ไขได้โดยไม่ต้อง deploy
  4. เพิ่มการแจ้งเตือน เมื่อมี login จาก IP ใหม่
  5. ทำ session cleanup อัตโนมัติ ผ่าน cron (ลบ session ที่หมดอายุ)