Skip to main content

Settings — OTP Verification

ภาพรวม​

OTP Verification คือที่เก็บข้อมูลรับรองและเทมเพลตข้อความสำหรับส่งรหัส OTP ระดับ LINE OA หนึ่งบัญชี หน้านี้ ไม่ได้กำหนดว่าฟอร์มใดต้องยืนยันตัวตน (ตั้งค่านั้นอยู่ในแท็บการยืนยันตัวตนของ Form builder) แต่เป็นชั้นล่างที่ตอบว่า OA นี้ส่ง OTP ได้ทางช่องทางใดบ้าง — รองรับ 2 ช่องทาง คือ SMS (ผ่าน ThaiBulkSMS) และอีเมล (ผ่าน SMTP ระดับ channel)

จุดที่ควรรู้ก่อนตั้งค่าคือ สองช่องทางนี้ทำงานต่างกันโดยพื้นฐาน — ช่องทาง SMS ให้ ThaiBulkSMS เป็นผู้สร้างและส่งรหัสเอง ระบบเราเพียงขอให้เขาส่งแล้วเก็บ token ไว้ถามกลับว่ารหัสที่ผู้ใช้พิมพ์ถูกหรือไม่ ดังนั้นข้อความ SMS ถูกตั้งค่าที่ฝั่ง ThaiBulkSMS ไม่ใช่ในหน้านี้ — หน้านี้จึงมีช่องเทมเพลตให้เฉพาะอีเมล ซึ่งเป็นช่องทางที่ระบบเราสร้างรหัส 6 หลักด้วยตัวเอง

บทบาทที่ใช้งาน — ผู้ดูแลระบบที่ตั้งค่าครั้งเดียวให้ทั้งบัญชี ก่อนทีมที่สร้างฟอร์มจะเปิดใช้ OTP ในฟอร์มของตนได้ (โมดูลนี้ ไม่มี ModuleGate ที่ backend — ดูหัวข้อ สิทธิ์การเข้าถึง)

เส้นทางผู้ใช้งาน​

  1. เปิดเมนู Settings > OTP Verification — ระบบแสดงฟอร์มได้เสมอแม้ยังไม่เคยตั้งค่ามาก่อน (GET /otp-config ตอบค่าว่างเปล่า/ค่าเริ่มต้น ไม่เคยตอบ 404) และเติมหัวข้อ/เนื้อหาอีเมลเริ่มต้นให้
  2. การ์ด SMS — ThaiBulkSMS มี 3 ช่อง
    • สวิตช์เปิดใช้งาน OTP ทาง SMS
    • ThaiBulkSMS Key
    • ThaiBulkSMS Secret — เป็นช่องรหัสผ่าน ค่าจะว่างเสมอตอนโหลดหน้า โดยแสดงค่าที่ปิดบังไว้เป็น placeholder ("ปัจจุบัน: ab****yz — เว้นว่างไว้เพื่อคงค่าเดิม") เพื่อให้รู้ว่ามี secret เก็บอยู่แล้ว เว้นว่างไว้หมายถึงคงค่าเดิม ไม่ใช่ล้างค่า
    • การ์ดนี้ไม่มีการตรวจฝั่งหน้าเว็บเลย — เปิดสวิตช์แล้วไม่กรอกอะไรก็กดบันทึกได้ แล้วรับข้อความ error จาก backend
  3. การ์ดอีเมล มี 3 ช่อง
    • สวิตช์เปิดใช้งาน OTP ทางอีเมล
    • หัวข้ออีเมล — ค่าเริ่มต้น รหัสยืนยันของคุณ (EN: Your verification code)
    • เนื้อหาอีเมล (HTML) — ค่าเริ่มต้น รหัส OTP ของคุณคือ <b>{otp}</b> จะหมดอายุใน 3 นาที (EN: Your OTP code is <b>{otp}</b>. It expires in 3 minutes.) ต้องมี {otp} เสมอ ใส่ {ref} เพิ่มได้ถ้าต้องการโชว์รหัสอ้างอิง
  4. กดบันทึก — ป้ายสถานะบนหัวการ์ดจะอัปเดตเป็น "กำหนดค่าแล้ว" เมื่อช่องทางนั้นครบเงื่อนไข (ป้ายนี้อ่านค่าที่บันทึกแล้ว ไม่ได้อ่านสถานะฟอร์มปัจจุบัน จึงอัปเดตหลัง refetch)
  5. ไปที่เมนู Form builder เปิดฟอร์มที่ต้องการ เปิด Profile Mapping พร้อมเลือก Member Database (จำเป็น เพราะรหัส OTP จะถูกส่งไปยังคอลัมน์ของระเบียนที่ค้นหาเจอ ไม่ใช่ค่าที่ผู้ใช้กรอก) แล้วไปที่แท็บการยืนยันตัวตนเพื่อเปิดสวิตช์ OTP ของฟอร์มนั้น — ช่องทางที่ยัง "ยังไม่ได้กำหนดค่า" จะถูก disable ไว้พร้อมลิงก์กลับมาหน้านี้

รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง​

ยืนยันจาก internal/modules/otpconfig/{controller,service,dto,crypto}.go ของ line-management-cms-api-go และ internal/otp/{service,session,mask}.go + internal/otp/{code,thaibulksms} ของ line-management-client-api-go:

  • secret ถูกเข้ารหัสก่อนเก็บด้วย AES-256-GCM และไม่มีทางอ่านค่าจริงกลับออกมาผ่าน API — คีย์ได้จาก SHA-256 ของ APP_ENCRYPT_SECRET มีการสุ่ม nonce ใหม่ทุกครั้ง และเก็บเป็น base64(nonce ‖ ciphertext ‖ tag) ลงคอลัมน์ thaibulksms_secret_enc เวลาอ่านออกมา ระบบถอดรหัสไว้ในหน่วยความจำเพื่อคำนวณค่าที่ปิดบัง (maskSecret เก็บอักขระ 2 ตัวหน้าและ 2 ตัวท้าย ที่เหลือแทนด้วย *; secret ยาวไม่เกิน 4 ตัวถูกปิดทั้งหมด) แล้วส่งออกเพียง thaibulksmsSecretMasked เท่านั้น — plaintext ไม่เคยอยู่ในโครงสร้างที่ส่งกลับ นี่ต่างจากเมนู BigQuery Sync อย่างชัดเจน ซึ่งเก็บ service account key เป็นข้อความธรรมดาและส่งกลับมาทั้งก้อน
  • ถ้าถอดรหัส secret ไม่สำเร็จ ระบบจะไม่ตอบ 500 — เช่นกรณี APP_ENCRYPT_SECRET ถูกเปลี่ยน/ตั้งค่าผิด ระบบเขียน log แล้วคืนค่าที่ปิดบังเป็นค่าว่างไปแทน (ท่าที "ไม่เชื่อ config ที่เสีย") อาการที่เห็นคือ placeholder ของช่อง secret กลับไปเป็น "กรอกรหัสลับ ThaiBulkSMS" ราวกับไม่มี secret เก็บอยู่ ทั้งที่ในฐานข้อมูลยังมี — และเพราะ smsConfigured ตรวจจาก ค่าที่เข้ารหัสไว้ ไม่ใช่ค่าที่ถอดรหัสได้ ป้ายสถานะจึงยังขึ้นว่า "กำหนดค่าแล้ว" อยู่ ทั้งที่ตอนส่งจริงจะใช้ secret ไม่ได้
  • "เว้นว่าง = คงค่าเดิม" ถูกจัดการที่ฝั่ง backend ในฟังก์ชันแยกที่ตรวจสอบได้ (resolveSecretEnc) — ส่ง secret มาไม่ว่างจะเข้ารหัสใหม่ทับของเดิม, ส่งมาว่างจะคงของเดิมไว้เสมอ, และไม่มีของเดิมกับไม่ส่งมาด้วยจะได้ "ไม่มี secret" ผลข้างเคียงที่ควรรู้: ไม่มีช่องทางใดเลยในหน้าเว็บที่จะ "ลบ" secret ที่เก็บไว้ เพราะการเว้นว่างหมายถึงคงค่าเดิมเสมอ
  • กฎข้ามฟิลด์ถูกตรวจ หลัง ผสานกับ secret เดิมแล้ว — จึงเปิดสวิตช์ SMS โดยไม่ต้องพิมพ์ secret ซ้ำได้ ถ้ามี secret เก็บอยู่แล้ว กฎที่ตรวจคือ
    • smsEnabled ต้องมี key ที่ไม่ว่าง และ secret ที่ผสานแล้วไม่ว่าง ไม่ครบได้ 400 ThaiBulkSMS key and secret are required when SMS OTP is enabled
    • emailEnabled ต้องมีหัวข้อที่ไม่ว่าง และ เนื้อหาที่มี {otp} ไม่ครบได้ 400 Email subject and a body containing {otp} are required when email OTP is enabled
  • ป้าย "กำหนดค่าแล้ว" หมายถึง "เปิดสวิตช์ไว้ และ ข้อมูลครบ" ไม่ใช่แค่ "บันทึกข้อมูลรับรองไว้แล้ว" — smsConfigured = smsEnabled && key != "" && secretEnc != "" และ emailConfigured = emailEnabled && subject != "" && strings.Contains(body, "{otp}") การปิดสวิตช์จึงทำให้ป้ายกลับไปเป็น "ยังไม่ได้กำหนดค่า" ทันทีทั้งที่ข้อมูลยังอยู่ครบ — และเพราะ Form builder อ่านสองค่านี้ไปตัดสินว่าจะให้เลือกช่องทางได้หรือไม่ การปิดสวิตช์ในหน้านี้จึงมีผลกับทุกฟอร์มที่ใช้ช่องทางนั้นทันที
  • ทุกฟิลด์ใน PUT /otp-config เป็น optional และสตริงถูก trim — ค่าที่ไม่ส่งมาหรือส่งเป็น null จะได้ false/"" ไม่ใช่ "ไม่แตะต้องค่าเดิม" ดังนั้น การ PUT แบบบางส่วน (partial update) จะล้างค่าที่ไม่ได้ส่งมา ยกเว้น secret ที่มีกฎ "เว้นว่าง = คงค่าเดิม" เป็นข้อยกเว้นเดียว หน้าเว็บส่งครบทุกฟิลด์ทุกครั้งจึงไม่เจอปัญหานี้ แต่การเรียก API ตรงต้องระวัง
  • {otp} ถูกตรวจว่าเป็นสตริงตรงตัวและตรวจซ้ำสามชั้น — หน้าเว็บตรวจ (value.includes('{otp}') แบบสนตัวพิมพ์เล็ก/ใหญ่), cms-api ตรวจตอนบันทึก, และ client-api ตรวจอีกครั้งตอนกำลังส่ง OTP จริง (Email OTP template is missing the {otp} placeholder.) ส่วน {ref} ไม่มีการตรวจในชั้นใดเลย ใส่หรือไม่ใส่ก็ได้ — และหัวข้ออีเมลไม่ถูกตรวจ placeholder เลย ถ้าใส่ {otp} ในหัวข้อจะถูกส่งออกไปตามตัวอักษร เพราะการแทนที่ทำเฉพาะกับเนื้อหา
  • ค่าที่ควบคุมพฤติกรรม OTP จริงไม่ได้อยู่ในหน้านี้เลย — ตัวเลขทั้งชุดถูกกำหนดเป็นค่าคงที่ในซอร์สของ line-management-client-api-go ไม่มีที่ใดใน CMS ให้แก้
    • อายุรหัส 3 นาที (TTL ของ session ใน Redis)
    • กรอกผิดได้ 5 ครั้ง ครั้งที่ 5 ทำลาย session
    • ส่งได้รวม 3 ครั้งต่อ session (นับครั้งแรกด้วย)
    • ต้องรอ 60 วินาทีก่อนส่งซ้ำ เกินได้ 429 OTP_RESEND_COOLDOWN ตัวเลข "3 นาที" ในเนื้อหาอีเมลเริ่มต้นเป็นเพียงข้อความ ไม่ได้อ่านจากค่าคงที่ — ถ้าค่าคงที่ถูกแก้ในอนาคต เนื้อหาอีเมลจะไม่ตามไปด้วย
  • SMS กับอีเมลใช้กลไกรหัสไม่เหมือนกันเลย
    • SMS — ระบบเรียก POST /v2/otp/request ของ ThaiBulkSMS (form-urlencoded: key, secret, msisdn) แล้วเก็บ token ที่ได้ไว้ใน session เราไม่เคยรู้ค่ารหัส ตอนตรวจก็ส่ง token+pin ไปถาม POST /v2/otp/verify เบอร์โทรถูกแปลงเป็นรูปแบบท้องถิ่นก่อนส่ง (+66/66… → 0…, ตัดอักขระที่ไม่ใช่ตัวเลขทั้งหมด)
    • อีเมล — ระบบสุ่มรหัส 6 หลักเองด้วย crypto/rand เก็บเป็น bcrypt hash ใน session (ไม่เคยเก็บ plaintext) และเทียบด้วย bcrypt แบบ constant-time ตอนตรวจ แล้วส่งอีเมลผ่าน SMTP ระดับ channel โดยแทนที่ {otp} และ {ref} ในเนื้อหา
  • session ของ OTP ไม่เก็บเบอร์/อีเมลจริงไว้เลย — เก็บเฉพาะค่าที่ปิดบัง (0819188051 → 08xxxxx051, [email protected] → a***@e***.com) และเส้นทางส่งจะค้นหาระเบียนสมาชิกใหม่ทุกครั้งรวมถึงตอนส่งซ้ำ — ค่าปลายทางจึงไม่เคยถูกเชื่อจากฝั่งผู้ใช้ และไม่มีอะไรที่รั่วเบอร์/อีเมลอยู่ใน Redis
  • client-api ตรวจความพร้อมของช่องทางซ้ำอีกครั้งตอนส่งจริง — ตรวจสวิตช์, key, secret สำหรับ SMS และตรวจสวิตช์, หัวข้อ, เนื้อหาที่ไม่ว่าง, และ {otp} สำหรับอีเมล ตอบเป็นรหัสข้อผิดพลาดที่ผู้กรอกเห็นได้ (OTP_NOT_CONFIGURED) และมีรหัสแยกสำหรับกรณีระเบียนที่จับคู่ได้ไม่มีข้อมูลติดต่อในช่องทางที่เลือก (OTP_NO_CONTACT)
  • การอ่าน/เขียน config จำกัดขอบเขตด้วย line_oa_id เพียงค่าเดียว ไม่ได้กรอง organization_id — ปลอดภัยเพราะคอลัมน์ line_oa_id มี unique constraint (หนึ่งแถวต่อหนึ่ง OA) และ line_oa_id มาจาก claim ใน token แต่ต่างจากโมดูลอื่นในระบบที่กรองสองชั้น ค่า organization_id ถูกใช้แค่ตอน INSERT แถวใหม่
  • ไม่มีเส้นทางลบ — โมดูลนี้มีแค่ 2 เส้นทาง (GET และ PUT) และเป็น upsert แบบ singleton ต่อ OA

เมนูที่เกี่ยวข้องกันในเส้นทางนี้​

  • Form builder — เป็นผู้ใช้ค่าการตั้งค่านี้เพียงรายเดียว แท็บการยืนยันตัวตนอ่าน smsConfigured/emailConfigured จากที่นี่มาตัดสินว่าจะเปิดให้เลือกได้หรือไม่ และเปิดได้เพียง 1 ช่องยืนยันต่อฟอร์ม นอกจากนี้ backend ของ Form builder ยังอ่านตาราง otp_config ตรง ๆ อีกชั้น (validateOtpConfigGuard) เพื่อปฏิเสธ 400 ถ้าฟอร์มขอเปิดช่องทางที่ยังไม่พร้อม — การเรียก API ตรงเพื่อข้ามหน้าเว็บจึงเปิด OTP ไม่ได้
  • Member Database — เป็นเงื่อนไขบังคับของฝั่งฟอร์มที่เปิด OTP เพราะรหัสถูกส่งไปยังคอลัมน์ของระเบียนสมาชิกที่จับคู่ได้ ทั้งตัวเลือก "คอลัมน์ที่จะยืนยัน" ในแท็บนั้นก็ดึงมาจากคอลัมน์ของฐานข้อมูลที่เลือก ไม่ใช่จากคำถามในฟอร์ม
  • สามเมนูนี้ผูกกันแบบต่อเนื่องเป็นลูกโซ่ — ปิด Profile Mapping หรือถอด Member Database ออก แท็บยืนยันตัวตนใน Form builder จะเปิดไม่ได้ทันที (และระบบจะปิดสวิตช์ให้อัตโนมัติ); ปิดสวิตช์ช่องทางในหน้านี้ ฟอร์มที่ใช้ช่องทางนั้นจะขึ้นสถานะ error ในแท็บและกดบันทึกใหม่ไม่ผ่าน

จุดที่ควรระวัง​

  • ไม่มีปุ่มทดสอบส่ง OTP จริงและไม่มีหน้าประวัติการส่งในเมนูนี้ — ตรวจแล้วโมดูลนี้มีเพียง 2 endpoint (GET/PUT) และปุ่มเดียวคือปุ่มบันทึก ต้องทดลองส่งฟอร์มจริงเพื่อยืนยันว่าข้อมูลรับรองใช้งานได้
  • ปิดสวิตช์ช่องทางใดช่องทางหนึ่งมีผลทันทีกับทุกฟอร์มที่เปิดใช้ช่องทางนั้นอยู่ — ตรวจสอบก่อนว่ามีฟอร์มที่กำลังใช้งานอยู่หรือไม่
  • ไม่มีทางอ่านค่า Secret เดิมกลับมาดูได้ และไม่มีทางลบมันด้วย — ถ้าจำไม่ได้ต้องออก Key/Secret ชุดใหม่จากผู้ให้บริการแล้วกรอกทับ
  • ข้อความ SMS ไม่ได้ตั้งค่าที่นี่ — ThaiBulkSMS เป็นผู้สร้างและส่งรหัสเอง ถ้าต้องการแก้ข้อความหรือชื่อผู้ส่งของ SMS ต้องไปทำที่ระบบของ ThaiBulkSMS ช่องเทมเพลตในหน้านี้มีผลกับ อีเมลเท่านั้น
  • การ์ด SMS ไม่มีการตรวจฝั่งหน้าเว็บเลย — เปิดสวิตช์แล้วปล่อยช่องว่างก็กดบันทึกได้ จะเห็น error จาก backend หลังกดเท่านั้น (ต่างจากการ์ดอีเมลที่ตรวจ {otp} ให้ก่อน)
  • ตัวเลข 3 นาทีในเนื้อหาอีเมลเริ่มต้นเป็นข้อความล้วน — ถ้าเปลี่ยนเนื้อหาแล้วอ้างเวลาอื่น จะไม่ตรงกับพฤติกรรมจริง (TTL คงที่ 3 นาทีในซอร์สของ client-api)
  • ถ้า APP_ENCRYPT_SECRET ถูกเปลี่ยน ป้ายสถานะจะยังบอกว่า "กำหนดค่าแล้ว" แต่ placeholder ของ secret จะกลับไปเป็นค่าว่างและการส่ง SMS จริงจะล้มเหลว — สังเกตอาการนี้ให้ดี วิธีแก้คือกรอก secret ใหม่ทับเพื่อเข้ารหัสด้วยคีย์ปัจจุบัน
  • เรียก PUT /otp-config แบบบางส่วนจะล้างค่าที่ไม่ได้ส่งมา (ยกเว้น secret) — ควรอ่านค่าปัจจุบันด้วย GET แล้วส่งกลับให้ครบเสมอถ้าเรียก API ตรง

สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)​

  • ฝั่งหน้าเว็บ — ถูกครอบด้วย PermissionGuard ตรวจ action VIEW ของ subject otp-config และรายการในเมนูข้าง (รวมถึง quick access) ก็ถูกกรองด้วยเงื่อนไขเดียวกัน โดย subject ที่ปลดล็อกให้คือ form-builder ซึ่งใช้ร่วมกับเมนู Form builder (mapping: 'form-builder' → [FORM_BUILDER, OTP_CONFIG]) — ผู้ที่ดู Form builder ได้จึงเห็นเมนูนี้ด้วยเสมอ นี่คือการควบคุมที่แน่นกว่าเมนู User Attribute Setup และ BigQuery Sync ซึ่งไม่มี PermissionGuard เลย
  • ฝั่งเซิร์ฟเวอร์ — ยืนยันแล้วว่า ไม่มี ModuleGate ตรวจแล้วทั้ง repository: modulegate.ModuleGate(...) ถูกเรียกใช้ใน 15 โมดูล และ otpconfig ไม่ใช่หนึ่งในนั้น ผู้ดูแลระดับแพลตฟอร์มจึงไม่สามารถปิดโมดูลนี้ให้องค์กรใดองค์กรหนึ่งได้ (แม้ปิดโมดูล form-builder ไปแล้ว หน้าตั้งค่า OTP ก็ยังเรียกได้อยู่)
  • policy annotation มีประกาศไว้แต่เป็น no-op — CheckPolicies(read/update, otp-config) ถูกผูกไว้ทั้งสองเส้นทาง และคอมเมนต์ในไฟล์ระบุตรงว่า "auth.CheckPolicies เป็น no-op ในวันนี้ — เก็บ metadata ไว้เผื่อวันที่เปิดการบังคับใช้" ดังนั้น ในทางปฏิบัติผู้ใช้คนใดก็ตามที่ล็อกอินอยู่และมี token ที่ผูก OA ไว้ สามารถอ่านและแก้การตั้งค่า OTP ของ OA นั้นได้ทั้งหมด ไม่ว่าจะมีบทบาทใด
  • ทั้งสองเส้นทางลงทะเบียนบน authed group จึงยังอยู่หลัง JwtAuthGuard ระดับ global ซึ่งบังคับให้ token ต้องมี claim lineOaId — นี่คือจุดที่โมดูลนี้ควบคุมแน่นกว่า BigQuery Sync ซึ่งลงทะเบียนบน public group ด้วย @Public() แล้วใช้ strategy jwt-login ที่ไม่บังคับ claim นั้น
  • สิ่งที่ชดเชยการไม่มี ModuleGate อยู่บ้าง — ข้อมูลรับรองถูกเข้ารหัสก่อนเก็บและคืนออกมาเพียงค่าที่ปิดบัง จึงต่างจาก BigQuery Sync ที่ผู้ล็อกอินคนใดก็อ่าน service account key ได้ทั้งก้อน ในโมดูลนี้ผู้ที่เข้าถึงได้จะแก้หรือทำลายการตั้งค่าได้ แต่ไม่สามารถขโมยข้อมูลรับรองที่เก็บไว้ไปใช้ต่อได้
  • ข้อสรุปเทียบกับเอกสารเชิงเทคนิคชุดเดิม — คำกล่าวที่ว่า OTP Verification และ BigQuery Sync "ไม่มี ModuleGate และไม่มีการบังคับ policy จริงเลย เปิดให้ผู้ใช้ที่ล็อกอินคนใดก็ได้" ตรวจแล้วว่าถูกต้อง โดยมีรายละเอียดเพิ่มสองข้อ: (1) BigQuery Sync อ่อนกว่าอีกขั้นเพราะอยู่บน public group + jwt-login, และ (2) OTP Verification ยังมี PermissionGuard ฝั่งหน้าเว็บและการเข้ารหัสข้อมูลรับรองเป็นชั้นป้องกันที่เหลืออยู่
  • ไม่มีเส้นทางใดต้องเป็น Super Admin และไม่มีเส้นทางลบในโมดูลนี้

ไฟล์อ้างอิงจากซอร์สโค้ด​

Frontend (cms-web-2026)

  • src/app/otp-config/page.tsx — จุดเข้าและ PermissionGuard (subject otp-config)
  • src/components/otp-config/otp-config-form.tsx — การ์ด SMS/อีเมล, ป้ายสถานะ, placeholder ของ secret, ตัวตรวจ {otp}
  • src/services/otp-config.service.ts, src/services/types/otp-config.type.ts — endpoint ทั้งสองและความหมายของ *Configured
  • src/components/ability/permission-map.ts — mapping 'form-builder' → [FORM_BUILDER, OTP_CONFIG]
  • src/components/form-builder/form/verification/verification-form.tsx — ฝั่งผู้บริโภคที่อ่านสถานะความพร้อมของช่องทาง

Backend (line-management-cms-api-go)

  • internal/modules/otpconfig/controller.go — 2 เส้นทางบน authed group, CheckPolicies ที่เป็น no-op, ไม่มี ModuleGate
  • internal/modules/otpconfig/service.go — upsert แบบ singleton, resolveSecretEnc, การถอดรหัสเพื่อคำนวณค่าที่ปิดบัง
  • internal/modules/otpconfig/crypto.go — AES-256-GCM และ maskSecret
  • internal/modules/otpconfig/dto.go — กฎข้ามฟิลด์และตรรกะของ smsConfigured/emailConfigured
  • internal/modules/formbuilder/otp.go — ด่านตรวจ otp_config ฝั่ง Form builder ที่อ่านตารางนี้โดยตรง

Client API (line-management-client-api-go) — เป็นผู้ส่งและตรวจ OTP จริง

  • internal/otp/session.go — TTL 3 นาที, กรอกผิดได้ 5 ครั้ง, ส่งได้ 3 ครั้ง, รอ 60 วินาที
  • internal/otp/service.go — การโหลด config ต่อ OA, การตรวจความพร้อมซ้ำ, การแทนที่ {otp}/{ref}
  • internal/otp/code/code.go — การสุ่มรหัส 6 หลักและ bcrypt (ช่องทางอีเมลเท่านั้น)
  • internal/otp/thaibulksms/thaibulksms.go — /v2/otp/request และ /v2/otp/verify (ThaiBulkSMS เป็นผู้สร้างรหัส)
  • internal/otp/mask.go — การแปลงเบอร์เป็นรูปแบบท้องถิ่นและการปิดบังเบอร์/อีเมล

ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ ตั้งค่ายืนยัน OTP