Aula 15 – Golang para Web – Limpando o código
Aula 15 – Golang para Web – Limpando o código
Voltar para página principal do blog
Todas as aulas desse curso
Aula 14
Se gostarem do conteúdo dêem um joinha 👍 na página do Código Fluente no
Facebook
Link do código fluente no Pinterest
Meus links de afiliados:
Hostinger
Digital Ocean
One.com
Código da aula: Github
Melhore seu NETWORKING
Participe de comunidades de desenvolvedores:
Fiquem a vontade para me adicionar ao linkedin.
E também para me seguir no GITHUB.
Aula 15 – Golang para Web – Limpando o código
Vamos organizar melhor o código e adicionar um CSS para estilizar o aplicativo.
Veja quanta repetição de código fizemos para tratar o erro interno de servidor.
web_app/routes/routes.go
package routes
import (
"net/http"
"github.com/gorilla/mux"
"../middleware"
"../models"
"../sessions"
"../utils"
)
func NewRouter() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/", middleware.AuthRequired(indexGetHandler)).Methods("GET")
r.HandleFunc("/", middleware.AuthRequired(indexPostHandler)).Methods("POST")
r.HandleFunc("/login", loginGetHandler).Methods("GET")
r.HandleFunc("/login", loginPostHandler).Methods("POST")
r.HandleFunc("/register", registerGetHandler).Methods("GET")
r.HandleFunc("/register", registerPostHandler).Methods("POST")
fs := http.FileServer(http.Dir("./static/"))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs))
r.HandleFunc("/{username}",
middleware.AuthRequired(userGetHandler)).Methods("GET")
return r
}
func indexGetHandler(w http.ResponseWriter, r *http.Request) {
updates, err := models.GetAllUpdates()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
utils.ExecuteTemplate(w, "index.html", struct {
Title string
Updates []*models.Update
} {
Title: "All updates",
Updates: updates,
})
}
func indexPostHandler(w http.ResponseWriter, r *http.Request) {
session, _ := sessions.Store.Get(r, "session")
untypedUserId := session.Values["user_id"]
userId, ok := untypedUserId.(int64)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
r.ParseForm()
body := r.PostForm.Get("update")
err := models.PostUpdate(userId, body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
http.Redirect(w, r, "/", 302)
}
func userGetHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
username := vars["username"]
user, err := models.GetUserByUsername(username)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
userId, err := user.GetId()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
updates, err := models.GetUpdates(userId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
utils.ExecuteTemplate(w, "index.html", struct {
Title string
Updates []*models.Update
} {
Title: username,
Updates: updates,
})
}
func loginGetHandler(w http.ResponseWriter, r *http.Request) {
utils.ExecuteTemplate(w, "login.html", nil)
}
func loginPostHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
user, err := models.AuthenticateUser(username, password)
if err != nil {
switch err {
case models.ErrUserNotFound:
utils.ExecuteTemplate(w, "login.html", "unknown user")
case models.ErrInvalidLogin:
utils.ExecuteTemplate(w, "login.html", "invalid login")
default:
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
}
return
}
userId, err := user.GetId()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
session, _ := sessions.Store.Get(r, "session")
session.Values["user_id"] = userId
session.Save(r, w)
http.Redirect(w, r, "/", 302)
}
func registerGetHandler(w http.ResponseWriter, r *http.Request) {
utils.ExecuteTemplate(w, "register.html", nil)
}
func registerPostHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
err := models.RegisterUser(username, password)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
http.Redirect(w, r, "/login", 302)
}
O que vamos fazer é colocar esse código em uma função chamada InternalServerError() em arquivo chamado errors.go dentro da pasta utils.
web_app/utils/errors.go
package utils
import (
"net/http"
)
func InternalServerError(w http.ResponseWriter) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
}
Agora no web_app/routes/routes.go temos que fazer a chamada da InternalServerError() do erros.go.
Além disso temos a rota /logout.
No ExecuteTemplate que renderiza o index.html, temos um booleano o DisplayForm pra saber se o usuário atual, o que tá na session, é o mesmo usuário da url, dependendo ele exibe o form ou não.
Por exemplo, se o usuário logado for toti e fizer um post, mas na rota, vamos supor, /maria, não faz nenhum sentido o usuário toti postar algo no endpoint da maria.
Por isso, no userGetHandler(), ocorre a comparação, currentUserId == userId.
Ele verifica se o usuário logado, o da session, é o mesmo do endpoint.
Na indexGetHandler() o DisplayForm é definido como true, porque na rota index( / ), ele vai mostrar de qualquer jeito o form.
package routes
import (
"net/http"
"github.com/gorilla/mux"
"../middleware" "../models"
"../sessions"
"../utils"
)
func NewRouter() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/", middleware.AuthRequired(indexGetHandler)).Methods("GET")
r.HandleFunc("/", middleware.AuthRequired(indexPostHandler)).Methods("POST")
r.HandleFunc("/login", loginGetHandler).Methods("GET")
r.HandleFunc("/login", loginPostHandler).Methods("POST")
r.HandleFunc("/logout", logoutGetHandler).Methods("GET")
r.HandleFunc("/register", registerGetHandler).Methods("GET")
r.HandleFunc("/register", registerPostHandler).Methods("POST")
fs := http.FileServer(http.Dir("./static/"))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs))
r.HandleFunc("/{username}",
middleware.AuthRequired(userGetHandler)).Methods("GET")
return r
}
func indexGetHandler(w http.ResponseWriter, r *http.Request) {
updates, err := models.GetAllUpdates()
if err != nil {
utils.InternalServerError(w)
return
}
utils.ExecuteTemplate(w, "index.html", struct {
Title string
Updates []*models.Update
DisplayForm bool
} {
Title: "All updates",
Updates: updates,
DisplayForm: true,
})
}
func indexPostHandler(w http.ResponseWriter, r *http.Request) {
session, _ := sessions.Store.Get(r, "session")
untypedUserId := session.Values["user_id"]
userId, ok := untypedUserId.(int64)
if !ok {
utils.InternalServerError(w)
return
}
r.ParseForm()
body := r.PostForm.Get("update")
err := models.PostUpdate(userId, body)
if err != nil {
utils.InternalServerError(w)
return
}
http.Redirect(w, r, "/", 302)
}
func userGetHandler(w http.ResponseWriter, r *http.Request) {
session, _ := sessions.Store.Get(r, "session")
untypedUserId := session.Values["user_id"]
currentUserId, ok := untypedUserId.(int64)
if !ok {
utils.InternalServerError(w)
return
}
vars := mux.Vars(r)
username := vars["username"]
user, err := models.GetUserByUsername(username)
if err != nil {
utils.InternalServerError(w)
return
}
userId, err := user.GetId()
if err != nil {
utils.InternalServerError(w)
return
}
updates, err := models.GetUpdates(userId)
if err != nil {
utils.InternalServerError(w)
return
}
utils.ExecuteTemplate(w, "index.html", struct {
Title string
Updates []*models.Update
DisplayForm bool
} {
Title: username,
Updates: updates,
DisplayForm: currentUserId == userId,
})
}
func loginGetHandler(w http.ResponseWriter, r *http.Request) {
utils.ExecuteTemplate(w, "login.html", nil)
}
func loginPostHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
user, err := models.AuthenticateUser(username, password)
if err != nil {
switch err {
case models.ErrUserNotFound:
utils.ExecuteTemplate(w, "login.html", "unknown user")
case models.ErrInvalidLogin:
utils.ExecuteTemplate(w, "login.html", "invalid login")
default:
utils.InternalServerError(w)
}
return
}
userId, err := user.GetId()
if err != nil {
utils.InternalServerError(w)
return
}
session, _ := sessions.Store.Get(r, "session")
session.Values["user_id"] = userId
session.Save(r, w)
http.Redirect(w, r, "/", 302)
}
func logoutGetHandler(w http.ResponseWriter, r *http.Request) {
session, _ := sessions.Store.Get(r, "session")
delete(session.Values, "user_id")
session.Save(r, w)
http.Redirect(w, r, "/login", 302)
}
func registerGetHandler(w http.ResponseWriter, r *http.Request) {
utils.ExecuteTemplate(w, "register.html", nil)
}
// Esse trecho é para evitar que um usuário subrescreva um
// outro que já exista no banco na hora de se registrar.
if err == models.ErrUsernameTaken {
utils.ExecuteTemplate(w, "register.html", "username taken")
return
} else if err != nil {
utils.InternalServerError(w)
return
}
http.Redirect(w, r, "/login", 302)
}
Agora no web_app/templates/register.html
Se o dado passado for um erro, ou seja, se tiver ocorrido um erro, vai ser criada uma nova div com o nome da classe “error“, que irá conter a string de erro.
web_app/templates/register.html
<html>
<head>
<title>Register</title>
</head>
<body>
{{ if . }}
<div class="error">{{ . }}</div>
{{ end }}
<form method="POST">
<div>Username: <input name="username"></div>
<div>Password: <input name="password"></div>
<div>
<button type="submit">Register</button>
</div>
</form>
</body>
</html>
Seguindo
A próxima refatoração vai ser do web_app/models/user.go.
Nós estamos usando o id do usuário, na verdade a key do usuário, como string, e tamo pegando esse valor da session.
Vamos mudar isso, para id int64, e pegar esse valor do Redis.
Mudar para int64, é porque fica mais fácil para o Redis recuperar esse id ele sendo um int.
web_app/models/user.go
package models
import (
"fmt"
"errors"
"github.com/go-redis/redis"
"golang.org/x/crypto/bcrypt"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidLogin = errors.New("invalid login")
ErrUsernameTaken = errors.New("username taken")
)
type User struct {
id int64
}
func NewUser(username string, hash []byte) (*User, error) {
exists, err := client.HExists("user:by-username", username).Result()
if exists {
return nil, ErrUsernameTaken
}
id, err := client.Incr("user:next-id").Result()
if err != nil {
return nil, err
}
key := fmt.Sprintf("user:%d", id)
pipe := client.Pipeline()
pipe.HSet(key, "id", id)
pipe.HSet(key, "username", username)
pipe.HSet(key, "hash", hash)
pipe.HSet("user:by-username", username, id)
_, err = pipe.Exec()
if err != nil {
return nil, err
}
return &User{id}, nil
}
func (user *User) GetId() (int64, error) {
return user.id, nil
}
func (user *User) GetUsername() (string, error) {
key := fmt.Sprintf("user:%d", user.id)
return client.HGet(key, "username").Result()
}
func (user *User) GetHash() ([]byte, error) {
key := fmt.Sprintf("user:%d", user.id)
return client.HGet(key, "hash").Bytes()
}
func (user *User) Authenticate(password string) error {
hash, err := user.GetHash()
if err != nil {
return err
}
err = bcrypt.CompareHashAndPassword(hash, []byte(password))
if err == bcrypt.ErrMismatchedHashAndPassword {
return ErrInvalidLogin
}
return err
}
func GetUserById(id int64) (*User, error) {
return &User{id}, nil
}
func GetUserByUsername(username string) (*User, error) {
id, err := client.HGet("user:by-username", username).Int64()
if err == redis.Nil {
return nil, ErrUserNotFound
} else if err != nil {
return nil, err
}
return GetUserById(id)
}
func AuthenticateUser(username, password string) (*User, error) {
user, err := GetUserByUsername(username)
if err != nil {
return nil, err
}
return user, user.Authenticate(password)
}
func RegisterUser(username, password string) error {
cost := bcrypt.DefaultCost
hash, err := bcrypt.GenerateFromPassword([]byte(password), cost)
if err != nil {
return err
}
_, err = NewUser(username, hash)
return err
}
Vamos refatorar o update de forma parecida como fizemos no user.
web_app/models/update.go
package models
import (
"fmt"
"strconv"
)
type Update struct {
id int64
}
func NewUpdate(userId int64, body string) (*Update, error) {
id, err := client.Incr("update:next-id").Result()
if err != nil {
return nil, err
}
key := fmt.Sprintf("update:%d", id)
pipe := client.Pipeline()
pipe.HSet(key, "id", id)
pipe.HSet(key, "user_id", userId)
pipe.HSet(key, "body", body)
pipe.LPush("updates", id)
pipe.LPush(fmt.Sprintf("user:%d:updates", userId), id)
_, err = pipe.Exec()
if err != nil {
return nil, err
}
return &Update{id}, nil
}
func (update *Update) GetBody() (string, error) {
key := fmt.Sprintf("update:%d", update.id)
return client.HGet(key, "body").Result()
}
func (update *Update) GetUser() (*User, error) {
key := fmt.Sprintf("update:%d", update.id)
userId, err := client.HGet(key, "user_id").Int64()
if err != nil {
return nil, err
}
return GetUserById(userId)
}
func queryUpdates(key string) ([]*Update, error) {
updateIds, err := client.LRange(key, 0, 10).Result()
if err != nil {
return nil, err
}
updates := make([]*Update, len(updateIds))
for i, strId := range updateIds {
id, err := strconv.Atoi(strId)
if err != nil {
return nil, err
}
updates[i] = &Update{int64(id)}
}
return updates, nil
}
func GetAllUpdates() ([]*Update, error) {
return queryUpdates("updates")
}
func GetUpdates(userId int64) ([]*Update, error) {
key := fmt.Sprintf("user:%d:updates", userId)
return queryUpdates(key)
}
func PostUpdate(userId int64, body string) error {
_, err := NewUpdate(userId, body)
return err
}
E para que o usuário consiga postar através de sua url(exemplo: localhost:8080/toti), vamos alterar o web_app/templates/index.html.
O .DisplayForm é um booleano que vai ser testado para saber se é para renderizar o update-form ou não.
Ele testa se o id do usuário da sessão é igual ao id da url do usuário, se for true, ele renderiza o update-form.
web_app/templates/index.html
<html>
<head>
<title>{{ .Title }}</title>
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<nav>
<a href="/logout">logout</a>
</nav>
<main>
<h1>{{ .Title }}</h1>
{{ if .DisplayForm }}
<div id="update-form">
<form action="/" method="POST">
<textarea name="update"></textarea>
<div>
<button type="submit">Post Update</button>
</div>
</form>
</div>
{{ end }}
{{ range .Updates }}
<div class="update">
<div>
<strong><a href="/{{ .GetUser.GetUsername }}">{{ .GetUser.GetUsername }}</a> wrote:</strong>
</div>
<div>{{ .GetBody }}</div>
</div>
{{ end }}
</main>
</body>
</html>
Estilização com CSS
web_app/static/index.css
* {
margin: 0;
padding: 0;
}
body {
background: #f0f0f0;
}
nav {
padding: 0.5em;
background: #fff;
border-bottom: 1px solid #aaa;
text-align: right;
}
main {
margin: 0 auto;
max-width: 640px;
}
main > .update {
padding: 0.5em;
background: #fff;
border: 1px solid #aaa;
margin-bottom: 1em;
}
#update-form {
text-align: right;
margin-bottom: 1em;
}
#update-form textarea {
width: 100%;
margin-bottom: 0.5em;
resize: vertical;
}
Alguns comandos REDIS úteis que podemos testar:
redis-cli
keys *
hgetall user:1
hgetall user:by-username
hset user:by-username fulano 4
redis-cli flushall
Teste agora rodando o servidor com:
go run main.go