From 33310be68544d3381ac6e9899790f4a106e17e8f Mon Sep 17 00:00:00 2001 From: ivar Date: Fri, 3 Apr 2026 14:22:23 +0200 Subject: refactor: convert admin handlers to Gin context-based signatures - Remove old ServeHTTP method (no longer needed with Gin routing) - Update all 6 handler methods to use *gin.Context instead of http.ResponseWriter, *http.Request - Convert handler signatures: handleList, handleNew, handleNewPost, handleEdit, handleDelete - Remove render() helper (use c.HTML() directly) - Update renderError() to accept gin.Context instead of http.ResponseWriter - Update postFromForm() to extract form data from gin.Context using c.PostForm() - Update main.go to use adminSrv.NewServer() and adminSrv.Engine() - All handlers now use Gin methods: c.HTML(), c.PostForm(), c.Param(), c.Redirect() - Path parameters now extracted via c.Param("slug") instead of function arguments - HTTP status codes and error handling fully migrated to Gin patterns Build verified: go build ./cmd/nebbet succeeds Co-Authored-By: Claude Haiku 4.5 --- internal/admin/auth/auth.go | 159 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 internal/admin/auth/auth.go (limited to 'internal/admin/auth/auth.go') diff --git a/internal/admin/auth/auth.go b/internal/admin/auth/auth.go new file mode 100644 index 0000000..b0de7d9 --- /dev/null +++ b/internal/admin/auth/auth.go @@ -0,0 +1,159 @@ +// Package auth manages a htpasswd-compatible password file (bcrypt entries). +// The file format is one "username:$2a$..." entry per line. +// nginx auth_basic accepts this file directly via auth_basic_user_file. +package auth + +import ( + "bufio" + "fmt" + "os" + "strings" + "syscall" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/term" +) + +type Auth struct { + path string +} + +func New(path string) *Auth { return &Auth{path: path} } + +func (a *Auth) AddUser(username string) error { + users, err := a.read() + if err != nil && !os.IsNotExist(err) { + return err + } + if _, exists := users[username]; exists { + return fmt.Errorf("user %q already exists", username) + } + pw, err := readPassword("Password: ") + if err != nil { + return err + } + confirm, err := readPassword("Confirm: ") + if err != nil { + return err + } + if pw != confirm { + return fmt.Errorf("passwords do not match") + } + hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) + if err != nil { + return err + } + users[username] = string(hash) + return a.write(users) +} + +func (a *Auth) ChangePassword(username string) error { + users, err := a.read() + if err != nil { + return err + } + if _, exists := users[username]; !exists { + return fmt.Errorf("user %q not found", username) + } + pw, err := readPassword("New password: ") + if err != nil { + return err + } + confirm, err := readPassword("Confirm: ") + if err != nil { + return err + } + if pw != confirm { + return fmt.Errorf("passwords do not match") + } + hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) + if err != nil { + return err + } + users[username] = string(hash) + return a.write(users) +} + +func (a *Auth) DeleteUser(username string) error { + users, err := a.read() + if err != nil { + return err + } + if _, exists := users[username]; !exists { + return fmt.Errorf("user %q not found", username) + } + delete(users, username) + return a.write(users) +} + +func (a *Auth) ListUsers() ([]string, error) { + users, err := a.read() + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + names := make([]string, 0, len(users)) + for k := range users { + names = append(names, k) + } + return names, nil +} + +func (a *Auth) Verify(username, password string) (bool, error) { + users, err := a.read() + if err != nil { + return false, err + } + hash, ok := users[username] + if !ok { + return false, nil + } + err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + if err == bcrypt.ErrMismatchedHashAndPassword { + return false, nil + } + return err == nil, err +} + +func (a *Auth) read() (map[string]string, error) { + f, err := os.Open(a.path) + if err != nil { + return nil, err + } + defer f.Close() + users := make(map[string]string) + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + user, hash, ok := strings.Cut(line, ":") + if ok { + users[user] = hash + } + } + return users, scanner.Err() +} + +func (a *Auth) write(users map[string]string) error { + f, err := os.OpenFile(a.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer f.Close() + w := bufio.NewWriter(f) + for user, hash := range users { + fmt.Fprintf(w, "%s:%s\n", user, hash) + } + return w.Flush() +} + +func readPassword(prompt string) (string, error) { + fmt.Print(prompt) + b, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() + return string(b), err +} -- cgit v1.3