Toko Kue: Proteksi Halaman & Info User (Supabase Auth)

Toko Kue: Proteksi Halaman & Info User (Supabase Auth)

Jaga halaman pentingmu hanya untuk member! Pelajari cara melindungi rute di Next.js agar hanya bisa diakses oleh pengguna yang sudah login, dan cara menampilkan informasi pengguna (seperti email) menggunakan Supabase Auth.

"Area Khusus Member" di Toko Kue: Lindungi Halaman & Sapa Pengguna!

Toko Kue kita udah punya pintu masuk (Login & Registrasi) pake Supabase Auth! Keren! Sekarang, gimana caranya kita bikin beberapa "ruangan" di toko kita jadi eksklusif cuma buat pengguna yang udah login? Misalnya, halaman Checkout, atau nanti kalau ada halaman Profil Pengguna.

Selain itu, kita juga pengen dong bisa nyapa pengguna yang udah login dengan namanya (atau emailnya) di header atau di halaman tertentu.

Di bagian ini, kita bakal belajar cara:

  1. Ngelindungin halaman (rute) di Next.js biar cuma bisa diakses sama pengguna yang udah login.
  2. Nampilin informasi pengguna yang lagi login.

Kita bakal banyak manfaatin status sesi dari Supabase Auth yang udah kita singgung di komponen Navbar sebelumnya.

Mengelola Status Autentikasi Secara Global (Review CartContext atau Buat AuthContext)

Cara paling enak buat tau status login pengguna di banyak komponen adalah dengan nyimpen informasi sesi atau pengguna di React Context. Di studi kasus Toko Kue, kita udah punya cart-context.tsx yang di-provide di RootLayout.

Kita bisa memperluas CartContext untuk sekalian nyimpen info user, atau bikin AuthContext baru khusus buat urusan autentikasi. Untuk menjaga modularitas, membuat AuthContext baru seringkali lebih baik, tapi untuk contoh ini, mari kita bayangkan kita memodifikasi CartContext (atau RootLayout jika state user ada di sana) untuk juga menyediakan info user.

Modifikasi Konseptual di cart-context.tsx atau RootLayout (jika state user ada di sana): (Ingat, di materi sebelumnya, kita sudah membuat Navbar.tsx yang mengambil state user. Logika serupa bisa dipusatkan).

tsxtsx
// Contoh konseptual jika state user dikelola di CartProvider atau RootLayout
// File: src/context/cart-context.tsx
 
// ... import lainnya ...
import { User, Session } from '@supabase/supabase-js';
 
// Tambahkan user dan session ke CartContextType
interface CartContextType {
  // ... (props keranjang yang sudah ada) ...
  user: User | null;
  session: Session | null;
  isLoadingAuth: boolean; // Buat nandain kalau status auth lagi dicek
  signOut: () => Promise<void>;
}
 
// Di dalam CartProvider (atau RootLayout)
export function CartProvider({ children }: CartProviderProps) {
  // ... (state keranjang yang sudah ada) ...
  const [user, setUser] = useState<User | null>(null);
  const [session, setSession] = useState<Session | null>(null);
  const [isLoadingAuth, setIsLoadingAuth] = useState(true); // Awalnya true
  const router = useRouter(); // Jika mau redirect dari sini
 
  useEffect(() => {
    async function getSessionData() {
      const { data: { session: currentSession }, error } = await supabase.auth.getSession();
      if (error) {
        console.error("Error getting session:", error);
      }
      setSession(currentSession);
      setUser(currentSession?.user ?? null);
      setIsLoadingAuth(false); // Selesai loading status auth awal
    }
    getSessionData();
 
    const { data: authListener } = supabase.auth.onAuthStateChange((_event, newSession) => {
      console.log("Auth state changed:", _event, newSession);
      setSession(newSession);
      setUser(newSession?.user ?? null);
      setIsLoadingAuth(false); // Selesai loading juga pas ada perubahan
      
      // Contoh: kalau event-nya SIGNED_OUT dan kita lagi di halaman terproteksi, redirect
      // if (_event === 'SIGNED_OUT' && window.location.pathname.startsWith('/checkout')) {
      //    router.push('/auth/signin');
      // }
    });
 
    return () => {
     authListener?.subscription?.unsubscribe();
    };
  }, [router]); // Tambahkan router jika digunakan di effect
 
  const signOut = async () => {
    await supabase.auth.signOut();
    // setUser(null); setSession(null); // akan dihandle oleh onAuthStateChange
    router.push('/auth/signin'); // Arahkan setelah logout
  };
 
  const contextValue = {
    // ... (value keranjang yang sudah ada) ...
    user,
    session,
    isLoadingAuth,
    signOut
  };
 
  return (
    <CartContext.Provider value={contextValue}>
      {children}
    </CartContext.Provider>
  );
}

Code selengkapnya bisa kamu cek disini GitHub cart-context

Penting:

  • "use client"; wajib ada di file provider context ini.
  • isLoadingAuth: State ini berguna biar kita bisa nampilin UI loading sementara pas aplikasi pertama kali ngecek status login pengguna.
  • supabase.auth.getSession(): Dipake buat dapetin sesi aktif pas pertama kali komponen di-mount.
  • supabase.auth.onAuthStateChange(): "Ngedengerin" perubahan status login/logout secara real-time dan ngupdate state user dan session.

Langkah 1: Melindungi Halaman Checkout (src/app/checkout/page.tsx)

Kita mau halaman /checkout cuma bisa diakses kalau pengguna udah login. Kalau belum, kita arahin dia ke halaman login.

Modifikasi src/app/checkout/page.tsx:

tsxtsx
// src/app/checkout/page.tsx
"use client";
 
import React, { useState, FormEvent, useEffect } from 'react'; // Tambah useEffect
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useCart } from '@/context/cart-context'; // Impor useCart
 
export default function HalamanCheckout() {
  const router = useRouter();
  // Ambil user, session, dan isLoadingAuth dari context
  const { itemDiKeranjang, total_harga, kosongkanKeranjang, user, session, isLoadingAuth } = useCart(); 
 
  // ... (state form lainnya tetap sama: nama, email, alamat, dll.)
  const [nama, setNama] = useState('');
  // ... dst ...
 
  // Efek buat ngecek status login
  useEffect(() => {
    // Jangan redirect kalau status auth masih loading atau kalau sudah ada sesi
    if (!isLoadingAuth && !session) {
      router.push('/auth/signin?redirect=/checkout'); // Arahkan ke login, simpan halaman tujuan
    }
  }, [user, session, isLoadingAuth, router]); // Dependensi: user, session, isLoadingAuth, router
 
  const handleSubmitCheckout = (event: FormEvent<HTMLFormElement>) => {
    // ... (logika handleSubmitCheckout tetap sama) ...
    // Pastikan user ada sebelum proses
    if (!user) {
        alert("Anda harus login untuk melakukan checkout.");
        router.push('/auth/signin?redirect=/checkout');
        return;
    }
    // ... (lanjutan logika submit)
    event.preventDefault(); 
    if (!nama || !email || !alamat /*...*/) { /* validasi */ return; }
    console.log("Checkout oleh:", user.email, { /* data form */ });
    alert(`Terima kasih, ${nama}! Pesanan Anda sedang diproses.`);
    kosongkanKeranjang(); 
    router.push('/terima-kasih');
  };
 
  // Tampilkan UI loading kalau status auth belum selesai dicek
  if (isLoadingAuth) {
    return <p className="text-center py-10">Memeriksa status autentikasi...</p>;
  }
 
  // Kalau udah selesai loading auth TAPI gak ada sesi/user, 
  // idealnya udah keredirect, tapi ini buat jaga-jaga atau kalau redirect belum jalan
  if (!session || !user) {
    return (
        <div className="text-center py-10">
            <p>Anda harus login untuk mengakses halaman ini.</p>
            <Link href="/auth/signin?redirect=/checkout" className="text-blue-600 hover:underline">
                Silakan Login
            </Link>
        </div>
    );
  }
 
  // Kondisi keranjang kosong (tetap)
  if (itemDiKeranjang.length === 0) {
    // ... (kode pesan keranjang kosong tetap sama) ...
    return (
        <div className="text-center py-10">
          <h1 className="text-2xl font-bold mb-4">Keranjang Anda Kosong</h1>
          <p className="mb-6">Anda tidak bisa checkout jika keranjang masih kosong.</p>
          <Link href="/" className="bg-pink-500 hover:bg-pink-600 text-white font-semibold py-3 px-6 rounded-lg">
            Kembali Belanja
          </Link>
        </div>
      );
  }
 
  // Form checkout (tetap sama)
  return (
    <div className="container mx-auto p-4 md:p-8 max-w-2xl">
      <h1 className="text-3xl font-bold text-gray-800 mb-2 text-center">Formulir Checkout</h1>
      <p className="text-center text-sm text-gray-600 mb-6">Logged in sebagai: {user.email}</p>
      {/* ... (sisa kode form checkout tetap sama) ... */}
      <form onSubmit={handleSubmitCheckout} className="bg-white shadow-md rounded-lg px-8 pt-6 pb-8 mb-4">
        {/* ... field-field form ... */}
         <div className="mb-4">
          <label htmlFor="nama" className="block text-gray-700 text-sm font-bold mb-2">Nama Lengkap <span className="text-red-500">*</span></label>
          <input type="text" id="nama" value={nama} onChange={(e) => setNama(e.target.value)} required 
                 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline focus:border-blue-500" />
        </div>
        {/* ... field email, alamat, telepon, catatan ... */}
        <div className="mb-6 p-4 bg-gray-100 rounded">
          <h3 className="font-semibold text-lg mb-2">Ringkasan Pesanan</h3>
          {/* ... ringkasan pesanan ... */}
        </div>
        <div className="flex items-center justify-between">
          <button type="submit"
                  className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-6 rounded focus:outline-none focus:shadow-outline transition-colors">
            Proses Pesanan
          </button>
          <Link href="/keranjang" className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800">
            Kembali ke Keranjang
          </Link>
        </div>
      </form>
    </div>
  );
}

Bedah Perubahan di HalamanCheckout.tsx:

  • useCart(): Kita sekarang ngambil user, session, dan isLoadingAuth dari CartContext kita (atau AuthContext kalau kamu bikin terpisah).
  • useEffect buat Cek Login:
    • Efek ini jalan pas user, session, atau isLoadingAuth berubah.
    • Kalau isLoadingAuth udah false (artinya pengecekan sesi awal udah selesai) DAN session-nya gak ada (artinya gak login), kita router.push('/auth/signin?redirect=/checkout').
    • Parameter ?redirect=/checkout itu trik biar setelah login berhasil, kita bisa arahin pengguna balik lagi ke halaman checkout.
  • UI Loading & Not Logged In: Kita nambahin tampilan sementara pas isLoadingAuth masih true, dan pesan kalau pengguna belum login (meskipun idealnya udah ke-redirect).
  • Di handleSubmitCheckout, kita juga bisa nambahin pengecekan if (!user) sebelum proses.
  • Kita juga nampilin email pengguna yang lagi login.

Langkah 2: Menampilkan Info Pengguna (Contoh di Navbar.tsx)

Kita udah pernah bikin Navbar.tsx yang nampilin status login. Sekarang kita pastikan dia pake state user dari context yang udah kita siapkan.

File src/app/components/navbar.tsx (Review & Penyesuaian):

tsxtsx
// src/app/components/navbar.tsx
"use client";
import React from 'react'; // useEffect dan useState tidak lagi dikelola di sini jika user state dari context
import Link from 'next/link';
import { useCart } from '@/context/cart-context'; // Pake useCart yang sekarang punya info user & signOut
// import { supabase } from '@/lib/supabase-client'; // Tidak perlu import supabase langsung di sini lagi
// import { User } from '@supabase/supabase-js'; // Tipe User bisa diimpor dari context atau @supabase/supabase-js
import { useRouter } from 'next/navigation';
 
export default function Navbar() {
  const { user, signOut, totalItem, isLoadingAuth } = useCart(); // Ambil user dan signOut dari context
  const router = useRouter(); // router tetap dibutuhkan untuk redirect setelah signOut
 
  // Logika useEffect buat getSession dan onAuthStateChange sekarang ada di CartProvider/AuthProvider
 
  const handleSignOut = async () => {
    await signOut(); // Panggil fungsi signOut dari context
    // Redirect udah dihandle di fungsi signOut di context atau bisa juga di sini
  };
 
  return (
    <nav className="bg-gray-800 p-4 text-white">
      <div className="container mx-auto flex justify-between items-center">
        <Link href="/" className="font-bold text-xl hover:text-gray-300">
          Toko Kue Mahir.dev
        </Link>
        <div className="flex items-center space-x-4">
          <Link href="/keranjang" className="hover:text-gray-300">
            Keranjang ({isLoadingAuth ? '...' : totalItem})
          </Link>
          {isLoadingAuth ? (
            <span>Loading...</span>
          ) : user ? (
            <>
              <span className="hidden sm:inline">Halo, {user.email}</span>
              <button 
                onClick={handleSignOut} 
                className="bg-red-500 hover:bg-red-600 px-3 py-1 rounded text-sm"
              >
                Logout
              </button>
            </>
          ) : (
            <>
              <Link href="/auth/signin" className="hover:text-gray-300">Masuk</Link>
              <Link href="/auth/signup" className="bg-green-500 hover:bg-green-600 px-3 py-1 rounded text-sm">
                Daftar
              </Link>
            </>
          )}
        </div>
      </div>
    </nav>
  );
}

Perubahan di Navbar.tsx:

  • Sekarang dia ngambil user, signOut, totalItem, dan isLoadingAuth dari useCart().
  • Logika useEffect buat getSession dan onAuthStateChange udah dipindahin ke CartProvider (atau AuthProvider jika ada). Navbar jadi lebih "bersih" dan cuma fokus nampilin UI berdasarkan state dari context.
  • handleSignOut sekarang manggil fungsi signOut dari context.

Code lengkap bisa anda lihat di github

Alternatif Proteksi Rute: Middleware Next.js (Lebih Advance)

Cara pake useEffect buat redirect di Client Component itu lumayan oke buat kasus simpel. Tapi, buat proteksi rute yang lebih robust dan aman (terutama biar konten halaman terproteksi gak sempet ke-render sama sekali di klien kalau belum login), Next.js nyediain fitur Middleware.

  • Middleware itu kode yang jalan di server sebelum request nyampe ke halaman. Kamu bisa pake middleware buat ngecek sesi pengguna (misal, dari cookie Supabase), terus nge-redirect ke halaman login kalau sesi gak valid.
  • Ini topik yang lebih advance, tapi bagus buat diketahui. Kamu bisa bikin file middleware.ts (atau .js) di root proyekmu (sejajar src/ atau app/).

Ini di luar cakupan panduan dasar kita, tapi sebagai gambaran, kamu bisa ngelakuin ini.


Dengan adanya proteksi halaman dan tampilan info pengguna, aplikasi Toko Kue kita jadi kerasa lebih personal dan aman. Pengguna yang belum login gak bisa "nyelonong" ke halaman checkout, dan pengguna yang udah login bisa disapa dengan baik.

Ini adalah langkah penting dalam ngebangun aplikasi web yang punya manajemen pengguna. Kamu bisa ngembangin lagi, misalnya bikin halaman profil pengguna di mana mereka bisa ngubah data mereka sendiri.

Uji Pemahamanmu!

Memeriksa status login...

Studi Kasus Toko Kue: Mengamankan Halaman Checkout dan Menampilkan Info Pengguna yang Login dengan Supabase Auth - Dasar Supabase (Backend as a Service) - Dasar Supabase (Backend as a Service)