Tutorial: ระบบ Login ด้วย Firebase (React + Protected Route)

เอกสารสอนแบบ Step-by-step สำหรับวิชา Frontend Framework (ระดับปริญญาตรี) โดยเน้นทำตามได้จริงในโปรเจค React

ผู้สอน: ผู้ช่วยศาสตราจารย์ ปองพล นิลพฤกษ์

React Firebase Authentication Protected Route Service Layer (authService / profileService) Responsive + Light Theme
ขอบเขตที่เราจะทำ
  • สร้าง Firebase Project + เปิด Email/Password
  • เชื่อม Firebase ใน React ผ่าน src/lib/firebase.js
  • ทำหน้า Register และ Login
  • ทำ Route แบบป้องกันด้วย ProtectedRoute
  • เพิ่ม Header แสดงสถานะและปุ่ม Logout
  • อธิบายแนวคิด “Auth State” และ “Loading”
  • แยก logic เป็น services เพื่อดูเป็นงานจริง
  • ทดสอบ End-to-End: Register → Logout → Login → เข้า Dashboard

Step 1 — ภาพรวมระบบ และโครงสร้างไฟล์

เป้าหมายของระบบนี้คือเพิ่ม “การยืนยันตัวตน” ให้โปรเจคเดิม (ที่มีหน้า/ธีมเดิมอยู่แล้ว) โดยใช้ Firebase Authentication และป้องกันบางหน้าให้เข้าได้เฉพาะผู้ที่ล็อกอินเท่านั้น

แนวคิดสำคัญที่ต้องเข้าใจก่อน
  • Authentication = ระบบยืนยันตัวตน (Login/Register)
  • Authorization = ใครมีสิทธิ์เข้าหน้าไหน (ในที่นี้ใช้ ProtectedRoute ช่วย “กันหน้า”)
  • Auth State = สถานะผู้ใช้ ณ ขณะนั้น (user / null) และต้องมี loading ตอนเริ่มแอป
  • Service Layer = แยก logic ติดต่อ Firebase ไว้ใน src/services เพื่อ UI เรียกใช้ได้ง่าย

ไฟล์ที่เกี่ยวข้อง (ตามที่คุณเพิ่ม)

  • src/lib/firebase.js — จุดเดียวสำหรับ initialize Firebase และ export auth (และ db ถ้ามี Firestore)
  • src/services/authService.js — register/login/logout/onAuthStateChanged
  • src/services/profileService.js — create/read/update โปรไฟล์ (ถ้าต้องการ)
  • src/pages/Register.jsx — UI + เรียก authService.register
  • src/pages/Login.jsx — UI + เรียก authService.login
  • src/routes/AppRoutes.jsx — รวม Routes ทั้งหมดของแอป
  • src/routes/ProtectedRoute.jsx — กัน route ที่ต้อง login
  • src/layout/AppHeader.jsx — แสดงปุ่ม Login/Logout ตามสถานะผู้ใช้
  • src/main.jsx — จุดเริ่ม React + Router
ข้อควรระวัง
ในขั้นตอนต่อไป เราจะทำให้ระบบ “ทำงานได้จริง” โดยใช้โค้ดมาตรฐาน หากโครงโปรเจคเดิมของคุณใช้ชื่อหน้า/route ไม่เหมือนตัวอย่าง ให้แก้เฉพาะ path และชื่อ component ให้ตรงกับโปรเจคคุณ

Step 2 — สร้าง Firebase Project และเปิด Authentication (Email/Password)

2.1

สร้าง Firebase Project

Firebase Console
  1. เข้า Firebase Console → Create project
  2. ตั้งชื่อโปรเจค (ตามวิชา/งานของคุณ)
  3. เลือก/ไม่เลือก Google Analytics ตามต้องการ (สำหรับแลบนี้ไม่จำเป็น)
2.2

เพิ่ม Web App และคัดลอก Firebase Config

firebaseConfig
  1. Project settings → Your apps → Add app → Web
  2. ตั้งชื่อแอป (เช่น frontend-framework)
  3. คัดลอก firebaseConfig (apiKey, authDomain, projectId, ...)
เราจะนำ config ไปใส่ใน src/lib/firebase.js
ตามโจทย์ของคุณ: “src/lib/firebase.js ให้ใช้ค่าจาก firebase metadata ใส่ได้โดยตรง”
2.3

เปิด Authentication แบบ Email/Password

Auth
  1. Authentication → Get started
  2. Sign-in method → Email/Password → Enable
  3. Save
2.4

(Optional) เปิด Firestore สำหรับเก็บโปรไฟล์ผู้ใช้

Firestore
  1. Firestore Database → Create database
  2. สำหรับแลบ: เริ่มด้วย Test mode (ง่ายสุด) แล้วค่อยสอนเรื่อง Rules ภายหลัง
  3. เลือก region ให้เหมาะสม
หมายเหตุเรื่องความปลอดภัย
Test mode เหมาะกับการเรียน/ทดลองเท่านั้น หากใช้งานจริงควรตั้ง Security Rules ให้เข้มงวด

Step 3 — ติดตั้ง Firebase SDK ในโปรเจค React

Firebase จะถูกใช้งานผ่าน SDK ในฝั่ง Frontend ดังนั้นเราต้องติดตั้งแพ็กเกจ firebase

คำสั่งติดตั้ง
npm install firebase
ตรวจสอบ
  • ติดตั้งสำเร็จแล้วให้ดูใน package.json จะมี dependency ชื่อ firebase
  • รัน dev server ได้ปกติ (npm run dev หรือคำสั่งของโปรเจคคุณ)

Step 4 — สร้าง src/lib/firebase.js (Initialize Firebase)

ไฟล์นี้คือ “จุดเดียว” ที่เราจะ initialize Firebase แล้ว export ตัวแปรที่ต้องใช้ เช่น auth และ db (ถ้ามี)

หลักการออกแบบ
  • อย่ากระจาย config ไปหลายไฟล์ → รวมไว้ใน src/lib/firebase.js
  • ไฟล์อื่นต้อง import จากที่นี่เท่านั้น (เช่น import { auth } from "../lib/firebase")
ไฟล์ src/lib/firebase.js
// src/lib/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
// (Optional) ถ้าใช้ Firestore ให้เปิดบรรทัดนี้
// import { getFirestore } from "firebase/firestore";

// ✅ ใส่ค่า config จาก Firebase Console ได้โดยตรง (ตามโจทย์)
const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_AUTH_DOMAIN",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_STORAGE_BUCKET",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID",
};

// Initialize Firebase
export const app = initializeApp(firebaseConfig);

// Auth
export const auth = getAuth(app);

// (Optional) Firestore
// export const db = getFirestore(app);
ต้องแก้ตรงไหน?
ให้แทนค่า YOUR_... ทั้งหมดด้วย config จริงจาก Firebase Console (Step 2.2)

Step 5 — สร้าง src/services/authService.js (Service Layer สำหรับ Auth)

จุดประสงค์ของ authService คือทำให้หน้า UI (Login/Register) เรียกใช้งาน Firebase ได้ง่าย และโค้ดสะอาด (UI ไม่ต้องรู้รายละเอียด Firebase API มากเกินไป)

ฟังก์ชันขั้นต่ำที่ควรมี
  • register(email, password)
  • login(email, password)
  • logout()
  • subscribeAuthState(callback) เพื่อฟังสถานะผู้ใช้ (สำคัญมากสำหรับ ProtectedRoute)
ไฟล์ src/services/authService.js
// src/services/authService.js
import { auth } from "../lib/firebase";
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  onAuthStateChanged,
} from "firebase/auth";

export async function register(email, password) {
  const cred = await createUserWithEmailAndPassword(auth, email, password);
  return cred.user; // user มี uid, email, ...
}

export async function login(email, password) {
  const cred = await signInWithEmailAndPassword(auth, email, password);
  return cred.user;
}

export async function logout() {
  await signOut(auth);
}

export function subscribeAuthState(callback) {
  // callback จะถูกเรียกทุกครั้งที่ user เปลี่ยน (login/logout/refresh แล้ว restore session)
  // return เป็นฟังก์ชัน unsubscribe
  return onAuthStateChanged(auth, callback);
}
ทำไมต้องมี subscribeAuthState?
เพราะเมื่อเรา refresh หน้าเว็บ Firebase จะ “ตรวจ session” ก่อนว่าเคย login ไว้ไหม ระหว่างนั้นเราต้องมีสถานะ loading เพื่อไม่ให้ route เด้งผิดจังหวะ

Step 6 — สร้าง src/services/profileService.js (Optional: Firestore โปรไฟล์)

Firebase Auth เก็บข้อมูลผู้ใช้พื้นฐาน (เช่น uid/email) แต่ถ้าเราต้องการข้อมูลเพิ่มเติม เช่น ชื่อ-สกุล, คณะ, role เรามักเก็บใน Firestore แยกเป็น collection (เช่น profiles)

ถ้าโปรเจคของคุณยังไม่ได้ใช้ Firestore
สามารถข้าม Step นี้ไปได้เลย และทำเฉพาะ Auth ก็พอสำหรับแลบพื้นฐาน
ไฟล์ src/services/profileService.js (ตัวอย่าง)
// src/services/profileService.js
// ใช้เมื่อคุณเปิด Firestore และ export db จาก src/lib/firebase.js แล้ว
import { db } from "../lib/firebase";
import { doc, setDoc, getDoc, updateDoc, serverTimestamp } from "firebase/firestore";

const col = "profiles";

export async function createUserProfile(uid, data) {
  const ref = doc(db, col, uid);
  await setDoc(ref, {
    ...data,
    uid,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp(),
  });
}

export async function getUserProfile(uid) {
  const ref = doc(db, col, uid);
  const snap = await getDoc(ref);
  return snap.exists() ? snap.data() : null;
}

export async function updateUserProfile(uid, data) {
  const ref = doc(db, col, uid);
  await updateDoc(ref, {
    ...data,
    updatedAt: serverTimestamp(),
  });
}
อย่าลืม!
ถ้าใช้ profileService ให้กลับไปเปิด db ใน src/lib/firebase.js ด้วย และต้องติดตั้ง/เปิด Firestore ใน Firebase Console แล้ว

Step 7 — สร้างหน้า src/pages/Register.jsx

หน้านี้จะมีฟอร์มสมัครสมาชิก และเมื่อกดสมัคร จะเรียก authService.register จากนั้นอาจสร้างโปรไฟล์ใน Firestore (ถ้าใช้) และนำทางไปหน้าที่ต้องการ

สิ่งที่ควรมีใน Register
  • ฟอร์ม: email / password / confirm password
  • validation เบื้องต้น: password ตรงกันไหม
  • แสดง error แบบอ่านง่าย
  • เมื่อสำเร็จ: navigate ไปหน้า login หรือหน้า dashboard
ไฟล์ src/pages/Register.jsx (ตัวอย่างใช้งานได้จริง)
// src/pages/Register.jsx
import { useMemo, useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import { register as registerUser } from "../services/authService";
// (Optional) ถ้าต้องการสร้างโปรไฟล์ ให้เปิด import ข้างล่าง
// import { createUserProfile } from "../services/profileService";

function friendlyAuthError(err) {
  const code = err?.code || "";
  if (code.includes("auth/email-already-in-use")) return "อีเมลนี้ถูกใช้งานแล้ว";
  if (code.includes("auth/invalid-email")) return "รูปแบบอีเมลไม่ถูกต้อง";
  if (code.includes("auth/weak-password")) return "รหัสผ่านอ่อนเกินไป (ควรยาวอย่างน้อย 6 ตัวอักษร)";
  return "สมัครสมาชิกไม่สำเร็จ กรุณาลองใหม่อีกครั้ง";
}

export default function Register() {
  const nav = useNavigate();

  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [confirm, setConfirm] = useState("");

  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  const canSubmit = useMemo(() => {
    if (!email || !password || !confirm) return false;
    if (password !== confirm) return false;
    return true;
  }, [email, password, confirm]);

  async function onSubmit(e) {
    e.preventDefault();
    setError("");

    if (password !== confirm) {
      setError("รหัสผ่านและยืนยันรหัสผ่านไม่ตรงกัน");
      return;
    }

    try {
      setLoading(true);
      const user = await registerUser(email, password);

      // (Optional) สร้างโปรไฟล์ใน Firestore
      // await createUserProfile(user.uid, { email: user.email, displayName: "" });

      // เลือกแนวทางการนำทาง:
      // 1) ให้ไปหน้า Login เพื่อให้นักศึกษาเห็น flow ชัดเจน
      nav("/login");
      // 2) หรือไปหน้า dashboard เลย (ถ้าคุณต้องการ)
      // nav("/dashboard");
    } catch (err) {
      setError(friendlyAuthError(err));
    } finally {
      setLoading(false);
    }
  }

  return (
    <div style={{ maxWidth: 420, margin: "0 auto", padding: 16 }}>
      <h2>Register</h2>

      {error ? (
        <div style={{ margin: "10px 0", color: "#b91c1c" }}>{error}</div>
      ) : null}

      <form onSubmit={onSubmit}>
        <div style={{ display: "grid", gap: 10 }}>
          <label>
            Email
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="student@email.com"
              required
              style={{ width: "100%", padding: 10, marginTop: 6 }}
            />
          </label>

          <label>
            Password
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              placeholder="อย่างน้อย 6 ตัวอักษร"
              required
              style={{ width: "100%", padding: 10, marginTop: 6 }}
            />
          </label>

          <label>
            Confirm Password
            <input
              type="password"
              value={confirm}
              onChange={(e) => setConfirm(e.target.value)}
              required
              style={{ width: "100%", padding: 10, marginTop: 6 }}
            />
          </label>

          <button
            type="submit"
            disabled={!canSubmit || loading}
            style={{
              padding: 10,
              cursor: (!canSubmit || loading) ? "not-allowed" : "pointer",
            }}
          >
            {loading ? "Creating account..." : "Create account"}
          </button>
        </div>
      </form>

      <p style={{ marginTop: 12 }}>
        มีบัญชีแล้ว? <Link to="/login">ไปหน้า Login</Link>
      </p>
    </div>
  );
}
จุดสำคัญ
  • ทำไมต้องแยก friendlyAuthError → เพื่อให้อ่านข้อความ error เข้าใจ
  • ทำไมต้อง loading → กันการกดซ้ำและทำให้ UX ดีขึ้น

Step 8 — สร้างหน้า src/pages/Login.jsx

หน้านี้จะรับ email/password แล้วเรียก authService.login เมื่อสำเร็จนำทางไปหน้า protected เช่น Dashboard

ไฟล์ src/pages/Login.jsx (ตัวอย่างใช้งานได้จริง)
// src/pages/Login.jsx
import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { login as loginUser } from "../services/authService";

function friendlyAuthError(err) {
  const code = err?.code || "";
  if (code.includes("auth/invalid-credential")) return "อีเมลหรือรหัสผ่านไม่ถูกต้อง";
  if (code.includes("auth/user-not-found")) return "ไม่พบผู้ใช้นี้ในระบบ";
  if (code.includes("auth/wrong-password")) return "รหัสผ่านไม่ถูกต้อง";
  if (code.includes("auth/too-many-requests")) return "ลองใหม่ภายหลัง (มีการพยายามหลายครั้งเกินไป)";
  return "เข้าสู่ระบบไม่สำเร็จ กรุณาลองใหม่อีกครั้ง";
}

export default function Login() {
  const nav = useNavigate();
  const loc = useLocation();

  // ถ้าโดน redirect มาจาก ProtectedRoute เราจะส่ง state: { from: "/dashboard" } มาได้
  const from = loc.state?.from || "/dashboard";

  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  async function onSubmit(e) {
    e.preventDefault();
    setError("");

    try {
      setLoading(true);
      await loginUser(email, password);
      nav(from, { replace: true });
    } catch (err) {
      setError(friendlyAuthError(err));
    } finally {
      setLoading(false);
    }
  }

  return (
    <div style={{ maxWidth: 420, margin: "0 auto", padding: 16 }}>
      <h2>Login</h2>

      {error ? (
        <div style={{ margin: "10px 0", color: "#b91c1c" }}>{error}</div>
      ) : null}

      <form onSubmit={onSubmit}>
        <div style={{ display: "grid", gap: 10 }}>
          <label>
            Email
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              style={{ width: "100%", padding: 10, marginTop: 6 }}
            />
          </label>

          <label>
            Password
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              style={{ width: "100%", padding: 10, marginTop: 6 }}
            />
          </label>

          <button type="submit" disabled={loading} style={{ padding: 10 }}>
            {loading ? "Signing in..." : "Sign in"}
          </button>
        </div>
      </form>

      <p style={{ marginTop: 12 }}>
        ยังไม่มีบัญชี? <Link to="/register">สมัครสมาชิก</Link>
      </p>
    </div>
  );
}

Step 9 — รวม Routes ใน src/routes/AppRoutes.jsx

แนวทางที่แนะนำคือ “รวมเส้นทาง (routes) ไว้ไฟล์เดียว” เพื่อให้ดูภาพรวมง่าย และดูแลต่อได้ง่าย

แนวคิด Public vs Protected
  • Public (เข้าได้ทุกคน): /login, /register
  • Protected (ต้อง login): /dashboard, /profile ฯลฯ
ไฟล์ src/routes/AppRoutes.jsx (ตัวอย่าง)
// src/routes/AppRoutes.jsx
import { Routes, Route, Navigate } from "react-router-dom";
import Login from "../pages/Login";
import Register from "../pages/Register";
import ProtectedRoute from "./ProtectedRoute";

// ✅ ตัวอย่างหน้า protected (ให้คุณแทนเป็น Dashboard ของโปรเจคเดิม)
function DashboardPlaceholder() {
  return <div style={{ padding: 16 }}><h2>Dashboard</h2><p>หน้านี้ต้อง Login ก่อน</p></div>;
}

export default function AppRoutes() {
  return (
    <Routes>
      {/* Default */}
      <Route path="/" element={<Navigate to="/dashboard" replace />} />

      {/* Public */}
      <Route path="/login" element={<Login />} />
      <Route path="/register" element={<Register />} />

      {/* Protected */}
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <DashboardPlaceholder />
          </ProtectedRoute>
        }
      />

      {/* 404 */}
      <Route path="*" element={<div style={{ padding: 16 }}>Not Found</div>} />
    </Routes>
  );
}
สิ่งที่คุณต้องแก้ให้ตรงโปรเจคเดิม
  • แทน DashboardPlaceholder ด้วยหน้า Dashboard จริงของคุณ
  • ถ้าโปรเจคเดิมมี layout (เช่น sidebar) ให้ครอบใน ProtectedRoute หรือย้ายไปโครง routes ตามที่คุณใช้

Step 10 — สร้าง src/routes/ProtectedRoute.jsx (หัวใจของการกันหน้า)

ProtectedRoute ต้องทำ 3 อย่างให้ครบ: (1) แสดง Loading ระหว่างเช็ค auth state, (2) ถ้าไม่ login ให้ redirect ไป /login, (3) ถ้า login แล้วให้ render children

ทำไมต้องมี Loading?
ตอนเปิดเว็บใหม่ Firebase ยังไม่รู้ทันทีว่า user เคย login ไว้ไหม → ต้องรอ callback จาก onAuthStateChanged
ถ้าเรา redirect เร็วเกินไป นักศึกษาจะเจออาการ “เด้งไป login ทั้งที่ login อยู่แล้ว”
ไฟล์ src/routes/ProtectedRoute.jsx
// src/routes/ProtectedRoute.jsx
import { useEffect, useState } from "react";
import { Navigate, useLocation } from "react-router-dom";
import { subscribeAuthState } from "../services/authService";

export default function ProtectedRoute({ children }) {
  const loc = useLocation();

  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = subscribeAuthState((u) => {
      setUser(u || null);
      setLoading(false);
    });
    return unsubscribe;
  }, []);

  if (loading) {
    return (
      <div style={{ padding: 16 }}>
        <p>Loading auth state...</p>
      </div>
    );
  }

  if (!user) {
    return <Navigate to="/login" replace state={{ from: loc.pathname }} />;
  }

  return children;
}

Step 11 — อัปเดต src/main.jsx ให้ Router ทำงาน

main.jsx ต้องครอบแอปด้วย BrowserRouter และ render AppRoutes (ถ้าโปรเจคเดิมมี layout หลัก ให้จัดวางในระดับนี้หรือใน AppRoutes ตามโครงที่คุณใช้)

ไฟล์ src/main.jsx (ตัวอย่าง)
// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import AppRoutes from "./routes/AppRoutes";

// (Optional) ถ้าคุณมี global CSS/theme เดิม ให้ import ของเดิมไว้ตามโปรเจค
// import "./index.css";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <BrowserRouter>
      <AppRoutes />
    </BrowserRouter>
  </React.StrictMode>
);

Step 12 — สร้าง src/layout/AppHeader.jsx เพื่อแสดงสถานะ + Logout

Header ที่ดี “เห็นผลของระบบล็อกอินแบบชัดเจน” เช่น แสดง email เมื่อ login และมีปุ่ม Logout

รูปแบบแนะนำ
  • subscribeAuthState เพื่อรู้ว่า login อยู่ไหม
  • ถ้า login → แสดง email + ปุ่ม Logout
  • ถ้าไม่ login → แสดงปุ่มไปหน้า Login/Register
ไฟล์ src/layout/AppHeader.jsx (ตัวอย่าง)
// src/layout/AppHeader.jsx
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { logout, subscribeAuthState } from "../services/authService";

export default function AppHeader() {
  const nav = useNavigate();
  const [user, setUser] = useState(null);

  useEffect(() => {
    const un = subscribeAuthState((u) => setUser(u || null));
    return un;
  }, []);

  async function onLogout() {
    await logout();
    nav("/login");
  }

  return (
    <header
      style={{
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        gap: 10,
        padding: "12px 16px",
        borderBottom: "1px solid #e2e8f0",
        background: "#fff",
        position: "sticky",
        top: 0,
        zIndex: 50,
      }}
    >
      <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
        <strong>My App</strong>
        <nav style={{ display: "flex", gap: 10 }}>
          <Link to="/dashboard">Dashboard</Link>
        </nav>
      </div>

      <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
        {user ? (
          <>
            <span style={{ fontSize: 13, color: "#475569" }}>
              {user.email}
            </span>
            <button onClick={onLogout} style={{ padding: "8px 10px" }}>
              Logout
            </button>
          </>
        ) : (
          <>
            <Link to="/login">Login</Link>
            <Link to="/register">Register</Link>
          </>
        )}
      </div>
    </header>
  );
}
อย่าลืม “นำ Header ไปใช้จริง”
คุณต้อง import และวาง <AppHeader /> ใน layout ของโปรเจค (เช่นในหน้า Dashboard หรือ Layout หลัก) เพื่อให้เห็นปุ่ม Logout

Step 13 — Diagram อธิบายการไหลของระบบ (เข้าใจ Protected Route)

Diagram ให้เห็นภาพรวมว่าเกิดอะไรขึ้นเมื่อเข้า route ที่ต้องป้องกัน

ทริค
ลอง “Login แล้วกด refresh” และสังเกตว่า ProtectedRoute ยังต้อง Loading สั้น ๆ เพื่อ restore session เสมอ

Step 14 — ทดสอบ End-to-End + Debug Checklist

ทดสอบแบบ Flow

  1. เปิด /register → สมัครด้วยอีเมลใหม่
  2. ไปหน้า /login → login ด้วยบัญชีที่สมัคร
  3. เมื่อ login สำเร็จ → เข้า /dashboard
  4. กด Logout → ควรเด้งกลับไป /login
  5. ลองพิมพ์ /dashboard ขณะยังไม่ login → ต้องถูก redirect ไป login
  6. login แล้วกด refresh หน้า /dashboard → ยังอยู่ได้ (มี loading สั้น ๆ ได้)
Debug Checklist (ปัญหาที่เจอบ่อย)
  • หน้า protected เด้งไป login ทั้งที่ login แล้ว → ตรวจว่า ProtectedRoute มี loading และใช้ onAuthStateChanged จริง
  • Error: Firebase config ผิด → ตรวจค่าทั้งหมดใน firebaseConfig
  • Register ไม่ได้ (email already in use) → อีเมลซ้ำ ให้ลองอีเมลใหม่
  • Login ไม่ได้ → ตรวจ email/password และดู err.code ใน console
  • ถ้าใช้ Firestore แล้ว error db undefined → อย่าลืม export db ใน firebase.js

Appendix — แนะนำ “การวาง Layout” ให้เข้ากับธีมเดิมของโปรเจค

โปรเจคเดิมมี Layout/Sidebar/Theme อยู่แล้ว (จาก tutorial ก่อนหน้า) แนวทางแนะนำคือสร้าง “Layout หลัก” แล้วให้ protected pages อยู่ใต้ layout นั้น

ตัวอย่างแนวคิด (ไม่บังคับ)
  • สร้าง AppLayout ที่รวม <AppHeader /> + Sidebar เดิม
  • ใน AppRoutes: ให้ protected route render <AppLayout><Dashboard /></AppLayout>
  • Public pages (Login/Register) ไม่ต้องใช้ layout หนัก ๆ เพื่อให้หน้าสะอาด
Routes
  - /login      (Public) -> <Login />
  - /register   (Public) -> <Register />
  - /dashboard  (Protected) -> <ProtectedRoute><AppLayout><Dashboard /></AppLayout></ProtectedRoute>
ลองทดสอบในกรณีอื่น ๆ
ให้นักศึกษาลองทดสอบในกรณีอื่น ๆ เพิ่มเติมได้