Skip to main content

จัดการรหัสผ่าน

ภาพรวม

CMS มีเส้นทางจัดการรหัสผ่าน 4 แบบ ครอบคลุมทั้งกรณีที่ผู้ใช้ช่วยเหลือตัวเองและกรณีที่ผู้ดูแลระบบต้องเข้าไปช่วย โดยทุกเส้นทางอยู่ใต้โมดูล user ของ CMS API

กรณีการใช้งานจุดเริ่มต้นผู้ใช้งาน
ลืมรหัสผ่าน (ขอลิงก์ตั้งใหม่)หน้า /forgot-passwordผู้ใช้ทุกคน (ไม่ต้องเข้าสู่ระบบ)
ตั้งหรือรีเซ็ตรหัสผ่านจากลิงก์ในอีเมลหน้า /reset-passwordผู้ใช้ที่ได้รับลิงก์
เปลี่ยนรหัสผ่านของตัวเองmodal จากเมนูโปรไฟล์บนแถบด้านบนผู้ใช้ที่เข้าสู่ระบบแล้ว
สั่งรีเซ็ตรหัสผ่านให้ผู้ใช้อื่นตารางในหน้าจัดการผู้ใช้ผู้ดูแลระบบ

จุดที่ควรทราบเกี่ยวกับลิงก์รีเซ็ตรหัสผ่าน คือ token ในลิงก์ไม่ใช่ JWT แต่เป็นข้อมูล JSON ที่ถูกเข้ารหัสแบบ base64 จากฝั่งเซิร์ฟเวอร์ ภายในบรรจุรหัสผู้ใช้ รหัสอ้างอิง วันหมดอายุ และธง isNewAccount หน้าเว็บจะถอดรหัสและตรวจวันหมดอายุด้วยตัวเองก่อน แล้วจึงสอบถามเซิร์ฟเวอร์ว่ารหัสอ้างอิงยังใช้ได้อยู่หรือไม่

ธง isNewAccount ใช้สลับข้อความบนหน้าจอระหว่าง "สร้างรหัสผ่าน" (สำหรับผู้ใช้ใหม่ที่ผู้ดูแลระบบสร้างบัญชีให้) กับ "รีเซ็ตรหัสผ่าน" (สำหรับผู้ใช้เดิมที่ลืมรหัสผ่าน)

เกณฑ์รหัสผ่าน

หน้าตั้งรหัสผ่านใหม่และ modal เปลี่ยนรหัสผ่านใช้เกณฑ์เดียวกัน 6 ข้อ

  1. ความยาว 8 ถึง 16 ตัวอักษร
  2. มีตัวพิมพ์ใหญ่อย่างน้อยหนึ่งตัว
  3. มีตัวพิมพ์เล็กอย่างน้อยหนึ่งตัว
  4. มีอักขระพิเศษอย่างน้อยหนึ่งตัว
  5. มีตัวเลขอย่างน้อยหนึ่งตัว
  6. ประกอบด้วยอักขระภาษาอังกฤษเท่านั้น

Business Flow

1. ลืมรหัสผ่าน

  1. ผู้ใช้เปิดหน้า /forgot-password และกรอกอีเมลที่ใช้สมัคร ข้าง label มีปุ่มช่วยเหลือที่อธิบายเงื่อนไขการกรอก
  2. ปุ่มยืนยันจะถูกปิดไว้จนกว่าอีเมลที่กรอกจะอยู่ในรูปแบบที่ถูกต้อง
  3. เมื่อกดยืนยัน ระบบเรียก POST /user/forgot-password เพื่อขอให้เซิร์ฟเวอร์ส่งลิงก์ตั้งรหัสผ่านใหม่
  4. เมื่อสำเร็จ ระบบเปิด modal ยืนยันว่าได้ส่งลิงก์ไปยังอีเมลนั้นแล้ว โดยเน้นชื่ออีเมลไว้ในข้อความ กดปุ่มยืนยันแล้วจะกลับไปหน้า /login
  5. หากไม่พบอีเมลในระบบ จะแสดงข้อความผิดพลาดใต้ช่องกรอกโดยยังคงค่าที่กรอกไว้

2. ตั้งหรือรีเซ็ตรหัสผ่านจากลิงก์

  1. ผู้ใช้กดลิงก์ในอีเมล ซึ่งพาไปที่ /reset-password พร้อม token
  2. ระบบตรวจสอบ token ตามลำดับ
    • รูปแบบไม่ใช่ base64 — เปิด modal แจ้งว่าลิงก์ไม่ถูกต้อง
    • ถอดรหัสไม่สำเร็จ — ถือว่าลิงก์ใช้ไม่ได้
    • เกินวันหมดอายุที่ระบุใน token — ถือว่าลิงก์หมดอายุ โดยไม่ต้องถามเซิร์ฟเวอร์
    • ยังไม่หมดอายุ — เรียก GET /user/reset-password/{code} เพื่อยืนยันกับเซิร์ฟเวอร์ว่ารหัสอ้างอิงยังใช้ได้
  3. ระหว่างตรวจสอบจะแสดงหน้าจอกำลังโหลด หากลิงก์ใช้ไม่ได้จะแสดงหน้าจอแจ้งเตือนพร้อมปุ่มกลับไปหน้าเข้าสู่ระบบ โดยข้อความจะต่างกันตามว่าเป็นการสร้างรหัสผ่านครั้งแรกหรือการรีเซ็ต
  4. เมื่อ token ผ่านการตรวจสอบ ระบบแสดงฟอร์มตั้งรหัสผ่านที่มีช่องรหัสผ่านใหม่ ช่องยืนยัน และรายการตรวจสอบเกณฑ์ 6 ข้อที่อัปเดตตามการพิมพ์
  5. ปุ่มยืนยันจะถูกปิดไว้จนกว่าจะกรอกครบทั้งสองช่อง ไม่มีข้อผิดพลาดค้างอยู่ และผ่านเกณฑ์ครบทุกข้อ
  6. เมื่อกดยืนยัน ระบบส่งรหัสผ่านใหม่พร้อมรหัสผู้ใช้และรหัสอ้างอิงไปยัง POST /user/reset-password แล้วพากลับไปหน้า /login
  7. หากไม่สำเร็จ ระบบแปลรหัสข้อผิดพลาดจากเซิร์ฟเวอร์เป็นข้อความที่เข้าใจได้ กรณีที่พบบ่อยคือ token หมดอายุหรือไม่ถูกต้อง และการตั้งรหัสผ่านซ้ำกับรหัสผ่าน 10 ครั้งล่าสุด

3. เปลี่ยนรหัสผ่านของตัวเอง

  1. ผู้ใช้ที่เข้าสู่ระบบแล้วคลิกรูปโปรไฟล์บนแถบด้านบน แล้วเลือก "เปลี่ยนรหัสผ่าน"
  2. Modal แสดงสามช่อง ได้แก่ รหัสผ่านปัจจุบัน รหัสผ่านใหม่ และยืนยันรหัสผ่านใหม่ พร้อมการ์ดรายการตรวจสอบเกณฑ์ 6 ข้อ
  3. รายการตรวจสอบเปลี่ยนสีตามสถานะแบบเรียลไทม์ (เทาคือยังไม่เริ่มพิมพ์ เขียวคือผ่าน แดงคือไม่ผ่าน) และระบบแจ้งทันทีเมื่อรหัสผ่านยืนยันไม่ตรงกัน
  4. ปุ่มยืนยันจะเปิดใช้งานเมื่อผ่านทุกเงื่อนไข จากนั้นระบบเรียก POST /user/change-password
  5. เมื่อสำเร็จจะแสดงข้อความยืนยันและปิด modal พร้อมล้างค่าในฟอร์มทั้งหมด หากไม่สำเร็จจะแสดงข้อความผิดพลาดจากเซิร์ฟเวอร์

4. ผู้ดูแลระบบสั่งรีเซ็ตรหัสผ่านให้ผู้ใช้อื่น

  1. ในตารางหน้าจัดการผู้ใช้ แต่ละแถวมีปุ่มรูปกุญแจสำหรับสั่งรีเซ็ตรหัสผ่าน
  2. เมื่อกด ระบบจะแสดงกล่องยืนยันก่อนดำเนินการ เพื่อป้องกันการกดโดยไม่ตั้งใจ
  3. เมื่อยืนยัน ระบบเรียก POST /user/{userId}/admin-reset-password แล้วแสดงข้อความยืนยันผล
  4. เซิร์ฟเวอร์เป็นผู้ส่งอีเมลลิงก์ตั้งรหัสผ่านใหม่ให้ผู้ใช้รายนั้น ซึ่งจะเข้าสู่เส้นทางที่ 2 ต่อไป

หน้าจอและองค์ประกอบหลัก

หน้าลืมรหัสผ่าน

หน้า /forgot-password มีฟอร์มช่องเดียวคืออีเมล พร้อมตรรกะควบคุมสถานะปุ่มแยกไว้เป็นฟังก์ชันเฉพาะ และใช้ modal ยืนยันร่วมกับหน้าอื่นในระบบ ข้อความใน modal รองรับการเน้นข้อความและการขึ้นบรรทัดใหม่

หน้าตั้งรหัสผ่านใหม่

หน้า /reset-password แบ่งเป็นสามสถานะการแสดงผล คือ กำลังตรวจสอบ ลิงก์ใช้ไม่ได้ และพร้อมตั้งรหัสผ่าน โดย container เป็นผู้ถอดรหัส token ตรวจวันหมดอายุ สอบถามเซิร์ฟเวอร์ และแปลรหัสข้อผิดพลาด

คอมโพเนนต์รายการตรวจสอบรหัสผ่าน

คอมโพเนนต์ PasswordChecklist (src/components/common/password/password-checklist.tsx) รวมช่องรหัสผ่าน ช่องยืนยัน และรายการตรวจสอบ 6 ข้อไว้ด้วยกัน พร้อมส่งสถานะ "ผ่านเกณฑ์แล้วหรือยัง" กลับไปให้หน้าที่เรียกใช้ควบคุมปุ่มยืนยัน คอมโพเนนต์นี้ถูกใช้ร่วมกันระหว่างหน้าตั้งรหัสผ่านใหม่และ modal เปลี่ยนรหัสผ่าน

Modal นี้ถูกเรียกจากเมนูโปรไฟล์บนแถบด้านบน จึงเข้าถึงได้จากทุกหน้าที่มีแถบด้านบน และตรวจสอบเงื่อนไขทั้งหมดแบบเรียลไทม์ระหว่างพิมพ์

ปลายทาง API ที่เกี่ยวข้อง

ฟังก์ชันด้านรหัสผ่านทั้งหมดรวมอยู่ในบริการจัดการผู้ใช้ (src/services/user-management.service.ts) ภายใต้ prefix user

การทำงานปลายทาง
ขอลิงก์ตั้งรหัสผ่านใหม่POST /user/forgot-password
ตรวจสอบรหัสอ้างอิงในลิงก์GET /user/reset-password/{code}
บันทึกรหัสผ่านใหม่จากลิงก์POST /user/reset-password
เปลี่ยนรหัสผ่านของตัวเองPOST /user/change-password
ผู้ดูแลระบบสั่งรีเซ็ตรหัสผ่านPOST /user/{userId}/admin-reset-password

จุดเชื่อมต่อกับฟีเจอร์อื่น

  • เข้าสู่ระบบ — ทุกเส้นทางจบลงด้วยการกลับมาที่หน้า /login
  • จัดการผู้ใช้ — การสร้างผู้ใช้ใหม่จะทำให้ผู้ใช้ได้รับลิงก์ที่มีธง isNewAccount ซึ่งเปลี่ยนหน้าจอเป็นโหมด "สร้างรหัสผ่าน" และปุ่มรีเซ็ตรหัสผ่านของผู้ดูแลระบบก็อยู่ในตารางหน้านี้
  • สมัครใช้งาน — ใช้เกณฑ์ตรวจสอบรหัสผ่านคนละชุดกับหน้าตั้งรหัสผ่านใหม่ จึงควรระวังเมื่อสื่อสารกับผู้ใช้
  • สิทธิ์การเข้าถึง — หน้าลืมรหัสผ่านและหน้าตั้งรหัสผ่านใหม่เป็นหน้าสาธารณะ ส่วน modal เปลี่ยนรหัสผ่านต้องเข้าสู่ระบบแล้ว และปุ่มรีเซ็ตของผู้ดูแลระบบอยู่ในหน้าที่ต้องมีสิทธิ์ดูข้อมูลผู้ใช้
  • ข้อสังเกตทางเทคนิค — ฟังก์ชันด้านรหัสผ่านทั้งหมดเรียกผ่าน axios instance กลาง รวมถึงฟังก์ชันที่ใช้จากหน้าสาธารณะ ซึ่งหมายความว่าหากเซิร์ฟเวอร์ตอบ HTTP 401 ตัวดักจับกลางจะบังคับออกจากระบบ

รายละเอียดฝั่ง Backend (CMS API)

จุดที่มักทำให้สับสนคือ route ทั้งหมดของเรื่องรหัสผ่านอยู่ใต้ /api/user/... แต่ โค้ดที่ทำงานจริงอยู่ในโมดูล auth ไม่ใช่โมดูล user เพราะตอนย้ายระบบมาจากเวอร์ชันเดิม เส้นทางรหัสผ่านถูกยกมาพร้อมกับ auth ทั้งชุด ส่วนโมดูลประวัติรหัสผ่านแยกออกมาเป็นโมดูลที่ไม่มี route ของตัวเองเลย มีไว้ให้โมดูลอื่น (จัดการผู้ใช้ และสมัครใช้งาน) เรียกใช้เท่านั้น

Rate limit ที่บังคับจริงบนแต่ละ endpoint

นี่คือข้อมูลที่มองจากฝั่งหน้าเว็บไม่เห็น และเป็นสาเหตุที่การกดซ้ำเร็ว ๆ จะถูกปฏิเสธ

Endpointข้อจำกัดต้องมี token
POST /api/user/forgot-password3 ครั้ง ต่อ 10 วินาทีไม่ต้อง
POST /api/user/reset-password3 ครั้ง ต่อ 10 วินาทีไม่ต้อง
GET /api/user/reset-password/:code3 ครั้ง ต่อ 10 วินาทีไม่ต้อง
POST /api/user/change-password5 ครั้ง ต่อ 60 วินาทีต้องผ่าน JWT guard

สิ่งที่ backend ทำในแต่ละเส้นทาง

ลืมรหัสผ่าน

  • สร้างโค้ดสุ่ม 16 ตัวอักษรแบบ hex (จาก 8 bytes ของ generator ระดับ cryptographic) แล้วเขียนแถวใหม่ลงตาราง forgot_password
  • ตั้ง วันหมดอายุเป็น 1 วันนับจากเวลาที่ขอ โดยตัดเศษวินาทีออก เพื่อให้พฤติกรรมตรงกับระบบเวอร์ชันก่อนหน้า
  • ข้อความในอีเมลจัดรูปแบบเวลาหมดอายุตามเขตเวลา Asia/Bangkok เช่น ระบุเป็น (UTC+7) ท้ายประโยค ไม่ได้ใช้เวลาท้องถิ่นของผู้รับ
  • ถ้าหาผู้ใช้จากอีเมลไม่พบ จะตอบกลับด้วย error code AUT_101 ซึ่งหมายถึงไม่พบผู้ใช้สำหรับการรีเซ็ตรหัสผ่าน — นี่คือที่มาของข้อความผิดพลาดใต้ช่องกรอกอีเมล

ตรวจโค้ดและตั้งรหัสผ่านใหม่

  • GET /api/user/reset-password/:code มีหน้าที่ตอบว่าโค้ดนั้น ยังไม่หมดอายุและยังไม่ถูกใช้ — จุดนี้สำคัญเพราะหน้าเว็บตรวจวันหมดอายุจาก token ได้เอง แต่ตรวจไม่ได้ว่าโค้ดถูกใช้ไปแล้วหรือยัง
  • ตอนบันทึกรหัสผ่านใหม่ backend ทำ 3 อย่างในคำขอเดียว: เข้ารหัสด้วย bcrypt แล้วอัปเดตช่องรหัสผ่านในตาราง user → บันทึกลงตาราง password_history → ทำเครื่องหมายว่าโค้ดนั้นถูกใช้แล้ว
  • เพราะโค้ดถูกทำเครื่องหมายว่าใช้แล้ว ลิงก์รีเซ็ตหนึ่งใบใช้ได้ครั้งเดียว การกดลิงก์เดิมซ้ำหลังตั้งรหัสผ่านสำเร็จจะได้หน้าจอลิงก์ใช้ไม่ได้

เปลี่ยนรหัสผ่านขณะล็อกอินอยู่

  • ตรวจรหัสผ่านเดิมก่อนเสมอ แล้วจึงตรวจว่ารหัสผ่านใหม่ไม่ซ้ำกับที่บันทึกไว้ในประวัติ ถ้าซ้ำจะถูกปฏิเสธ — นี่คือกลไกเดียวกับที่ทำให้หน้าตั้งรหัสผ่านใหม่ขึ้นข้อความว่าห้ามใช้รหัสผ่านเดิมซ้ำ
  • เมื่อสำเร็จจะบันทึกรหัสผ่านใหม่ลงประวัติต่อทันที ทำให้รหัสผ่านที่เพิ่งเปลี่ยนกลายเป็นค่าที่ห้ามใช้ซ้ำในครั้งถัดไปด้วย

ข้อสังเกตที่ควรทราบ

  • การเปลี่ยนรหัสผ่านไม่ได้ล้าง session — backend อัปเดตรหัสผ่านและประวัติ แต่ไม่ได้ลบ session ใน Redis ให้อัตโนมัติ ดังนั้นอุปกรณ์อื่นที่ถือ token เดิมไว้จะยังใช้งานต่อได้จนกว่า token จะหมดอายุหรือถูกเพิกถอน
  • ประวัติรหัสผ่านถูกใช้ร่วมกันหลายเส้นทาง — ทั้งตอนสมัครใช้งาน ตอนผู้ดูแลสร้างผู้ใช้ใหม่ ตอนรีเซ็ต และตอนเปลี่ยนรหัสผ่านเอง ล้วนเขียนลงตารางเดียวกัน กฎห้ามใช้ซ้ำจึงครอบทุกช่องทางพร้อมกัน
  • อีเมลทุกฉบับส่งผ่าน Mailer กลางของระบบ ที่ตั้งค่าจาก env กลุ่ม MAIL_SMTP_* ถ้าตั้งค่าผิด อาการที่เห็นคือ API ตอบสำเร็จแต่ผู้ใช้ไม่ได้รับอีเมล
  • rate limit นับแบบ 3 ครั้งต่อ 10 วินาทีถือว่าค่อนข้างแคบ สำหรับหน้าที่ผู้ใช้อาจกดผิดหลายครั้ง ทีมสนับสนุนที่ทดสอบเส้นทางนี้ติด ๆ กันควรเผื่อเวลารอระหว่างรอบ