- Added a new AGENTS.md file to document the project structure and conventions. - Updated .gitignore to include node_modules and refined cursor rules. - Introduced new backend and frontend components for improved map interactions, including context menus and controls. - Enhanced API composables for better admin and authentication functionalities. - Refactored existing components for cleaner code and improved user experience. - Updated README.md to clarify production asset serving and user setup instructions.
217 lines
5.3 KiB
Go
217 lines
5.3 KiB
Go
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
|
|
})
|
|
}
|