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:
- Halaman dan Form Login.
- Logika verifikasi password menggunakan
bcrypt
. - 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