diff options
| author | ivar <i@oiee.no> | 2026-04-03 14:22:23 +0200 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2026-04-03 14:22:23 +0200 |
| commit | 33310be68544d3381ac6e9899790f4a106e17e8f (patch) | |
| tree | 385102a7216ffba17d054f11032dae4447371d52 /internal/admin/auth/auth.go | |
| parent | a8914a8f18c345e934bce93b37845a9dfe0ad73e (diff) | |
| download | nebbet.no-33310be68544d3381ac6e9899790f4a106e17e8f.tar.xz nebbet.no-33310be68544d3381ac6e9899790f4a106e17e8f.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'internal/admin/auth/auth.go')
| -rw-r--r-- | internal/admin/auth/auth.go | 159 |
1 files changed, 159 insertions, 0 deletions
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 +} |
