v1.0.2762026-11-08
Security — ใส่การตรวจสิทธิ์ให้ `configx` และย้ายการส่งออก Excel ของสมาชิกเข้า `memberx`
· **`configx` (ตั้งค่าหน้าเว็บ)**: เดิมไม่มีเช็คอะไรเลย ใครก็แก้ลิงก์ถ่ายทอดสด/ข้อความหน้าแรก/ลิงก์ออนไลน์ (`save_config` รับ HTML ดิบ) และอ่านประวัติการแก้ไขได้ — ตอนนี้ทุกหน้าตั้งค่าและปุ่มบันทึกต้อง **ล็อกอิน + อยู่กลุ่ม Admin (2) หรือ ฝ่าย Impact (4)** (ตามคนที่แก้จริงใน `configx_logs`); ถูกปฏิเสธ = JSON `flag 0` พร้อมข้อความสำหรับ AJAX / redirect หน้า login หรือหน้า "ไม่มีสิทธิ์" สำหรับหน้าเว็บ
· **CHANGELOG / ประวัติเวอร์ชัน**: หน้า `configx/version` และ `configx/get_changelog_json` ต้องล็อกอิน (หรือยืนยันตัวตน LIFF) และ **ตัดหัวข้อ `### Security` ออก** ยกเว้น Admin ที่เห็นเต็ม · หน้า LIFF ของพี่เลี้ยง (หน้าต่าง "ประวัติเวอร์ชัน") ส่ง `line_user_id` ไปยืนยัน — ผู้ที่ยังไม่ผ่านการอนุมัติ/ไม่ได้ล็อกอินได้แค่เลขเวอร์ชัน
· **ส่งออก Excel รายชื่อสมาชิก**: `export/export_member.php` เดิมไม่ตรวจสิทธิ์ ใครก็ POST `id_list` แล้วโหลดข้อมูลสมาชิก (รวมเลขบัตรประชาชน ที่อยู่ รหัสแรกเข้า) ได้ และต่อ `id_list` เข้า SQL ตรงๆ — ย้ายเข้า `memberx/export_member_set` (POST รายการ id) + `memberx/export_member` (ดาวน์โหลด) ขั้นตอนใช้งานเหมือนเดิมทุกอย่าง แต่ต้องล็อกอิน + สิทธิ์ `manage` ของสมาชิก, `id_list` ผ่าน `intval`, ดึงเฉพาะคอลัมน์ที่ตารางใช้ (ไม่ดึง hash รหัสผ่าน), escape ทุกค่าที่พิมพ์ลง Excel · เปลี่ยน URL ใน view 8 หน้าที่เรียกใช้ (ทะเบียนสมาชิก v1/v2, e-learning 3 หน้า, tlcc/vid, member_study 2 หน้า)
· **ลบไฟล์**: `export/export_member.php`, `export/export_treat.php` (ตัวแปลง HTML→Excel ที่เปิดโล่ง; เรียกจาก path `/home/export/...` ซึ่งบน production ไม่มี จึงไม่ได้ใช้งานอยู่แล้ว), `export/simple_html_dom.php` (ไลบรารีที่มีแต่ `export_treat` ใช้)
· **ข้อจำกัดเดิมที่ยังอยู่**: PHP ตั้ง `max_input_vars = 1000` ถ้าเลือกส่งออกเกิน 1,000 รายชื่อ รายการจะถูกตัด (เป็นมาก่อนแล้ว ไม่ได้แก้ในรอบนี้)
· ทดสอบด้วย **CodeIgniter ตัวจริงบน MySQL 8 ชั่วคราว** (ลบทิ้งแล้ว ไม่มีข้อมูลจริง) 47 กรณี: ไม่ล็อกอิน / สมาชิกทั่วไป / ฝ่าย Impact / Admin / ผู้มี `manage` แต่ไม่ใช่ Admin / ผู้ใช้ LIFF ที่อนุมัติแล้วและยังรออนุมัติ — **โค้ดใหม่ผ่าน 47/47; `configx` เดิมรันชุดเดียวกันตก 23 ข้อ** (ไม่ล็อกอินบันทึกค่าได้, อ่าน CHANGELOG เต็มได้)
· **ไม่ต้องรัน SQL**
v1.0.2752026-11-08
Security — ย้ายความลับที่ตกหล่นจาก v1.0.274 เข้าไฟล์ความลับ (2 จุด)
· ตรวจซ้ำทั้ง repo ด้วยวิธีที่รัดกุมขึ้น (ค้นตามชื่อฟิลด์ที่บ่งว่าเป็นความลับ + รูปแบบค่า ไม่พึ่งคำบอกใบ้ในบรรทัด และไม่ตัดสตริงที่ขึ้นต้นด้วย `/`) พบที่เหลือ 2 จุดและย้ายแล้ว: โทเคน LINE ช่องทาง SNMRI ใน `liff.php` (`line_msg_token_snmri`) และ Facebook App Secret ใน `views/fd.php` (`facebook_app_secret`)
· ค่าที่ใช้งานอยู่เหมือนเดิม ไม่มีพฤติกรรมเปลี่ยน · ตรวจซ้ำหลังแก้: ไม่เหลือความลับเขียนตรงในโค้ดที่ track อยู่ (ยกเว้นคีย์ Google Maps ฝั่งเบราว์เซอร์ในหน้า view ซึ่งต้องเปิดเผยอยู่แล้วและควรจำกัดโดเมนที่คอนโซล Google)
· เพิ่มคีย์ใน `secrets.example.php` และตารางใน `docs/BUSINESS.md`
· **ไม่ต้องรัน SQL**
v1.0.2742026-11-08
Security — ย้ายความลับของบริการภายนอกทั้งหมดออกจากโค้ด เข้าไฟล์ความลับ
· ต่อจาก v1.0.273: ความลับที่เคยเขียนตรงในโค้ด (LINE Messaging/Login, LINE Notify, Telegram, YouTube API, ระบบชำระเงิน, API บุคคลที่สาม) ย้ายไปอยู่ในไฟล์ความลับนอกโฟลเดอร์เว็บทั้งหมด และโค้ดอ่านผ่าน `tl_secret('ชื่อคีย์')` — **ค่าที่ใช้งานอยู่เหมือนเดิมทุกตัว ไม่มีพฤติกรรมเปลี่ยน** (ยังไม่ได้เปลี่ยนค่าที่ผู้ให้บริการ)
· เปลี่ยนค่าในอนาคต = แก้บรรทัดเดียวในไฟล์ความลับบนเซิร์ฟเวอร์ ไม่ต้อง deploy · รายชื่อคีย์และที่ใช้อยู่ใน `docs/BUSINESS.md` และ `application/config/secrets.example.php`
· แก้ใน `hook`, `hook2`, `hook_tl`, `liff`, `mission`, `notify`, `testimony`, `treat`, `api` (controller), `api_helper` (helper), `constants.php`; ลบค่าความลับที่ติดอยู่ในคอมเมนต์ทิ้ง และเอาโทเคนที่ฝังใน JavaScript ของหน้าคำพยาน (ส่วนที่ถูกปิดไว้) ออก
· `mission.php`: ค่าเริ่มต้นของ property ที่เป็นความลับย้ายไปกำหนดใน constructor (PHP ไม่อนุญาตเรียกฟังก์ชันในค่าเริ่มต้น) · `testimony.php`: พารามิเตอร์ `line_noti` เปลี่ยนค่าเริ่มต้นเป็น `null` (ฟังก์ชันใช้ `$this->token` อยู่แล้ว)
· ทดสอบ: แปลงด้วย PHP tokenizer แล้วพิสูจน์ย้อนกลับ (แปลง `tl_secret()` กลับเป็นค่าเดิมต้องเท่ากับต้นฉบับทุกไบต์ ยกเว้นข้อความในคอมเมนต์ที่ตั้งใจลบ) ผ่านทั้ง 11 ไฟล์; ค่าในไฟล์ความลับตรงกับ hash ของต้นฉบับทุกคีย์; `php -l` ผ่านทุกไฟล์
· **ไม่ต้องรัน SQL**
v1.0.2732026-11-08
Security — ย้ายความลับออกจาก git และเปลี่ยนไปใช้บัญชีฐานข้อมูลที่มีสิทธิ์จำกัด
· **เหตุผล**: ซอร์สโค้ดพร้อมประวัติ git เคยถูกเปิดให้ดาวน์โหลดผ่านเว็บ (ปิดแล้วที่ระดับเซิร์ฟเวอร์) จึงถือว่าความลับที่เคยเขียนในโค้ดรั่วไปแล้ว ต้องเปลี่ยนค่าใหม่ และไม่เก็บความลับไว้ใน git อีก
· **โค้ด**: เพิ่ม `tl_secret('คีย์')` ใน `application/config/constants.php` อ่านความลับจากไฟล์ที่อยู่นอกโฟลเดอร์เว็บและไม่อยู่ใน git (หรือ path ใน env `TL_SECRETS_FILE`) — ถ้าไม่พบไฟล์/คีย์ production จะเชื่อมต่อฐานข้อมูลไม่ได้ (ล้มแบบปลอดภัย ไม่ย้อนใช้ค่าเดิม) · ตัวอย่างโครงไฟล์ที่ `application/config/secrets.example.php`
· ใช้ `tl_secret()` ใน `database.php` (บัญชี/รหัสผ่าน DB ของ production), `config.php` (`encryption_key`) และ `export/export_member.php` (เดิมฝังรหัสผ่านไว้เอง) · เครื่อง dev (localhost) ยังใช้ค่า dev เดิม ไม่ต้องตั้งค่าอะไรเพิ่ม · ลบรหัสผ่านเก่าที่ comment ทิ้งไว้ใน `database.php`
· **ฝั่งเซิร์ฟเวอร์ (ไม่ผ่าน git)**: แอปเลิกเชื่อมฐานข้อมูลด้วยบัญชี `root` เปลี่ยนเป็นบัญชีเฉพาะแอปที่มีสิทธิ์เพียงฐานข้อมูล `truelife_inchris` (สร้างฐานข้อมูล/ผู้ใช้ใหม่ อ่านตารางระบบ หรืออ่านไฟล์ของระบบไม่ได้) รหัสผ่านและ `encryption_key` เปลี่ยนเป็นค่าสุ่มใหม่
· **ผลที่ผู้ใช้จะเห็น**: ทุกคนต้อง **ล็อกอินใหม่ 1 ครั้ง** หลัง deploy (เปลี่ยนคีย์ที่ใช้เซ็นคุกกี้ session) ไม่มีข้อมูลเสียหาย; ไม่มีโค้ดส่วนไหนใช้ `encryption_key` เข้ารหัสข้อมูลที่เก็บถาวร
· **ไม่ต้องรัน SQL**
v1.0.2722026-11-08
Security — ปิดช่องโหว่ `member_study` (สำเนาเก่าของ memberx) แบบเดียวกับ v1.0.271
· **ปัญหา**: `member_study` เป็นสำเนาเก่าของ `memberx` ที่ไม่มีเมนู/route ชี้มา (access log ย้อนหลัง 10 วันไม่พบการเรียก route จริง มีแต่บอตสแกนไฟล์ซอร์ส) แต่ **ใครก็เรียกได้โดยไม่ล็อกอิน** เหมือนกัน — endpoint ที่ยังทำงานได้และรั่ว: `get_json` (คืนข้อมูลสมาชิกทั้งแถว รวม **hash รหัสผ่านและรหัสแรกเข้า**), `ck_dup` (คืนทั้งแถว + ต่อ SQL ตรงๆ → SQL injection ใส่ `' OR '1'='1` ได้สมาชิกทุกคน), `check_guest`/`check_mentor` (แสดงรายชื่อ + SQL ต่อจาก URL), `set_orderx` (เปิดผ่าน URL แก้ `sortx` ได้ + SQL ต่อ URL)
· `get_datatable` / `removex` / `savex` / `print_excel` ใน controller นี้ **พังอยู่เดิมแล้ว** (เรียก `$this->memberx_model` ทั้งที่โหลดแค่ `member_study_model` → fatal) จึงไม่เคยรั่วจริง แต่ใส่การตรวจสิทธิ์ไว้ด้วย กันกรณีมีคนแก้ให้ทำงานภายหลัง
· **แก้ (`application/controllers/member_study.php`)**: ใช้แนวเดียวกับ memberx — ทุก endpoint ที่แตะข้อมูลสมาชิกต้องล็อกอิน + มีสิทธิ์ตาม `nano_action` (controller `member`): `manage` (ดู/เช็คชื่อซ้ำ/หน้า check_*), `edit`/`create` (savex), `delete` (removex); endpoint จังหวัด/อำเภอ/ตำบลคงเปิด; ถูกปฏิเสธ → HTTP 200 + ข้อความ/JSON ว่าง (AJAX) หรือ redirect `auth` / 403 (เปิดใน browser)
· `get_json` ไม่ส่ง hash `password`; `ck_dup` คืนเฉพาะ `membercode, fname, lname, section`; `intval`/`escape_str` ค่าที่ต่อ SQL (controller + `member_study_model`: `removex`, `savex`, `MONTH()`); `set_orderx` เปลี่ยนเป็น private (`_set_orderx`)
· ทดสอบด้วย **CodeIgniter ตัวจริงบน MySQL 8 ชั่วคราว** (ลบทิ้งแล้ว ไม่มีข้อมูลจริง) 37 กรณี × 4 ตัวตน (ไม่ล็อกอิน / ล็อกอินไม่มีสิทธิ์ / มีแค่ manage / แอดมิน): **โค้ดใหม่ผ่าน 37/37; โค้ดเดิมรันชุดเดียวกันตก 23 ข้อ**
· **ไม่ต้องรัน SQL**; ไม่กระทบการใช้งานจริง (ไม่มีหน้า/เมนูใช้ controller นี้) — ถ้าไม่มีแผนใช้ ควรลบ `member_study` ทั้งชุด (controller, model, views) ในโอกาสหน้า
· **ยังไม่ได้แก้**: controller อื่นที่เช็คล็อกอินถูก comment ไว้ (`guest`, `media`, `panrak`, `page`, `paysbuy`, `tlcc`) และ `api` ที่ไม่มีการเช็คล็อกอินเลย — ต้องไล่ตรวจแยกว่า endpoint ไหนเปิดสาธารณะโดยตั้งใจ (เช่น LIFF)
v1.0.2712026-11-08
Security — ปิดช่องโหว่ทะเบียนสมาชิก (`memberx`): endpoint ทั้งหมดเปิดโล่ง ใครก็ดึง/แก้/ลบข้อมูลสมาชิกได้โดยไม่ล็อกอิน
· **ปัญหา (พบระหว่างแก้ v1.0.270)**: ตัวเช็คล็อกอินใน constructor ของ `memberx` ถูก comment ไว้ มีแค่หน้า `index()` ที่เช็คสิทธิ์ ส่วน endpoint ที่หน้าจอเรียกด้วย AJAX **ไม่เช็คอะไรเลย** — เรียกตรงๆ โดยไม่ล็อกอินแล้ว: `get_datatable` คืนรายชื่อสมาชิกทั้งหมด (5,316 รายการ ชื่อ/เบอร์โทร/วันเกิด/ที่อยู่), `get_json` คืนข้อมูลสมาชิกทั้งแถวรวม **hash รหัสผ่านและรหัสแรกเข้า**, `ck_dup` คืนทั้งแถว (และต่อ SQL ตรงๆ → SQL injection), `savex` แก้ไข/เพิ่มสมาชิกได้ (รวมตั้งรหัสผ่านใหม่ให้ใครก็ได้), `removex` **ลบสมาชิกถาวร**ได้, `set_orderx`/`check_guest`/`check_mentor` เปิดผ่าน URL (มี SQL ที่ต่อค่าจาก URL ตรงๆ)
· **แก้ (`application/controllers/memberx.php`)**: ทุก endpoint ที่แตะข้อมูลสมาชิกต้อง **ล็อกอิน + มีสิทธิ์** ตาม `nano_action` (controller `member`) ซึ่งเป็นชุดเดียวกับที่หน้าจอใช้ซ่อน/แสดงปุ่มอยู่แล้ว:
- `get_datatable` / `get_json` / `ck_dup` → สิทธิ์ `manage` (gid 2, 3, 6, 8)
- `savex` → สมาชิกเดิม = `edit`, รหัสใหม่ = `create` (gid 2, 3, 6) · `removex` → `delete` (gid 2, 3, 6)
- `check_guest` / `check_mentor` / `print_excel` → `manage`, `load_csv` → `create` (ยังไม่ล็อกอิน = redirect หน้า login, ไม่มีสิทธิ์ = 403)
- `get_zip` / `get_districts` / `get_amphures` (ข้อมูลจังหวัด/อำเภอ/ตำบลสาธารณะ ใช้ในหน้าสมัครด้วย) **คงเปิดไว้**
- ตอบปฏิเสธเป็น **HTTP 200 + ข้อความตามรูปแบบเดิมที่ JS อ่าน** (`get_datatable` → ตารางว่าง + `error`, `savex` → ข้อความให้ `alert`, `removex` → `0`) เพราะ JS ของหน้านี้ไม่มี `error` callback ถ้าตอบ 401/403 จะเงียบหาย — ผู้ใช้ที่ session หมดอายุจะเห็นข้อความ "หมดเวลาการเข้าสู่ระบบ กรุณาเข้าสู่ระบบใหม่" แทนที่จะบันทึกไม่ได้เงียบๆ
· **ลดข้อมูลที่ส่งออก**: `get_json` ไม่ส่ง hash รหัสผ่าน (`password`) ไปเบราว์เซอร์อีก (คง `first_pass` ไว้ เพราะหน้าแก้ไขแสดง "รหัสแรกเข้า" ให้แอดมินแจ้งสมาชิก); `ck_dup` คืนเฉพาะ `membercode, fname, lname, section` (หน้าจอใช้แค่นี้) แทน `select *`
· **ปิด SQL injection**: `ck_dup` ใช้ `escape_str`; `membercode` ใน `savex`/`removex` และ `month` ใน `get_datatable` ผ่าน `intval` (ทั้ง controller และ `memberx_model`); `check_guest`/`check_mentor` ตัดอักขระพิเศษจากรหัสทีม + escape ชื่อพี่เลี้ยงก่อนต่อ SQL; `set_orderx` เปลี่ยนเป็น **private** `_set_orderx()` (เรียกจาก `savex` เท่านั้น ไม่เปิดผ่าน URL) และ escape/intval ค่าที่ต่อ SQL
· **พฤติกรรมที่คงเดิม**: แอดมินใช้งานได้เหมือนเดิมทุกอย่าง (รายการ/เปิดแก้ไข/บันทึก/เพิ่ม/ลบ/เช็คชื่อซ้ำ/ลง `member_logs`/จัดลำดับ `sortx`), ฟอร์มเพิ่มสมาชิกที่ส่ง `membercode=0` ยังทำงานตามเดิม
· ทดสอบด้วย **CodeIgniter ตัวจริง (controller/model/session/auth_model จริง) บน MySQL 8 ชั่วคราว** (ลบทิ้งแล้ว ไม่มีข้อมูลจริง) 54 กรณี แยก 4 ตัวตน: ไม่ล็อกอิน / ล็อกอินแต่ไม่มีสิทธิ์ / มีแค่ `manage` / แอดมิน — **โค้ดใหม่ผ่าน 54/54; โค้ดเดิมรันชุดเดียวกันตก 37 ข้อ** (ไม่ล็อกอินดึงรายชื่อ/hash รหัสผ่านได้, `savex`/`removex` สำเร็จ, `ck_dup` โดน SQL injection ฯลฯ) ยืนยันว่าชุดทดสอบจับช่องโหว่ได้จริง
· **ไม่ต้องรัน SQL**; ผู้ใช้ที่มีสิทธิ์อยู่แล้วไม่ต้องทำอะไร
· **ยังไม่ได้แก้ (ควรทำต่อ)**: `member_study` เป็นสำเนาเก่าของ memberx และมีรูปแบบเดียวกัน (access log ย้อนหลัง 10 วันไม่พบการเรียก route `member_study/...` จริง มีแต่บอตสแกนหาไฟล์ซอร์ส `/application/...php`); controller อื่นที่เช็คล็อกอินถูก comment ไว้ (`guest`, `media`, `panrak`, `page`, `paysbuy`, `tlcc`) ต้องไล่ตรวจแยก
v1.0.2702026-11-08
Fix — หน้าทะเบียนสมาชิก (`/memberx`) หลุดการล็อกอินทุกประมาณ 5 นาที (ทำให้ "บันทึกได้ราว 3 รายการแล้วหลุด")
· **อาการ**: แก้ไข/บันทึกข้อมูลสมาชิกต่อเนื่อง สักพักแล้วรีเฟรชหน้าได้หน้า "ยังไม่ล็อกอิน" ต้องล็อกอินใหม่ (ใน log เห็นซ้ำหลายรอบ: บันทึกหลายรายการ → `GET /memberx` ได้หน้าเล็ก 9 KB แทนหน้าเต็ม 28 KB → `auth/logout` → `auth/submit` ล็อกอินใหม่) — คำขอบันทึก (`memberx/savex`) เองไม่ได้เช็คล็อกอิน จึงยังตอบสำเร็จหลัง session หลุดไปแล้ว ผู้ใช้เลยมารู้ตอนรีเฟรช
· **สาเหตุ (race ใน session ของ CodeIgniter 2)**: ไลบรารี session จะ **เปลี่ยนรหัส session ทุก `sess_time_to_update` = 300 วินาที** ทุกครั้งที่มีคำขอที่ไม่ใช่ AJAX (`MY_Session` เดิมยกเว้นเฉพาะ AJAX) — หลังบันทึกแต่ละครั้งหน้าจะโหลดตารางใหม่ แล้วเบราว์เซอร์ยิงรูปโปรไฟล์ที่ไม่มีไฟล์ `/src/photo/NNN.jpg` เป็น **404 พร้อมกันเป็นสิบๆ ตัว** (ใน log เห็น ~40 ตัวในวินาทีเดียว) ทุกตัววิ่งผ่าน CodeIgniter ถือรหัส session เดิมชุดเดียวกัน → ตัวแรกเปลี่ยนรหัสในฐานข้อมูล ตัวที่เหลือหารหัสเก่าไม่เจอ จึงสร้าง session ว่างใหม่ (1 แถวใน `ci_sessions` ต่อคำขอ) และสั่งเขียนทับ cookie ของผู้ใช้ → หลุดล็อกอิน (เกิดตอนโหลดตารางครั้งแรกหลังครบ 5 นาทีนับจากโหลดหน้าล่าสุด)
· **แก้**: `application/libraries/MY_Session.php::sess_update()` ต่ออายุ session **ในที่เดิม** — อัปเดตเฉพาะ `last_activity` (ฐานข้อมูล + cookie) **โดยไม่เปลี่ยนรหัส session** คำขอที่ซ้อนกันจึงเจอ session เดิมเสมอและเขียนค่าเดียวกัน ไม่มี race; อายุ session ยังต่อเนื่อง (sliding 20 ชม.) เหมือนเดิม, AJAX ยังไม่แตะ session เหมือนเดิม, ไม่ถึงรอบ 5 นาทีก็ไม่เขียนฐานข้อมูล, session หมดอายุจริงยังถูกล้างตามเดิม
· **ข้อแลกเปลี่ยน**: ไม่มีการหมุนรหัส session ทุก 5 นาทีแล้ว (ป้องกัน session fixation ในทางทฤษฎีลดลงเล็กน้อย — ของเดิมก็ไม่ได้หมุนรหัสตอนล็อกอินอยู่แล้ว)
· ทดสอบด้วย **ไลบรารี session ตัวจริงของ CodeIgniter (`system/libraries/Session.php`) บน MySQL 8 ชั่วคราว** (ลบทิ้งแล้ว): จำลองคำขอ 12 ตัวพร้อมกันหลังครบ 5 นาที — **โค้ดเดิม: ล็อกอินได้แค่ตัวแรก (100000000000), อีก 11 ตัวถูกสร้าง session ว่าง (12 แถวใน DB) และคำขอถัดไปของผู้ใช้หลุดล็อกอิน (ตรงกับอาการจริง)**; **โค้ดใหม่: ทั้ง 12 ตัวยังล็อกอินอยู่, 1 แถวใน DB, คำขอถัดไปล็อกอินอยู่**; ทดสอบเพิ่ม: คำขอเดียวหลังครบ 5 นาที (รหัสเดิม + last_activity ใหม่), session ยังสด (ไม่เขียน DB), AJAX (ไม่แตะ), หมดอายุ 20 ชม. (ถูกล้าง)
· ไม่ต้องรัน SQL; ผู้ใช้ที่ล็อกอินอยู่ไม่ต้องล็อกอินใหม่ (cookie/session รูปแบบเดิมใช้ต่อได้)
· **พบระหว่างตรวจ (ยังไม่ได้แก้ — ดูหมายเหตุ):** endpoint ของ `memberx` (`get_datatable`, `savex`, `removex` ฯลฯ) **ไม่ตรวจการล็อกอินเลย** (ตัวตรวจใน constructor ถูก comment ไว้ มีแค่หน้า `index()` ที่เช็คสิทธิ์) เรียกโดยไม่ล็อกอินแล้วได้รายชื่อสมาชิกทั้งหมดกลับมา — ต้องแก้แยกต่างหากโดยเร็ว
v1.0.2692026-10-06
Change — หน้ากรอกคนรับเชื่อ (LIFF): ธีมสี น้ำเงิน-ขาว
· เปลี่ยนโทนสีทั้งหน้าจากพีช-ชมพูเป็น **น้ำเงิน-ขาว**: หัวและปุ่มบันทึกไล่สีน้ำเงิน (#3b82f6 → #1b46b8), พื้นหลังหน้าฟ้าขาวอ่อน (#f1f5fc), การ์ดขาวเงาโทนน้ำเงิน, ข้อความ/ป้ายกำกับ/ไอคอนในช่องเป็นโทนเทาน้ำเงิน, ชิปสถานะรับที่เลือก ปฏิทิน และรายการเตือนชื่อซ้ำเป็นน้ำเงิน, ไอคอนหัวการ์ดเป็นเฉดน้ำเงิน/ฟ้า/คราม/เขียวน้ำทะเล, วงกลมตัวอักษรในรายการล่าสุดเป็นเฉดน้ำเงิน-ฟ้า
· **สีที่สื่อความหมายคงเดิม**: แดง = ผิดพลาด/ช่องบังคับ/ปุ่มลบ, เหลือง-ส้ม = เตือน ("ยังไม่ได้รับเชื่อ", แถบกำลังแก้ไข, รอตรวจสอบสิทธิ์, ปุ่มแก้ไข), เขียว = บันทึกสำเร็จ
· **แก้สีทั้งหน้าได้ที่ตัวแปร `:root { --ac-... }` ชุดเดียว** ที่หัวบล็อก `