Đăng nhập & Phân quyền — Ai được vào, ai không
Mục tiêu
Tích hợp hệ thống đăng nhập (Auth) vào app. Hiểu luồng logic If/Else trong thực tế: “nếu đã đăng nhập thì thấy nội dung, nếu chưa thì phải login”.
Sau bài này, bạn sẽ:
- ✅ Tích hợp được Supabase Auth (đăng ký, đăng nhập, đăng xuất)
- ✅ Hiểu luồng If/Else trong thực tế (protected routes)
- ✅ Cấu hình Row Level Security để mỗi user chỉ thấy dữ liệu của mình
Nội dung chính
5.1 — Tại sao cần đăng nhập?
Không phải app nào cũng cần login. Nhưng khi app cần:
- Lưu dữ liệu riêng cho từng người
- Phân biệt ai là ai
- Bảo vệ thông tin cá nhân
→ Cần hệ thống Authentication (xác thực).
Mẹo: Nếu app của bạn chỉ hiển thị thông tin công khai (ví dụ: landing page, blog), bạn KHÔNG cần Auth. Chỉ cần Auth khi có dữ liệu riêng tư hoặc cần phân biệt người dùng.
5.2 — Khái niệm CS lồng ghép: If/Else trong thực tế
LUỒNG LOGIC ĐĂNG NHẬP:
┌─────────────────┐
│ Người dùng mở app │
└────────┬────────┘
│
┌─────▼─────┐
│ Đã đăng │
│ nhập chưa? │
└─────┬─────┘
│
┌────────┴────────┐
│ │
┌────▼────┐ ┌────▼────┐
│ CHƯA │ │ RỒI │
│ (false) │ │ (true) │
└────┬────┘ └────┬────┘
│ │
┌─────────▼──────────┐ ┌──▼──────────────┐
│ Hiện trang Login │ │ Hiện trang chủ │
│ "Vui lòng đăng nhập│ │ "Xin chào, Thảo"│
│ để tiếp tục" │ │ + hiện dữ liệu │
└────────────────────┘ └─────────────────┘
Đây chính là If/Else mà lập trình viên dùng hàng ngày:
NẾU (user đã đăng nhập) {
→ Hiện trang chủ với dữ liệu cá nhân
} NGƯỢC LẠI {
→ Chuyển về trang Login
}
Luồng đăng nhập chi tiết hơn:
1. User mở app
↓
2. App kiểm tra: có session/token không?
↓
3a. CÓ → Gửi token lên Supabase để xác minh
↓ ↓
↓ Token hợp lệ → Hiện app + dữ liệu user
↓ Token hết hạn → Chuyển về Login
↓
3b. KHÔNG → Chuyển về trang Login
↓
4. User nhập email + mật khẩu → Bấm "Đăng nhập"
↓
5. Supabase kiểm tra email + mật khẩu
↓
6a. Đúng → Trả về token → Lưu vào trình duyệt → Redirect về trang chủ
6b. Sai → Hiện thông báo "Email hoặc mật khẩu không đúng"
Mẹo: “Token” giống như vé vào cửa rạp phim. Mỗi lần mở app, bạn đưa vé ra. Nếu vé hợp lệ thì vào được. Nếu vé hết hạn thì phải mua vé mới (đăng nhập lại).
5.3 — Tích hợp Auth bằng Supabase
Supabase có sẵn hệ thống Auth. Prompt cho AI trong Cursor:
Thêm hệ thống đăng nhập vào app bằng Supabase Auth.
FILE CẦN TẠO/SỬA:
- src/pages/Login.jsx — trang đăng nhập
- src/pages/Signup.jsx — trang đăng ký
- src/components/ProtectedRoute.jsx — wrapper kiểm tra auth
- src/App.jsx — cập nhật routing
TRANG /login:
- Form gồm: Email input, Password input, nút "Đăng nhập"
- Link "Chưa có tài khoản? Đăng ký" dẫn đến /signup
- Hiện thông báo lỗi rõ ràng nếu sai email/mật khẩu
- Sau khi đăng nhập thành công → redirect về trang chủ /
- Style: form ở giữa trang, max-width 400px, có padding và shadow
TRANG /signup:
- Form gồm: Email, Mật khẩu, Xác nhận mật khẩu, nút "Đăng ký"
- Validate: mật khẩu >= 6 ký tự, xác nhận mật khẩu phải khớp
- Link "Đã có tài khoản? Đăng nhập" dẫn về /login
- Sau khi đăng ký → hiện thông báo "Kiểm tra email để xác nhận"
PROTECTED ROUTE:
- Tạo component ProtectedRoute bọc các trang cần đăng nhập
- Nếu chưa đăng nhập → redirect về /login
- Nếu đã đăng nhập → hiện nội dung bình thường
NAVBAR CẬP NHẬT:
- Nếu chưa đăng nhập: hiện nút "Đăng nhập"
- Nếu đã đăng nhập: hiện email user + nút "Đăng xuất"
- Nút "Đăng xuất" gọi supabase.auth.signOut()
Dùng Supabase Auth với email/password.
Tham chiếu @src/lib/supabase.js cho kết nối.
5.4 — Khái niệm: Row Level Security (RLS)
KHÔNG CÓ RLS: CÓ RLS:
───────────── ────────
User A thấy tất cả: User A chỉ thấy của mình:
┌─── Task của A ───┐ ┌─── Task của A ───┐
│ Mua sữa │ │ Mua sữa │
│ Đi gym │ │ Đi gym │
├─── Task của B ───┤ └──────────────────┘
│ Họp 3pm │
│ Gọi khách │ User B chỉ thấy của mình:
└──────────────────┘ ┌─── Task của B ───┐
│ Họp 3pm │
← Ai cũng thấy hết! │ Gọi khách │
└──────────────────┘
Cách bật RLS trong Supabase:
1. Vào Supabase Dashboard → Table Editor → chọn bảng tasks
2. Bấm "RLS" (hoặc vào Authentication → Policies)
3. Bấm "Enable RLS" cho bảng tasks
4. Tạo policy mới:
- Policy name: "Users can view own tasks"
- Target roles: authenticated
- USING expression: auth.uid() = user_id
- Điều này nghĩa: chỉ hiện row nào có user_id = user đang đăng nhập
5. Tạo thêm policy cho INSERT:
- "Users can insert own tasks"
- WITH CHECK expression: auth.uid() = user_id
Mẹo: Nhớ thêm cột
user_id(kiểu uuid) vào bảng tasks. Khi thêm task mới, gửi kèmuser_id= user hiện tại. Không có cột này thì RLS không biết task nào của ai.
Prompt yêu cầu AI cập nhật RLS:
Cập nhật app To-do để hỗ trợ Row Level Security:
1. Thêm cột user_id (uuid) vào bảng tasks trên Supabase
(tôi sẽ làm thủ công trên dashboard)
2. Khi thêm task mới, tự động gửi kèm user_id = user hiện tại:
user_id: (await supabase.auth.getUser()).data.user.id
3. Khi fetch tasks, Supabase sẽ tự động lọc theo RLS policy
(không cần filter thủ công trong code)
Tham chiếu @src/lib/supabase.js và @src/pages/Home.jsx.
5.5 — Tùy chọn nâng cao: Đăng nhập bằng Google
Thêm nút "Đăng nhập bằng Google" vào trang Login.
Yêu cầu:
- Nút có icon Google, text "Đăng nhập bằng Google"
- Đặt phía trên form email/password, ngăn cách bằng dòng "hoặc"
- Dùng supabase.auth.signInWithOAuth({ provider: 'google' })
- Redirect callback URL: window.location.origin
Lưu ý: tôi sẽ cần cấu hình Google OAuth trong
Supabase Dashboard → Authentication → Providers → Google
Lỗi thường gặp
Vấn đề: “Đăng nhập lỗi” — nhập đúng email/mật khẩu nhưng không vào được
→ Kiểm tra Console (F12) xem lỗi gì. Phổ biến nhất: (1) Chưa bật Email provider trong Supabase → Authentication → Providers → Email → bật “Enable Email Signup”. (2) URL/Key trong .env bị sai — copy lại từ Supabase Dashboard.
Vấn đề: “Email xác nhận không đến” — đăng ký xong không nhận được email → Kiểm tra thư mục Spam/Junk. Nếu không có, vào Supabase → Authentication → Settings → tắt “Enable email confirmations” (chỉ tắt khi đang develop, deploy thì bật lại). Hoặc dùng email thật (Gmail) thay vì email giả.
Vấn đề: “Trang trắng sau khi đăng nhập” — login xong nhưng không redirect
→ Kiểm tra code redirect. Sau khi signInWithPassword thành công, cần gọi navigate('/') hoặc kiểm tra lại ProtectedRoute. Nói AI: “Sau khi đăng nhập thành công, redirect về trang chủ. Hiện tại nó đang bị kẹt ở trang login”.
Vấn đề: “RLS bật rồi nhưng user vẫn thấy dữ liệu của người khác”
→ Kiểm tra: (1) Bảng có cột user_id chưa? (2) Policy USING expression có đúng auth.uid() = user_id không? (3) Khi insert data, có gửi kèm user_id không? Thiếu 1 trong 3 thứ này đều gây lỗi.
Vấn đề: “Đăng xuất rồi mà vẫn thấy dữ liệu”
→ Sau khi signOut, cần clear state/redirect. Nói AI: “Khi user bấm đăng xuất, clear toàn bộ state và redirect về trang /login”.
Bài tập thực hành
Nhiệm vụ: Thêm Auth vào app To-do ở Bài 4.
Bước 1 (8 phút): Tạo trang Login và Signup bằng prompt ở phần 5.3
- Tiêu chí: có form đăng nhập, form đăng ký, validation cơ bản
- Gợi ý nếu bị stuck: bắt đầu chỉ với trang Login trước, test xong rồi tạo trang Signup
Bước 2 (7 phút): Đăng ký 2 tài khoản test và kiểm tra login/logout
- Tiêu chí: đăng nhập → thấy trang chủ, đăng xuất → về trang login
- Gợi ý nếu bị stuck: nếu email xác nhận không đến, tắt email confirmation trong Supabase Settings
Bước 3 (10 phút): Thêm cột user_id vào bảng tasks, bật RLS, mỗi user chỉ thấy task của mình
- Tiêu chí: User A và User B thêm task riêng, không thấy task của nhau
- Gợi ý nếu bị stuck: xem lại phần 5.4 về cách tạo RLS policy
Thời gian: 25 phút Deliverable: Screenshot trang Login + Screenshot 2 user khác nhau thấy data khác nhau
Tổng kết bài 5
| Bạn đã học được | Chi tiết |
|---|---|
| Authentication | Hệ thống xác thực “ai là ai” |
| If/Else thực tế | Nếu login rồi → trang chủ, chưa → trang login |
| Login flow | Token, session, redirect, protected routes |
| Row Level Security | Mỗi user chỉ thấy dữ liệu của mình |
Kiểm tra kiến thức
1. Token trong hệ thống đăng nhập hoạt động như thế nào?
2. Row Level Security (RLS) trong Supabase giải quyết vấn đề gì?
3. ProtectedRoute dùng để làm gì?
4. Để RLS hoạt động đúng, bảng dữ liệu cần có gì?
5. Khi nào app KHÔNG cần hệ thống đăng nhập (Auth)?