Web App Testing – POST – Profile Page dan Authentication

Untuk proses authentication, kita akan menambahkan fungsi baru pada file cmd/web/handlers.go yaitu authenticate().

Kemudian modifikasi fungsi Login() pada file cmd/web/handlers.go, kita akan lakukan modifikasi yaitu:

  • Menambahkan kode redirect jika validasi gagal dan credentials tidak valid.
  • Memanggil fungsi authenticate().

Kemudian manambahkan fungsi Profile pada file cmd/web/handler.go, digunakan untuk handling ketika user berhasil login

Serta memperbaiki fungsi render() pada file cmd/web/handler.go dengan mengganti parameter data menjadi td dan menambahkan field pada struct TemplateData serta menambahkan kode untuk mengisi data error dan flash dari session.

package main

import (
	"html/template"
	"log"
	"net/http"
	"path"
	"time"
	"webapp/pkg/data"
)

var templatePath = "./templates/"

func (app *application) Home(w http.ResponseWriter, r *http.Request) {
	var td = make(map[string]any)

	if app.Session.Exists(r.Context(), "test") {
		msg := app.Session.GetString(r.Context(), "test")
		td["test"] = msg
	} else {
		app.Session.Put(r.Context(), "test", "Page visit at "+time.Now().UTC().String())
	}
	_ = app.render(w, r, "home.page.gohtml", &TemplateData{Data: td})
}

func (app *application) Profile(w http.ResponseWriter, r *http.Request) {
	_ = app.render(w, r, "profile.page.gohtml", &TemplateData{})
}

type TemplateData struct {
	IP    string
	Data  map[string]any
	Error string
	Flash string
	User  data.User
}

func (app *application) render(w http.ResponseWriter, r *http.Request, t string, td *TemplateData) error {
	//parse template
	parsedTemplate, err := template.ParseFiles(path.Join(templatePath, t), path.Join(templatePath, "base.layout.gohtml"))
	if err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return err
	}

	//gunakan middleware yang telah kita buat.
	td.IP = app.ipFromContext(r.Context())

	td.Error = app.Session.PopString(r.Context(), "error")
	td.Flash = app.Session.PopString(r.Context(), "flash")

	//execute template
	err = parsedTemplate.Execute(w, td)
	if err != nil {
		return err
	}
	return nil
}

func (app *application) Login(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()

	if err != nil {
		log.Println(err)
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	//validation goes here
	form := NewForm(r.PostForm)
	form.Required("email", "password")

	if !form.Valid() {
		//redirect to login page with error message.
		app.Session.Put(r.Context(), "error", "Invalid login credentials")
		http.Redirect(w, r, "/", http.StatusSeeOther)
		return
	}

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

	user, err := app.DB.GetUserByEmail(email)
	if err != nil {
		//redirect to login page with error message.
		app.Session.Put(r.Context(), "error", "Invalid login")
		http.Redirect(w, r, "/", http.StatusSeeOther)
		return
	}

	//authenticate the user
	// if not authenticated, redirect with error
	if !app.authenticate(r, user, password) {
		app.Session.Put(r.Context(), "error", "Invalid login")
		http.Redirect(w, r, "/", http.StatusSeeOther)
		return
	}

	//prevent fixation attack
	_ = app.Session.RenewToken(r.Context())

	//redirect to other page
	app.Session.Put(r.Context(), "flash", "Successfully loged in")
	http.Redirect(w, r, "user/profile", http.StatusSeeOther)
}

func (app *application) authenticate(r *http.Request, user *data.User, password string) bool {
	if valid, err := user.PasswordMatches(password); err != nil || !valid {
		return false
	}

	app.Session.Put(r.Context(), "user", user)
	return true
}

Selanjutnya kita lakukan modifikasi pada cmd/web/main.go, untuk registrasi data.user type di session.

package main

import (
	"encoding/gob"
	"flag"
	"log"
	"net/http"
	"webapp/pkg/data"
	"webapp/pkg/db"

	"github.com/alexedwards/scs/v2"
)

type application struct {
	DSN     string
	DB      db.PostgresConn //ubah struc menggunakan pacakge db
	Session *scs.SessionManager
}

func main() {
	gob.Register(data.User{})

	//setup app config
	app := application{}

	//database connection string
	flag.StringVar(&app.DSN, "dsn", "host=localhost port=5432 user=postgres password=postgres dbname=users sslmode=disable timezone=UTC connect_timeout=5", "Postgres Connection")
	flag.Parse()

	//connection to database
	conn, err := app.connectToDB()
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	app.DB = db.PostgresConn{DB: conn} //ubah menggunakan package db

	//get session manager
	app.Session = getSession()

	//start server
	log.Println("Starting server on port 8080...")
	err = http.ListenAndServe(":8080", app.routes())

	if err != nil {
		log.Fatal(err)
	}

}

Kemudian kita buat template baru yang akan menampilkan profile user bila berhasil login. Buat file templates/profile.page.gohtml

{{template "base" .}}

{{define "content"}}
    <div class="container">
        <div class="row">
            <div class="col">
                <h1 class="mt-3">User Profile</h1>
                <hr>
            </div>
        </div>
    </div>
{{end}}

Menambahkan element pada templates/base.layout.go untuk menampilkan informasi dari session.

{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
    <title>Home</title>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="content">
            {{with .Flash}}
                <div class="mt-3 alert alert-success" role="alert">
                    {{.}}
                </div>
            {{end}}
            {{with .Error}}
                <div class="mt-3 alert alert-danger" role="alert">
                    {{.}}
                </div>
            {{end}}            
        </div>
    </div>
</div>

{{block "content" .}}

{{end}}

</body>
</html>

{{end}}

Selanjutnya kita registrasikan route pada cmd/web/routes.go

package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

func (app *application) routes() http.Handler {
	mux := chi.NewRouter()

	//register middleware
	mux.Use(middleware.Recoverer)
	mux.Use(app.addIPToContext)
	mux.Use(app.Session.LoadAndSave) //middleware yang disediakan dari session manager

	//register routes
	mux.Get("/", app.Home)
	mux.Post("/login", app.Login)
	mux.Get("/user/profile", app.Profile)

	//static assets

	fileServer := http.FileServer(http.Dir("./static"))
	mux.Handle("/static/*", http.StripPrefix("/static/", fileServer))

	return mux
}

Jika kita jalankan aplikasi, lalu coba login dengan skenario

  • user atau password kosong.
  • user atau password salah.
  • user dan password benar.

Maka pada web browser akan ditampilkan informasi error message atau login berhasil.

Sampai disini kita sudah selesai membuat proses authentication. Pada modul selanjutnya kita akan mulai membuat membuat test untuk proses authentication yang menggunakan method Post.

Sharing is caring:

Leave a Comment