ลิงก์เชิญ/รีเซ็ต/ยืนยันอีเมลเปิดได้จากทุกเครื่อง เจ้าของร้านสมัครเองได้ (ADR-0009) และอีเมลของระบบส่งออกได้จริง
ทำที่ Supabase Dashboard ของโปรเจกต์ (สคริปต์ bash scripts/setup-deploy.sh พาทำครั้งแรก หน้านี้คือรายการที่ต้องคงไว้)
- Authentication → URL Configuration
ต้องลงท้ายด้วย
**เพราะแอปส่ง?next=/apply/?next=/auth/reset-passwordต่อท้าย — ถ้าใส่แบบไม่มี**Supabase จะถือว่าไม่ตรงและตัด{{ .RedirectTo }}เหลือแค่ Site URL (ลิงก์ในอีเมลกลายเป็น…vercel.app&token_hash=…เปิดไม่ได้ — เจอจริง 7 ก.ย. 2569)→ Site URL = โดเมนแอป (ปัจจุบัน https://tablezups.com) · Redirect URLs มี `https://<แอป>/auth/callback**` และ `http://localhost:3000/auth/callback**` (ใส่โดเมนเก่าที่ยังใช้ด้วย) - Authentication → Sign In / Providers → Email
ตั้งแต่ 7 ก.ย. 2569 เจ้าของร้านสมัครเองที่ /signup แล้วยื่นคำขอเปิดร้าน (บัญชีใหม่ไม่มีสิทธิ์ใด ๆ จนกว่าคุณจะอนุมัติ) · Confirm email ต้องเปิด ไม่งั้นอีเมลปลอมสมัครได้ · CAPTCHA อยู่ในขั้นถัดไป (ไม่บังคับ เปิดเมื่อเจอสแปม)
→ “Allow new users to sign up” = **เปิด** · “Confirm email” = เปิด - (ไม่บังคับ) Attack Protection → CAPTCHA ด้วย Cloudflare Turnstile
ทำเมื่อเจอสแปมสมัคร/ลองรหัสผ่าน — ลำดับห้ามสลับ:
- สร้าง widget ที่ Cloudflare Turnstile → Domain = โดเมนแอป (ใส่
localhostด้วยถ้าจะทดสอบในเครื่อง) → Widget Mode: Managed- เปลี่ยนโดเมนเมื่อไหร่ ต้องกลับมาเพิ่มชื่อโฮสต์ใหม่ใน Hostname Management ของ widget ทุกครั้ง — ไม่งั้น Turnstile ตอบ error 110200 บนโดเมนใหม่ ช่องยืนยันขึ้นข้อความ “โหลดช่องยืนยันไม่สำเร็จ” และล็อกอินไม่ได้เลยทั้งระบบ (เจอจริงตอนย้ายมา tablezups.com — ADR-0033) · แก้แล้วมีผลทันที ไม่ต้อง redeploy เพราะ site key ไม่เปลี่ยน
- เอา Site key ใส่เป็น env
NEXT_PUBLIC_TURNSTILE_SITE_KEYบน Vercel แล้ว redeploy ก่อน (ค่านี้ถูกฝังตอน build) - ค่อยกลับมาเปิด Supabase → Authentication → Attack Protection → Enable CAPTCHA → Turnstile แล้วใส่ Secret key (secret อยู่ที่ Supabase เท่านั้น ไม่ต้องใส่ใน env ของแอป)
ถ้าเปิดสวิตช์ใน Supabase ก่อน redeploy: ทุกคนจะสมัคร/ล็อกอิน/ขอรีเซ็ตไม่ได้ทันที เพราะแอปยังไม่ได้ส่ง token · ปิดกลับได้ทันทีที่ Supabase (โค้ดไม่ต้องแก้)
เปิดแล้วต้องทดสอบ 3 อย่าง(1) สมัครใหม่ที่ /signup และเปิดลิงก์ยืนยันจากอีเมลจริง (2) ล็อกอินและ “ลืมรหัสผ่าน?” (3) ปุ่ม “ส่งอีเมลรีเซ็ต” ในหน้าทีมของร้านและหน้าผู้ใช้ทั้งระบบ — ปุ่มสองตัวหลังไม่มีช่องยืนยัน จึงส่งผ่านสิทธิ์ผู้ดูแล (service role) ถ้าเจอ “ส่งอีเมลรีเซ็ตไม่สำเร็จ” ให้ใช้ “สร้างลิงก์รีเซ็ต (ใช้ครั้งเดียว)” แทนแล้วแจ้งผู้พัฒนา
→ หน้า /signup /login และ “ลืมรหัสผ่าน?” มีช่องยืนยัน “ไม่ใช่บอท” (ส่วนใหญ่ผู้ใช้ไม่เห็นอะไรเลย ผ่านเองอัตโนมัติ) - สร้าง widget ที่ Cloudflare Turnstile → Domain = โดเมนแอป (ใส่
- Authentication → Email Templates เปลี่ยนลิงก์จาก {{ .ConfirmationURL }} เป็นรูปแบบ token_hash ที่สร้างจาก {{ .SiteURL }}
- Confirm signup:
{{ .SiteURL }}/auth/callback?next=/apply&token_hash={{ .TokenHash }}&type=signup - Reset Password:
{{ .SiteURL }}/auth/callback?next=/auth/reset-password&token_hash={{ .TokenHash }}&type=recovery - Invite user:
{{ .SiteURL }}/auth/callback?next=/auth/reset-password&token_hash={{ .TokenHash }}&type=invite - Magic Link:
{{ .SiteURL }}/auth/callback?next=/me&token_hash={{ .TokenHash }}&type=magiclink
ใช้
{{ .SiteURL }}ไม่ใช่{{ .RedirectTo }}เพราะ RedirectTo จะถูกตัดเหลือ Site URL ทันทีที่ redirect ของแอปไม่ตรง allow list · free tier ที่ใช้ SMTP เริ่มต้นแก้เทมเพลตไม่ได้ ต้องตั้ง custom SMTP ก่อน (ขั้นถัดไป) · ถ้ายังใช้เทมเพลตเดิม ลิงก์ก็ยังทำงานผ่านหน้า/auth/finishแต่ต้องเปิดในเบราว์เซอร์ที่รัน JavaScript→ ลิงก์ตรวจฝั่งเซิร์ฟเวอร์ เปิดจากเครื่องไหนก็ได้ ไม่พึ่ง JavaScript และไม่พึ่ง allow list - Confirm signup:
- Project Settings → Authentication → SMTP Settings
ค่าเริ่มต้นของ Supabase ส่งได้ประมาณ 3 ฉบับ/ชั่วโมงจาก noreply@mail.app.supabase.io — ไม่พอสำหรับใช้จริง และแก้เทมเพลตไม่ได้ · หลังตั้ง SMTP เอง rate limit อีเมลของ Supabase เป็น 30 ฉบับ/ชั่วโมง (Authentication → Rate Limits)
→ ใช้ SMTP ของตัวเอง — ไม่มีโดเมน: Brevo (smtp-relay.brevo.com:587, user = อีเมลบัญชี, pass = SMTP key, ฟรี 300 ฉบับ/วัน) · มีโดเมน: Resend (smtp.resend.com:465, user resend, pass = API key) - Storage→ bucket สำหรับรูปผัง/โลโก้ (public) สลิปมัดจำ `slips` สลิปค่าบริการ `billing` และรูปแชท `support` (private) มีอยู่ (สคริปต์ `scripts/setup-supabase-storage.ts` สร้างให้)
ถ้าเห็นข้อความนี้ (ที่ผู้ใช้รายงานมา)
| ถ้าเห็นข้อความนี้ | แปลว่า | ให้ทำ |
|---|---|---|
| “ลิงก์หมดอายุหรือยังไม่ได้เปิดจากอีเมล กรุณาขอลิงก์ใหม่” | ลิงก์เกิน 1 ชั่วโมง ถูกใช้แล้ว โดเมนไม่อยู่ใน Redirect URLs หรือเทมเพลตส่ง token มาในรูปแบบที่เซิร์ฟเวอร์ไม่เห็น | ตรวจขั้น 1 และ 3 · ระหว่างแก้ ใช้ “สร้างลิงก์รีเซ็ต (ใช้ครั้งเดียว)” จากหน้าทีม/ผู้ใช้ (ไม่พึ่งอีเมล) |
| “ลิงก์ในอีเมลเป็น https://<แอป>&token_hash=… (ไม่มี /auth/callback) เปิดไม่ได้” | เทมเพลตใช้ {{ .RedirectTo }} แต่ redirect ของแอปไม่ตรง allow list จึงเหลือแค่ Site URL | ใช้เทมเพลตแบบ {{ .SiteURL }}/auth/callback?… ตามขั้น 3 และใส่ `**` ท้าย Redirect URLs |
| “ยังไม่ได้ตั้งค่า Supabase” | แอปไม่มี NEXT_PUBLIC_SUPABASE_URL/ANON_KEY | ตั้ง env แล้ว redeploy |
| “ยืนยันว่าไม่ใช่บอทไม่สำเร็จ กรุณาโหลดหน้าใหม่แล้วลองอีกครั้ง” | เปิด CAPTCHA ใน Supabase แล้ว แต่แอปส่ง token ไม่ได้ (ยังไม่ได้ตั้ง Site key/ยังไม่ redeploy, โดเมนที่เปิดอยู่ไม่ได้อยู่ใน Hostname Management ของ widget, หรือ token หมดอายุ) | ถ้าเพิ่งเปลี่ยนโดเมน ให้เพิ่มโฮสต์ใหม่ใน Cloudflare Turnstile ก่อน (มีผลทันที) · ไม่ใช่กรณีนั้นให้ตรวจ `NEXT_PUBLIC_TURNSTILE_SITE_KEY` บน Vercel แล้ว redeploy · แก้ด่วนให้ปิด Attack Protection ที่ Supabase ก่อน · ตรวจอัตโนมัติ: `npx tsx scripts/check-captcha-live.ts https://<โดเมน>` |
| “ระบบยังไม่เปิดให้สมัครเอง — ติดต่อผู้ดูแลระบบ” | “Allow new users to sign up” ยังปิดอยู่ | เปิดตามขั้น 2 |