all repos — auth-boilerplate @ d9c78a5c5d146159e794e6e265b69320bd05653b

A simple Go web-app boilerplate.

minor tweaks
Marco Andronaco andronacomarco@gmail.com
Thu, 10 Oct 2024 13:58:07 +0200
commit

d9c78a5c5d146159e794e6e265b69320bd05653b

parent

e32a2dfeddcff5b2677fdffb65af0a78b614f797

M .env.example.env.example

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

+APP_PORT=3000 +APP_BASE_URL=http://localhost:3000 APP_PEPPER=AnyRandomString APP_SMTP_EMAIL=your-address@gmail.com APP_SMTP_PASSWORD=yourpassword
M .gitignore.gitignore

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

.env +data *.db __debug_bin* dist
A functions.go

@@ -0,0 +1,95 @@

+package main + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/birabittoh/auth-boilerplate/email" +) + +func login(w http.ResponseWriter, userID uint, remember bool) { + var duration time.Duration + if remember { + 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, userID, duration) + http.SetCookie(w, cookie) +} + +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 sendResetEmail(address, token string) { + resetURL := fmt.Sprintf("%s/reset-password-confirm?token=%s", baseUrl, token) + err := sendEmail(email.Email{ + To: []string{address}, + 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", address, resetURL) + } +} + +func readSessionCookie(r *http.Request) (userID *uint, err error) { + cookie, err := r.Cookie("session_token") + if err != nil { + return + } + return ks.Get(cookie.Value) +} + +// Middleware to check if the user is logged in +func loginRequired(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID, err := readSessionCookie(r) + if err != nil { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + ctx := context.WithValue(r.Context(), userContextKey, *userID) + next(w, r.WithContext(ctx)) + } +} + +func getLoggedUser(r *http.Request) (user User, ok bool) { + userID, ok := r.Context().Value(userContextKey).(uint) + db.Find(&user, userID) + return user, ok +}
M handlers.gohandlers.go

@@ -1,12 +1,8 @@

package main import ( - "fmt" - "log" "net/http" "time" - - "github.com/birabittoh/auth-boilerplate/email" ) func examplePage(w http.ResponseWriter, r *http.Request) {

@@ -24,7 +20,12 @@ templates.ExecuteTemplate(w, "register.html", nil)

} func getLoginHandler(w http.ResponseWriter, r *http.Request) { - templates.ExecuteTemplate(w, "login.html", nil) + _, err := readSessionCookie(r) + if err != nil { + templates.ExecuteTemplate(w, "login.html", nil) + return + } + http.Redirect(w, r, "/", http.StatusFound) } func getResetPasswordHandler(w http.ResponseWriter, r *http.Request) {

@@ -48,7 +49,14 @@ Email: email,

PasswordHash: hashedPassword, Salt: salt, } + db.Create(&user) + if user.ID == 0 { + http.Error(w, "Username or email already exists.", http.StatusConflict) + return + } + + login(w, user.ID, false) http.Redirect(w, r, "/login", http.StatusFound) return }

@@ -66,20 +74,7 @@ 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) + login(w, user.ID, remember == "on") http.Redirect(w, r, "/", http.StatusFound) return }

@@ -107,17 +102,7 @@ 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) - } + sendResetEmail(user.Email, resetToken) http.Redirect(w, r, "/login", http.StatusFound) return
M main.gomain.go

@@ -1,12 +1,11 @@

package main import ( - "context" - "errors" "html/template" "log" "net/http" "os" + "path/filepath" "time" "github.com/birabittoh/auth-boilerplate/auth"

@@ -21,17 +20,25 @@ type key int

type User struct { gorm.Model - Username string - Email string + Username string `gorm:"unique"` + Email string `gorm:"unique"` PasswordHash string Salt string } + +const ( + dataDir = "data" + dbName = "app.db" +) var ( db *gorm.DB g *auth.Auth m *email.Client + baseUrl string + port string + ks = myks.New[uint](0) durationDay = 24 * time.Hour durationWeek = 7 * durationDay

@@ -40,51 +47,36 @@ )

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 +func main() { + err := godotenv.Load() + if err != nil { + log.Println("Error loading .env file") } + port = os.Getenv("APP_PORT") if port == "" { - port = "587" + port = "3000" } - return email.NewClient(address, password, host, port) -} - -func sendEmail(mail email.Email) error { - if m == nil { - return errors.New("email client is not initialized") + baseUrl = os.Getenv("APP_BASE_URL") + if baseUrl == "" { + baseUrl = "http://localhost:" + port } - return m.Send(mail) -} -func main() { - err := godotenv.Load() - if err != nil { - log.Println("Error loading .env file") - } + // Init auth and email + g = auth.NewAuth(os.Getenv("APP_PEPPER")) + m = loadEmailConfig() - // Connessione al database SQLite - db, err = gorm.Open(sqlite.Open("database.db"), &gorm.Config{}) + os.MkdirAll(dataDir, os.ModePerm) + dbPath := filepath.Join(dataDir, dbName) + "?_pragma=foreign_keys(1)" + db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) if err != nil { log.Fatal(err) } - // Creazione della tabella utenti db.AutoMigrate(&User{}) - // Inizializzazione di gauth - g = auth.NewAuth(os.Getenv("APP_PEPPER")) - m = loadEmailConfig() - - // Gestione delle route + // Handle routes http.HandleFunc("GET /", loginRequired(examplePage)) http.HandleFunc("GET /register", getRegisterHandler) http.HandleFunc("GET /login", getLoginHandler)

@@ -97,33 +89,8 @@ http.HandleFunc("POST /register", postRegisterHandler)

http.HandleFunc("POST /reset-password", postResetPasswordHandler) http.HandleFunc("POST /reset-password-confirm", postResetPasswordConfirmHandler) - port := ":8080" - log.Println("Server running on port " + port) - log.Fatal(http.ListenAndServe(port, nil)) -} - -// 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") - if err != nil { - http.Redirect(w, r, "/login", http.StatusFound) - return - } - - userID, err := ks.Get(cookie.Value) - if err != nil { - http.Redirect(w, r, "/login", http.StatusFound) - return - } - - ctx := context.WithValue(r.Context(), userContextKey, *userID) - next(w, r.WithContext(ctx)) - } -} - -func getLoggedUser(r *http.Request) (user User, ok bool) { - userID, ok := r.Context().Value(userContextKey).(uint) - db.Find(&user, userID) - return user, ok + // Start serving + log.Println("Port: " + port) + log.Println("Server started: " + baseUrl) + log.Fatal(http.ListenAndServe(":"+port, nil)) }
M templates/login.htmltemplates/login.html

@@ -6,12 +6,12 @@ </head>

<body> <h1>Login</h1> <form method="post" action="/login"> - <label>Username: <input type="text" name="username"></label><br> + <label>Username: <input type="text" name="username" autocomplete="off"></label><br> <label>Password: <input type="password" name="password"></label><br> - <label>Ricordami: <input type="checkbox" name="remember"></label><br> + <label>Remember me: <input type="checkbox" name="remember"></label><br> <input type="submit" value="Login"> </form> - <a href="/register">Registrati</a> + <a href="/register">Sign up</a> <a href="/reset-password">Reset password</a> </body> </html>
M templates/register.htmltemplates/register.html

@@ -12,5 +12,6 @@ <label>Password: <input type="password" name="password"></label><br>

<input type="submit" value="Registrati"> </form> <a href="/login">Login</a> + <a href="/reset-password">Reset password</a> </body> </html>
M templates/reset_password.htmltemplates/reset_password.html

@@ -9,6 +9,7 @@ <form method="post" action="/reset-password">

<label>Email: <input type="email" name="email"></label><br> <input type="submit" value="Reset Password"> </form> + <a href="/register">Sign up</a> <a href="/login">Login</a> </body> </html>