จัดการรหัสผ่าน
ภาพรวม
CMS มีเส้นทางจัดการรหัสผ่าน 4 แบบ ครอบคลุมทั้งกรณีที่ผู้ใช้ช่วยเหลือตัวเองและกรณีที่ผู้ดูแลระบบต้องเข้าไปช่วย โดยทุกเส้นทางอยู่ใต้โมดูล user ของ CMS API
| กรณีการใช้งาน | จุดเริ่มต้น | ผู้ใช้งาน |
|---|---|---|
| ลืมรหัสผ่าน (ขอลิงก์ตั้งใหม่) | หน้า /forgot-password | ผู้ใช้ทุกคน (ไม่ต้องเข้าสู่ระบบ) |
| ตั้งหรือรีเซ็ตรหัสผ่านจากลิงก์ในอีเมล | หน้า /reset-password | ผู้ใช้ที่ได้รับลิงก์ |
| เปลี่ยนรหัสผ่านของตัวเอง | modal จากเมนูโปรไฟล์บนแถบด้านบน | ผู้ใช้ที่เข้าสู่ระบบแล้ว |
| สั่งรีเซ็ตรหัสผ่านให้ผู้ใช้อื่น | ตารางในหน้าจัดการผู้ใช้ | ผู้ดูแลระบบ |
จุดที่ควรทราบเกี่ยวกับลิงก์รีเซ็ตรหัสผ่าน คือ token ในลิงก์ไม่ใช่ JWT แต่เป็นข้อมูล JSON ที่ถูกเข้ารหัสแบบ base64 จากฝั่งเซิร์ฟเวอร์ ภายในบรรจุรหัสผู้ใช้ รหัสอ้างอิง วันหมดอายุ และธง isNewAccount หน้าเว็บจะถอดรหัสและตรวจวันหมดอายุด้วยตัวเองก่อน แล้วจึงสอบถามเซิร์ฟเวอร์ว่ารหัสอ้างอิงยังใช้ได้อยู่หรือไม่
ธง isNewAccount ใช้สลับข้อความบนหน้าจอระหว่าง "สร้างรหัสผ่าน" (สำหรับผู้ใช้ใหม่ที่ผู้ดูแลระบบสร้างบัญชีให้) กับ "รีเซ็ตรหัสผ่าน" (สำหรับผู้ใช้เดิมที่ลืมรหัสผ่าน)
เกณฑ์รหัสผ่าน
หน้าตั้งรหัสผ่านใหม่และ modal เปลี่ยนรหัสผ่านใช้เกณฑ์เดียวกัน 6 ข้อ
- ความยาว 8 ถึง 16 ตัวอักษร
- มีตัวพิมพ์ใหญ่อย่างน้อยหนึ่งตัว
- มีตัวพิมพ์เล็กอย่างน้อยหนึ่งตัว
- มีอักขระพิเศษอย่างน้อยหนึ่งตัว
- มีตัวเลขอย่างน้อยหนึ่งตัว
- ประกอบด้วยอักขระภาษาอังกฤษเท่านั้น
Business Flow
1. ลืมรหัสผ่าน
- ผู้ใช้เปิดหน้า
/forgot-passwordและกรอกอีเมลที่ใช้สมัคร ข้าง label มีปุ่มช่วยเหลือที่อธิบายเงื่อนไขการกรอก - ปุ่มยืนยันจะถูกปิดไว้จนกว่าอีเมลที่กรอกจะอยู่ในรูปแบบที่ถูกต้อง
- เมื่อกดยืนยัน ระบบเรียก
POST /user/forgot-passwordเพื่อขอให้เซิร์ฟเวอร์ส่งลิงก์ตั้งรหัสผ่านใหม่ - เมื่อสำเร็จ ระบบเปิด modal ยืนยันว่าได้ส่งลิงก์ไปยังอีเมลนั้นแล้ว โดยเน้นชื่ออีเมลไว้ในข้อความ กดปุ่มยืนยันแล้วจะกลับไปหน้า
/login - หากไม่พบอีเมลในระบบ จะแสดงข้อความผิดพลาดใต้ช่องกรอกโดยยังคงค่าที่กรอกไว้
2. ตั้งหรือรีเซ็ตรหัสผ่านจากลิงก์
- ผู้ใช้กดลิงก์ในอีเมล ซึ่งพาไปที่
/reset-passwordพร้อม token - ระบบตรวจสอบ token ตามลำดับ
- รูปแบบไม่ใช่ base64 — เปิด modal แจ้งว่าลิงก์ไม่ถูกต้อง
- ถอดรหัสไม่สำเร็จ — ถือว่าลิงก์ใช้ไม่ได้
- เกินวันหมดอายุที่ระบุใน token — ถือว่าลิงก์หมดอายุ โดยไม่ต้องถามเซิร์ฟเวอร์
- ยังไม่หมดอายุ — เรียก
GET /user/reset-password/{code}เพื่อยืนยันกับเซิร์ฟเวอร์ว่ารหัสอ้างอิงยังใช้ได้
- ระหว่างตรวจสอบจะแสดงหน้าจอกำลังโหลด หากลิงก์ใช้ไม่ได้จะแสดงหน้าจอแจ้งเตือนพร้อมปุ่มกลับไปหน้าเข้าสู่ระบบ โดยข้อความจะต่างกันตามว่าเป็นการสร้างรหัสผ่านครั้งแรกหรือการรีเซ็ต
- เมื่อ token ผ่านการตรวจสอบ ระบบแสดงฟอร์มตั้งรหัสผ่านที่มีช่องรหัสผ่านใหม่ ช่องยืนยัน และรายการตรวจสอบเกณฑ์ 6 ข้อที่อัปเดตตามการพิมพ์
- ปุ่มยืนยันจะถูกปิดไว้จนกว่าจะกรอกครบทั้งสองช่อง ไม่มีข้อผิดพลาดค้างอยู่ และผ่านเกณฑ์ครบทุกข้อ
- เมื่อกดยืนยัน ระบบส่งรหัสผ่านใหม่พร้อมรหัสผู้ใช้และรหัสอ้างอิงไปยัง
POST /user/reset-passwordแล้วพากลับไปหน้า/login - หากไม่สำเร็จ ระบบแปลรหัสข้อผิดพลาดจากเซิร์ฟเวอร์เป็นข้อความที่เข้าใจได้ กรณีที่พบบ่อยคือ token หมดอายุหรือไม่ถูกต้อง และการตั้งรหัสผ่านซ้ำกับรหัสผ่าน 10 ครั้งล่าสุด
3. เปลี่ยนรหัสผ่านของตัวเอง
- ผู้ใช้ที่เข้าสู่ระบบแล้วคลิกรูปโปรไฟล์บนแถบด้านบน แล้วเลือก "เปลี่ยนรหัสผ่าน"
- Modal แสดงสามช่อง ได้แก่ รหัสผ่านปัจจุบัน รหัสผ่านใหม่ และยืนยันรหัสผ่านใหม่ พร้อมการ์ดรายการตรวจสอบเกณฑ์ 6 ข้อ
- รายการตรวจสอบเปลี่ยนสีตามสถานะแบบเรียลไทม์ (เทาคือยังไม่เริ่มพิมพ์ เขียวคือผ่าน แดงคือไม่ผ่าน) และระบบแจ้งทันทีเมื่อรหัสผ่านยืนยันไม่ตรงกัน
- ปุ่มยืนยันจะเปิดใช้งานเมื่อผ่านทุกเงื่อนไข จากนั้นระบบเรียก
POST /user/change-password - เมื่อสำเร็จจะแสดงข้อความยืนยันและปิด modal พร้อมล้างค่าในฟอร์มทั้งหมด หากไม่สำเร็จจะแสดงข้อความผิดพลาดจากเซิร์ฟเวอร์
4. ผู้ดูแลระบบสั่งรีเซ็ตรหัสผ่านให้ผู้ใช้อื่น
- ในตารางหน้าจัดการผู้ใช้ แต่ละแถวมีปุ่มรูปกุญแจสำหรับสั่งรีเซ็ตรหัสผ่าน
- เมื่อกด ระบบจะแสดงกล่องยืนยันก่อนดำเนินการ เพื่อป้องกันการกดโดยไม่ตั้งใจ
- เมื่อยืนยัน ระบบเรียก
POST /user/{userId}/admin-reset-passwordแล้วแสดงข้อความยืนยันผล - เซิร์ฟเวอร์เป็นผู้ส่งอีเมลลิงก์ตั้งรหัสผ่านใหม่ให้ผู้ใช้รายนั้น ซึ่งจะเข้าสู่เส้นทางที่ 2 ต่อไป
หน้าจอและองค์ประกอบหลัก
หน้าลืมรหัสผ่าน
หน้า /forgot-password มีฟอร์มช่องเดียวคืออีเมล พร้อมตรรกะควบคุมสถานะปุ่มแยกไว้เป็นฟังก์ชันเฉพาะ และใช้ modal ยืนยันร่วมกับหน้าอื่นในระบบ ข้อความใน modal รองรับการเน้นข้อความและการขึ้นบรรทัดใหม่
หน้าตั้งรหัสผ่านใหม่
หน้า /reset-password แบ่งเป็นสามสถานะการแสดงผล คือ กำลังตรวจสอบ ลิงก์ใช้ไม่ได้ และพร้อมตั้งรหัสผ่าน โดย container เป็นผู้ถอดรหัส token ตรวจวันหมดอายุ สอบถามเซิร์ฟเวอร์ และแปลรหัสข้อผิดพลาด
คอมโพเนนต์รายการตรวจสอบรหัสผ่าน
คอมโพเนนต์ PasswordChecklist (src/components/common/password/password-checklist.tsx) รวมช่องรหัสผ่าน ช่องยืนยัน และรายการตรวจสอบ 6 ข้อไว้ด้วยกัน พร้อมส่งสถานะ "ผ่านเกณฑ์แล้วหรือยัง" กลับไปให้หน้าที่เรียกใช้ควบคุมปุ่มยืนยัน คอมโพเนนต์นี้ถูกใช้ร่วมกันระหว่างหน้าตั้งรหัสผ่านใหม่และ modal เปลี่ยนรหัสผ่าน
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-password | 3 ครั้ง ต่อ 10 วินาที | ไม่ต้อง |
POST /api/user/reset-password | 3 ครั้ง ต่อ 10 วินาที | ไม่ต้อง |
GET /api/user/reset-password/:code | 3 ครั้ง ต่อ 10 วินาที | ไม่ต้อง |
POST /api/user/change-password | 5 ครั้ง ต่อ 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 วินาทีถือว่าค่อนข้างแคบ สำหรับหน้าที่ผู้ใช้อาจกดผิดหลายครั้ง ทีมสนับสนุนที่ทดสอบเส้นทางนี้ติด ๆ กันควรเผื่อเวลารอระหว่างรอบ