package auth import ( "encoding/json" "errors" "net/http" "os" "path/filepath" "sync" "github.com/boltdb/bolt" ) // LogFunc is a function type for logging. type LogFunc func(format string, v ...interface{}) // Options holds configuration for AuthManager. type Options struct { DBPath string Log LogFunc } // AuthManager handles authentication. type AuthManager struct { db *bolt.DB log LogFunc mu sync.RWMutex } const ( authBucket = "auth" passKey = "password" ) // NewAuthManager creates a new AuthManager. func NewAuthManager(opt Options) (*AuthManager, error) { dir := filepath.Dir(opt.DBPath) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0755); err != nil { return nil, err } } db, err := bolt.Open(opt.DBPath, 0755, nil) if err != nil { return nil, err } // Ensure bucket exists err = db.Update(func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte(authBucket)) return err }) if err != nil { db.Close() return nil, err } return &AuthManager{db: db, log: opt.Log}, nil } // SetPassword sets the password (overwrites any existing). func (a *AuthManager) SetPassword(password string) error { a.mu.Lock() defer a.mu.Unlock() return a.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(authBucket)) return b.Put([]byte(passKey), []byte(password)) }) } // ChangePassword changes password if oldpassword matches. func (a *AuthManager) ChangePassword(oldPassword, newPassword string) error { a.mu.Lock() defer a.mu.Unlock() return a.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(authBucket)) stored := b.Get([]byte(passKey)) if stored == nil || string(stored) != oldPassword { return errors.New("old password incorrect") } return b.Put([]byte(passKey), []byte(newPassword)) }) } // ResetPassword removes the password. func (a *AuthManager) ResetPassword() error { a.mu.Lock() defer a.mu.Unlock() return a.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(authBucket)) return b.Delete([]byte(passKey)) }) } // ValidatePassword checks password from request func (a *AuthManager) ValidatePassword(password string) (bool, error) { a.mu.RLock() defer a.mu.RUnlock() var ok bool err := a.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(authBucket)) stored := b.Get([]byte(passKey)) ok = stored != nil && string(stored) == password return nil }) return ok, err } // UserIsLoggedIn checks if the user is logged in via cookie. func (a *AuthManager) UserIsLoggedIn(r *http.Request) bool { cookie, err := r.Cookie("dezukvm_auth") return err == nil && cookie.Value == "1" } // HandleFunc wraps an http.HandlerFunc with auth check. func (a *AuthManager) HandleFunc(pattern string, handler http.HandlerFunc, mux *http.ServeMux) { mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { if a.UserIsLoggedIn(r) { handler(w, r) return } w.WriteHeader(http.StatusUnauthorized) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) }) } // LoginUser sets a session/cookie if password is correct func (a *AuthManager) LoginUser(w http.ResponseWriter, r *http.Request) error { var req struct { Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return err } a.mu.RLock() defer a.mu.RUnlock() var ok bool err := a.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(authBucket)) stored := b.Get([]byte(passKey)) ok = stored != nil && string(stored) == req.Password return nil }) if err != nil { return err } if !ok { return errors.New("unauthorized") } // Set a simple session cookie http.SetCookie(w, &http.Cookie{ Name: "dezukvm_auth", Value: "1", Path: "/", HttpOnly: true, MaxAge: 86400, }) return nil } // LogoutUser removes the session/cookie for the user. func (a *AuthManager) LogoutUser(w http.ResponseWriter, r *http.Request) error { http.SetCookie(w, &http.Cookie{ Name: "dezukvm_auth", Value: "", Path: "/", HttpOnly: true, MaxAge: -1, }) return nil } // Close closes the underlying DB. func (a *AuthManager) Close() error { return a.db.Close() }