From 5b0e33aef95b80cac2a2aa39f97af9af0c6cb464 Mon Sep 17 00:00:00 2001
From: Gregor Best <gbe@unobtanium.de>
Date: Sun, 6 Feb 2022 18:23:56 +0100
Subject: [PATCH] a whole lot of cleanups

---
 .golangci.yaml          |  6 ++-
 auth/auth.go            |  9 ++---
 auth/auth_test.go       |  1 +
 handler-details.go      | 88 ++++++++++++++++++++---------------------
 handler-index.go        | 71 +++++++++++++++------------------
 handler-user.go         |  2 +
 session/authprovider.go | 85 ++++++++++++---------------------------
 storage/storage.go      | 54 +++++++++++++------------
 8 files changed, 140 insertions(+), 176 deletions(-)

diff --git a/.golangci.yaml b/.golangci.yaml
index e9b0045..c64a3b2 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -9,4 +9,8 @@ issues:
       source: "Log\\("
     - linters:
         - gomnd
-      source: "\\*time.(Second|Minute|Hour)"
\ No newline at end of file
+      source: "\\*time.(Second|Minute|Hour)"
+linters-settings:
+  funlen:
+    statements: 50
+    lines: 70
\ No newline at end of file
diff --git a/auth/auth.go b/auth/auth.go
index 4fc6830..d447e0c 100644
--- a/auth/auth.go
+++ b/auth/auth.go
@@ -57,8 +57,7 @@ func Require(next http.Handler, authFailed http.Handler, provider Provider) http
 			return
 		}
 
-		level.Info(log.Get(r)).
-			Log("msg", "no session, using anon auth")
+		level.Info(log.Get(r)).Log("msg", "no session, using anon auth")
 
 		user := &User{
 			Name: "anon",
@@ -90,8 +89,7 @@ func Require(next http.Handler, authFailed http.Handler, provider Provider) http
 			}
 
 			// Some error that isn't because the session is expired or doesn't exist (DB?)
-			level.Error(log.Get(r)).
-				Log("error", err, "msg", "can't authenticate user")
+			level.Error(log.Get(r)).Log("error", err, "msg", "can't authenticate user")
 
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -99,8 +97,7 @@ func Require(next http.Handler, authFailed http.Handler, provider Provider) http
 
 		user.CanWrite = true
 
-		level.Info(log.Get(r)).
-			Log("user", *user, "msg", "user authenticated")
+		level.Info(log.Get(r)).Log("user", *user, "msg", "user authenticated")
 
 		ctx := context.WithValue(r.Context(), key, *user)
 
diff --git a/auth/auth_test.go b/auth/auth_test.go
index 1bef78a..a3c6ff5 100644
--- a/auth/auth_test.go
+++ b/auth/auth_test.go
@@ -19,6 +19,7 @@ func (a authProvider) Valid(ctx context.Context, token string) (*User, error) {
 	return nil, ErrAuthFailed
 }
 
+//nolint:funlen
 func TestRequire(t *testing.T) {
 	var (
 		ap     authProvider
diff --git a/handler-details.go b/handler-details.go
index 0ce2b30..a22c22f 100644
--- a/handler-details.go
+++ b/handler-details.go
@@ -62,31 +62,44 @@ const (
 
 const maxCommentLen = 1024
 
-func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) {
-	action := r.FormValue("action")
-	if strings.HasPrefix(action, "delete-comment-") {
-		level.Debug(log.Get(r)).
-			Log("action", action, "msg", "handling comment action")
+func (h Handler) deleteComment(w http.ResponseWriter, r *http.Request, action string) {
+	level.Debug(log.Get(r)).
+		Log("action", action, "msg", "handling comment action")
 
-		parts := strings.Split(action, "-")
+	parts := strings.Split(action, "-")
 
-		commentID, err := strconv.Atoi(parts[len(parts)-1])
-		if err != nil {
-			httpError(w, r, "bad request", err, http.StatusBadRequest)
-			return
-		}
+	commentID, err := strconv.Atoi(parts[len(parts)-1])
+	if err != nil {
+		httpError(w, r, "bad request", err, http.StatusBadRequest)
+		return
+	}
 
-		err = h.Q.DeleteComment(r.Context(), int32(commentID))
-		if err != nil {
-			httpError(w, r, "internal server error", err, http.StatusInternalServerError)
-			return
-		}
+	err = h.Q.DeleteComment(r.Context(), int32(commentID))
+	if err != nil {
+		httpError(w, r, "internal server error", err, http.StatusInternalServerError)
+		return
+	}
+
+	level.Info(log.Get(r)).
+		Log("id", commentID, "msg", "deleted comment")
+
+	http.Redirect(w, r, "/details?id="+r.FormValue("id"), http.StatusSeeOther)
+}
 
-		level.Info(log.Get(r)).
-			Log("id", commentID, "msg", "deleted comment")
+func (h Handler) deleteEntry(w http.ResponseWriter, r *http.Request, id int) {
+	err := h.Q.DeleteWine(r.Context(), int32(id))
+	if err != nil {
+		httpError(w, r, "can't delete wine", err, http.StatusInternalServerError)
+		return
+	}
 
-		http.Redirect(w, r, "/details?id="+r.FormValue("id"), http.StatusSeeOther)
+	http.Redirect(w, r, "/", http.StatusSeeOther)
+}
 
+func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) {
+	action := r.FormValue("action")
+	if strings.HasPrefix(action, "delete-comment-") {
+		h.deleteComment(w, r, action)
 		return
 	}
 
@@ -106,14 +119,7 @@ func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if action == actionDelete {
-		err = h.Q.DeleteWine(r.Context(), int32(id))
-		if err != nil {
-			httpError(w, r, "can't delete wine", err, http.StatusInternalServerError)
-			return
-		}
-
-		http.Redirect(w, r, "/", http.StatusSeeOther)
-
+		h.deleteEntry(w, r, id)
 		return
 	}
 
@@ -155,8 +161,7 @@ func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) {
 				Comment: comment,
 			})
 			if err != nil {
-				httpError(w, r, "can't add comment", err, http.StatusInternalServerError)
-				return err
+				return fmt.Errorf("adding comment: %w", err)
 			}
 		}
 
@@ -164,15 +169,13 @@ func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) {
 		if len(r.MultipartForm.File["picture"]) != 0 {
 			picFile, picHdr, err := r.FormFile("picture")
 			if err != nil {
-				httpError(w, r, "can't open picture file", err, http.StatusInternalServerError)
-				return err
+				return fmt.Errorf("opening picture: %w", err)
 			}
 			defer picFile.Close()
 
 			err = storage.AddPicture(ctx, q, id, picFile, picHdr.Header.Get("Content-Type"))
 			if err != nil {
-				httpError(w, r, "can't store picture", err, http.StatusBadRequest)
-				return err
+				return fmt.Errorf("storing picture: %w", err)
 			}
 		}
 
@@ -190,21 +193,19 @@ func (h Handler) detailsPost(w http.ResponseWriter, r *http.Request) {
 			},
 		}
 
-		level.Debug(log.Get(r)).Log("update", fmt.Sprint(params))
-
 		err = q.UpdateWine(ctx, params)
 		if err != nil {
-			httpError(w, r, "can't store update", err, http.StatusInternalServerError)
-			return err
+			return fmt.Errorf("updating wine: %w", err)
 		}
 
 		return nil
 	})
 	if err != nil {
+		httpError(w, r, "can't store update", err, http.StatusInternalServerError)
 		return
 	}
 
-	http.Redirect(w, r, "/details?id="+r.FormValue("id"), http.StatusSeeOther)
+	http.Redirect(w, r, "/details?id="+strconv.Itoa(id), http.StatusSeeOther)
 }
 
 func (h Handler) details() http.Handler {
@@ -249,27 +250,24 @@ func (h Handler) details() http.Handler {
 		err = h.Q.RunTx(r.Context(), func(ctx context.Context, q *query.Queries) error {
 			data.Wine, err = q.LoadWine(ctx, int32(id))
 			if err != nil {
-				httpError(w, r, "can't load wine data", err, http.StatusInternalServerError)
-				return err
+				return fmt.Errorf("loading wine data: %w", err)
 			}
 
 			data.Comments, err = q.ListComments(ctx, int32(id))
 			if err != nil {
-				httpError(w, r, "can't load comments", err, http.StatusInternalServerError)
-				return err
+				return fmt.Errorf("loading comments: %w", err)
 			}
 
 			return nil
 		})
 		if err != nil {
+			httpError(w, r, "loading data failed", err, http.StatusInternalServerError)
 			return
 		}
 
 		err = tpl.ExecuteTemplate(w, "details.tpl", data)
 		if err != nil {
-			level.Error(log.Get(r)).
-				Log("error", err,
-					"msg", "can't execute details template")
+			level.Error(log.Get(r)).Log("error", err, "msg", "can't execute details template")
 
 			return
 		}
diff --git a/handler-index.go b/handler-index.go
index 99bdc27..519297b 100644
--- a/handler-index.go
+++ b/handler-index.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"database/sql"
 	"errors"
+	"fmt"
 	"html/template"
 	"net/http"
 	"strconv"
@@ -17,45 +18,44 @@ import (
 	"git.c3pb.de/gbe/invinoveritas/storage/query"
 )
 
+type indexTemplateData struct {
+	Wines []query.ListWinesRow
+	User  auth.User
+}
+
+func (h Handler) listWines(w http.ResponseWriter, r *http.Request, tpl *template.Template) {
+	wines, err := h.Q.ListWines(r.Context())
+	if err != nil {
+		httpError(w, r, "can't list wines", err, http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Add("content-type", "text/html")
+
+	data := indexTemplateData{
+		Wines: wines,
+		User:  auth.Get(r),
+	}
+
+	err = tpl.ExecuteTemplate(w, "index.tpl", data)
+	if err != nil {
+		level.Error(log.Get(r)).Log("error", err, "msg", "can't execute index template")
+	}
+}
+
 func (h Handler) index() http.Handler {
 	var (
 		once sync.Once
 		tpl  *template.Template
 	)
 
-	type templateData struct {
-		Wines []query.ListWinesRow
-		User  auth.User
-	}
-
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		once.Do(func() {
 			tpl = template.Must(template.ParseFS(templateFS, "templates/base.tpl", "templates/index.tpl"))
 		})
 
-		level.Debug(log.Get(r)).Log("user", auth.Get(r))
-
 		if r.Method == "GET" {
-			wines, err := h.Q.ListWines(r.Context())
-			if err != nil {
-				httpError(w, r, "can't list wines", err, http.StatusInternalServerError)
-				return
-			}
-
-			w.Header().Add("content-type", "text/html")
-
-			data := templateData{
-				Wines: wines,
-				User:  auth.Get(r),
-			}
-
-			err = tpl.ExecuteTemplate(w, "index.tpl", data)
-			if err != nil {
-				level.Error(log.Get(r)).
-					Log("error", err,
-						"msg", "can't execute index template")
-			}
-
+			h.listWines(w, r, tpl)
 			return
 		}
 
@@ -104,20 +104,14 @@ func (h Handler) index() http.Handler {
 
 			res, err := q.InsertWine(ctx, params)
 			if err != nil {
-				httpError(w, r, "can't store wine", err, http.StatusInternalServerError)
-
-				return err
+				return fmt.Errorf("storing wine: %w", err)
 			}
 
 			id, err = res.LastInsertId()
 			if err != nil {
-				httpError(w, r, "can't store wine", err, http.StatusInternalServerError)
-
-				return err
+				return fmt.Errorf("storing wine: %w", err)
 			}
 
-			level.Debug(log.Get(r)).Log("msg", "inserted new wine", "id", id)
-
 			// See if there's an image file uploaded and persist it if that's the case.
 			if r.MultipartForm == nil || len(r.MultipartForm.File["picture"]) == 0 {
 				return nil
@@ -125,20 +119,19 @@ func (h Handler) index() http.Handler {
 
 			picFile, picHdr, err := r.FormFile("picture")
 			if err != nil {
-				httpError(w, r, "can't open picture file", err, http.StatusInternalServerError)
-				return err
+				return fmt.Errorf("loading picture data: %w", err)
 			}
 			defer picFile.Close()
 
 			err = storage.AddPicture(ctx, q, int(id), picFile, picHdr.Header.Get("Content-Type"))
 			if err != nil {
-				httpError(w, r, "can't load picture", err, http.StatusBadRequest)
-				return err
+				return fmt.Errorf("storing picture: %w", err)
 			}
 
 			return nil
 		})
 		if err != nil {
+			httpError(w, r, "updating data failed", err, http.StatusInternalServerError)
 			return
 		}
 
diff --git a/handler-user.go b/handler-user.go
index a0442e3..1879b19 100644
--- a/handler-user.go
+++ b/handler-user.go
@@ -10,6 +10,7 @@ import (
 	"git.c3pb.de/gbe/invinoveritas/auth"
 	"git.c3pb.de/gbe/invinoveritas/log"
 	"git.c3pb.de/gbe/invinoveritas/session"
+	"github.com/go-kit/kit/log/level"
 )
 
 const maxUserNameLen = 80
@@ -205,6 +206,7 @@ func (h Handler) user(page pageName) http.Handler {
 
 		err = h.up.UpdatePassword(r.Context(), user.Name, r.PostForm.Get("old-pw"), r.PostForm.Get("new-pw"), false)
 		if err != nil {
+			level.Error(log.GetContext(r.Context())).Log("msg", "user update failed", "error", err)
 			data.FormErrors["OldPW"] = err
 			return
 		}
diff --git a/session/authprovider.go b/session/authprovider.go
index 9fe93a1..ee03304 100644
--- a/session/authprovider.go
+++ b/session/authprovider.go
@@ -128,6 +128,7 @@ func (a Provider) Valid(ctx context.Context, token string) (*auth.User, error) {
 
 // Handler returns an HTTP handler that can be used to ask a user to authenticate themselves. It loads templates
 // from the given FS.
+//nolint:funlen
 func (a Provider) Handler(templateFS fs.FS) http.Handler {
 	var (
 		once sync.Once
@@ -138,16 +139,30 @@ func (a Provider) Handler(templateFS fs.FS) http.Handler {
 		Error string
 	}
 
+	respondUnauthorized := func(w http.ResponseWriter) error {
+		w.WriteHeader(http.StatusUnauthorized)
+
+		rd := responseData{
+			Error: "authentication failed",
+		}
+
+		err := tpl.ExecuteTemplate(w, "auth.tpl", rd)
+		if err != nil {
+			return fmt.Errorf("executing auth template: %w", err)
+		}
+
+		return auth.ErrAuthFailed
+	}
+
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		once.Do(func() {
 			tpl = template.Must(template.ParseFS(templateFS, "templates/base.tpl", "templates/auth.tpl"))
 		})
 
-		if r.Method == "GET" {
+		if r.Method == http.MethodGet {
 			err := tpl.ExecuteTemplate(w, "auth.tpl", nil)
 			if err != nil {
-				level.Error(log.Get(r)).
-					Log("error", err, "msg", "can't execute auth template")
+				level.Error(log.Get(r)).Log("error", err, "msg", "can't execute auth template")
 			}
 
 			return
@@ -159,38 +174,15 @@ func (a Provider) Handler(templateFS fs.FS) http.Handler {
 
 		log := kitlog.With(log.Get(r), "user", userName)
 
-		level.Debug(log).
-			Log("pass", password, "debug", "attempting auth")
-
 		var token string
 
 		err := a.q.RunTx(r.Context(), func(ctx context.Context, q *query.Queries) error {
 			userData, err := q.GetAuthData(ctx, userName)
 			if errors.Is(err, sql.ErrNoRows) {
-				// Either no such user or no users at all
-				count, err := q.CountUsers(ctx)
-				if err != nil {
-					http.Error(w, err.Error(), http.StatusInternalServerError)
-					return fmt.Errorf("getting user count: %w", err)
-				}
-
-				level.Debug(log).
-					Log("num_users", count, "msg", "got user count")
-
-				w.WriteHeader(http.StatusUnauthorized)
-
-				rd := responseData{
-					Error: "authentication failed",
-				}
-
-				err = tpl.ExecuteTemplate(w, "auth.tpl", rd)
-				if err != nil {
-					return fmt.Errorf("executing auth template: %w", err)
-				}
-
-				return auth.ErrAuthFailed
+				return respondUnauthorized(w)
 			}
 			if err != nil {
+				http.Error(w, err.Error(), http.StatusInternalServerError)
 				return fmt.Errorf("getting salted password: %w", err)
 			}
 
@@ -201,29 +193,13 @@ func (a Provider) Handler(templateFS fs.FS) http.Handler {
 			}
 
 			if hashed != userData.Password {
-				level.Error(log).
-					Log("msg", "password mismatch")
-
-				w.WriteHeader(http.StatusUnauthorized)
-
-				rd := responseData{
-					Error: "authentication failed",
-				}
+				level.Error(log).Log("msg", "password mismatch")
 
-				err := tpl.ExecuteTemplate(w, "auth.tpl", rd)
-				if err != nil {
-					return fmt.Errorf("executing auth template: %w", err)
-				}
-
-				return auth.ErrAuthFailed
+				return respondUnauthorized(w)
 			}
 
 			log = kitlog.With(log, "user_id", userData.UserID)
 
-			// All good
-			level.Info(log).
-				Log("msg", "auth looks good")
-
 			addr := r.Header.Get("X-Forwarded-For")
 			if addr == "" {
 				addr = r.RemoteAddr
@@ -251,9 +227,6 @@ func (a Provider) Handler(templateFS fs.FS) http.Handler {
 			HttpOnly: true, // Might need to be unset if we do a REST-style API
 		}
 
-		level.Debug(log).
-			Log("cookie", fmt.Sprintf("%#v", cookie), "msg", "setting token cookie")
-
 		http.SetCookie(w, &cookie)
 
 		// Redirect to initially requested URL
@@ -321,16 +294,13 @@ func (a Provider) ListUsers(ctx context.Context) ([]string, error) {
 }
 
 func (a Provider) UpdatePassword(ctx context.Context, userName, passOld, passNew string, createNew bool) error {
-	err := a.q.RunTx(ctx, func(ctx context.Context, q *query.Queries) error {
+	return a.q.RunTx(ctx, func(ctx context.Context, q *query.Queries) error {
 		hashedNew, err := hashPassword(ctx, q, userName, passNew)
 		if err != nil {
 			return err
 		}
 
 		if createNew {
-			level.Info(log.GetContext(ctx)).
-				Log("name", userName, "msg", "creating new user")
-
 			err = q.InsertUser(ctx, query.InsertUserParams{
 				Name:     userName,
 				Password: hashedNew,
@@ -383,16 +353,11 @@ func (a Provider) UpdatePassword(ctx context.Context, userName, passOld, passNew
 
 		return nil
 	})
-	if err != nil {
-		level.Error(log.GetContext(ctx)).
-			Log("msg", "user update failed", "error", err)
-		return err
-	}
-
-	return nil
 }
 
 func (a Provider) CreateUser(ctx context.Context, userName string) (string, error) {
+	level.Info(log.GetContext(ctx)).Log("name", userName, "msg", "creating new user")
+
 	// generate random password
 	const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 
diff --git a/storage/storage.go b/storage/storage.go
index 3d8451c..5faa733 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -76,44 +76,25 @@ func AddPicture(ctx context.Context, q *query.Queries, wineID int, fh io.Reader,
 //go:embed migrations/*.sql
 var migrationFS embed.FS //nolint:gochecknoglobals
 
-func Open(ctx context.Context, dbPath, dumpPath string, logger kitlog.Logger) (*sql.DB, error) {
-	db, err := sql.Open("sqlite", dbPath)
-	if err != nil {
-		return nil, err
-	}
-
-	level.Debug(logger).Log("db_path", dbPath)
-
-	pragmas := []string{
-		"PRAGMA busy_timeout = 600000",
-		"PRAGMA journal_mode = WAL",
-	}
-
-	for _, p := range pragmas {
-		_, err = db.ExecContext(ctx, p)
-		if err != nil {
-			return nil, fmt.Errorf("running %q: %w", p, err)
-		}
-	}
-
+func migrateDB(ctx context.Context, db *sql.DB) error {
 	// Make sure that we have a table for the migrations, the rest runs in a transaction.
-	_, err = db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS migrations (name text, unique(name))`)
+	_, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS migrations (name text, unique(name))`)
 	if err != nil {
-		return nil, fmt.Errorf("creating migrations table: %w", err)
+		return fmt.Errorf("creating migrations table: %w", err)
 	}
 
 	q := query.New(db)
 
 	entries, err := migrationFS.ReadDir("migrations")
 	if err != nil {
-		return nil, fmt.Errorf("reading embedded migration FS: %w", err)
+		return fmt.Errorf("reading embedded migration FS: %w", err)
 	}
 
 	sort.Slice(entries, func(i, j int) bool {
 		return entries[i].Name() < entries[j].Name()
 	})
 
-	err = q.RunTx(ctx, func(ctx context.Context, q *query.Queries) error {
+	return q.RunTx(ctx, func(ctx context.Context, q *query.Queries) error {
 		for _, e := range entries {
 			// Check if we need to apply that migration
 			count, err := q.CountMigrations(ctx, e.Name())
@@ -148,8 +129,31 @@ func Open(ctx context.Context, dbPath, dumpPath string, logger kitlog.Logger) (*
 
 		return nil
 	})
+}
+
+func Open(ctx context.Context, dbPath, dumpPath string, logger kitlog.Logger) (*sql.DB, error) {
+	db, err := sql.Open("sqlite", dbPath)
+	if err != nil {
+		return nil, err
+	}
+
+	level.Debug(logger).Log("db_path", dbPath)
+
+	pragmas := []string{
+		"PRAGMA busy_timeout = 600000",
+		"PRAGMA journal_mode = WAL",
+	}
+
+	for _, p := range pragmas {
+		_, err = db.ExecContext(ctx, p)
+		if err != nil {
+			return nil, fmt.Errorf("running %q: %w", p, err)
+		}
+	}
+
+	err = migrateDB(ctx, db)
 	if err != nil {
-		return nil, fmt.Errorf("applying migrations: %w", err)
+		return nil, fmt.Errorf("migrating db: %w", err)
 	}
 
 	return db, nil
-- 
GitLab