package web import ( "encoding/json" "io" "net/http" "github.com/go-chi/chi/v5" "github.com/darroyo/nasctl/internal/db" "github.com/darroyo/nasctl/internal/modules/users" "github.com/darroyo/nasctl/internal/system" ) type userRequest struct { Username string `json:"username"` Groups []string `json:"groups"` SMBEnabled bool `json:"smb_enabled"` Disabled bool `json:"disabled"` // Password is write-only; it is never returned or logged. Password string `json:"password"` } func (req userRequest) validate() error { if err := system.ValidateUsername(req.Username); err != nil { return err } for _, group := range req.Groups { if err := system.ValidateUsername(group); err != nil { return err } } return nil } func (req userRequest) toModel() db.User { return db.User{ Username: req.Username, Groups: req.Groups, SMBEnabled: req.SMBEnabled, Disabled: req.Disabled, PendingPassword: req.Password, } } func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { list, err := s.DB.ListUsers() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"users": list}) } func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } user, err := s.DB.GetUser(id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, user) } func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { req, err := decodeUserRequest(r.Body) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := req.validate(); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } user, err := s.DB.CreateUser(req.toModel()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if err := s.DB.MarkDirty(users.ModuleName); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusCreated, user) } func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } req, err := decodeUserRequest(r.Body) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := req.validate(); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } existing, err := s.DB.GetUser(id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } model := req.toModel() // Preserve a previously stored pending password if none is supplied now. if model.PendingPassword == "" { model.PendingPassword = existing.PendingPassword } user, err := s.DB.UpdateUser(id, model) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } if err := s.DB.MarkDirty(users.ModuleName); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, user) } func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } if err := s.DB.DeleteUser(id); err != nil { writeError(w, http.StatusNotFound, err.Error()) return } if err := s.DB.MarkDirty(users.ModuleName); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.WriteHeader(http.StatusNoContent) } func decodeUserRequest(body io.ReadCloser) (userRequest, error) { defer body.Close() var req userRequest if err := json.NewDecoder(body).Decode(&req); err != nil { return userRequest{}, err } return req, nil }