Membangun Web App E-Commerce dengan Go: #5 - Login & Manajemen Sesi

Membangun Web App E-Commerce dengan Go: #5 - Login & Manajemen Sesi

Lengkapi alur autentikasi! Di bagian ini, kita akan membangun fitur login, memverifikasi password, dan mengelola sesi pengguna menggunakan cookie yang aman.

Penulis: Novian Hidayat
Tanggal: 1 Juli 2025

Selamat datang di bagian kelima! Pada artikel sebelumnya, kita telah berhasil membuat sistem registrasi. Sekarang, saatnya memberikan pengguna cara untuk masuk kembali ke akun mereka.

Di bagian ini, kita akan menyelesaikan alur autentikasi dengan membangun:

  1. Halaman dan Form Login.
  2. Logika verifikasi password menggunakan bcrypt.
  3. Sistem manajemen sesi sederhana namun aman menggunakan signed cookies untuk menjaga pengguna tetap login.

Setelah bagian ini selesai, kita akan memiliki sistem autentikasi yang lengkap dan fungsional!

Langkah 1: Membuat Halaman Login

Sama seperti registrasi, kita mulai dengan membuat tampilan HTML untuk form login.

1. Buat Template HTML

Buat file baru web/templates/login.page.html:

<!-- web/templates/login.page.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <!-- Kita bisa menggunakan kembali style dari halaman registrasi untuk konsistensi -->
    <style>
        body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; min-height: 80vh; background-color: #f4f4f9; }
        .container { width: 100%; max-width: 400px; }
        form { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
        h2 { text-align: center; color: #2c3e50; margin-top: 0; }
        .form-group { margin-bottom: 1rem; }
        label { display: block; margin-bottom: 0.5rem; }
        input { width: 95%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; }
        button { width: 100%; padding: 0.75rem; border: none; background-color: #27ae60; color: white; border-radius: 4px; font-size: 1rem; cursor: pointer; }
        button:hover { background-color: #229954; }
    </style>
</head>
<body>
    <div class="container">
        <form action="/login" method="POST">
            <h2>Masuk ke Akun Anda</h2>
            <div class="form-group">
                <label for="email">Email</label>
                <input type="email" id="email" name="email" required>
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" id="password" name="password" required>
            </div>
            <button type="submit">Login</button>
        </form>
    </div>
</body>
</html>

2. Tambahkan Handler dan Rute

Buka cmd/web/handler/auth.go dan tambahkan dua method baru ke AuthHandler:

  • ShowLoginForm: Untuk menampilkan form (GET /login).
  • HandleLogin: Untuk memproses data login (POST /login).
// cmd/web/handler/auth.go

// ... (kode yang sudah ada)

// ShowLoginForm menampilkan halaman login
func (h *AuthHandler) ShowLoginForm(w http.ResponseWriter, r *http.Request) {
	view.Render(w, "login.page.html", nil)
}

// HandleLogin memproses data dari form login
func (h *AuthHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
	// 1. Parse form
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Gagal memproses form", http.StatusBadRequest)
		return
	}

	email := r.PostForm.Get("email")
	password := r.PostForm.Get("password")

	// 2. Cari pengguna berdasarkan email (akan kita implementasikan)
	// user, err := h.UserRepo.GetUserByEmail(email) ...

	// 3. Verifikasi password (akan kita implementasikan)
	// bcrypt.CompareHashAndPassword(...) ...

	// 4. Buat sesi (akan kita implementasikan)
	log.Printf("Percobaan login untuk email: %s", email)

	// 5. Redirect ke halaman utama setelah berhasil login
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

Sekarang, daftarkan rute-rute baru ini di cmd/web/router/router.go:

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

func New(userRepo *repository.UserRepository) http.Handler {
	// ... (kode yang sudah ada)

	authHandler := &handler.AuthHandler{UserRepo: userRepo}

	r.Get("/", handler.ShowHomePage)
	r.Get("/register", authHandler.ShowRegistrationForm)
	r.Post("/register", authHandler.HandleRegistration)

	// Rute baru untuk login
	r.Get("/login", authHandler.ShowLoginForm)
	r.Post("/login", authHandler.HandleLogin)

	return r
}

Jalankan aplikasi dan buka http://localhost:8080/login untuk melihat halaman login baru kita!

Langkah 2: Mengambil Data Pengguna & Verifikasi Password

Sekarang kita akan mengisi logika di HandleLogin.

1. Buat Method GetUserByEmail di Repository

Buka internal/repository/user_repo.go dan tambahkan method baru untuk mengambil data pengguna berdasarkan alamat email mereka.

// internal/repository/user_repo.go
// ... (imports)

// ... (struct UserRepository dan method CreateUser)

func (r *UserRepository) GetUserByEmail(email string) (*models.User, error) {
	query := `SELECT id, email, password_hash, full_name FROM users WHERE email = $1`
	var user models.User
	err := r.DB.QueryRow(context.Background(), query, email).Scan(&user.ID, &user.Email, &user.PasswordHash, &user.FullName)
	if err != nil {
		// Kita akan menangani error 'no rows' secara spesifik di handler
		return nil, err
	}
	return &user, nil
}

2. Implementasikan Logika Verifikasi di Handler

Kembali ke cmd/web/handler/auth.go dan lengkapi HandleLogin.

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

func (h *AuthHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Gagal memproses form", http.StatusBadRequest)
		return
	}

	email := r.PostForm.Get("email")
	password := r.PostForm.Get("password")

	// 1. Cari pengguna berdasarkan email
	user, err := h.UserRepo.GetUserByEmail(email)
	if err != nil {
		// Jika tidak ada baris yang ditemukan, email tidak terdaftar.
		// Kita berikan pesan error yang umum untuk keamanan.
		log.Printf("Email tidak ditemukan: %s, error: %v", email, err)
		http.Error(w, "Email atau password salah", http.StatusUnauthorized)
		return
	}

	// 2. Verifikasi password
	err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
	if err != nil {
		// Jika password tidak cocok, bcrypt akan mengembalikan error.
		log.Printf("Password salah untuk email: %s", email)
		http.Error(w, "Email atau password salah", http.StatusUnauthorized)
		return
	}

	// Jika berhasil sampai sini, email dan password valid!
	log.Printf("Pengguna berhasil login: %s", user.Email)

	// (Langkah selanjutnya: membuat sesi)

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

Catatan Keamanan: Memberikan pesan error yang sama (“Email atau password salah”) baik untuk email yang tidak ada maupun password yang salah adalah praktik yang baik untuk mencegah user enumeration attacks.

Langkah 3: Manajemen Sesi dengan Cookies

Setelah pengguna berhasil login, kita perlu “mengingat” mereka. Cara paling umum adalah dengan membuat session cookie.

Kita akan menggunakan library gorilla/sessions yang populer dan aman.

1. Instal gorilla/sessions

go get github.com/gorilla/sessions

2. Inisialisasi Session Store

Kita perlu sebuah “kunci rahasia” untuk mengenkripsi dan menandatangani cookie kita agar tidak bisa dipalsukan. Simpan kunci ini di file .env.

Buka file .env Anda dan tambahkan baris baru:

# .env
# ... (DATABASE_URL)
SESSION_SECRET="kunci-rahasia-yang-sangat-panjang-dan-sulit-ditebak"

Ganti dengan string acak Anda sendiri yang panjangnya 32 atau 64 karakter.

Sekarang, kita buat sebuah file untuk mengelola sesi. Buat cmd/web/session/session.go:

mkdir -p cmd/web/session
touch cmd/web/session/session.go

Isi dengan kode berikut:

// cmd/web/session/session.go
package session

import (
	"encoding/gob"
	"os"

	"github.com/google/uuid"
	"github.com/gorilla/sessions"
)

var Store *sessions.CookieStore

func init() {
  gob.Register(uuid.UUID{})
	// Ambil kunci dari environment variable
	secret := os.Getenv("SESSION_SECRET")
	if secret == "" {
		// Di produksi, ini seharusnya menyebabkan panic
		secret = "kunci-default-hanya-untuk-pengembangan"
	}
	Store = sessions.NewCookieStore([]byte(secret))

	// Konfigurasi opsi cookie
	Store.Options = &sessions.Options{
		Path:     "/",
		MaxAge:   86400 * 7, // 7 hari
		HttpOnly: true,      // Mencegah akses via JavaScript
		// Secure: true,     // Aktifkan di produksi (membutuhkan HTTPS)
	}
}

3. Gunakan Sesi di Handler Login

Terakhir, modifikasi HandleLogin di auth.go untuk membuat sesi setelah verifikasi berhasil.

// cmd/web/handler/auth.go
import (
    // ...
    "github.com/kodekilat/go-ecommerce/cmd/web/session" // Tambahkan import
)

// ...

func (h *AuthHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
    // ... (kode verifikasi email dan password)

	// Jika verifikasi berhasil:
    log.Printf("Pengguna berhasil login: %s (ID: %s)", user.Email, user.ID)

	// 1. Dapatkan sesi atau buat yang baru
	sess, _ := session.Store.Get(r, "auth-session")

	// 2. Set nilai di dalam sesi
	sess.Values["user_id"] = user.ID
	sess.Values["user_email"] = user.Email

	// 3. Simpan sesi (ini akan mengirim cookie ke browser)
	err = sess.Save(r, w)
	if err != nil {
		log.Printf("Gagal menyimpan sesi: %v", err)
		http.Error(w, "Gagal login", http.StatusInternalServerError)
		return
	}

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

Penutup

Selamat! Anda telah berhasil membangun sistem autentikasi yang lengkap. Pengguna kini bisa mendaftar, login, dan status login mereka akan tersimpan di browser menggunakan session cookie yang aman.

Di bagian selanjutnya, kita akan memanfaatkan sesi ini untuk:

  • Membuat middleware otentikasi untuk melindungi halaman-halaman tertentu.
  • Menampilkan informasi pengguna yang sedang login di halaman.
  • Menambahkan fungsionalitas Logout.

Kita sudah sangat dekat dengan inti dari fungsionalitas e-commerce. Terus ikuti seri ini!

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

Diskusi

Tutorial Lainnya