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 และ exportauth(และdbถ้ามี Firestore)src/services/authService.js— register/login/logout/onAuthStateChangedsrc/services/profileService.js— create/read/update โปรไฟล์ (ถ้าต้องการ)src/pages/Register.jsx— UI + เรียก authService.registersrc/pages/Login.jsx— UI + เรียก authService.loginsrc/routes/AppRoutes.jsx— รวม Routes ทั้งหมดของแอปsrc/routes/ProtectedRoute.jsx— กัน route ที่ต้อง loginsrc/layout/AppHeader.jsx— แสดงปุ่ม Login/Logout ตามสถานะผู้ใช้src/main.jsx— จุดเริ่ม React + Router
Step 2 — สร้าง Firebase Project และเปิด Authentication (Email/Password)
สร้าง Firebase Project
- เข้า Firebase Console → Create project
- ตั้งชื่อโปรเจค (ตามวิชา/งานของคุณ)
- เลือก/ไม่เลือก Google Analytics ตามต้องการ (สำหรับแลบนี้ไม่จำเป็น)
เพิ่ม Web App และคัดลอก Firebase Config
- Project settings → Your apps → Add app → Web
- ตั้งชื่อแอป (เช่น
frontend-framework) - คัดลอก
firebaseConfig(apiKey, authDomain, projectId, ...)
src/lib/firebase.js
เปิด Authentication แบบ Email/Password
- Authentication → Get started
- Sign-in method → Email/Password → Enable
- Save
(Optional) เปิด Firestore สำหรับเก็บโปรไฟล์ผู้ใช้
- Firestore Database → Create database
- สำหรับแลบ: เริ่มด้วย Test mode (ง่ายสุด) แล้วค่อยสอนเรื่อง Rules ภายหลัง
- เลือก region ให้เหมาะสม
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
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
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);
}
Step 6 — สร้าง src/services/profileService.js (Optional: Firestore โปรไฟล์)
Firebase Auth เก็บข้อมูลผู้ใช้พื้นฐาน (เช่น uid/email) แต่ถ้าเราต้องการข้อมูลเพิ่มเติม เช่น ชื่อ-สกุล, คณะ, role
เรามักเก็บใน Firestore แยกเป็น collection (เช่น profiles)
// 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(),
});
}
db ใน src/lib/firebase.js ด้วย
และต้องติดตั้ง/เปิด Firestore ใน Firebase Console แล้ว
Step 7 — สร้างหน้า src/pages/Register.jsx
หน้านี้จะมีฟอร์มสมัครสมาชิก และเมื่อกดสมัคร จะเรียก authService.register
จากนั้นอาจสร้างโปรไฟล์ใน Firestore (ถ้าใช้) และนำทางไปหน้าที่ต้องการ
- ฟอร์ม: email / password / confirm password
- validation เบื้องต้น: password ตรงกันไหม
- แสดง error แบบอ่านง่าย
- เมื่อสำเร็จ: navigate ไปหน้า login หรือหน้า dashboard
// 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
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 (เข้าได้ทุกคน):
/login,/register - Protected (ต้อง login):
/dashboard,/profileฯลฯ
// 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
onAuthStateChangedถ้าเรา redirect เร็วเกินไป นักศึกษาจะเจออาการ “เด้งไป login ทั้งที่ login อยู่แล้ว”
// 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
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
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>
);
}
<AppHeader /> ใน layout ของโปรเจค (เช่นในหน้า Dashboard หรือ Layout หลัก)
เพื่อให้เห็นปุ่ม Logout
Step 13 — Diagram อธิบายการไหลของระบบ (เข้าใจ Protected Route)
Diagram ให้เห็นภาพรวมว่าเกิดอะไรขึ้นเมื่อเข้า route ที่ต้องป้องกัน
ผู้ใช้เปิดหน้า /dashboard
|
v
ProtectedRoute เริ่มทำงาน
|
v
(Loading = true) รอฟัง onAuthStateChanged(...)
|
+------------------------------+
| |
v v
ได้ user (login อยู่) ได้ null (ยังไม่ login)
| |
v v
render children (Dashboard) redirect ไป /login พร้อม state.from="/dashboard"
Step 14 — ทดสอบ End-to-End + Debug Checklist
ทดสอบแบบ Flow
- เปิด
/register→ สมัครด้วยอีเมลใหม่ - ไปหน้า
/login→ login ด้วยบัญชีที่สมัคร - เมื่อ login สำเร็จ → เข้า
/dashboard - กด Logout → ควรเด้งกลับไป
/login - ลองพิมพ์
/dashboardขณะยังไม่ login → ต้องถูก redirect ไป login - login แล้วกด refresh หน้า
/dashboard→ ยังอยู่ได้ (มี loading สั้น ๆ ได้)
- หน้า protected เด้งไป login ทั้งที่ login แล้ว → ตรวจว่า ProtectedRoute มี loading และใช้
onAuthStateChangedจริง - Error: Firebase config ผิด → ตรวจค่าทั้งหมดใน
firebaseConfig - Register ไม่ได้ (email already in use) → อีเมลซ้ำ ให้ลองอีเมลใหม่
- Login ไม่ได้ → ตรวจ email/password และดู
err.codeใน console - ถ้าใช้ Firestore แล้ว error
dbundefined → อย่าลืม exportdbใน 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>