Form builder
ภาพรวม
Form builder คือเครื่องมือสร้างฟอร์มออนไลน์ (คล้าย Google Form) แล้วเผยแพร่เป็นลิงก์ให้ผู้ใช้ LINE กรอก จากนั้นดูคำตอบ สถิติ และส่งออกเป็น CSV ได้ในระบบ CMS เหมาะกับฟอร์มลงทะเบียนกิจกรรม แบบสำรวจ หรือฟอร์มยืนยันตัวตนก่อนรับสิทธิ์
จุดที่ทำให้เมนูนี้ต่างจากฟอร์มทั่วไปคือ Profile Mapping — เชื่อมฟอร์มเข้ากับฐานข้อมูลสมาชิกจากเมนู Member Database เพื่อให้ฟอร์มค้นหาระเบียนสมาชิกจากคำตอบที่กรอกเข้ามา แล้วตัดสินใจต่อว่าจะรับหรือปฏิเสธการส่ง และ (ถ้าเปิด) จะส่ง OTP ไปยืนยันตัวตนที่เบอร์/อีเมลใน ระเบียนที่ค้นหาเจอ ไม่ใช่ค่าที่ผู้กรอกพิมพ์เข้ามา
บทบาทที่ใช้งาน — ทีมการตลาดหรือทีมดูแลลูกค้าที่ต้องเก็บข้อมูลจากผู้ใช้ LINE (โมดูลนี้มี ModuleGate บังคับจริงที่ backend และเป็นโมดูลที่มีการตรวจสอบภายในเข้มที่สุดชุดหนึ่งในระบบ — ดูหัวข้อ สิทธิ์การเข้าถึง)
เส้นทางผู้ใช้งาน
1. ดูรายการฟอร์ม
เปิดเมนู Form builder หน้ารายการไม่มีแท็บ มีเพียงตัวกรอง 2 ช่อง — ค้นหาจากชื่อ/คำสำคัญ และเลือกสถานะ (ตัวเลือกที่หน้าเว็บเสนอมีแค่ active / inactive / draft ทั้งที่ backend รู้จักสถานะ delete ด้วย เพราะการลบเป็น soft delete ที่เปลี่ยนสถานะเป็น delete) เรียงตามวันที่สร้างใหม่ไปเก่า และค่าตัวกรองถูกจดจำใน sessionStorage
ตารางมี 7 คอลัมน์ — ชื่อ, คำอธิบาย, ลิงก์ (กดคัดลอกได้), วันที่สร้าง (เรียงได้), สถานะ, จำนวนคำตอบ, และปุ่มแก้ไข / ดูคำตอบ / ลบ
2. สร้างฟอร์ม
- กดสร้างใหม่ กรอกชื่อฟอร์ม (ไม่เกิน 200 ตัวอักษร) และคำอธิบาย (ไม่เกิน 250 ตัวอักษร) — ชื่อต้องไม่ซ้ำกับฟอร์มอื่นใน OA เดียวกัน
- เพิ่มคำถามได้สูงสุด 10 ข้อ — ไม่นับหัวข้อ (Header 1/Header 2) และเส้นคั่น (Divider) ซึ่งเพิ่มได้ไม่จำกัด ชนิดคำถามมีทั้งหมด 22 แบบ แบ่งเป็น
- ช่องกรอกทั่วไป — ข้อความสั้น, ย่อหน้า, ตัวเลข, วันที่, URL, อัปโหลดไฟล์, ข้อความกำหนดเงื่อนไขเอง (custom input)
- ข้อมูลส่วนบุคคล — ชื่อ, นามสกุล, อีเมล, เบอร์โทร, เลขบัตรประชาชน, วันเกิด — 6 ชนิดนี้ใส่ได้ชนิดละ 1 ข้อต่อฟอร์มเท่านั้น
- ตัวเลือก — เลือกข้อเดียว, เลือกหลายข้อ, ดรอปดาวน์, ดรอปดาวน์เลือกหลายข้อ, ยอมรับเงื่อนไข (terms)
- ตรวจสอบฐานข้อมูล (DB Validation) — ปุ่มนี้จะถูกล็อกไว้จนกว่าจะเปิด Profile Mapping และเลือกฐานข้อมูลแล้ว เมื่อปลดล็อกจะถูกเลื่อนขึ้นมาเป็นตัวเลือกแรก
- ตั้งเงื่อนไขการแสดงคำถาม (เช่น แสดงคำถาม B เมื่อคำถาม A ตอบว่า…) — ต้องกรอกครบทั้งคำถามแม่ ตัวดำเนินการ และค่า (ยกเว้นตัวดำเนินการ "ไม่ว่าง" ที่ไม่ต้องมีค่า)
- ปรับแต่งรูปแบบ — รูปหน้าปก, ชุดสี 7 ค่า และข้อความบนปุ่ม ดูตัวอย่างสดที่คอลัมน์ขวา
- ตั้งหน้าขอบคุณ — โหมด
default(ค่าคงที่ ทุกช่องถูกล็อก) หรือcustomเลือก action ได้ 5 แบบ (ปิดหน้าต่าง / เปิดลิงก์ / ไปที่แชท OA / เปลี่ยนริชเมนูจากเมนู Rich menu / ไม่มีปุ่ม) - กดบันทึกเป็น draft หรือเผยแพร่เป็น active — เผยแพร่แล้วย้อนกลับเป็น draft ไม่ได้ (ตัวเลือก "ฉบับร่าง" จะหายไปจากฟอร์มเมื่อสถานะปัจจุบันไม่ใช่ draft) แต่สลับ active ↔ inactive ได้อิสระ
3. เชื่อมกับ Member Database (Profile Mapping)
แผงตั้งค่าแบ่งเป็น 3 ขั้นชัดเจน
- แหล่งข้อมูล — เลือกฐานข้อมูลจากเมนู Member Database (บังคับเมื่อเปิดสวิตช์แล้ว) รายการดึงมาแค่ 100 รายการแรก
- จับคู่ฟิลด์ — ระบบแสดงคอลัมน์ของฐานข้อมูลที่เลือก แต่ละคอลัมน์กด "เพิ่มช่องกรอก" ได้ ระบบจะสร้างคำถามชนิด DB Validation ให้อัตโนมัติ หากไม่จับคู่คอลัมน์ใดเลยจะขึ้นคำเตือนว่า "ฟอร์มจะรับใครก็ได้"
- ผลลัพธ์ — กำหนดสองกรณี
- พบระเบียน — เลือก custom attribute ที่จะประทับ (ไม่บังคับ) พร้อมค่าที่จะเขียน (ค่าเริ่มต้น
true) และเลือกเปิด "หนึ่งบัญชี LINE ต่อหนึ่งระเบียน" ได้ พร้อมข้อความแจ้งเมื่อระเบียนถูกผูกไว้แล้ว - ไม่พบระเบียน — เลือกได้ 2 ทาง คือ ปฏิเสธการส่ง (แสดงข้อความที่กำหนด ไม่บันทึกอะไรเลย) หรือ สมัครเป็นสมาชิกใหม่ (รับข้อมูล แต่ไม่ประทับ attribute ยืนยัน)
- พบระเบียน — เลือก custom attribute ที่จะประทับ (ไม่บังคับ) พร้อมค่าที่จะเขียน (ค่าเริ่มต้น
4. เปิดการยืนยันตัวตนด้วย OTP (ไม่บังคับ)
- แท็บ การยืนยันตัวตน จะเปิดสวิตช์ได้เฉพาะเมื่อ Profile Mapping เปิดอยู่ + เลือกฐานข้อมูลแล้ว + ฐานข้อมูลนั้นมีคอลัมน์ ถ้าเงื่อนไขหลุดไประหว่างแก้ไข ระบบจะปิดสวิตช์ให้อัตโนมัติ
- แท็บนี้อ่านสถานะความพร้อมจากเมนู Settings > OTP Verification — ช่องทางที่ยัง "ยังไม่ได้กำหนดค่า" จะถูก disable และมีคำเตือนพร้อมลิงก์ไปตั้งค่า ถ้าไม่มีช่องทางใดพร้อมเลย ปุ่มเพิ่มช่องยืนยันจะกดไม่ได้
- เลือกช่องยืนยันได้เพียง 1 ช่องเท่านั้น — เลือกคอลัมน์ของระเบียนสมาชิก (ไม่ใช่คำถามในฟอร์ม) และช่องทางส่ง (SMS หรืออีเมล)
- ปรับข้อความบนหน้ากรอก OTP ได้ 4 ค่า — หัวข้อ (80 ตัวอักษร), คำอธิบาย (ใส่
{destination}เพื่อแทรกปลายทางที่ถูกปิดบัง), ข้อความบนปุ่มยืนยัน (40), ข้อความปุ่มส่งซ้ำ (40) - แท็บนี้จะหายไปทั้งแท็บ ถ้าผู้ใช้ไม่มีสิทธิ์ดู
member-database
5. ดูคำตอบและส่งออก
- กดปุ่มดูคำตอบจากหน้ารายการฟอร์ม
- ดูการ์ดสถิติ 3 ใบ — ทั้งหมด / ส่งครบถ้วน / ยังไม่ครบถ้วน กดที่การ์ดเพื่อกรองตาราง
- ตารางมี 3 คอลัมน์ตรึงซ้าย (เวลาส่ง, ส่งครบหรือไม่, LINE User ID แบบคัดลอกได้) แล้วต่อด้วยคอลัมน์ที่สร้างอัตโนมัติตามคำถามในฟอร์ม (ไม่นับหัวข้อ/เส้นคั่น) คำตอบชนิดไฟล์แสดงลิงก์ "ดูไฟล์" เปิดดู PDF/รูปในหน้าต่างซ้อนได้
- กดส่งออก CSV — ไฟล์มี UTF-8 BOM นำหน้าเพื่อให้ Excel อ่านภาษาไทยได้ถูกต้อง ชื่อไฟล์สร้างจากฝั่งหน้าเว็บเป็น
form-responses-<formHash>-<timestamp>.csv
รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง
ยืนยันจาก internal/modules/formbuilder/{controller,service,dto,otp,thankyou,contrast}.go ของ line-management-cms-api-go:
form_hashคำนวณจากชื่อฟอร์มด้วย SHA-256 และมี unique index ระดับ "ทั้งฐานข้อมูล" — ค่านี้คือhex(sha256(title))[:12]และคอลัมน์form_builder.form_hashมีUNIQUE INDEXที่ไม่ได้ผูกกับline_oa_idเลย ผลที่ตามมามีสองข้อที่ควรรู้- ต่าง OA กันก็ตั้งชื่อฟอร์มซ้ำกันไม่ได้ — การตรวจชื่อซ้ำ (
existsByTitle) กรองด้วยline_oa_idจึงผ่าน แต่ INSERT จะไปชนกับ unique index แล้วกลายเป็น error 500 ไม่ใช่ข้อความ "Title is already exist" ที่อ่านรู้เรื่อง - ฟอร์มที่ลบไปแล้วยังจองชื่อไว้ — soft delete เปลี่ยนสถานะเป็น
deleteแต่ไม่ล้างform_hashส่วนexistsByTitleตัดสถานะdeleteออกจากการตรวจ จึงสร้างฟอร์มชื่อเดิมซ้ำได้ในสายตาหน้าเว็บ แต่จะพังตอน INSERT form_hashไม่ถูกคำนวณใหม่เมื่อเปลี่ยนชื่อฟอร์ม — ลิงก์ที่เผยแพร่ไปแล้วจึงยังใช้ได้ต่อ แต่ค่า hash จะไม่ตรงกับชื่อใหม่อีก
- ต่าง OA กันก็ตั้งชื่อฟอร์มซ้ำกันไม่ได้ — การตรวจชื่อซ้ำ (
PUT /form-builder/:idที่ไม่ส่งthemeมาด้วยจะได้ 500 ทุกครั้ง — เป็นพฤติกรรมที่พอร์ตมาโดยเจตนา โค้ด TS เดิมอ่านtheme.coverแบบไม่มี guard เมื่อthemeเป็นundefined/nullจะโยน TypeError ที่ไม่ใช่ HttpException → Nest ตอบ 500 ฝั่ง Go จึงจำลอง 500 นั้นไว้ (errThemeCoverTypeError) หน้าเว็บจริงส่ง theme มาทุกครั้งจึงไม่เจอ แต่การเรียก API ตรงต้องส่งthemeมาด้วยเสมอ (เส้นทางสร้างฟอร์มก็เจอปัญหาเดียวกันเมื่อแนบcoverImageมาพร้อมกับ theme ที่ว่าง)thankYouถูกบังคับรูปทรงแบบปิด (closed shape) ต่างจากthemeที่รับทุกอย่าง — คีย์ที่ไม่รู้จักในทุกระดับคือ 400 เพราะบล็อกนี้คุมการนำทางและการสลับริชเมนู กฎที่ตรวจจริงมีตัวอย่างเช่นaction.urlต้องเป็นสกีมา https หรือ line เท่านั้น —httpถูกปฏิเสธ (ไม่ upgrade ให้) มี allowlist กันjavascript:/data:แม้จะแทรกอักขระควบคุมเพื่ออำพราง และปฏิเสธ URL ที่มี userinfo (https://[email protected]/)action.urlจำเป็นเมื่อ type เป็นurlและต้องเป็น null/ไม่มีเลยเมื่อ type เป็นอย่างอื่น (กันกรณีrich_menu_idค้างอยู่แล้วยิงตอนกดปุ่มปิด) กฎเดียวกันใช้กับrich_menu_idauto_close_secondsต้องอยู่ระหว่าง 3–60 วินาที และเป็น null เสมอเมื่อ action เป็นurlหรือnonemessageผ่าน deny-list ของแท็ก/สกีมาที่เป็นอันตราย (<script,<iframe,javascript:,srcdoc=, event handler แบบon…=ฯลฯ) — คอมเมนต์ในซอร์สระบุชัดว่านี่ไม่ใช่ sanitizer ความปลอดภัยจริงมาจากฝั่ง client-web ที่เรนเดอร์ผ่าน TiptapViewer
- การเลือกริชเมนูในหน้าขอบคุณถูกตรวจกรรมสิทธิ์จริงที่ฐานข้อมูล — ระบบนับ
rich_menuที่idตรง และline_oa_idตรงกับ OA ของฟอร์มเอง (บนเส้นทาง update ใช้ค่าที่เก็บไว้ในฐานข้อมูล ไม่ใช่ OA ของผู้เรียก) และต้องมีสถานะactive— พลาดแล้วได้ 404 ที่แยกไม่ออกจาก "ไม่มีเมนูนี้" โดยเจตนา นอกจากนี้ถ้าฟอร์มไม่ได้บังคับ LINE login การเลือก actionrich_menuจะถูกปฏิเสธ 400 เพราะไม่มี lineUserId ให้สลับเมนู - การเปิด OTP มีด่านกันการเปิดนอกช่องทาง (out-of-band) —
validateOtpConfigGuardอ่านตารางotp_configของ OA ผู้เรียกโดยตรงทุกครั้งที่สร้าง/แก้ฟอร์ม แล้วปฏิเสธ 400 พร้อมข้อความชี้ไปที่หน้าตั้งค่า OTP ถ้าช่องทางที่ฟอร์มขอเปิดยังไม่พร้อม การเรียก API ตรงเพื่อข้ามหน้าเว็บจึงเปิด OTP ไม่ได้ profileMapping.otpถูกจำกัดที่ 1 ช่องยืนยันในระดับ backend ไม่ใช่แค่ UI — ตัวตรวจสอบมีกฎfields may contain only one verification fieldตรง ๆ พร้อมกฎว่า "เปิด OTP แต่ไม่มี field" คือ 400 ("เป็นบั๊กของ editor ไม่ใช่สถานะที่ถูกต้อง") ส่วนคีย์อื่นของprofileMappingยังรับทุกอย่างเหมือนเดิม — มีแต่คีย์ย่อยotpที่ถูกล็อกรูปทรง- การตรวจสีตามมาตรฐาน WCAG ถูกเขียนไว้ครบแต่ยัง "ไม่เปิดใช้" ในการบันทึก — ฝั่ง backend ตรวจแค่ว่าสองคีย์ใหม่ (
primaryColor,secondaryColor) เป็นรหัสสี hex จริง (#RGB/#RRGGBB/#RRGGBBAA) ส่วนตาราง 5 คู่ที่ต้องได้อัตราคอนทราสต์ขั้นต่ำ (ข้อความ/พื้นหน้า 4.5:1, ป้ายปุ่ม/ปุ่ม 3:1 ฯลฯ) มีสูตรคำนวณครบแต่ยังไม่บล็อกการบันทึก เพราะคีย์สีเดิม 5 ค่าไม่ได้ถูกบังคับรูปแบบไว้ ถ้าเปิดกะทันหันจะทำให้ฟอร์มเก่าบันทึกไม่ได้ทั้งหมด — คำเตือนจึงไปโผล่เป็น alert ในหน้าเว็บแทน (ที่คนเลือกสีแก้ได้ทันที) - การอัปโหลดรูปหน้าปกตรวจชนิดไฟล์จาก magic bytes ไม่ใช่นามสกุล — backend รับ
image/jpeg,image/png,image/gif,image/webpและอ่านหัวไฟล์เพื่อกำหนดนามสกุลจริง ขณะที่หน้าเว็บรับแค่ JPG/PNG และบังคับเพิ่ม (ขนาดต่ำกว่า 10 MB, กว้างอย่างน้อย 1024px, สัดส่วน 4:1 พร้อมเครื่องมือครอป) — GIF/WebP จึงอัปโหลดผ่าน API ได้แต่อัปผ่านหน้าเว็บไม่ได้ และถ้าอัปโหลดล้มเหลว ระบบจะบันทึกฟอร์มต่อโดยตั้งtheme.coverเป็น null ไม่ใช่ตอบ error - หน้ารายการ/คำตอบถูกจำกัดขอบเขต OA จริงแต่ด้วยวิธีต่างกัน —
GET /form-builder/:idกรองด้วยid AND line_oa_id, ส่วนDELETEและPUTตรวจกรรมสิทธิ์แยกก่อนแล้วตอบ 404/400 เมื่อไม่ใช่ของตัวเอง แต่ทั้งสองเส้นทางที่ id ไม่มีอยู่จริง จะได้ HTTP 200 พร้อม body เป็นnullไม่ใช่ 404 (พฤติกรรมเดิมจาก TSfindOneที่คงไว้โดยเจตนา) ส่วนคำตอบ/สถิติ/CSV ที่อ้างด้วยform_hashถูกจำกัดขอบเขตด้วยการ JOIN กลับไปที่form_builderแล้วกรองline_oa_idเพราะตารางform_submissionไม่มีคอลัมน์เจ้าของ
เมนูที่เกี่ยวข้องกันในเส้นทางนี้
- Member Database — เป็นเงื่อนไขบังคับของ Profile Mapping และเป็นเงื่อนไขทางอ้อมของ OTP ด้วย (ปิด Profile Mapping = แท็บยืนยันตัวตนเปิดไม่ได้) และเป็นแหล่งของ "คอลัมน์ระเบียน" ที่ OTP จะส่งรหัสไปให้
- Settings > OTP Verification — เป็นชั้นล่างที่บอกว่า OA นี้ส่ง OTP ได้ทางช่องทางใด แท็บยืนยันตัวตนอ่าน
smsConfigured/emailConfiguredจากที่นี่มาตัดสินว่าจะเปิดให้เลือกได้หรือไม่ และ backend ยังตรวจซ้ำอีกชั้นตอนบันทึกฟอร์ม - Rich menu — action
rich_menuของหน้าขอบคุณต้องอ้างริชเมนูที่เป็นของ OA ของฟอร์มและมีสถานะactive(หน้าเว็บดึงมาแค่ 100 รายการแรก) - Segment audience — เลือกฟอร์มนี้เป็นเงื่อนไขสร้างกลุ่มเป้าหมายจากคำตอบได้ (ตรวจแล้วในเมนูนั้นมี section ของ form-builder โดยเฉพาะ)
- Attribute Setup — แหล่ง custom attribute ที่ใช้เป็นปลายทางใน Field Mapping และ attribute "ยืนยันแล้ว" ของ Profile Mapping — แต่มีข้อควรรู้: หน้าจับคู่ฟิลด์ของ Form builder ไม่ได้อ่านจาก
/attribute-masterโดยตรง มันอ่านจากGET /template-message/merge-tagsแล้วคัดเอาคีย์ที่ขึ้นต้นด้วยcustom.มาใช้
จุดที่ควรระวัง
- ชื่อฟอร์มไม่สามารถซ้ำกับ OA อื่นได้ (เพราะ
form_hashunique ระดับฐานข้อมูล) และฟอร์มที่ลบแล้วยังจองชื่อไว้ — เจอ error 500 ที่อ่านไม่รู้เรื่องได้จากสองกรณีนี้ ให้เปลี่ยนชื่อเป็นแบบที่จำเพาะกับ OA ของตัวเองไว้ก่อน - ข้อจำกัดที่มีแค่ฝั่งหน้าเว็บ ไม่มีการตรวจซ้ำที่ backend เลย ได้แก่ จำนวนคำถามสูงสุด 10 ข้อ, ความยาวชื่อฟอร์ม 200 ตัวอักษร / คำอธิบาย 250 / ชื่อคำถาม 200, กฎ "ข้อมูลส่วนบุคคลชนิดละ 1 ข้อ", กฎ "ต้องมีคำถามอย่างน้อย 1 ข้อและต้องมีข้อที่บังคับกรอก" และกฎขนาด/สัดส่วนรูปหน้าปก — เรียก API ตรงข้ามข้อจำกัดเหล่านี้ได้ทั้งหมด
- ข้อความ "คุณสามารถเพิ่มตัวเลือกได้สูงสุด 10 ตัวเลือก" ในหน้าตั้งคำถามเป็นข้อความอธิบายเท่านั้น ปุ่มเพิ่มตัวเลือกไม่ได้ถูกปิดที่ตัวเลือกที่ 10 จึงใส่เกินได้จริง
- เผยแพร่แล้วกลับเป็น draft ไม่ได้ — ตรวจสอบให้ครบก่อนกดเผยแพร่ ถ้าต้องการหยุดรับคำตอบชั่วคราวให้เปลี่ยนเป็น inactive แทน
- การกด "ทั้งหมด" บนการ์ดสถิติไม่ได้ล้างตัวกรอง แต่ส่ง
submitStatus=totalไปที่ API ตรง ๆ (ไม่ใช่ค่าcomplete/incompleteที่ backend รู้จัก) — ผลลัพธ์ที่ได้จึงเท่ากับไม่กรองอะไร เพราะ backend ตกลงมาที่กรณี default - หน้าแก้ไขฟอร์ม (
/form-builder/form) ตรวจสิทธิ์ด้วย subjectline-oa-managementไม่ใช่form-builderเหมือนหน้ารายการและหน้าคำตอบ — เป็นความไม่สอดคล้องที่ควรทราบเมื่อจัดสิทธิ์ให้ผู้ใช้ - การลบฟอร์มในหน้ารายการเป็น soft delete และ error ที่เกิดขึ้นระหว่างลบถูกกลืนไปเงียบ ๆ (catch เปล่า) — ถ้าฟอร์มไม่หายจากตารางให้รีเฟรชและตรวจดูอีกครั้ง
สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)
- ฝั่งหน้าเว็บ — หน้ารายการและหน้าคำตอบถูกครอบด้วย
PermissionGuardตรวจ actionVIEWของ subjectform-builderแต่ หน้าฟอร์มแก้ไขตรวจ subjectline-oa-management(ดูจุดที่ควรระวัง) ระบบสิทธิ์ฝั่งหน้าเว็บมีเพียง actionVIEWและEXPORTจึงไม่มีการแยกสิทธิ์สร้าง/แก้/ลบในระดับ UI - ฝั่งเซิร์ฟเวอร์ — ทั้ง 9 เส้นทางของโมดูลนี้ถูกครอบด้วย
ModuleGate("form-builder")ที่บังคับใช้จริง ถ้าผู้ดูแลระดับแพลตฟอร์มปิดโมดูลนี้ให้องค์กรใด ผู้ใช้ประเภท customer ในองค์กรนั้นจะได้ 403 ทันทีแม้เรียก API ตรง (ผู้ใช้ประเภท onemoby ข้ามด่านนี้ได้เสมอ และด่านนี้ออกแบบให้ fail open — ถ้าอ่านค่าไม่สำเร็จจะปล่อยผ่าน ไม่ใช่ปฏิเสธ) - policy annotation ระดับการกระทำมีประกาศไว้เพียง 2 เส้นทาง (
createบนPOST /form-builderและupdateบนPUT /form-builder/:id) และวิ่งผ่าน middleware ที่เป็น no-op ทั้งหมด —internal/modules/auth/policy.goระบุตรงว่าการบังคับ policy ระดับ API ถูกปิดไว้โดยเจตนา (PoliciesGuard.canActivateคืนค่า true เสมอในต้นทาง TS) permission มีผลแค่กับการซ่อน/แสดงเมนูในหน้าเว็บ - ข้อสังเกตที่แก้ไขจากเอกสารเชิงเทคนิคชุดเดิม — เอกสารเดิมบรรยาย Form builder ว่าเป็นโมดูลที่ "บังคับสิทธิ์เข้มที่สุดในระบบ" ซึ่งไม่ตรงตามตัวอักษร: ปัจจุบันมี 15 โมดูล ที่มี
ModuleGateเหมือนกัน (campaign, rich-message, rich-menu, audiences, workflow, attribute-master, customer-database, import-mapping, auto-response, friend-track, template-message, api-key, trigger-rule, report-all-friends และ form-builder) และโมดูล attribute-master ยังประกาศ policy ระดับการกระทำครบทั้ง 10 เส้นทางมากกว่า form-builder ด้วย สิ่งที่ทำให้ Form builder เข้มจริงคือการตรวจสอบภายในบริการ ไม่ใช่ middleware — ตรวจกรรมสิทธิ์ข้าม OA เอง (ตอบ 404 ไม่ใช่ 403), ตรวจกรรมสิทธิ์ริชเมนูด้วย query แยก, ด่านกันการเปิด OTP นอกช่องทาง, allowlist สกีมา URL, deny-list เนื้อหา และตัวตรวจ closed-shape ของthankYou/profileMapping.otpซึ่งไม่มีโมดูลอื่นในระบบทำครบชุดนี้ - ไม่มีเส้นทางใดในโมดูลนี้เป็น
@Public()ทุกเส้นทางอยู่หลัง JwtAuthGuard ระดับ global
ไฟล์อ้างอิงจากซอร์สโค้ด
Frontend (cms-web-2026)
src/app/form-builder/page.tsx,src/app/form-builder/form/page.tsx,src/app/form-builder/response/page.tsx— จุดเข้าและ PermissionGuard ของแต่ละหน้าsrc/components/form-builder/list/—form-builder.container.tsx,form-builder-filter.tsx,form-builder-table.tsxsrc/components/form-builder/form/—form-builder-form.tsx(การตรวจสอบก่อนบันทึกทั้งชุด),hooks/useFormBuilder.ts,hooks/useQuestionOperations.ts(กฎจำนวนคำถาม),components/ProfileMappingSettings.tsx,verification/verification-form.tsx(แท็บ OTP),thank-you-form/,theme-form/form-colors-section.tsx(คำเตือนคอนทราสต์)src/components/form-builder/form/constants/form-builder.constants.ts—MAX_QUESTIONS, กลุ่มชนิดคำถามsrc/components/form-builder/constants/theme-form.constants.ts— ค่าสีเริ่มต้น 7 ค่าและกฎรูปหน้าปกsrc/components/form-builder/response/— การ์ดสถิติ, ตารางคำตอบ, การส่งออก CSV
Backend (line-management-cms-api-go)
internal/modules/formbuilder/controller.go— 9 เส้นทาง,ModuleGate("form-builder"), การอ่านฟอร์มแบบ multipartinternal/modules/formbuilder/service.go—form_hash, การตรวจชื่อซ้ำ, กรรมสิทธิ์ข้าม OA, ตรรกะ 500 ของtheme, CSV exportinternal/modules/formbuilder/dto.go— กฎตรวจข้อมูลขาเข้าและกฎ mime type ของรูปหน้าปกinternal/modules/formbuilder/otp.go— closed shape ของprofileMapping.otpและด่านตรวจotp_configinternal/modules/formbuilder/thankyou.go— closed shape ของthankYou, allowlist URL, deny-list เนื้อหาinternal/modules/formbuilder/contrast.go— สูตร WCAG, ค่าสีเริ่มต้น, เหตุผลที่ยังไม่บังคับคอนทราสต์
ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ สร้างฟอร์มและคำตอบ (Form Builder)