package services import ( "encoding/json" "strconv" "github.com/andyleap/hnh-map/internal/app" "github.com/andyleap/hnh-map/internal/app/store" "go.etcd.io/bbolt" "golang.org/x/crypto/bcrypt" ) // AdminService handles admin business logic (users, settings, maps, wipe). type AdminService struct { st *store.Store } // NewAdminService creates an AdminService. func NewAdminService(st *store.Store) *AdminService { return &AdminService{st: st} } // ListUsers returns all usernames. func (s *AdminService) ListUsers() ([]string, error) { var list []string err := s.st.View(func(tx *bbolt.Tx) error { return s.st.ForEachUser(tx, func(k, _ []byte) error { list = append(list, string(k)) return nil }) }) return list, err } // GetUser returns user auths by username. func (s *AdminService) GetUser(username string) (auths app.Auths, found bool) { s.st.View(func(tx *bbolt.Tx) error { raw := s.st.GetUser(tx, username) if raw == nil { return nil } var u app.User json.Unmarshal(raw, &u) auths = u.Auths found = true return nil }) return auths, found } // CreateOrUpdateUser creates or updates a user. // Returns (true, nil) when admin user was created and didn't exist before (temp admin bootstrap). func (s *AdminService) CreateOrUpdateUser(username string, pass string, auths app.Auths) (adminCreated bool, err error) { err = s.st.Update(func(tx *bbolt.Tx) error { existed := s.st.GetUser(tx, username) != nil u := app.User{} raw := s.st.GetUser(tx, username) if raw != nil { json.Unmarshal(raw, &u) } if pass != "" { hash, e := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) if e != nil { return e } u.Pass = hash } u.Auths = auths raw, _ = json.Marshal(u) if e := s.st.PutUser(tx, username, raw); e != nil { return e } if username == "admin" && !existed { adminCreated = true } return nil }) return adminCreated, err } // DeleteUser removes a user and their tokens. func (s *AdminService) DeleteUser(username string) error { return s.st.Update(func(tx *bbolt.Tx) error { uRaw := s.st.GetUser(tx, username) if uRaw != nil { var u app.User json.Unmarshal(uRaw, &u) for _, tok := range u.Tokens { s.st.DeleteToken(tx, tok) } } return s.st.DeleteUser(tx, username) }) } // GetSettings returns prefix, defaultHide, title. func (s *AdminService) GetSettings() (prefix string, defaultHide bool, title string, err error) { err = s.st.View(func(tx *bbolt.Tx) error { if v := s.st.GetConfig(tx, "prefix"); v != nil { prefix = string(v) } if v := s.st.GetConfig(tx, "defaultHide"); v != nil { defaultHide = true } if v := s.st.GetConfig(tx, "title"); v != nil { title = string(v) } return nil }) return prefix, defaultHide, title, err } // UpdateSettings updates config keys. func (s *AdminService) UpdateSettings(prefix *string, defaultHide *bool, title *string) error { return s.st.Update(func(tx *bbolt.Tx) error { if prefix != nil { s.st.PutConfig(tx, "prefix", []byte(*prefix)) } if defaultHide != nil { if *defaultHide { s.st.PutConfig(tx, "defaultHide", []byte("1")) } else { s.st.DeleteConfig(tx, "defaultHide") } } if title != nil { s.st.PutConfig(tx, "title", []byte(*title)) } return nil }) } // ListMaps returns all maps. func (s *AdminService) ListMaps() ([]app.MapInfo, error) { var maps []app.MapInfo err := s.st.View(func(tx *bbolt.Tx) error { return s.st.ForEachMap(tx, func(k, v []byte) error { mi := app.MapInfo{} json.Unmarshal(v, &mi) if id, err := strconv.Atoi(string(k)); err == nil { mi.ID = id } maps = append(maps, mi) return nil }) }) return maps, err } // GetMap returns a map by ID. func (s *AdminService) GetMap(id int) (*app.MapInfo, bool) { var mi *app.MapInfo s.st.View(func(tx *bbolt.Tx) error { raw := s.st.GetMap(tx, id) if raw != nil { mi = &app.MapInfo{} json.Unmarshal(raw, mi) mi.ID = id } return nil }) return mi, mi != nil } // UpdateMap updates map name, hidden, priority. func (s *AdminService) UpdateMap(id int, name string, hidden, priority bool) error { return s.st.Update(func(tx *bbolt.Tx) error { mi := app.MapInfo{} raw := s.st.GetMap(tx, id) if raw != nil { json.Unmarshal(raw, &mi) } mi.ID = id mi.Name = name mi.Hidden = hidden mi.Priority = priority raw, _ = json.Marshal(mi) return s.st.PutMap(tx, id, raw) }) } // ToggleMapHidden flips the hidden flag. func (s *AdminService) ToggleMapHidden(id int) (*app.MapInfo, error) { var mi *app.MapInfo err := s.st.Update(func(tx *bbolt.Tx) error { raw := s.st.GetMap(tx, id) mi = &app.MapInfo{} if raw != nil { json.Unmarshal(raw, mi) } mi.ID = id mi.Hidden = !mi.Hidden raw, _ = json.Marshal(mi) return s.st.PutMap(tx, id, raw) }) return mi, err } // Wipe deletes grids, markers, tiles, maps buckets. func (s *AdminService) Wipe() error { return s.st.Update(func(tx *bbolt.Tx) error { for _, b := range [][]byte{ store.BucketGrids, store.BucketMarkers, store.BucketTiles, store.BucketMaps, } { if s.st.BucketExists(tx, b) { if err := s.st.DeleteBucket(tx, b); err != nil { return err } } } return nil }) }