Membangun Web App E-Commerce dengan Go: #6 - Middleware & Halaman Terlindungi

Membangun Web App E-Commerce dengan Go: #6 - Middleware & Halaman Terlindungi

Saatnya memanfaatkan sesi login! Pelajari cara membuat middleware untuk melindungi halaman, menampilkan data pengguna, dan menambahkan fungsionalitas logout.

Penulis: Novian Hidayat
Tanggal: 1 Juli 2025

Selamat datang kembali! Di bagian sebelumnya, kita telah berhasil membangun sistem registrasi dan login yang lengkap. Pengguna bisa masuk, dan aplikasi kita “mengingat” mereka melalui session cookie.

Sekarang, apa gunanya login jika semua halaman bisa diakses oleh siapa saja? Di bagian ini, kita akan membuat sistem autentikasi kita benar-benar berguna dengan:

  1. Membuat Middleware Otentikasi: Sebuah “penjaga gerbang” yang akan melindungi rute-rute sensitif.
  2. Mengelola Konteks Request: Menyimpan informasi pengguna yang login agar bisa diakses oleh handler lain.
  3. Membuat Halaman Akun Pengguna: Halaman sederhana yang hanya bisa diakses oleh pengguna yang sudah login.
  4. Mengimplementasikan Logout.

Mari kita amankan aplikasi kita!

Langkah 1: Membuat Middleware Otentikasi

Middleware di chi adalah sebuah fungsi yang “membungkus” http.Handler. Ia bisa melakukan sesuatu sebelum atau sesudah handler utama dieksekusi. Kita akan membuat middleware yang memeriksa sesi sebelum meneruskan request ke handler tujuan.

Buat direktori dan file baru untuk middleware kita:

mkdir -p cmd/web/middleware
touch cmd/web/middleware/auth.go

Isi file cmd/web/middleware/auth.go dengan kode berikut:

// cmd/web/middleware/auth.go
package middleware

import (
	"context"
	"net/http"

	"github.com/kodekilat/go-ecommerce/cmd/web/session" // Ganti path modul Anda
)

// Definisikan kunci unik untuk konteks
type contextKey string
const userContextKey = contextKey("user")

// Authenticate adalah middleware yang memeriksa sesi pengguna
func Authenticate(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 1. Dapatkan sesi dari request
		sess, _ := session.Store.Get(r, "auth-session")

		// 2. Periksa apakah user_id ada di sesi
		userID, ok := sess.Values["user_id"]
		if !ok || userID == nil {
			// Jika tidak ada, panggil handler berikutnya tanpa melakukan apa-apa
			// (artinya pengguna adalah tamu/guest)
			next.ServeHTTP(w, r)
			return
		}

		// 3. Jika user_id ada, simpan di konteks request
		ctx := context.WithValue(r.Context(), userContextKey, userID)
		
		// 4. Panggil handler berikutnya dengan request yang sudah dimodifikasi konteksnya
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

Penjelasan:

  • contextKey: Membuat tipe kunci sendiri adalah praktik terbaik untuk menghindari tabrakan kunci di konteks.
  • context.WithValue: Kita menggunakan konteks request untuk “membawa” user_id ke handler-handler selanjutnya. Ini adalah cara modern dan aman untuk berbagi data per-request.
  • Middleware ini tidak memblokir request. Ia hanya menambahkan user_id ke konteks jika pengguna sudah login.

Langkah 2: Menerapkan Middleware di Router

Sekarang, kita terapkan middleware ini ke semua rute kita. Buka cmd/web/router/router.go dan tambahkan r.Use(authMiddleware.Authenticate).

// cmd/web/router/router.go
import (
    // ...
    authMiddleware "github.com/kodekilat/go-ecommerce/cmd/web/middleware" // Import paket middleware
)

func New(userRepo *repository.UserRepository) http.Handler {
	r := chi.NewRouter()

	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)
	r.Use(authMiddleware.Authenticate) // Terapkan middleware otentikasi ke semua rute

	// ... (sisa kode)
}

Langkah 3: Membuat Middleware “Require Authentication”

Middleware Authenticate hanya menambahkan data ke konteks. Sekarang kita butuh middleware kedua yang benar-benar memblokir akses jika pengguna belum login.

Kembali ke cmd/web/middleware/auth.go dan tambahkan fungsi baru:

// cmd/web/middleware/auth.go
// ...

// RequireAuthentication adalah middleware yang menolak akses jika pengguna belum login
func RequireAuthentication(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Ambil user_id dari konteks
		userID := r.Context().Value(userContextKey)

		// Jika tidak ada user_id (pengguna adalah tamu), redirect ke halaman login
		if userID == nil {
			http.Redirect(w, r, "/login", http.StatusSeeOther)
			return
		}

		// Jika ada, lanjutkan ke handler tujuan
		next.ServeHTTP(w, r)
	})
}

Langkah 4: Melindungi Halaman Akun

Mari kita buat halaman “Akun Saya” yang hanya bisa diakses setelah login.

1. Buat Template & Handler

Buat template web/templates/account.page.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Akun Saya</title>
    <!-- Gunakan style yang sama -->
</head>
<body>
    <div class="container">
        <h2>Selamat Datang, {{.Email}}!</h2>
        <p>Ini adalah halaman akun Anda yang terlindungi.</p>
        <p>User ID Anda: {{.UserID}}</p>
        <br>
        <form action="/logout" method="POST">
            <button type="submit">Logout</button>
        </form>
    </div>
</body>
</html>

Buat handler baru di cmd/web/handler/user.go (file baru):

// cmd/web/handler/user.go
package handler

import (
	"net/http"
	"github.com/kodekilat/go-ecommerce/cmd/web/session"
	"github.com/kodekilat/go-ecommerce/cmd/web/view"
)

// Definisikan kunci konteks yang sama
type contextKey string
const userContextKey = contextKey("user")

func ShowAccountPage(w http.ResponseWriter, r *http.Request) {
	// Ambil data dari sesi (cara alternatif selain dari konteks)
	sess, _ := session.Store.Get(r, "auth-session")
	userID := sess.Values["user_id"]
	email := sess.Values["user_email"]

	pageData := struct {
		UserID interface{}
		Email  interface{}
	}{
		UserID: userID,
		Email:  email,
	}

	view.Render(w, "account.page.html", pageData)
}

2. Terapkan Middleware Pelindung di Router

Buka cmd/web/router/router.go. Kita akan membuat grup rute baru yang dilindungi oleh RequireAuthentication.

// cmd/web/router/router.go
// ... (imports)
import "github.com/kodekilat/go-ecommerce/cmd/web/handler" // Pastikan ada import user handler

// ...

func New(userRepo *repository.UserRepository) http.Handler {
    // ... (kode sebelumnya)

    // Rute publik (bisa diakses siapa saja)
	r.Get("/", handler.ShowHomePage)
    // ...

    // Grup rute yang memerlukan autentikasi
	r.Group(func(r chi.Router) {
		r.Use(authMiddleware.RequireAuthentication)

		// Daftarkan rute yang dilindungi di sini
		r.Get("/account", handler.ShowAccountPage)
	})

	return r
}

Sekarang, coba akses http://localhost:8080/account. Jika Anda belum login, Anda akan otomatis dialihkan ke /login. Jika sudah login, Anda akan melihat halaman akun Anda!

Langkah 5: Implementasi Logout

Logout pada dasarnya adalah proses menghancurkan sesi.

1. Buat Handler Logout

Tambahkan method HandleLogout ke cmd/web/handler/auth.go:

// cmd/web/handler/auth.go
// ...

func (h *AuthHandler) HandleLogout(w http.ResponseWriter, r *http.Request) {
	sess, _ := session.Store.Get(r, "auth-session")

	// Hapus sesi dengan mengatur MaxAge menjadi -1
	sess.Options.MaxAge = -1
	sess.Save(r, w)

	http.Redirect(w, r, "/", http.StatusSeeOther)
}

2. Daftarkan Rute Logout

Di cmd/web/router/router.go, tambahkan rute untuk logout. Ini bisa menjadi rute publik.

// cmd/web/router/router.go
// ...

func New(...) http.Handler {
    // ...

    authHandler := &handler.AuthHandler{UserRepo: userRepo}
	
    // ... (rute-rute lain)

    r.Post("/logout", authHandler.HandleLogout) // Tambahkan rute ini

    // ... (grup rute terproteksi)
    
    return r
}

Sekarang, tombol Logout di halaman akun Anda akan berfungsi!

Penutup

Kerja yang sangat bagus! Kita telah membangun pilar penting dari aplikasi web modern: middleware untuk otentikasi dan otorisasi. Aplikasi kita kini bisa membedakan antara pengguna tamu dan pengguna yang sudah login, serta melindungi konten yang sesuai.

Di bagian selanjutnya, kita akan kembali ke inti dari e-commerce: Manajemen Produk. Kita akan membangun fungsionalitas CRUD (Create, Read, Update, Delete) untuk produk dan mengintegrasikannya dengan Minio untuk upload gambar.

Anda sudah sangat jauh melangkah. Teruslah coding!

Untuk referensi, Anda dapat melihat kode final dari tutorial ini di repositori GitHub berikut: Source code lengkap

Diskusi

Tutorial Lainnya