all repos — auth-boilerplate @ e32a2dfeddcff5b2677fdffb65af0a78b614f797

A simple Go web-app boilerplate.

add email module
Marco Andronaco andronacomarco@gmail.com
Thu, 10 Oct 2024 11:47:12 +0200
commit

e32a2dfeddcff5b2677fdffb65af0a78b614f797

parent

e20c137b8edaa2e68e8dbe16bb78cf54655749c2

4 files changed, 243 insertions(+), 163 deletions(-)

jump to
M .env.example.env.example

@@ -1,1 +1,5 @@

APP_PEPPER=AnyRandomString +APP_SMTP_EMAIL=your-address@gmail.com +APP_SMTP_PASSWORD=yourpassword +APP_SMTP_HOST=smtp.gmail.com +APP_SMTP_PORT=587
A email/email.go

@@ -0,0 +1,44 @@

+package email + +import ( + "net/smtp" +) + +// EmailConfig rappresenta la configurazione necessaria per inviare un'email +type Client struct { + email string + + auth smtp.Auth + addr string +} + +// Email rappresenta il contenuto dell'email +type Email struct { + To []string + Subject string + Body string +} + +func NewClient(senderEmail, password, host, port string) *Client { + return &Client{ + addr: host + ":" + port, + auth: smtp.PlainAuth("", senderEmail, password, host), + } +} + +// Send invia l'email utilizzando la configurazione fornita +func (config *Client) Send(email Email) error { + // Costruzione del messaggio + msg := "From: " + config.email + "\n" + + "To: " + email.To[0] + "\n" + + "Subject: " + email.Subject + "\n\n" + + email.Body + + // Invio email tramite il server SMTP + err := smtp.SendMail(config.addr, config.auth, config.email, email.To, []byte(msg)) + if err != nil { + return err + } + + return nil +}
A handlers.go

@@ -0,0 +1,164 @@

+package main + +import ( + "fmt" + "log" + "net/http" + "time" + + "github.com/birabittoh/auth-boilerplate/email" +) + +func examplePage(w http.ResponseWriter, r *http.Request) { + user, ok := getLoggedUser(r) + if !ok { + http.Error(w, "Could not find user in context.", http.StatusInternalServerError) + return + } + + templates.ExecuteTemplate(w, "example.html", map[string]interface{}{"User": user}) +} + +func getRegisterHandler(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "register.html", nil) +} + +func getLoginHandler(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "login.html", nil) +} + +func getResetPasswordHandler(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "reset_password.html", nil) +} + +func postRegisterHandler(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("username") + email := r.FormValue("email") + password := r.FormValue("password") + + hashedPassword, salt, err := g.HashPassword(password) + if err != nil { + http.Error(w, "Could not hash your password.", http.StatusInternalServerError) + return + } + + user := User{ + Username: username, + Email: email, + PasswordHash: hashedPassword, + Salt: salt, + } + db.Create(&user) + http.Redirect(w, r, "/login", http.StatusFound) + return +} + +func postLoginHandler(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("username") + password := r.FormValue("password") + remember := r.FormValue("remember") + + var user User + db.Where("username = ?", username).First(&user) + + if user.ID == 0 || !g.CheckPassword(password, user.Salt, user.PasswordHash) { + http.Error(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + var duration time.Duration + if remember == "on" { + duration = durationWeek + } else { + duration = durationDay + } + + cookie, err := g.GenerateCookie(duration) + if err != nil { + http.Error(w, "Could not generate session cookie.", http.StatusInternalServerError) + } + + ks.Set(cookie.Value, user.ID, duration) + http.SetCookie(w, cookie) + http.Redirect(w, r, "/", http.StatusFound) + return +} + +func logoutHandler(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, g.GenerateEmptyCookie()) + http.Redirect(w, r, "/login", http.StatusFound) +} + +func postResetPasswordHandler(w http.ResponseWriter, r *http.Request) { + emailInput := r.FormValue("email") + + var user User + db.Where("email = ?", emailInput).First(&user) + + if user.ID == 0 { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + resetToken, err := g.GenerateRandomToken(32) + if err != nil { + http.Error(w, "Could not generate reset token.", http.StatusInternalServerError) + return + } + + ks.Set(resetToken, user.ID, time.Hour) + resetURL := fmt.Sprintf("http://localhost:8080/reset-password-confirm?token=%s", resetToken) + + err = sendEmail(email.Email{ + To: []string{user.Email}, + Subject: "Reset password", + Body: fmt.Sprintf("Use this link to reset your password: %s", resetURL), + }) + + if err != nil { + log.Printf("Could not send reset email for %s. Link: %s", user.Email, resetURL) + } + + http.Redirect(w, r, "/login", http.StatusFound) + return + +} + +func getResetPasswordConfirmHandler(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("token") + _, err := ks.Get(token) + if err != nil { + http.Error(w, "Token is invalid or expired.", http.StatusUnauthorized) + return + } + + templates.ExecuteTemplate(w, "new_password.html", nil) +} + +func postResetPasswordConfirmHandler(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("token") + userID, err := ks.Get(token) + if err != nil { + http.Error(w, "Token is invalid or expired.", http.StatusUnauthorized) + return + } + + var user User + db.First(&user, *userID) + + password := r.FormValue("password") + + hashedPassword, salt, err := g.HashPassword(password) + if err != nil { + http.Error(w, "Could not edit your password.", http.StatusInternalServerError) + return + } + + user.PasswordHash = hashedPassword + user.Salt = salt + db.Save(&user) + ks.Delete(token) + + http.Redirect(w, r, "/login", http.StatusFound) + return +}
M main.gomain.go

@@ -2,7 +2,7 @@ package main

import ( "context" - "fmt" + "errors" "html/template" "log" "net/http"

@@ -10,6 +10,7 @@ "os"

"time" "github.com/birabittoh/auth-boilerplate/auth" + "github.com/birabittoh/auth-boilerplate/email" "github.com/birabittoh/myks" "github.com/glebarez/sqlite" "github.com/joho/godotenv"

@@ -29,6 +30,7 @@

var ( db *gorm.DB g *auth.Auth + m *email.Client ks = myks.New[uint](0) durationDay = 24 * time.Hour

@@ -38,6 +40,31 @@ )

const userContextKey key = 0 +func loadEmailConfig() *email.Client { + address := os.Getenv("APP_SMTP_EMAIL") + password := os.Getenv("APP_SMTP_PASSWORD") + host := os.Getenv("APP_SMTP_HOST") + port := os.Getenv("APP_SMTP_PORT") + + if address == "" || password == "" || host == "" { + log.Println("Missing email configuration.") + return nil + } + + if port == "" { + port = "587" + } + + return email.NewClient(address, password, host, port) +} + +func sendEmail(mail email.Email) error { + if m == nil { + return errors.New("email client is not initialized") + } + return m.Send(mail) +} + func main() { err := godotenv.Load() if err != nil {

@@ -55,6 +82,7 @@ db.AutoMigrate(&User{})

// Inizializzazione di gauth g = auth.NewAuth(os.Getenv("APP_PEPPER")) + m = loadEmailConfig() // Gestione delle route http.HandleFunc("GET /", loginRequired(examplePage))

@@ -70,12 +98,11 @@ http.HandleFunc("POST /reset-password", postResetPasswordHandler)

http.HandleFunc("POST /reset-password-confirm", postResetPasswordConfirmHandler) port := ":8080" - log.Println("Server in ascolto su " + port) + log.Println("Server running on port " + port) log.Fatal(http.ListenAndServe(port, nil)) } -// Middleware per controllare se l'utente è loggato -// Funzione middleware per controllare se l'utente è autenticato +// Middleware per controllare se l'utente è loggato. func loginRequired(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session_token")

@@ -100,162 +127,3 @@ userID, ok := r.Context().Value(userContextKey).(uint)

db.Find(&user, userID) return user, ok } - -func examplePage(w http.ResponseWriter, r *http.Request) { - user, ok := getLoggedUser(r) - if !ok { - http.Error(w, "Utente non trovato nel contesto", http.StatusInternalServerError) - return - } - - templates.ExecuteTemplate(w, "example.html", map[string]interface{}{"User": user}) -} - -func getRegisterHandler(w http.ResponseWriter, r *http.Request) { - templates.ExecuteTemplate(w, "register.html", nil) -} - -func getLoginHandler(w http.ResponseWriter, r *http.Request) { - templates.ExecuteTemplate(w, "login.html", nil) -} - -func getResetPasswordHandler(w http.ResponseWriter, r *http.Request) { - templates.ExecuteTemplate(w, "reset_password.html", nil) -} - -// Gestione della registrazione -func postRegisterHandler(w http.ResponseWriter, r *http.Request) { - username := r.FormValue("username") - email := r.FormValue("email") - password := r.FormValue("password") - - hashedPassword, salt, err := g.HashPassword(password) - if err != nil { - log.Printf("Error: %v", err) - http.Error(w, "Errore durante la registrazione", http.StatusInternalServerError) - return - } - - user := User{ - Username: username, - Email: email, - PasswordHash: hashedPassword, - Salt: salt, - } - db.Create(&user) - http.Redirect(w, r, "/login", http.StatusFound) - return -} - -// Gestione del login -func postLoginHandler(w http.ResponseWriter, r *http.Request) { - username := r.FormValue("username") - password := r.FormValue("password") - remember := r.FormValue("remember") - - var user User - db.Where("username = ?", username).First(&user) - - if user.ID == 0 || !g.CheckPassword(password, user.Salt, user.PasswordHash) { - http.Error(w, "Credenziali non valide", http.StatusUnauthorized) - return - } - - var duration time.Duration - if remember == "on" { - duration = durationWeek - } else { - duration = durationDay - } - - cookie, err := g.GenerateCookie(duration) - if err != nil { - http.Error(w, "Errore nella generazione del token", http.StatusInternalServerError) - } - - ks.Set(cookie.Value, user.ID, duration) - http.SetCookie(w, cookie) - http.Redirect(w, r, "/", http.StatusFound) - return -} - -// Logout -func logoutHandler(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, g.GenerateEmptyCookie()) - http.Redirect(w, r, "/login", http.StatusFound) -} - -// Funzione per gestire la richiesta di reset password -func postResetPasswordHandler(w http.ResponseWriter, r *http.Request) { - email := r.FormValue("email") - - var user User - db.Where("email = ?", email).First(&user) - - if user.ID == 0 { - // Non riveliamo se l'email non esiste per motivi di sicurezza - http.Redirect(w, r, "/login", http.StatusFound) - return - } - - // Genera un token di reset - resetToken, err := g.GenerateRandomToken(32) - if err != nil { - http.Error(w, "Errore nella generazione del token di reset", http.StatusInternalServerError) - return - } - - // Imposta una scadenza di 1 ora per il reset token - ks.Set(resetToken, user.ID, time.Hour) - - // Simula l'invio di un'email con il link di reset (in questo caso viene stampato sul log) - resetURL := fmt.Sprintf("http://localhost:8080/reset-password-confirm?token=%s", resetToken) - log.Printf("Invio dell'email di reset per %s: %s", user.Email, resetURL) - - http.Redirect(w, r, "/login", http.StatusFound) - return - -} - -// Funzione per confermare il reset della password (usando il token) -func getResetPasswordConfirmHandler(w http.ResponseWriter, r *http.Request) { - token := r.URL.Query().Get("token") - _, err := ks.Get(token) - if err != nil { - http.Error(w, "Token non valido o scaduto", http.StatusUnauthorized) - return - } - - templates.ExecuteTemplate(w, "new_password.html", nil) -} - -func postResetPasswordConfirmHandler(w http.ResponseWriter, r *http.Request) { - token := r.URL.Query().Get("token") - userID, err := ks.Get(token) - if err != nil { - http.Error(w, "Token non valido o scaduto", http.StatusUnauthorized) - return - } - - var user User - db.First(&user, *userID) - - password := r.FormValue("password") - - // Hash della nuova password - hashedPassword, salt, err := g.HashPassword(password) - if err != nil { - http.Error(w, "Errore nella modifica della password", http.StatusInternalServerError) - return - } - - // Aggiorna l'utente con la nuova password e rimuove il token di reset - user.PasswordHash = hashedPassword - user.Salt = salt - db.Save(&user) - ks.Delete(token) - - // Reindirizza alla pagina di login - http.Redirect(w, r, "/login", http.StatusFound) - return -}