Update project structure and enhance frontend functionality
- 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.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -35,11 +36,11 @@ func (a *App) export(rw http.ResponseWriter, req *http.Request) {
|
||||
maps := map[int]mapData{}
|
||||
gridMap := map[string]int{}
|
||||
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
grids := tx.Bucket(store.BucketGrids)
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
tiles := tx.Bucket([]byte("tiles"))
|
||||
tiles := tx.Bucket(store.BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -94,11 +95,11 @@ func (a *App) export(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
markersb := tx.Bucket([]byte("markers"))
|
||||
markersb := tx.Bucket(store.BucketMarkers)
|
||||
if markersb == nil {
|
||||
return nil
|
||||
}
|
||||
markersgrid := markersb.Bucket([]byte("grid"))
|
||||
markersgrid := markersb.Bucket(store.BucketMarkersGrid)
|
||||
if markersgrid == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,24 +6,25 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func (a *App) hideMarker(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *App) HideMarker(rw http.ResponseWriter, req *http.Request) {
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
mb, err := tx.CreateBucketIfNotExists([]byte("markers"))
|
||||
mb, err := tx.CreateBucketIfNotExists(store.BucketMarkers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grid, err := mb.CreateBucketIfNotExists([]byte("grid"))
|
||||
grid, err := mb.CreateBucketIfNotExists(store.BucketMarkersGrid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idB, err := mb.CreateBucketIfNotExists([]byte("id"))
|
||||
idB, err := mb.CreateBucketIfNotExists(store.BucketMarkersID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -48,27 +49,27 @@ func (a *App) merge(rw http.ResponseWriter, req *http.Request) {
|
||||
newTiles := map[string]struct{}{}
|
||||
|
||||
err = a.db.Update(func(tx *bbolt.Tx) error {
|
||||
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
grids, err := tx.CreateBucketIfNotExists(store.BucketGrids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
tiles, err := tx.CreateBucketIfNotExists(store.BucketTiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := tx.CreateBucketIfNotExists([]byte("markers"))
|
||||
mb, err := tx.CreateBucketIfNotExists(store.BucketMarkers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mgrid, err := mb.CreateBucketIfNotExists([]byte("grid"))
|
||||
mgrid, err := mb.CreateBucketIfNotExists(store.BucketMarkersGrid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idB, err := mb.CreateBucketIfNotExists([]byte("id"))
|
||||
idB, err := mb.CreateBucketIfNotExists(store.BucketMarkersID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configb, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
configb, err := tx.CreateBucketIfNotExists(store.BucketConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -114,7 +115,7 @@ func (a *App) merge(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
mapB, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
mapB, err := tx.CreateBucketIfNotExists(store.BucketMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -19,7 +20,7 @@ func (a *App) doRebuildZooms() {
|
||||
saveGrid := map[zoomproc]string{}
|
||||
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("grids"))
|
||||
b := tx.Bucket(store.BucketGrids)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -30,7 +31,7 @@ func (a *App) doRebuildZooms() {
|
||||
saveGrid[zoomproc{grid.Coord, grid.Map}] = grid.ID
|
||||
return nil
|
||||
})
|
||||
tx.DeleteBucket([]byte("tiles"))
|
||||
tx.DeleteBucket(store.BucketTiles)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func (a *App) wipeTile(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *App) WipeTile(rw http.ResponseWriter, req *http.Request) {
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
@@ -37,7 +38,7 @@ func (a *App) wipeTile(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
grids := tx.Bucket(store.BucketGrids)
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -71,7 +72,7 @@ func (a *App) wipeTile(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(200)
|
||||
}
|
||||
|
||||
func (a *App) setCoords(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *App) SetCoords(rw http.ResponseWriter, req *http.Request) {
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
@@ -121,11 +122,11 @@ func (a *App) setCoords(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
tds := []*TileData{}
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
grids := tx.Bucket(store.BucketGrids)
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
tiles := tx.Bucket([]byte("tiles"))
|
||||
tiles := tx.Bucket(store.BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -52,7 +53,7 @@ func (a *App) apiLogin(rw http.ResponseWriter, req *http.Request) {
|
||||
if bootstrap != "" && body.Pass == bootstrap {
|
||||
var created bool
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
users, err := tx.CreateBucketIfNotExists(store.BucketUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,7 +136,7 @@ func (a *App) apiMe(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
out := meResponse{Username: s.Username, Auths: s.Auths}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
ub := tx.Bucket([]byte("users"))
|
||||
ub := tx.Bucket(store.BucketUsers)
|
||||
if ub != nil {
|
||||
uRaw := ub.Get([]byte(s.Username))
|
||||
if uRaw != nil {
|
||||
@@ -144,7 +145,7 @@ func (a *App) apiMe(rw http.ResponseWriter, req *http.Request) {
|
||||
out.Tokens = u.Tokens
|
||||
}
|
||||
}
|
||||
config := tx.Bucket([]byte("config"))
|
||||
config := tx.Bucket(store.BucketConfig)
|
||||
if config != nil {
|
||||
out.Prefix = string(config.Get([]byte("prefix")))
|
||||
}
|
||||
@@ -214,7 +215,7 @@ func (a *App) generateTokenForUser(username string) []string {
|
||||
token := hex.EncodeToString(tokenRaw)
|
||||
var tokens []string
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
ub, _ := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
ub, _ := tx.CreateBucketIfNotExists(store.BucketUsers)
|
||||
uRaw := ub.Get([]byte(username))
|
||||
u := User{}
|
||||
if uRaw != nil {
|
||||
@@ -224,7 +225,7 @@ func (a *App) generateTokenForUser(username string) []string {
|
||||
tokens = u.Tokens
|
||||
buf, _ := json.Marshal(u)
|
||||
ub.Put([]byte(username), buf)
|
||||
tb, _ := tx.CreateBucketIfNotExists([]byte("tokens"))
|
||||
tb, _ := tx.CreateBucketIfNotExists(store.BucketTokens)
|
||||
return tb.Put([]byte(token), []byte(username))
|
||||
})
|
||||
if err != nil {
|
||||
@@ -239,7 +240,7 @@ func (a *App) setUserPassword(username, pass string) error {
|
||||
return nil
|
||||
}
|
||||
return a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
users, err := tx.CreateBucketIfNotExists(store.BucketUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -266,7 +267,7 @@ func (a *App) apiAdminUsers(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
var list []string
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("users"))
|
||||
b := tx.Bucket(store.BucketUsers)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -293,7 +294,7 @@ func (a *App) apiAdminUserByName(rw http.ResponseWriter, req *http.Request, name
|
||||
}
|
||||
out.Username = name
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("users"))
|
||||
b := tx.Bucket(store.BucketUsers)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -331,7 +332,7 @@ func (a *App) apiAdminUserPost(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
tempAdmin := false
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
users, err := tx.CreateBucketIfNotExists(store.BucketUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -373,13 +374,13 @@ func (a *App) apiAdminUserDelete(rw http.ResponseWriter, req *http.Request, name
|
||||
return
|
||||
}
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, _ := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
users, _ := tx.CreateBucketIfNotExists(store.BucketUsers)
|
||||
u := User{}
|
||||
raw := users.Get([]byte(name))
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
}
|
||||
tokens, _ := tx.CreateBucketIfNotExists([]byte("tokens"))
|
||||
tokens, _ := tx.CreateBucketIfNotExists(store.BucketTokens)
|
||||
for _, tok := range u.Tokens {
|
||||
tokens.Delete([]byte(tok))
|
||||
}
|
||||
@@ -407,7 +408,7 @@ func (a *App) apiAdminSettingsGet(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
out := settingsResponse{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
c := tx.Bucket([]byte("config"))
|
||||
c := tx.Bucket(store.BucketConfig)
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -440,7 +441,7 @@ func (a *App) apiAdminSettingsPost(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
b, err := tx.CreateBucketIfNotExists(store.BucketConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -483,7 +484,7 @@ func (a *App) apiAdminMaps(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
var maps []mapInfoJSON
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
mapB := tx.Bucket([]byte("maps"))
|
||||
mapB := tx.Bucket(store.BucketMaps)
|
||||
if mapB == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -529,7 +530,7 @@ func (a *App) apiAdminMapByID(rw http.ResponseWriter, req *http.Request, idStr s
|
||||
return
|
||||
}
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
maps, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
maps, err := tx.CreateBucketIfNotExists(store.BucketMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -570,7 +571,7 @@ func (a *App) apiAdminMapToggleHidden(rw http.ResponseWriter, req *http.Request,
|
||||
}
|
||||
var mi MapInfo
|
||||
err = a.db.Update(func(tx *bbolt.Tx) error {
|
||||
maps, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
maps, err := tx.CreateBucketIfNotExists(store.BucketMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -605,9 +606,9 @@ func (a *App) apiAdminWipe(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
for _, bname := range []string{"grids", "markers", "tiles", "maps"} {
|
||||
if tx.Bucket([]byte(bname)) != nil {
|
||||
if err := tx.DeleteBucket([]byte(bname)); err != nil {
|
||||
for _, b := range [][]byte{store.BucketGrids, store.BucketMarkers, store.BucketTiles, store.BucketMaps} {
|
||||
if tx.Bucket(b) != nil {
|
||||
if err := tx.DeleteBucket(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -621,7 +622,7 @@ func (a *App) apiAdminWipe(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminRebuildZooms(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *App) APIAdminRebuildZooms(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -633,7 +634,7 @@ func (a *App) apiAdminRebuildZooms(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminExport(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *App) APIAdminExport(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -644,7 +645,7 @@ func (a *App) apiAdminExport(rw http.ResponseWriter, req *http.Request) {
|
||||
a.export(rw, req)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminMerge(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *App) APIAdminMerge(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -679,18 +680,18 @@ func (a *App) apiRouter(rw http.ResponseWriter, req *http.Request) {
|
||||
if path == "admin/wipeTile" || path == "admin/setCoords" || path == "admin/hideMarker" {
|
||||
switch path {
|
||||
case "admin/wipeTile":
|
||||
a.wipeTile(rw, req)
|
||||
a.WipeTile(rw, req)
|
||||
case "admin/setCoords":
|
||||
a.setCoords(rw, req)
|
||||
a.SetCoords(rw, req)
|
||||
case "admin/hideMarker":
|
||||
a.hideMarker(rw, req)
|
||||
a.HideMarker(rw, req)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case path == "oauth/providers":
|
||||
a.apiOAuthProviders(rw, req)
|
||||
a.APIOAuthProviders(rw, req)
|
||||
return
|
||||
case strings.HasPrefix(path, "oauth/"):
|
||||
rest := strings.TrimPrefix(path, "oauth/")
|
||||
@@ -703,9 +704,9 @@ func (a *App) apiRouter(rw http.ResponseWriter, req *http.Request) {
|
||||
action := parts[1]
|
||||
switch action {
|
||||
case "login":
|
||||
a.oauthLogin(rw, req, provider)
|
||||
a.OAuthLogin(rw, req, provider)
|
||||
case "callback":
|
||||
a.oauthCallback(rw, req, provider)
|
||||
a.OAuthCallback(rw, req, provider)
|
||||
default:
|
||||
http.Error(rw, "not found", http.StatusNotFound)
|
||||
}
|
||||
@@ -775,13 +776,13 @@ func (a *App) apiRouter(rw http.ResponseWriter, req *http.Request) {
|
||||
a.apiAdminWipe(rw, req)
|
||||
return
|
||||
case path == "admin/rebuildZooms":
|
||||
a.apiAdminRebuildZooms(rw, req)
|
||||
a.APIAdminRebuildZooms(rw, req)
|
||||
return
|
||||
case path == "admin/export":
|
||||
a.apiAdminExport(rw, req)
|
||||
a.APIAdminExport(rw, req)
|
||||
return
|
||||
case path == "admin/merge":
|
||||
a.apiAdminMerge(rw, req)
|
||||
a.APIAdminMerge(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,34 @@ type App struct {
|
||||
characters map[string]Character
|
||||
chmu sync.RWMutex
|
||||
|
||||
gridUpdates topic
|
||||
mergeUpdates mergeTopic
|
||||
gridUpdates Topic[TileData]
|
||||
mergeUpdates Topic[Merge]
|
||||
}
|
||||
|
||||
// GridStorage returns the grid storage path.
|
||||
func (a *App) GridStorage() string {
|
||||
return a.gridStorage
|
||||
}
|
||||
|
||||
// GridUpdates returns the tile updates topic for MapService.
|
||||
func (a *App) GridUpdates() *Topic[TileData] {
|
||||
return &a.gridUpdates
|
||||
}
|
||||
|
||||
// MergeUpdates returns the merge updates topic for MapService.
|
||||
func (a *App) MergeUpdates() *Topic[Merge] {
|
||||
return &a.mergeUpdates
|
||||
}
|
||||
|
||||
// GetCharacters returns a copy of all characters (for MapService).
|
||||
func (a *App) GetCharacters() []Character {
|
||||
a.chmu.RLock()
|
||||
defer a.chmu.RUnlock()
|
||||
chars := make([]Character, 0, len(a.characters))
|
||||
for _, v := range a.characters {
|
||||
chars = append(chars, v)
|
||||
}
|
||||
return chars
|
||||
}
|
||||
|
||||
// NewApp creates an App with the given storage paths and database.
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/response"
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -17,7 +19,7 @@ func (a *App) getSession(req *http.Request) *Session {
|
||||
}
|
||||
var s *Session
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
sessions := tx.Bucket([]byte("sessions"))
|
||||
sessions := tx.Bucket(store.BucketSessions)
|
||||
if sessions == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -33,7 +35,7 @@ func (a *App) getSession(req *http.Request) *Session {
|
||||
s.Auths = Auths{AUTH_ADMIN}
|
||||
return nil
|
||||
}
|
||||
users := tx.Bucket([]byte("users"))
|
||||
users := tx.Bucket(store.BucketUsers)
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -56,7 +58,7 @@ func (a *App) getSession(req *http.Request) *Session {
|
||||
|
||||
func (a *App) deleteSession(s *Session) {
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
|
||||
sessions, err := tx.CreateBucketIfNotExists(store.BucketSessions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -66,7 +68,7 @@ func (a *App) deleteSession(s *Session) {
|
||||
|
||||
func (a *App) saveSession(s *Session) {
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
|
||||
sessions, err := tx.CreateBucketIfNotExists(store.BucketSessions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -81,7 +83,7 @@ func (a *App) saveSession(s *Session) {
|
||||
func (a *App) getPage(req *http.Request) Page {
|
||||
p := Page{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
c := tx.Bucket([]byte("config"))
|
||||
c := tx.Bucket(store.BucketConfig)
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -93,7 +95,7 @@ func (a *App) getPage(req *http.Request) Page {
|
||||
|
||||
func (a *App) getUser(user, pass string) (u *User) {
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
users := tx.Bucket([]byte("users"))
|
||||
users := tx.Bucket(store.BucketUsers)
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -134,7 +136,7 @@ func (a *App) createSession(username string, tempAdmin bool) string {
|
||||
func (a *App) setupRequired() bool {
|
||||
var required bool
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
ub := tx.Bucket([]byte("users"))
|
||||
ub := tx.Bucket(store.BucketUsers)
|
||||
if ub == nil {
|
||||
required = true
|
||||
return nil
|
||||
@@ -151,7 +153,7 @@ func (a *App) setupRequired() bool {
|
||||
func (a *App) requireAdmin(rw http.ResponseWriter, req *http.Request) *Session {
|
||||
s := a.getSession(req)
|
||||
if s == nil || !s.Auths.Has(AUTH_ADMIN) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
response.JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -25,7 +26,7 @@ func (a *App) client(rw http.ResponseWriter, req *http.Request) {
|
||||
auth := false
|
||||
user := ""
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
tb := tx.Bucket([]byte("tokens"))
|
||||
tb := tx.Bucket(store.BucketTokens)
|
||||
if tb == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -33,7 +34,7 @@ func (a *App) client(rw http.ResponseWriter, req *http.Request) {
|
||||
if userName == nil {
|
||||
return nil
|
||||
}
|
||||
ub := tx.Bucket([]byte("users"))
|
||||
ub := tx.Bucket(store.BucketUsers)
|
||||
if ub == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -84,7 +85,7 @@ func (a *App) client(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *App) locate(rw http.ResponseWriter, req *http.Request) {
|
||||
grid := req.FormValue("gridID")
|
||||
err := a.db.View(func(tx *bbolt.Tx) error {
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
grids := tx.Bucket(store.BucketGrids)
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
@@ -53,21 +54,21 @@ func (a *App) gridUpdate(rw http.ResponseWriter, req *http.Request) {
|
||||
greq := GridRequest{}
|
||||
|
||||
err = a.db.Update(func(tx *bbolt.Tx) error {
|
||||
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
grids, err := tx.CreateBucketIfNotExists(store.BucketGrids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
tiles, err := tx.CreateBucketIfNotExists(store.BucketTiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mapB, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
mapB, err := tx.CreateBucketIfNotExists(store.BucketMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configb, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
configb, err := tx.CreateBucketIfNotExists(store.BucketConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -269,7 +270,7 @@ func (a *App) gridUpload(rw http.ResponseWriter, req *http.Request) {
|
||||
if ed.Season == 3 {
|
||||
needTile := false
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
b, err := tx.CreateBucketIfNotExists(store.BucketGrids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -283,7 +284,7 @@ func (a *App) gridUpload(rw http.ResponseWriter, req *http.Request) {
|
||||
return err
|
||||
}
|
||||
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
tiles, err := tx.CreateBucketIfNotExists(store.BucketTiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -346,7 +347,7 @@ func (a *App) gridUpload(rw http.ResponseWriter, req *http.Request) {
|
||||
mapid := 0
|
||||
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
b, err := tx.CreateBucketIfNotExists(store.BucketGrids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -33,15 +34,15 @@ func (a *App) uploadMarkers(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
err = a.db.Update(func(tx *bbolt.Tx) error {
|
||||
mb, err := tx.CreateBucketIfNotExists([]byte("markers"))
|
||||
mb, err := tx.CreateBucketIfNotExists(store.BucketMarkers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grid, err := mb.CreateBucketIfNotExists([]byte("grid"))
|
||||
grid, err := mb.CreateBucketIfNotExists(store.BucketMarkersGrid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idB, err := mb.CreateBucketIfNotExists([]byte("id"))
|
||||
idB, err := mb.CreateBucketIfNotExists(store.BucketMarkersID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -36,7 +37,7 @@ func (a *App) updatePositions(rw http.ResponseWriter, req *http.Request) {
|
||||
// Avoid holding db.View and chmu simultaneously to prevent deadlock.
|
||||
gridDataByID := make(map[string]GridData)
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
grids := tx.Bucket(store.BucketGrids)
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
584
internal/app/handlers/api.go
Normal file
584
internal/app/handlers/api.go
Normal file
@@ -0,0 +1,584 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app"
|
||||
"github.com/andyleap/hnh-map/internal/app/services"
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
User string `json:"user"`
|
||||
Pass string `json:"pass"`
|
||||
}
|
||||
|
||||
type meResponse struct {
|
||||
Username string `json:"username"`
|
||||
Auths []string `json:"auths"`
|
||||
Tokens []string `json:"tokens,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
}
|
||||
|
||||
// APILogin handles POST /map/api/login.
|
||||
func (h *Handlers) APILogin(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body loginRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
if u := h.Auth.GetUserByUsername(body.User); u != nil && u.Pass == nil {
|
||||
JSONError(rw, http.StatusUnauthorized, "Use OAuth to sign in", "OAUTH_ONLY")
|
||||
return
|
||||
}
|
||||
u := h.Auth.GetUser(body.User, body.Pass)
|
||||
if u == nil {
|
||||
if boot := h.Auth.BootstrapAdmin(body.User, body.Pass, services.GetBootstrapPassword()); boot != nil {
|
||||
u = boot
|
||||
} else {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
}
|
||||
sessionID := h.Auth.CreateSession(body.User, u.Auths.Has("tempadmin"))
|
||||
if sessionID == "" {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: sessionID,
|
||||
Path: "/",
|
||||
MaxAge: 24 * 7 * 3600,
|
||||
HttpOnly: true,
|
||||
Secure: req.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
JSON(rw, http.StatusOK, meResponse{Username: body.User, Auths: u.Auths})
|
||||
}
|
||||
|
||||
// APISetup handles GET /map/api/setup.
|
||||
func (h *Handlers) APISetup(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
JSON(rw, http.StatusOK, struct {
|
||||
SetupRequired bool `json:"setupRequired"`
|
||||
}{SetupRequired: h.Auth.SetupRequired()})
|
||||
}
|
||||
|
||||
// APILogout handles POST /map/api/logout.
|
||||
func (h *Handlers) APILogout(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := h.Auth.GetSession(req)
|
||||
if s != nil {
|
||||
h.Auth.DeleteSession(s)
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// APIMe handles GET /map/api/me.
|
||||
func (h *Handlers) APIMe(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := h.Auth.GetSession(req)
|
||||
if s == nil {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
out := meResponse{Username: s.Username, Auths: s.Auths}
|
||||
out.Tokens, out.Prefix = h.Auth.GetUserTokensAndPrefix(s.Username)
|
||||
JSON(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// APIMeTokens handles POST /map/api/me/tokens.
|
||||
func (h *Handlers) APIMeTokens(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := h.Auth.GetSession(req)
|
||||
if s == nil {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if !s.Auths.Has(app.AUTH_UPLOAD) {
|
||||
JSONError(rw, http.StatusForbidden, "Forbidden", "FORBIDDEN")
|
||||
return
|
||||
}
|
||||
tokens := h.Auth.GenerateTokenForUser(s.Username)
|
||||
if tokens == nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
JSON(rw, http.StatusOK, map[string][]string{"tokens": tokens})
|
||||
}
|
||||
|
||||
type passwordRequest struct {
|
||||
Pass string `json:"pass"`
|
||||
}
|
||||
|
||||
// APIMePassword handles POST /map/api/me/password.
|
||||
func (h *Handlers) APIMePassword(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := h.Auth.GetSession(req)
|
||||
if s == nil {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
var body passwordRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
if err := h.Auth.SetUserPassword(s.Username, body.Pass); err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// APIConfig handles GET /map/api/config.
|
||||
func (h *Handlers) APIConfig(rw http.ResponseWriter, req *http.Request) {
|
||||
s := h.Auth.GetSession(req)
|
||||
if s == nil {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
config, err := h.Map.GetConfig(s.Auths)
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
JSON(rw, http.StatusOK, config)
|
||||
}
|
||||
|
||||
// APIGetChars handles GET /map/api/v1/characters.
|
||||
func (h *Handlers) APIGetChars(rw http.ResponseWriter, req *http.Request) {
|
||||
s := h.Auth.GetSession(req)
|
||||
if !h.canAccessMap(s) {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if !s.Auths.Has(app.AUTH_MARKERS) && !s.Auths.Has(app.AUTH_ADMIN) {
|
||||
JSON(rw, http.StatusOK, []interface{}{})
|
||||
return
|
||||
}
|
||||
chars := h.Map.GetCharacters()
|
||||
JSON(rw, http.StatusOK, chars)
|
||||
}
|
||||
|
||||
// APIGetMarkers handles GET /map/api/v1/markers.
|
||||
func (h *Handlers) APIGetMarkers(rw http.ResponseWriter, req *http.Request) {
|
||||
s := h.Auth.GetSession(req)
|
||||
if !h.canAccessMap(s) {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if !s.Auths.Has(app.AUTH_MARKERS) && !s.Auths.Has(app.AUTH_ADMIN) {
|
||||
JSON(rw, http.StatusOK, []interface{}{})
|
||||
return
|
||||
}
|
||||
markers, err := h.Map.GetMarkers()
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
JSON(rw, http.StatusOK, markers)
|
||||
}
|
||||
|
||||
// APIGetMaps handles GET /map/api/maps.
|
||||
func (h *Handlers) APIGetMaps(rw http.ResponseWriter, req *http.Request) {
|
||||
s := h.Auth.GetSession(req)
|
||||
if !h.canAccessMap(s) {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
showHidden := s.Auths.Has(app.AUTH_ADMIN)
|
||||
maps, err := h.Map.GetMaps(showHidden)
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
JSON(rw, http.StatusOK, maps)
|
||||
}
|
||||
|
||||
// --- Admin API ---
|
||||
|
||||
// APIAdminUsers handles GET/POST /map/api/admin/users.
|
||||
func (h *Handlers) APIAdminUsers(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == http.MethodGet {
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
list, err := h.Admin.ListUsers()
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
JSON(rw, http.StatusOK, list)
|
||||
return
|
||||
}
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := h.requireAdmin(rw, req)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
User string `json:"user"`
|
||||
Pass string `json:"pass"`
|
||||
Auths []string `json:"auths"`
|
||||
}
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil || body.User == "" {
|
||||
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
adminCreated, err := h.Admin.CreateOrUpdateUser(body.User, body.Pass, body.Auths)
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
if body.User == s.Username {
|
||||
s.Auths = body.Auths
|
||||
}
|
||||
if adminCreated && s.Username == "admin" {
|
||||
h.Auth.DeleteSession(s)
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// APIAdminUserByName handles GET /map/api/admin/users/:name.
|
||||
func (h *Handlers) APIAdminUserByName(rw http.ResponseWriter, req *http.Request, name string) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
auths, found := h.Admin.GetUser(name)
|
||||
out := struct {
|
||||
Username string `json:"username"`
|
||||
Auths []string `json:"auths"`
|
||||
}{Username: name}
|
||||
if found {
|
||||
out.Auths = auths
|
||||
}
|
||||
JSON(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// APIAdminUserDelete handles DELETE /map/api/admin/users/:name.
|
||||
func (h *Handlers) APIAdminUserDelete(rw http.ResponseWriter, req *http.Request, name string) {
|
||||
if req.Method != http.MethodDelete {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := h.requireAdmin(rw, req)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
if err := h.Admin.DeleteUser(name); err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
if name == s.Username {
|
||||
h.Auth.DeleteSession(s)
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// APIAdminSettingsGet handles GET /map/api/admin/settings.
|
||||
func (h *Handlers) APIAdminSettingsGet(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
prefix, defaultHide, title, err := h.Admin.GetSettings()
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
JSON(rw, http.StatusOK, struct {
|
||||
Prefix string `json:"prefix"`
|
||||
DefaultHide bool `json:"defaultHide"`
|
||||
Title string `json:"title"`
|
||||
}{Prefix: prefix, DefaultHide: defaultHide, Title: title})
|
||||
}
|
||||
|
||||
// APIAdminSettingsPost handles POST /map/api/admin/settings.
|
||||
func (h *Handlers) APIAdminSettingsPost(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Prefix *string `json:"prefix"`
|
||||
DefaultHide *bool `json:"defaultHide"`
|
||||
Title *string `json:"title"`
|
||||
}
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
if err := h.Admin.UpdateSettings(body.Prefix, body.DefaultHide, body.Title); err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type mapInfoJSON struct {
|
||||
ID int `json:"ID"`
|
||||
Name string `json:"Name"`
|
||||
Hidden bool `json:"Hidden"`
|
||||
Priority bool `json:"Priority"`
|
||||
}
|
||||
|
||||
// APIAdminMaps handles GET /map/api/admin/maps.
|
||||
func (h *Handlers) APIAdminMaps(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
maps, err := h.Admin.ListMaps()
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
out := make([]mapInfoJSON, len(maps))
|
||||
for i, m := range maps {
|
||||
out[i] = mapInfoJSON{ID: m.ID, Name: m.Name, Hidden: m.Hidden, Priority: m.Priority}
|
||||
}
|
||||
JSON(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// APIAdminMapByID handles POST /map/api/admin/maps/:id.
|
||||
func (h *Handlers) APIAdminMapByID(rw http.ResponseWriter, req *http.Request, idStr string) {
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Priority bool `json:"priority"`
|
||||
}
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
if err := h.Admin.UpdateMap(id, body.Name, body.Hidden, body.Priority); err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// APIAdminMapToggleHidden handles POST /map/api/admin/maps/:id/toggle-hidden.
|
||||
func (h *Handlers) APIAdminMapToggleHidden(rw http.ResponseWriter, req *http.Request, idStr string) {
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
mi, err := h.Admin.ToggleMapHidden(id)
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
JSON(rw, http.StatusOK, mapInfoJSON{
|
||||
ID: mi.ID,
|
||||
Name: mi.Name,
|
||||
Hidden: mi.Hidden,
|
||||
Priority: mi.Priority,
|
||||
})
|
||||
}
|
||||
|
||||
// APIAdminWipe handles POST /map/api/admin/wipe.
|
||||
func (h *Handlers) APIAdminWipe(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
if err := h.Admin.Wipe(); err != nil {
|
||||
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// APIRouter routes /map/api/* requests.
|
||||
func (h *Handlers) APIRouter(rw http.ResponseWriter, req *http.Request) {
|
||||
path := strings.TrimPrefix(req.URL.Path, "/map/api")
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
||||
switch path {
|
||||
case "config":
|
||||
h.APIConfig(rw, req)
|
||||
return
|
||||
case "v1/characters":
|
||||
h.APIGetChars(rw, req)
|
||||
return
|
||||
case "v1/markers":
|
||||
h.APIGetMarkers(rw, req)
|
||||
return
|
||||
case "maps":
|
||||
h.APIGetMaps(rw, req)
|
||||
return
|
||||
}
|
||||
if path == "admin/wipeTile" || path == "admin/setCoords" || path == "admin/hideMarker" {
|
||||
switch path {
|
||||
case "admin/wipeTile":
|
||||
h.App.WipeTile(rw, req)
|
||||
case "admin/setCoords":
|
||||
h.App.SetCoords(rw, req)
|
||||
case "admin/hideMarker":
|
||||
h.App.HideMarker(rw, req)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case path == "oauth/providers":
|
||||
h.App.APIOAuthProviders(rw, req)
|
||||
return
|
||||
case strings.HasPrefix(path, "oauth/"):
|
||||
rest := strings.TrimPrefix(path, "oauth/")
|
||||
parts := strings.SplitN(rest, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
||||
return
|
||||
}
|
||||
provider := parts[0]
|
||||
action := parts[1]
|
||||
switch action {
|
||||
case "login":
|
||||
h.App.OAuthLogin(rw, req, provider)
|
||||
case "callback":
|
||||
h.App.OAuthCallback(rw, req, provider)
|
||||
default:
|
||||
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
||||
}
|
||||
return
|
||||
case path == "setup":
|
||||
h.APISetup(rw, req)
|
||||
return
|
||||
case path == "login":
|
||||
h.APILogin(rw, req)
|
||||
return
|
||||
case path == "logout":
|
||||
h.APILogout(rw, req)
|
||||
return
|
||||
case path == "me":
|
||||
h.APIMe(rw, req)
|
||||
return
|
||||
case path == "me/tokens":
|
||||
h.APIMeTokens(rw, req)
|
||||
return
|
||||
case path == "me/password":
|
||||
h.APIMePassword(rw, req)
|
||||
return
|
||||
case path == "admin/users":
|
||||
if req.Method == http.MethodPost {
|
||||
h.APIAdminUsers(rw, req)
|
||||
} else {
|
||||
h.APIAdminUsers(rw, req)
|
||||
}
|
||||
return
|
||||
case strings.HasPrefix(path, "admin/users/"):
|
||||
name := strings.TrimPrefix(path, "admin/users/")
|
||||
if name == "" {
|
||||
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
||||
return
|
||||
}
|
||||
if req.Method == http.MethodDelete {
|
||||
h.APIAdminUserDelete(rw, req, name)
|
||||
} else {
|
||||
h.APIAdminUserByName(rw, req, name)
|
||||
}
|
||||
return
|
||||
case path == "admin/settings":
|
||||
if req.Method == http.MethodGet {
|
||||
h.APIAdminSettingsGet(rw, req)
|
||||
} else {
|
||||
h.APIAdminSettingsPost(rw, req)
|
||||
}
|
||||
return
|
||||
case path == "admin/maps":
|
||||
h.APIAdminMaps(rw, req)
|
||||
return
|
||||
case strings.HasPrefix(path, "admin/maps/"):
|
||||
rest := strings.TrimPrefix(path, "admin/maps/")
|
||||
parts := strings.SplitN(rest, "/", 2)
|
||||
idStr := parts[0]
|
||||
if len(parts) == 2 && parts[1] == "toggle-hidden" {
|
||||
h.APIAdminMapToggleHidden(rw, req, idStr)
|
||||
return
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
h.APIAdminMapByID(rw, req, idStr)
|
||||
return
|
||||
}
|
||||
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
||||
return
|
||||
case path == "admin/wipe":
|
||||
h.APIAdminWipe(rw, req)
|
||||
return
|
||||
case path == "admin/rebuildZooms":
|
||||
h.App.APIAdminRebuildZooms(rw, req)
|
||||
return
|
||||
case path == "admin/export":
|
||||
h.App.APIAdminExport(rw, req)
|
||||
return
|
||||
case path == "admin/merge":
|
||||
h.App.APIAdminMerge(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
||||
}
|
||||
36
internal/app/handlers/handlers.go
Normal file
36
internal/app/handlers/handlers.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app"
|
||||
"github.com/andyleap/hnh-map/internal/app/services"
|
||||
)
|
||||
|
||||
// Handlers holds HTTP handlers and their dependencies.
|
||||
type Handlers struct {
|
||||
App *app.App
|
||||
Auth *services.AuthService
|
||||
Map *services.MapService
|
||||
Admin *services.AdminService
|
||||
}
|
||||
|
||||
// New creates Handlers with the given dependencies.
|
||||
func New(a *app.App, auth *services.AuthService, mapSvc *services.MapService, admin *services.AdminService) *Handlers {
|
||||
return &Handlers{App: a, Auth: auth, Map: mapSvc, Admin: admin}
|
||||
}
|
||||
|
||||
// requireAdmin returns session if admin, or writes 401 and returns nil.
|
||||
func (h *Handlers) requireAdmin(rw http.ResponseWriter, req *http.Request) *app.Session {
|
||||
s := h.Auth.GetSession(req)
|
||||
if s == nil || !s.Auths.Has(app.AUTH_ADMIN) {
|
||||
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// canAccessMap returns true if session has map or admin auth.
|
||||
func (h *Handlers) canAccessMap(s *app.Session) bool {
|
||||
return s != nil && (s.Auths.Has(app.AUTH_MAP) || s.Auths.Has(app.AUTH_ADMIN))
|
||||
}
|
||||
17
internal/app/handlers/response.go
Normal file
17
internal/app/handlers/response.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/response"
|
||||
)
|
||||
|
||||
// JSON writes v as JSON with the given status code.
|
||||
func JSON(rw http.ResponseWriter, status int, v any) {
|
||||
response.JSON(rw, status, v)
|
||||
}
|
||||
|
||||
// JSONError writes an error response in standard format.
|
||||
func JSONError(rw http.ResponseWriter, status int, msg, code string) {
|
||||
response.JSONError(rw, status, msg, code)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -51,15 +52,15 @@ func (a *App) getMarkers(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
markers := []FrontendMarker{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("markers"))
|
||||
b := tx.Bucket(store.BucketMarkers)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
grid := b.Bucket([]byte("grid"))
|
||||
grid := b.Bucket(store.BucketMarkersGrid)
|
||||
if grid == nil {
|
||||
return nil
|
||||
}
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
grids := tx.Bucket(store.BucketGrids)
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -99,7 +100,7 @@ func (a *App) getMaps(rw http.ResponseWriter, req *http.Request) {
|
||||
showHidden := s.Auths.Has(AUTH_ADMIN)
|
||||
maps := map[int]*MapInfo{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
mapB := tx.Bucket([]byte("maps"))
|
||||
mapB := tx.Bucket(store.BucketMaps)
|
||||
if mapB == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -131,7 +132,7 @@ func (a *App) config(rw http.ResponseWriter, req *http.Request) {
|
||||
Auths: s.Auths,
|
||||
}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("config"))
|
||||
b := tx.Bucket(store.BucketConfig)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,22 +7,23 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var migrations = []func(tx *bbolt.Tx) error{
|
||||
func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket([]byte("markers")) != nil {
|
||||
return tx.DeleteBucket([]byte("markers"))
|
||||
if tx.Bucket(store.BucketMarkers) != nil {
|
||||
return tx.DeleteBucket(store.BucketMarkers)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
grids, err := tx.CreateBucketIfNotExists(store.BucketGrids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
tiles, err := tx.CreateBucketIfNotExists(store.BucketTiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -51,22 +52,20 @@ var migrations = []func(tx *bbolt.Tx) error{
|
||||
})
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
b, err := tx.CreateBucketIfNotExists(store.BucketConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte("title"), []byte("HnH Automapper Server"))
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket([]byte("markers")) != nil {
|
||||
return tx.DeleteBucket([]byte("markers"))
|
||||
}
|
||||
// No-op: markers deletion already in migration 0
|
||||
return nil
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket([]byte("tiles")) != nil {
|
||||
if tx.Bucket(store.BucketTiles) != nil {
|
||||
allTiles := map[string]map[string]TileData{}
|
||||
tiles := tx.Bucket([]byte("tiles"))
|
||||
tiles := tx.Bucket(store.BucketTiles)
|
||||
err := tiles.ForEach(func(k, v []byte) error {
|
||||
zoom := tiles.Bucket(k)
|
||||
zoomTiles := map[string]TileData{}
|
||||
@@ -82,11 +81,11 @@ var migrations = []func(tx *bbolt.Tx) error{
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.DeleteBucket([]byte("tiles"))
|
||||
err = tx.DeleteBucket(store.BucketTiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tiles, err = tx.CreateBucket([]byte("tiles"))
|
||||
tiles, err = tx.CreateBucket(store.BucketTiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -115,18 +114,16 @@ var migrations = []func(tx *bbolt.Tx) error{
|
||||
return nil
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket([]byte("markers")) != nil {
|
||||
return tx.DeleteBucket([]byte("markers"))
|
||||
}
|
||||
// No-op: markers deletion already in migration 0
|
||||
return nil
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
highest := uint64(0)
|
||||
maps, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
maps, err := tx.CreateBucketIfNotExists(store.BucketMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
grids, err := tx.CreateBucketIfNotExists(store.BucketGrids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -138,6 +135,7 @@ var migrations = []func(tx *bbolt.Tx) error{
|
||||
return err
|
||||
}
|
||||
if _, ok := mapsFound[gd.Map]; !ok {
|
||||
mapsFound[gd.Map] = struct{}{}
|
||||
if uint64(gd.Map) > highest {
|
||||
highest = uint64(gd.Map)
|
||||
}
|
||||
@@ -157,7 +155,7 @@ var migrations = []func(tx *bbolt.Tx) error{
|
||||
return maps.SetSequence(highest + 1)
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
users := tx.Bucket([]byte("users"))
|
||||
users := tx.Bucket(store.BucketUsers)
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -176,7 +174,7 @@ var migrations = []func(tx *bbolt.Tx) error{
|
||||
})
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte("oauth_states"))
|
||||
_, err := tx.CreateBucketIfNotExists(store.BucketOAuthStates)
|
||||
return err
|
||||
},
|
||||
}
|
||||
@@ -184,7 +182,7 @@ var migrations = []func(tx *bbolt.Tx) error{
|
||||
// RunMigrations runs all pending migrations on the database.
|
||||
func RunMigrations(db *bbolt.DB) error {
|
||||
return db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
b, err := tx.CreateBucketIfNotExists(store.BucketConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
@@ -63,7 +64,7 @@ func (a *App) baseURL(req *http.Request) string {
|
||||
return scheme + "://" + host
|
||||
}
|
||||
|
||||
func (a *App) oauthLogin(rw http.ResponseWriter, req *http.Request, provider string) {
|
||||
func (a *App) OAuthLogin(rw http.ResponseWriter, req *http.Request, provider string) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -88,7 +89,7 @@ func (a *App) oauthLogin(rw http.ResponseWriter, req *http.Request, provider str
|
||||
}
|
||||
stRaw, _ := json.Marshal(st)
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("oauth_states"))
|
||||
b, err := tx.CreateBucketIfNotExists(store.BucketOAuthStates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -108,7 +109,7 @@ type googleUserInfo struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (a *App) oauthCallback(rw http.ResponseWriter, req *http.Request, provider string) {
|
||||
func (a *App) OAuthCallback(rw http.ResponseWriter, req *http.Request, provider string) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -127,7 +128,7 @@ func (a *App) oauthCallback(rw http.ResponseWriter, req *http.Request, provider
|
||||
}
|
||||
var st oauthState
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("oauth_states"))
|
||||
b := tx.Bucket(store.BucketOAuthStates)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -224,7 +225,7 @@ func (a *App) googleUserInfo(accessToken string) (sub, email string, err error)
|
||||
func (a *App) findOrCreateOAuthUser(provider, sub, email string) (string, *User) {
|
||||
var username string
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
users, err := tx.CreateBucketIfNotExists(store.BucketUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -283,7 +284,7 @@ func (a *App) findOrCreateOAuthUser(provider, sub, email string) (string, *User)
|
||||
func (a *App) getUserByUsername(username string) *User {
|
||||
var u *User
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
users := tx.Bucket([]byte("users"))
|
||||
users := tx.Bucket(store.BucketUsers)
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -297,7 +298,7 @@ func (a *App) getUserByUsername(username string) *User {
|
||||
}
|
||||
|
||||
// apiOAuthProviders returns list of configured OAuth providers.
|
||||
func (a *App) apiOAuthProviders(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *App) APIOAuthProviders(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
|
||||
25
internal/app/response/response.go
Normal file
25
internal/app/response/response.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// JSON writes v as JSON with the given status code.
|
||||
func JSON(rw http.ResponseWriter, status int, v any) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(status)
|
||||
if v != nil {
|
||||
_ = json.NewEncoder(rw).Encode(v)
|
||||
}
|
||||
}
|
||||
|
||||
// JSONError writes an error response in standard format: {"error": "message", "code": "CODE"}.
|
||||
func JSONError(rw http.ResponseWriter, status int, msg string, code string) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(status)
|
||||
_ = json.NewEncoder(rw).Encode(map[string]string{
|
||||
"error": msg,
|
||||
"code": code,
|
||||
})
|
||||
}
|
||||
37
internal/app/router.go
Normal file
37
internal/app/router.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// APIHandler is the interface for API routing (implemented by handlers.Handlers).
|
||||
type APIHandler interface {
|
||||
APIRouter(rw http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
// Router returns the HTTP router for the app.
|
||||
// publicDir is used for /js/ static file serving (e.g. "public").
|
||||
// apiHandler handles /map/api/* requests; if nil, uses built-in apiRouter.
|
||||
func (a *App) Router(publicDir string, apiHandler APIHandler) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Handle("/js/*", http.FileServer(http.Dir(publicDir)))
|
||||
r.HandleFunc("/client/*", a.client)
|
||||
r.HandleFunc("/logout", a.redirectLogout)
|
||||
|
||||
r.Route("/map", func(r chi.Router) {
|
||||
if apiHandler != nil {
|
||||
r.HandleFunc("/api/*", apiHandler.APIRouter)
|
||||
} else {
|
||||
r.HandleFunc("/api/*", a.apiRouter)
|
||||
}
|
||||
r.HandleFunc("/updates", a.watchGridUpdates)
|
||||
r.Handle("/grids/*", http.StripPrefix("/map/grids", http.HandlerFunc(a.gridTile)))
|
||||
})
|
||||
|
||||
r.HandleFunc("/*", a.serveSPARoot)
|
||||
|
||||
return r
|
||||
}
|
||||
216
internal/app/services/admin.go
Normal file
216
internal/app/services/admin.go
Normal file
@@ -0,0 +1,216 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
242
internal/app/services/auth.go
Normal file
242
internal/app/services/auth.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app"
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// AuthService handles authentication and session business logic.
|
||||
type AuthService struct {
|
||||
st *store.Store
|
||||
}
|
||||
|
||||
// NewAuthService creates an AuthService.
|
||||
func NewAuthService(st *store.Store) *AuthService {
|
||||
return &AuthService{st: st}
|
||||
}
|
||||
|
||||
// GetSession returns the session from the request cookie, or nil.
|
||||
func (s *AuthService) GetSession(req *http.Request) *app.Session {
|
||||
c, err := req.Cookie("session")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var sess *app.Session
|
||||
s.st.View(func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetSession(tx, c.Value)
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(raw, &sess); err != nil {
|
||||
return err
|
||||
}
|
||||
if sess.TempAdmin {
|
||||
sess.Auths = app.Auths{app.AUTH_ADMIN}
|
||||
return nil
|
||||
}
|
||||
uRaw := s.st.GetUser(tx, sess.Username)
|
||||
if uRaw == nil {
|
||||
sess = nil
|
||||
return nil
|
||||
}
|
||||
var u app.User
|
||||
if err := json.Unmarshal(uRaw, &u); err != nil {
|
||||
sess = nil
|
||||
return err
|
||||
}
|
||||
sess.Auths = u.Auths
|
||||
return nil
|
||||
})
|
||||
return sess
|
||||
}
|
||||
|
||||
// DeleteSession removes a session.
|
||||
func (s *AuthService) DeleteSession(sess *app.Session) {
|
||||
s.st.Update(func(tx *bbolt.Tx) error {
|
||||
return s.st.DeleteSession(tx, sess.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// SaveSession stores a session.
|
||||
func (s *AuthService) SaveSession(sess *app.Session) error {
|
||||
return s.st.Update(func(tx *bbolt.Tx) error {
|
||||
buf, err := json.Marshal(sess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.st.PutSession(tx, sess.ID, buf)
|
||||
})
|
||||
}
|
||||
|
||||
// CreateSession creates a session for username, returns session ID or empty string.
|
||||
func (s *AuthService) CreateSession(username string, tempAdmin bool) string {
|
||||
session := make([]byte, 32)
|
||||
if _, err := rand.Read(session); err != nil {
|
||||
return ""
|
||||
}
|
||||
sid := hex.EncodeToString(session)
|
||||
sess := &app.Session{
|
||||
ID: sid,
|
||||
Username: username,
|
||||
TempAdmin: tempAdmin,
|
||||
}
|
||||
if s.SaveSession(sess) != nil {
|
||||
return ""
|
||||
}
|
||||
return sid
|
||||
}
|
||||
|
||||
// GetUser returns user if username/password match.
|
||||
func (s *AuthService) GetUser(username, pass string) *app.User {
|
||||
var u *app.User
|
||||
s.st.View(func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetUser(tx, username)
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
json.Unmarshal(raw, &u)
|
||||
if u.Pass == nil {
|
||||
u = nil
|
||||
return nil
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword(u.Pass, []byte(pass)) != nil {
|
||||
u = nil
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
// GetUserByUsername returns user without password check (for OAuth-only check).
|
||||
func (s *AuthService) GetUserByUsername(username string) *app.User {
|
||||
var u *app.User
|
||||
s.st.View(func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetUser(tx, username)
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
// SetupRequired returns true if no users exist (first run).
|
||||
func (s *AuthService) SetupRequired() bool {
|
||||
var required bool
|
||||
s.st.View(func(tx *bbolt.Tx) error {
|
||||
if s.st.UserCount(tx) == 0 {
|
||||
required = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return required
|
||||
}
|
||||
|
||||
// BootstrapAdmin creates the first admin user if bootstrap env is set and no users exist.
|
||||
// Returns the user if created, nil otherwise.
|
||||
func (s *AuthService) BootstrapAdmin(username, pass, bootstrapEnv string) *app.User {
|
||||
if username != "admin" || pass == "" || bootstrapEnv == "" || pass != bootstrapEnv {
|
||||
return nil
|
||||
}
|
||||
var created bool
|
||||
var u *app.User
|
||||
s.st.Update(func(tx *bbolt.Tx) error {
|
||||
if s.st.GetUser(tx, "admin") != nil {
|
||||
return nil
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := app.User{
|
||||
Pass: hash,
|
||||
Auths: app.Auths{app.AUTH_ADMIN, app.AUTH_MAP, app.AUTH_MARKERS, app.AUTH_UPLOAD},
|
||||
}
|
||||
raw, _ := json.Marshal(user)
|
||||
if err := s.st.PutUser(tx, "admin", raw); err != nil {
|
||||
return err
|
||||
}
|
||||
created = true
|
||||
u = &user
|
||||
return nil
|
||||
})
|
||||
if created {
|
||||
return u
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBootstrapPassword returns HNHMAP_BOOTSTRAP_PASSWORD from env.
|
||||
func GetBootstrapPassword() string {
|
||||
return os.Getenv("HNHMAP_BOOTSTRAP_PASSWORD")
|
||||
}
|
||||
|
||||
// GetUserTokensAndPrefix returns tokens and config prefix for a user.
|
||||
func (s *AuthService) GetUserTokensAndPrefix(username string) (tokens []string, prefix string) {
|
||||
s.st.View(func(tx *bbolt.Tx) error {
|
||||
uRaw := s.st.GetUser(tx, username)
|
||||
if uRaw != nil {
|
||||
var u app.User
|
||||
json.Unmarshal(uRaw, &u)
|
||||
tokens = u.Tokens
|
||||
}
|
||||
if p := s.st.GetConfig(tx, "prefix"); p != nil {
|
||||
prefix = string(p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return tokens, prefix
|
||||
}
|
||||
|
||||
// GenerateTokenForUser adds a new token for user and returns the full list.
|
||||
func (s *AuthService) GenerateTokenForUser(username string) []string {
|
||||
tokenRaw := make([]byte, 16)
|
||||
if _, err := rand.Read(tokenRaw); err != nil {
|
||||
return nil
|
||||
}
|
||||
token := hex.EncodeToString(tokenRaw)
|
||||
var tokens []string
|
||||
s.st.Update(func(tx *bbolt.Tx) error {
|
||||
uRaw := s.st.GetUser(tx, username)
|
||||
u := app.User{}
|
||||
if uRaw != nil {
|
||||
json.Unmarshal(uRaw, &u)
|
||||
}
|
||||
u.Tokens = append(u.Tokens, token)
|
||||
tokens = u.Tokens
|
||||
buf, _ := json.Marshal(u)
|
||||
s.st.PutUser(tx, username, buf)
|
||||
return s.st.PutToken(tx, token, username)
|
||||
})
|
||||
return tokens
|
||||
}
|
||||
|
||||
// SetUserPassword sets password for user (empty pass = no change).
|
||||
func (s *AuthService) SetUserPassword(username, pass string) error {
|
||||
if pass == "" {
|
||||
return nil
|
||||
}
|
||||
return s.st.Update(func(tx *bbolt.Tx) error {
|
||||
uRaw := s.st.GetUser(tx, username)
|
||||
u := app.User{}
|
||||
if uRaw != nil {
|
||||
json.Unmarshal(uRaw, &u)
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Pass = hash
|
||||
raw, _ := json.Marshal(u)
|
||||
return s.st.PutUser(tx, username, raw)
|
||||
})
|
||||
}
|
||||
174
internal/app/services/map.go
Normal file
174
internal/app/services/map.go
Normal file
@@ -0,0 +1,174 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// MapService handles map, markers, grids, tiles business logic.
|
||||
type MapService struct {
|
||||
st *store.Store
|
||||
gridStorage string
|
||||
gridUpdates *app.Topic[app.TileData]
|
||||
mergeUpdates *app.Topic[app.Merge]
|
||||
getChars func() []app.Character
|
||||
}
|
||||
|
||||
// MapServiceDeps holds dependencies for MapService.
|
||||
type MapServiceDeps struct {
|
||||
Store *store.Store
|
||||
GridStorage string
|
||||
GridUpdates *app.Topic[app.TileData]
|
||||
MergeUpdates *app.Topic[app.Merge]
|
||||
GetChars func() []app.Character
|
||||
}
|
||||
|
||||
// NewMapService creates a MapService.
|
||||
func NewMapService(d MapServiceDeps) *MapService {
|
||||
return &MapService{
|
||||
st: d.Store,
|
||||
gridStorage: d.GridStorage,
|
||||
gridUpdates: d.GridUpdates,
|
||||
mergeUpdates: d.MergeUpdates,
|
||||
getChars: d.GetChars,
|
||||
}
|
||||
}
|
||||
|
||||
// GridStorage returns the grid storage path.
|
||||
func (s *MapService) GridStorage() string {
|
||||
return s.gridStorage
|
||||
}
|
||||
|
||||
// GetCharacters returns all characters (from in-memory map).
|
||||
func (s *MapService) GetCharacters() []app.Character {
|
||||
if s.getChars == nil {
|
||||
return nil
|
||||
}
|
||||
return s.getChars()
|
||||
}
|
||||
|
||||
// GetMarkers returns all markers as FrontendMarker list.
|
||||
func (s *MapService) GetMarkers() ([]app.FrontendMarker, error) {
|
||||
var markers []app.FrontendMarker
|
||||
err := s.st.View(func(tx *bbolt.Tx) error {
|
||||
grid := s.st.GetMarkersGridBucket(tx)
|
||||
if grid == nil {
|
||||
return nil
|
||||
}
|
||||
grids := tx.Bucket(store.BucketGrids)
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
return grid.ForEach(func(k, v []byte) error {
|
||||
marker := app.Marker{}
|
||||
json.Unmarshal(v, &marker)
|
||||
graw := grids.Get([]byte(marker.GridID))
|
||||
if graw == nil {
|
||||
return nil
|
||||
}
|
||||
g := app.GridData{}
|
||||
json.Unmarshal(graw, &g)
|
||||
markers = append(markers, app.FrontendMarker{
|
||||
Image: marker.Image,
|
||||
Hidden: marker.Hidden,
|
||||
ID: marker.ID,
|
||||
Name: marker.Name,
|
||||
Map: g.Map,
|
||||
Position: app.Position{
|
||||
X: marker.Position.X + g.Coord.X*100,
|
||||
Y: marker.Position.Y + g.Coord.Y*100,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return markers, err
|
||||
}
|
||||
|
||||
// GetMaps returns maps, optionally filtering hidden for non-admin.
|
||||
func (s *MapService) GetMaps(showHidden bool) (map[int]*app.MapInfo, error) {
|
||||
maps := make(map[int]*app.MapInfo)
|
||||
err := s.st.View(func(tx *bbolt.Tx) error {
|
||||
return s.st.ForEachMap(tx, func(k, v []byte) error {
|
||||
mapid, err := strconv.Atoi(string(k))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
mi := &app.MapInfo{}
|
||||
json.Unmarshal(v, mi)
|
||||
if mi.Hidden && !showHidden {
|
||||
return nil
|
||||
}
|
||||
maps[mapid] = mi
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return maps, err
|
||||
}
|
||||
|
||||
// GetConfig returns config (title) and auths for session.
|
||||
func (s *MapService) GetConfig(auths app.Auths) (app.Config, error) {
|
||||
config := app.Config{Auths: auths}
|
||||
err := s.st.View(func(tx *bbolt.Tx) error {
|
||||
title := s.st.GetConfig(tx, "title")
|
||||
if title != nil {
|
||||
config.Title = string(title)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return config, err
|
||||
}
|
||||
|
||||
// GetPage returns page title.
|
||||
func (s *MapService) GetPage() (app.Page, error) {
|
||||
p := app.Page{}
|
||||
err := s.st.View(func(tx *bbolt.Tx) error {
|
||||
title := s.st.GetConfig(tx, "title")
|
||||
if title != nil {
|
||||
p.Title = string(title)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return p, err
|
||||
}
|
||||
|
||||
// GetGrid returns GridData by ID.
|
||||
func (s *MapService) GetGrid(id string) (*app.GridData, error) {
|
||||
var gd *app.GridData
|
||||
err := s.st.View(func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetGrid(tx, id)
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
gd = &app.GridData{}
|
||||
return json.Unmarshal(raw, gd)
|
||||
})
|
||||
return gd, err
|
||||
}
|
||||
|
||||
// GetTile returns TileData for map/zoom/coord.
|
||||
func (s *MapService) GetTile(mapID int, c app.Coord, zoom int) *app.TileData {
|
||||
var td *app.TileData
|
||||
s.st.View(func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetTile(tx, mapID, zoom, c.Name())
|
||||
if raw != nil {
|
||||
td = &app.TileData{}
|
||||
json.Unmarshal(raw, td)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return td
|
||||
}
|
||||
|
||||
// ReportMerge sends a merge event.
|
||||
func (s *MapService) ReportMerge(from, to int, shift app.Coord) {
|
||||
s.mergeUpdates.Send(&app.Merge{
|
||||
From: from,
|
||||
To: to,
|
||||
Shift: shift,
|
||||
})
|
||||
}
|
||||
20
internal/app/store/buckets.go
Normal file
20
internal/app/store/buckets.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package store
|
||||
|
||||
// Bucket names for bbolt database.
|
||||
var (
|
||||
BucketUsers = []byte("users")
|
||||
BucketSessions = []byte("sessions")
|
||||
BucketTokens = []byte("tokens")
|
||||
BucketConfig = []byte("config")
|
||||
BucketGrids = []byte("grids")
|
||||
BucketTiles = []byte("tiles")
|
||||
BucketMaps = []byte("maps")
|
||||
BucketMarkers = []byte("markers")
|
||||
BucketOAuthStates = []byte("oauth_states")
|
||||
)
|
||||
|
||||
// Sub-bucket names within markers bucket.
|
||||
var (
|
||||
BucketMarkersGrid = []byte("grid")
|
||||
BucketMarkersID = []byte("id")
|
||||
)
|
||||
444
internal/app/store/db.go
Normal file
444
internal/app/store/db.go
Normal file
@@ -0,0 +1,444 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// Store provides access to bbolt database (bucket helpers, CRUD).
|
||||
type Store struct {
|
||||
db *bbolt.DB
|
||||
}
|
||||
|
||||
// New creates a Store for the given database.
|
||||
func New(db *bbolt.DB) *Store {
|
||||
return &Store{db: db}
|
||||
}
|
||||
|
||||
// View runs fn in a read-only transaction.
|
||||
func (s *Store) View(fn func(tx *bbolt.Tx) error) error {
|
||||
return s.db.View(fn)
|
||||
}
|
||||
|
||||
// Update runs fn in a read-write transaction.
|
||||
func (s *Store) Update(fn func(tx *bbolt.Tx) error) error {
|
||||
return s.db.Update(fn)
|
||||
}
|
||||
|
||||
// --- Users ---
|
||||
|
||||
// GetUser returns raw user bytes by username, or nil if not found.
|
||||
func (s *Store) GetUser(tx *bbolt.Tx, username string) []byte {
|
||||
b := tx.Bucket(BucketUsers)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Get([]byte(username))
|
||||
}
|
||||
|
||||
// PutUser stores user bytes by username.
|
||||
func (s *Store) PutUser(tx *bbolt.Tx, username string, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(username), raw)
|
||||
}
|
||||
|
||||
// DeleteUser removes a user.
|
||||
func (s *Store) DeleteUser(tx *bbolt.Tx, username string) error {
|
||||
b := tx.Bucket(BucketUsers)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(username))
|
||||
}
|
||||
|
||||
// ForEachUser calls fn for each user key.
|
||||
func (s *Store) ForEachUser(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
b := tx.Bucket(BucketUsers)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.ForEach(fn)
|
||||
}
|
||||
|
||||
// UserCount returns the number of users.
|
||||
func (s *Store) UserCount(tx *bbolt.Tx) int {
|
||||
b := tx.Bucket(BucketUsers)
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return b.Stats().KeyN
|
||||
}
|
||||
|
||||
// --- Sessions ---
|
||||
|
||||
// GetSession returns raw session bytes by ID, or nil if not found.
|
||||
func (s *Store) GetSession(tx *bbolt.Tx, id string) []byte {
|
||||
b := tx.Bucket(BucketSessions)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Get([]byte(id))
|
||||
}
|
||||
|
||||
// PutSession stores session bytes.
|
||||
func (s *Store) PutSession(tx *bbolt.Tx, id string, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketSessions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(id), raw)
|
||||
}
|
||||
|
||||
// DeleteSession removes a session.
|
||||
func (s *Store) DeleteSession(tx *bbolt.Tx, id string) error {
|
||||
b := tx.Bucket(BucketSessions)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(id))
|
||||
}
|
||||
|
||||
// --- Tokens ---
|
||||
|
||||
// GetTokenUser returns username for token, or nil if not found.
|
||||
func (s *Store) GetTokenUser(tx *bbolt.Tx, token string) []byte {
|
||||
b := tx.Bucket(BucketTokens)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Get([]byte(token))
|
||||
}
|
||||
|
||||
// PutToken stores token -> username mapping.
|
||||
func (s *Store) PutToken(tx *bbolt.Tx, token, username string) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketTokens)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(token), []byte(username))
|
||||
}
|
||||
|
||||
// DeleteToken removes a token.
|
||||
func (s *Store) DeleteToken(tx *bbolt.Tx, token string) error {
|
||||
b := tx.Bucket(BucketTokens)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(token))
|
||||
}
|
||||
|
||||
// --- Config ---
|
||||
|
||||
// GetConfig returns config value by key.
|
||||
func (s *Store) GetConfig(tx *bbolt.Tx, key string) []byte {
|
||||
b := tx.Bucket(BucketConfig)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Get([]byte(key))
|
||||
}
|
||||
|
||||
// PutConfig stores config value.
|
||||
func (s *Store) PutConfig(tx *bbolt.Tx, key string, value []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(key), value)
|
||||
}
|
||||
|
||||
// DeleteConfig removes a config key.
|
||||
func (s *Store) DeleteConfig(tx *bbolt.Tx, key string) error {
|
||||
b := tx.Bucket(BucketConfig)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(key))
|
||||
}
|
||||
|
||||
// --- Maps ---
|
||||
|
||||
// GetMap returns raw MapInfo bytes by ID.
|
||||
func (s *Store) GetMap(tx *bbolt.Tx, id int) []byte {
|
||||
b := tx.Bucket(BucketMaps)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Get([]byte(strconv.Itoa(id)))
|
||||
}
|
||||
|
||||
// PutMap stores MapInfo.
|
||||
func (s *Store) PutMap(tx *bbolt.Tx, id int, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(strconv.Itoa(id)), raw)
|
||||
}
|
||||
|
||||
// DeleteMap removes a map.
|
||||
func (s *Store) DeleteMap(tx *bbolt.Tx, id int) error {
|
||||
b := tx.Bucket(BucketMaps)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(strconv.Itoa(id)))
|
||||
}
|
||||
|
||||
// MapsNextSequence returns next map ID sequence.
|
||||
func (s *Store) MapsNextSequence(tx *bbolt.Tx) (uint64, error) {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketMaps)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return b.NextSequence()
|
||||
}
|
||||
|
||||
// MapsSetSequence sets map bucket sequence.
|
||||
func (s *Store) MapsSetSequence(tx *bbolt.Tx, v uint64) error {
|
||||
b := tx.Bucket(BucketMaps)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.SetSequence(v)
|
||||
}
|
||||
|
||||
// ForEachMap calls fn for each map.
|
||||
func (s *Store) ForEachMap(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
b := tx.Bucket(BucketMaps)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.ForEach(fn)
|
||||
}
|
||||
|
||||
// --- Grids ---
|
||||
|
||||
// GetGrid returns raw GridData bytes by ID.
|
||||
func (s *Store) GetGrid(tx *bbolt.Tx, id string) []byte {
|
||||
b := tx.Bucket(BucketGrids)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Get([]byte(id))
|
||||
}
|
||||
|
||||
// PutGrid stores GridData.
|
||||
func (s *Store) PutGrid(tx *bbolt.Tx, id string, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketGrids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(id), raw)
|
||||
}
|
||||
|
||||
// DeleteGrid removes a grid.
|
||||
func (s *Store) DeleteGrid(tx *bbolt.Tx, id string) error {
|
||||
b := tx.Bucket(BucketGrids)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(id))
|
||||
}
|
||||
|
||||
// ForEachGrid calls fn for each grid.
|
||||
func (s *Store) ForEachGrid(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
b := tx.Bucket(BucketGrids)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.ForEach(fn)
|
||||
}
|
||||
|
||||
// --- Tiles (nested: mapid -> zoom -> coord) ---
|
||||
|
||||
// GetTile returns raw TileData bytes.
|
||||
func (s *Store) GetTile(tx *bbolt.Tx, mapID, zoom int, coordKey string) []byte {
|
||||
tiles := tx.Bucket(BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
mapB := tiles.Bucket([]byte(strconv.Itoa(mapID)))
|
||||
if mapB == nil {
|
||||
return nil
|
||||
}
|
||||
zoomB := mapB.Bucket([]byte(strconv.Itoa(zoom)))
|
||||
if zoomB == nil {
|
||||
return nil
|
||||
}
|
||||
return zoomB.Get([]byte(coordKey))
|
||||
}
|
||||
|
||||
// PutTile stores TileData.
|
||||
func (s *Store) PutTile(tx *bbolt.Tx, mapID, zoom int, coordKey string, raw []byte) error {
|
||||
tiles, err := tx.CreateBucketIfNotExists(BucketTiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mapB, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(mapID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zoomB, err := mapB.CreateBucketIfNotExists([]byte(strconv.Itoa(zoom)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return zoomB.Put([]byte(coordKey), raw)
|
||||
}
|
||||
|
||||
// DeleteTilesBucket removes the tiles bucket (for wipe).
|
||||
func (s *Store) DeleteTilesBucket(tx *bbolt.Tx) error {
|
||||
if tx.Bucket(BucketTiles) == nil {
|
||||
return nil
|
||||
}
|
||||
return tx.DeleteBucket(BucketTiles)
|
||||
}
|
||||
|
||||
// ForEachTile calls fn for each tile in the nested structure.
|
||||
func (s *Store) ForEachTile(tx *bbolt.Tx, fn func(mapK, zoomK, coordK, v []byte) error) error {
|
||||
tiles := tx.Bucket(BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
return tiles.ForEach(func(mapK, _ []byte) error {
|
||||
mapB := tiles.Bucket(mapK)
|
||||
if mapB == nil {
|
||||
return nil
|
||||
}
|
||||
return mapB.ForEach(func(zoomK, _ []byte) error {
|
||||
zoomB := mapB.Bucket(zoomK)
|
||||
if zoomB == nil {
|
||||
return nil
|
||||
}
|
||||
return zoomB.ForEach(func(coordK, v []byte) error {
|
||||
return fn(mapK, zoomK, coordK, v)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// GetTilesMapBucket returns the bucket for a map's tiles, or nil.
|
||||
func (s *Store) GetTilesMapBucket(tx *bbolt.Tx, mapID int) *bbolt.Bucket {
|
||||
tiles := tx.Bucket(BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
return tiles.Bucket([]byte(strconv.Itoa(mapID)))
|
||||
}
|
||||
|
||||
// CreateTilesMapBucket creates and returns the bucket for a map's tiles.
|
||||
func (s *Store) CreateTilesMapBucket(tx *bbolt.Tx, mapID int) (*bbolt.Bucket, error) {
|
||||
tiles, err := tx.CreateBucketIfNotExists(BucketTiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(mapID)))
|
||||
}
|
||||
|
||||
// DeleteTilesMapBucket removes a map's tile bucket.
|
||||
func (s *Store) DeleteTilesMapBucket(tx *bbolt.Tx, mapID int) error {
|
||||
tiles := tx.Bucket(BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
return tiles.DeleteBucket([]byte(strconv.Itoa(mapID)))
|
||||
}
|
||||
|
||||
// --- Markers (nested: grid bucket, id bucket) ---
|
||||
|
||||
// GetMarkersGridBucket returns the markers-by-grid bucket.
|
||||
func (s *Store) GetMarkersGridBucket(tx *bbolt.Tx) *bbolt.Bucket {
|
||||
mb := tx.Bucket(BucketMarkers)
|
||||
if mb == nil {
|
||||
return nil
|
||||
}
|
||||
return mb.Bucket(BucketMarkersGrid)
|
||||
}
|
||||
|
||||
// GetMarkersIDBucket returns the markers-by-id bucket.
|
||||
func (s *Store) GetMarkersIDBucket(tx *bbolt.Tx) *bbolt.Bucket {
|
||||
mb := tx.Bucket(BucketMarkers)
|
||||
if mb == nil {
|
||||
return nil
|
||||
}
|
||||
return mb.Bucket(BucketMarkersID)
|
||||
}
|
||||
|
||||
// CreateMarkersBuckets creates markers, grid, and id buckets.
|
||||
func (s *Store) CreateMarkersBuckets(tx *bbolt.Tx) (*bbolt.Bucket, *bbolt.Bucket, error) {
|
||||
mb, err := tx.CreateBucketIfNotExists(BucketMarkers)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
grid, err := mb.CreateBucketIfNotExists(BucketMarkersGrid)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
idB, err := mb.CreateBucketIfNotExists(BucketMarkersID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return grid, idB, nil
|
||||
}
|
||||
|
||||
// MarkersNextSequence returns next marker ID.
|
||||
func (s *Store) MarkersNextSequence(tx *bbolt.Tx) (uint64, error) {
|
||||
mb := tx.Bucket(BucketMarkers)
|
||||
if mb == nil {
|
||||
return 0, nil
|
||||
}
|
||||
idB := mb.Bucket(BucketMarkersID)
|
||||
if idB == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return idB.NextSequence()
|
||||
}
|
||||
|
||||
// --- OAuth states ---
|
||||
|
||||
// GetOAuthState returns raw state bytes.
|
||||
func (s *Store) GetOAuthState(tx *bbolt.Tx, state string) []byte {
|
||||
b := tx.Bucket(BucketOAuthStates)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Get([]byte(state))
|
||||
}
|
||||
|
||||
// PutOAuthState stores state.
|
||||
func (s *Store) PutOAuthState(tx *bbolt.Tx, state string, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketOAuthStates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(state), raw)
|
||||
}
|
||||
|
||||
// DeleteOAuthState removes state.
|
||||
func (s *Store) DeleteOAuthState(tx *bbolt.Tx, state string) error {
|
||||
b := tx.Bucket(BucketOAuthStates)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(state))
|
||||
}
|
||||
|
||||
// --- Bucket existence (for wipe) ---
|
||||
|
||||
// BucketExists returns true if the bucket exists.
|
||||
func (s *Store) BucketExists(tx *bbolt.Tx, name []byte) bool {
|
||||
return tx.Bucket(name) != nil
|
||||
}
|
||||
|
||||
// DeleteBucket removes a bucket.
|
||||
func (s *Store) DeleteBucket(tx *bbolt.Tx, name []byte) error {
|
||||
if tx.Bucket(name) == nil {
|
||||
return nil
|
||||
}
|
||||
return tx.DeleteBucket(name)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ type TileData struct {
|
||||
|
||||
func (a *App) GetTile(mapid int, c Coord, z int) (td *TileData) {
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
tiles := tx.Bucket([]byte("tiles"))
|
||||
tiles := tx.Bucket(store.BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -59,7 +60,7 @@ func (a *App) GetTile(mapid int, c Coord, z int) (td *TileData) {
|
||||
|
||||
func (a *App) SaveTile(mapid int, c Coord, z int, f string, t int64) {
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
tiles, err := tx.CreateBucketIfNotExists(store.BucketTiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -82,14 +83,14 @@ func (a *App) SaveTile(mapid int, c Coord, z int, f string, t int64) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.gridUpdates.send(td)
|
||||
a.gridUpdates.Send(td)
|
||||
return zoom.Put([]byte(c.Name()), raw)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (a *App) reportMerge(from, to int, shift Coord) {
|
||||
a.mergeUpdates.send(&Merge{
|
||||
a.mergeUpdates.Send(&Merge{
|
||||
From: from,
|
||||
To: to,
|
||||
Shift: shift,
|
||||
@@ -121,13 +122,13 @@ func (a *App) watchGridUpdates(rw http.ResponseWriter, req *http.Request) {
|
||||
c := make(chan *TileData, 1000)
|
||||
mc := make(chan *Merge, 5)
|
||||
|
||||
a.gridUpdates.watch(c)
|
||||
a.mergeUpdates.watch(mc)
|
||||
a.gridUpdates.Watch(c)
|
||||
a.mergeUpdates.Watch(mc)
|
||||
|
||||
tileCache := make([]TileCache, 0, 100)
|
||||
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
tiles := tx.Bucket([]byte("tiles"))
|
||||
tiles := tx.Bucket(store.BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,18 +2,21 @@ package app
|
||||
|
||||
import "sync"
|
||||
|
||||
type topic struct {
|
||||
c []chan *TileData
|
||||
// Topic is a generic pub/sub for broadcasting updates.
|
||||
type Topic[T any] struct {
|
||||
c []chan *T
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *topic) watch(c chan *TileData) {
|
||||
// Watch subscribes a channel to receive updates.
|
||||
func (t *Topic[T]) Watch(c chan *T) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.c = append(t.c, c)
|
||||
}
|
||||
|
||||
func (t *topic) send(b *TileData) {
|
||||
// Send broadcasts to all subscribers.
|
||||
func (t *Topic[T]) Send(b *T) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
for i := 0; i < len(t.c); i++ {
|
||||
@@ -27,7 +30,8 @@ func (t *topic) send(b *TileData) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *topic) close() {
|
||||
// Close closes all subscriber channels.
|
||||
func (t *Topic[T]) Close() {
|
||||
for _, c := range t.c {
|
||||
close(c)
|
||||
}
|
||||
@@ -38,35 +42,3 @@ type Merge struct {
|
||||
From, To int
|
||||
Shift Coord
|
||||
}
|
||||
|
||||
type mergeTopic struct {
|
||||
c []chan *Merge
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *mergeTopic) watch(c chan *Merge) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.c = append(t.c, c)
|
||||
}
|
||||
|
||||
func (t *mergeTopic) send(b *Merge) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
for i := 0; i < len(t.c); i++ {
|
||||
select {
|
||||
case t.c[i] <- b:
|
||||
default:
|
||||
close(t.c[i])
|
||||
t.c[i] = t.c[len(t.c)-1]
|
||||
t.c = t.c[:len(t.c)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *mergeTopic) close() {
|
||||
for _, c := range t.c {
|
||||
close(c)
|
||||
}
|
||||
t.c = t.c[:0]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user