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):
| Field | Type | คำอธิบาย |
|---|---|---|
sessionId | UUID (PK) | รหัสเฉพาะของ session |
refreshToken | STRING | refresh token ที่ใช้ขอ JWT ใหม่ |
userSession | TEXT (JSON) | เก็บข้อมูล user ที่ login รวม jwtId |
userId | UUID | รหัส user |
role | STRING | role ปัจจุบัน |
expireDate | DATE | วันหมดอายุ |
client | ENUM | WEB / MOBILE / WEB_ADMIN |
currentProfile | TEXT | profileKey ปัจจุบัน ([empId]:[sourceDb]) |
tokenCD | TEXT | JWT ของ 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 เฉพาะ
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
SELECT
client,
COUNT(*) AS active_sessions
FROM Sessions
WHERE expireDate > NOW()
GROUP BY client;ดู session ที่หมดอายุแล้วแต่ยังไม่ถูกลบ
SELECT COUNT(*) FROM Sessions WHERE expireDate < NOW();การ revoke JWT (force logout เฉพาะ session)
-- ลบ session เฉพาะ sessionId
DELETE FROM Sessions WHERE sessionId = '<session-uuid>';หลังลบ — JWT ของ session นั้นจะไม่สามารถ refresh ได้อีก แต่ JWT ปัจจุบันยังใช้ได้จนกว่าจะหมดอายุ (15 นาที default)
การ force logout ทุก session ของ user
-- ลบ session ทั้งหมดของ user
DELETE FROM Sessions WHERE userId = '<user-uuid>';ผู้ใช้จะถูก logout จากทุกอุปกรณ์ — ทั้งเว็บและมือถือ
การ force logout ทุกคน (emergency)
-- เก็บ 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:
SELECT * FROM loginSessions
WHERE userId = '<user-uuid>'
ORDER BY createdAt DESC
LIMIT 20;การ reset password
ผ่านหน้า forget password (ปกติ)
ผู้ใช้ทำเองผ่าน https://vtrc.redcross.or.th/forget-password:
- กรอก identityCard + email
- รับ OTP (เก็บใน MariaDB ตาราง OTP ของ cu-central, TTL 3 นาที — ไม่ใช่ Redis; vtrc-api proxy
requestOtpCD) - กรอก OTP + password ใหม่
- cu-central-api เขียน password ใหม่ไปยัง LDAP/AD
ผ่านการ intervention admin (กรณีผู้ใช้ไม่สามารถทำเอง)
ต้องประสานงานกับผู้ดูแล LDAP/AD ที่ on-prem เพราะ vtrc-api ไม่ได้เก็บ password โดยตรง
ขั้นตอน:
- ผู้ดูแล LDAP รีเซ็ต password ใน AD ให้ผู้ใช้
- ผู้ใช้ login ด้วย password ใหม่ที่ได้รับ
- ระบบจะบังคับให้เปลี่ยน password ในการ login ครั้งแรก
กรณี bypass — mobile PIN
มือถือมี PIN bypass สำหรับ login บางกรณี — รายละเอียดใน Volume 9 บทที่ 2
ถ้าต้องการลบ PIN ของมือถือ:
-- ดูว่ามี field ที่เก็บ PIN ใน Users หรือไม่
DESCRIBE Users;field ที่เกี่ยวข้องต้องตรวจสอบจาก model จริง
การจัดการ role
role ของ user เก็บในตาราง RoleAccess ที่อ้างถึงตาราง Role:
-- ดู 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
-- หา 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
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:
{
id: '93361f06-6f55-11eb-9439-0242ac130002',
key: 'fd77b56f-1489-4fb1-a094-a614f0d16198',
APP_TYPE: 'WEB',
APP_NAME: 'VTRC-WEB'
}ดู device key ทั้งหมด
# ใน 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:
{
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):
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
วิธีที่ใช้บ่อย:
- ลบ session ทั้งหมดของ user (force logout)
- เปลี่ยน password ใน LDAP (ถ้าต้องการกันการ login ใหม่)
- ลบ role ทั้งหมดของ user
การตรวจสอบ suspicious activity
ดู login จาก IP ที่ผิดปกติ
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 ที่ผิดปกติ
-- 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'
ตรวจสอบ:
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
แนะนำ
- เพิ่ม audit log สำหรับการ revoke session, reset password, เปลี่ยน role
- สร้าง admin panel สำหรับการกระทำเหล่านี้ (ปัจจุบันทำด้วย SQL manual)
- เปลี่ยน device key ไปเก็บใน database แทน config.js เพื่อให้แก้ไขได้โดยไม่ต้อง deploy
- เพิ่มการแจ้งเตือน เมื่อมี login จาก IP ใหม่
- ทำ session cleanup อัตโนมัติ ผ่าน cron (ลบ session ที่หมดอายุ)