Add initial project structure with backend and frontend setup
- Created backend structure with Go, including main application logic and API endpoints. - Added Docker support for both development and production environments. - Introduced frontend using Nuxt 3 with Tailwind CSS for styling. - Included configuration files for Docker and environment variables. - Established basic documentation for contributing, development, and deployment processes. - Set up .gitignore and .dockerignore files to manage ignored files in the repository.
This commit is contained in:
1124
internal/app/admin.go
Normal file
1124
internal/app/admin.go
Normal file
File diff suppressed because it is too large
Load Diff
832
internal/app/api.go
Normal file
832
internal/app/api.go
Normal file
@@ -0,0 +1,832 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// --- Auth API ---
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func (a *App) 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 {
|
||||
http.Error(rw, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
u := a.getUser(body.User, body.Pass)
|
||||
if u == nil {
|
||||
// Bootstrap: first admin via env HNHMAP_BOOTSTRAP_PASSWORD when no users exist
|
||||
if body.User == "admin" && body.Pass != "" {
|
||||
bootstrap := os.Getenv("HNHMAP_BOOTSTRAP_PASSWORD")
|
||||
if bootstrap != "" && body.Pass == bootstrap {
|
||||
var created bool
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if users.Get([]byte("admin")) != nil {
|
||||
return nil
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(body.Pass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := User{Pass: hash, Auths: Auths{AUTH_ADMIN, AUTH_MAP, AUTH_MARKERS, AUTH_UPLOAD}}
|
||||
raw, _ := json.Marshal(u)
|
||||
users.Put([]byte("admin"), raw)
|
||||
created = true
|
||||
return nil
|
||||
})
|
||||
if created {
|
||||
u = &User{Auths: Auths{AUTH_ADMIN, AUTH_MAP, AUTH_MARKERS, AUTH_UPLOAD}}
|
||||
}
|
||||
}
|
||||
}
|
||||
if u == nil {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
sessionID := a.createSession(body.User, u.Auths.Has("tempadmin"))
|
||||
if sessionID == "" {
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: sessionID,
|
||||
Path: "/",
|
||||
MaxAge: 24 * 7 * 3600,
|
||||
HttpOnly: true,
|
||||
Secure: req.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(meResponse{
|
||||
Username: body.User,
|
||||
Auths: u.Auths,
|
||||
})
|
||||
}
|
||||
|
||||
// setupRequired returns true if no users exist (first run).
|
||||
func (a *App) setupRequired() bool {
|
||||
var required bool
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
ub := tx.Bucket([]byte("users"))
|
||||
if ub == nil {
|
||||
required = true
|
||||
return nil
|
||||
}
|
||||
if ub.Stats().KeyN == 0 {
|
||||
required = true
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return required
|
||||
}
|
||||
|
||||
func (a *App) apiSetup(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(struct {
|
||||
SetupRequired bool `json:"setupRequired"`
|
||||
}{SetupRequired: a.setupRequired()})
|
||||
}
|
||||
|
||||
func (a *App) apiLogout(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := a.getSession(req)
|
||||
if s != nil {
|
||||
a.deleteSession(s)
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *App) apiMe(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := a.getSession(req)
|
||||
if s == nil {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
out := meResponse{Username: s.Username, Auths: s.Auths}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
ub := tx.Bucket([]byte("users"))
|
||||
if ub != nil {
|
||||
uRaw := ub.Get([]byte(s.Username))
|
||||
if uRaw != nil {
|
||||
u := User{}
|
||||
json.Unmarshal(uRaw, &u)
|
||||
out.Tokens = u.Tokens
|
||||
}
|
||||
}
|
||||
config := tx.Bucket([]byte("config"))
|
||||
if config != nil {
|
||||
out.Prefix = string(config.Get([]byte("prefix")))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(out)
|
||||
}
|
||||
|
||||
// createSession creates a session for username, returns session ID or empty string.
|
||||
func (a *App) createSession(username string, tempAdmin bool) string {
|
||||
session := make([]byte, 32)
|
||||
if _, err := rand.Read(session); err != nil {
|
||||
return ""
|
||||
}
|
||||
sid := hex.EncodeToString(session)
|
||||
s := &Session{
|
||||
ID: sid,
|
||||
Username: username,
|
||||
TempAdmin: tempAdmin,
|
||||
}
|
||||
a.saveSession(s)
|
||||
return sid
|
||||
}
|
||||
|
||||
// --- Cabinet API ---
|
||||
|
||||
func (a *App) apiMeTokens(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := a.getSession(req)
|
||||
if s == nil {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if !s.Auths.Has(AUTH_UPLOAD) {
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
tokens := a.generateTokenForUser(s.Username)
|
||||
if tokens == nil {
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string][]string{"tokens": tokens})
|
||||
}
|
||||
|
||||
type passwordRequest struct {
|
||||
Pass string `json:"pass"`
|
||||
}
|
||||
|
||||
func (a *App) apiMePassword(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := a.getSession(req)
|
||||
if s == nil {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
var body passwordRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
http.Error(rw, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := a.setUserPassword(s.Username, body.Pass); err != nil {
|
||||
http.Error(rw, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// generateTokenForUser adds a new token for user and returns the full list.
|
||||
func (a *App) generateTokenForUser(username string) []string {
|
||||
tokenRaw := make([]byte, 16)
|
||||
if _, err := rand.Read(tokenRaw); err != nil {
|
||||
return nil
|
||||
}
|
||||
token := hex.EncodeToString(tokenRaw)
|
||||
var tokens []string
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
ub, _ := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
uRaw := ub.Get([]byte(username))
|
||||
u := User{}
|
||||
if uRaw != nil {
|
||||
json.Unmarshal(uRaw, &u)
|
||||
}
|
||||
u.Tokens = append(u.Tokens, token)
|
||||
tokens = u.Tokens
|
||||
buf, _ := json.Marshal(u)
|
||||
ub.Put([]byte(username), buf)
|
||||
tb, _ := tx.CreateBucketIfNotExists([]byte("tokens"))
|
||||
return tb.Put([]byte(token), []byte(username))
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// setUserPassword sets password for user (empty pass = no change).
|
||||
func (a *App) setUserPassword(username, pass string) error {
|
||||
if pass == "" {
|
||||
return nil
|
||||
}
|
||||
return a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := User{}
|
||||
raw := users.Get([]byte(username))
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
}
|
||||
u.Pass, _ = bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
raw, _ = json.Marshal(u)
|
||||
return users.Put([]byte(username), raw)
|
||||
})
|
||||
}
|
||||
|
||||
// --- Admin API (require admin auth) ---
|
||||
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (a *App) apiAdminUsers(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
var list []string
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("users"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.ForEach(func(k, _ []byte) error {
|
||||
list = append(list, string(k))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(list)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminUserByName(rw http.ResponseWriter, req *http.Request, name string) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
var out struct {
|
||||
Username string `json:"username"`
|
||||
Auths []string `json:"auths"`
|
||||
}
|
||||
out.Username = name
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("users"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
raw := b.Get([]byte(name))
|
||||
if raw != nil {
|
||||
u := User{}
|
||||
json.Unmarshal(raw, &u)
|
||||
out.Auths = u.Auths
|
||||
}
|
||||
return nil
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(out)
|
||||
}
|
||||
|
||||
type adminUserBody struct {
|
||||
User string `json:"user"`
|
||||
Pass string `json:"pass"`
|
||||
Auths []string `json:"auths"`
|
||||
}
|
||||
|
||||
func (a *App) apiAdminUserPost(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := a.requireAdmin(rw, req)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
var body adminUserBody
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil || body.User == "" {
|
||||
http.Error(rw, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tempAdmin := false
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Username == "admin" && users.Get([]byte("admin")) == nil {
|
||||
tempAdmin = true
|
||||
}
|
||||
u := User{}
|
||||
raw := users.Get([]byte(body.User))
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
}
|
||||
if body.Pass != "" {
|
||||
u.Pass, _ = bcrypt.GenerateFromPassword([]byte(body.Pass), bcrypt.DefaultCost)
|
||||
}
|
||||
u.Auths = body.Auths
|
||||
raw, _ = json.Marshal(u)
|
||||
return users.Put([]byte(body.User), raw)
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if body.User == s.Username {
|
||||
s.Auths = body.Auths
|
||||
}
|
||||
if tempAdmin {
|
||||
a.deleteSession(s)
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminUserDelete(rw http.ResponseWriter, req *http.Request, name string) {
|
||||
if req.Method != http.MethodDelete {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := a.requireAdmin(rw, req)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, _ := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
u := User{}
|
||||
raw := users.Get([]byte(name))
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
}
|
||||
tokens, _ := tx.CreateBucketIfNotExists([]byte("tokens"))
|
||||
for _, tok := range u.Tokens {
|
||||
tokens.Delete([]byte(tok))
|
||||
}
|
||||
return users.Delete([]byte(name))
|
||||
})
|
||||
if name == s.Username {
|
||||
a.deleteSession(s)
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type settingsResponse struct {
|
||||
Prefix string `json:"prefix"`
|
||||
DefaultHide bool `json:"defaultHide"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func (a *App) apiAdminSettingsGet(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
out := settingsResponse{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
c := tx.Bucket([]byte("config"))
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
out.Prefix = string(c.Get([]byte("prefix")))
|
||||
out.DefaultHide = c.Get([]byte("defaultHide")) != nil
|
||||
out.Title = string(c.Get([]byte("title")))
|
||||
return nil
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(out)
|
||||
}
|
||||
|
||||
type settingsBody struct {
|
||||
Prefix *string `json:"prefix"`
|
||||
DefaultHide *bool `json:"defaultHide"`
|
||||
Title *string `json:"title"`
|
||||
}
|
||||
|
||||
func (a *App) apiAdminSettingsPost(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
var body settingsBody
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
http.Error(rw, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if body.Prefix != nil {
|
||||
b.Put([]byte("prefix"), []byte(*body.Prefix))
|
||||
}
|
||||
if body.DefaultHide != nil {
|
||||
if *body.DefaultHide {
|
||||
b.Put([]byte("defaultHide"), []byte("1"))
|
||||
} else {
|
||||
b.Delete([]byte("defaultHide"))
|
||||
}
|
||||
}
|
||||
if body.Title != nil {
|
||||
b.Put([]byte("title"), []byte(*body.Title))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type mapInfoJSON struct {
|
||||
ID int `json:"ID"`
|
||||
Name string `json:"Name"`
|
||||
Hidden bool `json:"Hidden"`
|
||||
Priority bool `json:"Priority"`
|
||||
}
|
||||
|
||||
func (a *App) apiAdminMaps(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
var maps []mapInfoJSON
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
mapB := tx.Bucket([]byte("maps"))
|
||||
if mapB == nil {
|
||||
return nil
|
||||
}
|
||||
return mapB.ForEach(func(k, v []byte) error {
|
||||
mi := MapInfo{}
|
||||
json.Unmarshal(v, &mi)
|
||||
if id, err := strconv.Atoi(string(k)); err == nil {
|
||||
mi.ID = id
|
||||
}
|
||||
maps = append(maps, mapInfoJSON{
|
||||
ID: mi.ID,
|
||||
Name: mi.Name,
|
||||
Hidden: mi.Hidden,
|
||||
Priority: mi.Priority,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(maps)
|
||||
}
|
||||
|
||||
type adminMapBody struct {
|
||||
Name string `json:"name"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Priority bool `json:"priority"`
|
||||
}
|
||||
|
||||
func (a *App) apiAdminMapByID(rw http.ResponseWriter, req *http.Request, idStr string) {
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(rw, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Method == http.MethodPost {
|
||||
// update map
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
var body adminMapBody
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
http.Error(rw, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := a.db.Update(func(tx *bbolt.Tx) error {
|
||||
maps, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw := maps.Get([]byte(strconv.Itoa(id)))
|
||||
mi := MapInfo{}
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &mi)
|
||||
}
|
||||
mi.ID = id
|
||||
mi.Name = body.Name
|
||||
mi.Hidden = body.Hidden
|
||||
mi.Priority = body.Priority
|
||||
raw, _ = json.Marshal(mi)
|
||||
return maps.Put([]byte(strconv.Itoa(id)), raw)
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminMapToggleHidden(rw http.ResponseWriter, req *http.Request, idStr string) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(rw, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
var mi MapInfo
|
||||
err = a.db.Update(func(tx *bbolt.Tx) error {
|
||||
maps, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw := maps.Get([]byte(strconv.Itoa(id)))
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &mi)
|
||||
}
|
||||
mi.ID = id
|
||||
mi.Hidden = !mi.Hidden
|
||||
raw, _ = json.Marshal(mi)
|
||||
return maps.Put([]byte(strconv.Itoa(id)), raw)
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(mapInfoJSON{
|
||||
ID: mi.ID,
|
||||
Name: mi.Name,
|
||||
Hidden: mi.Hidden,
|
||||
Priority: mi.Priority,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) apiAdminWipe(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminRebuildZooms(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
a.doRebuildZooms()
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminExport(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
a.export(rw, req)
|
||||
}
|
||||
|
||||
func (a *App) apiAdminMerge(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
a.merge(rw, req)
|
||||
}
|
||||
|
||||
// --- Redirects (for old URLs) ---
|
||||
|
||||
func (a *App) redirectRoot(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path != "/" {
|
||||
http.NotFound(rw, req)
|
||||
return
|
||||
}
|
||||
if a.setupRequired() {
|
||||
http.Redirect(rw, req, "/map/setup", http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.Redirect(rw, req, "/map/profile", http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *App) redirectLogin(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path != "/login" {
|
||||
http.NotFound(rw, req)
|
||||
return
|
||||
}
|
||||
http.Redirect(rw, req, "/map/login", http.StatusFound)
|
||||
}
|
||||
|
||||
// --- API router: /map/api/... ---
|
||||
|
||||
func (a *App) apiRouter(rw http.ResponseWriter, req *http.Request) {
|
||||
path := strings.TrimPrefix(req.URL.Path, "/map/api")
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
||||
// Delegate to existing handlers
|
||||
switch path {
|
||||
case "config":
|
||||
a.config(rw, req)
|
||||
return
|
||||
case "v1/characters":
|
||||
a.getChars(rw, req)
|
||||
return
|
||||
case "v1/markers":
|
||||
a.getMarkers(rw, req)
|
||||
return
|
||||
case "maps":
|
||||
a.getMaps(rw, req)
|
||||
return
|
||||
}
|
||||
if path == "admin/wipeTile" || path == "admin/setCoords" || path == "admin/hideMarker" {
|
||||
switch path {
|
||||
case "admin/wipeTile":
|
||||
a.wipeTile(rw, req)
|
||||
case "admin/setCoords":
|
||||
a.setCoords(rw, req)
|
||||
case "admin/hideMarker":
|
||||
a.hideMarker(rw, req)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case path == "setup":
|
||||
a.apiSetup(rw, req)
|
||||
return
|
||||
case path == "login":
|
||||
a.apiLogin(rw, req)
|
||||
return
|
||||
case path == "logout":
|
||||
a.apiLogout(rw, req)
|
||||
return
|
||||
case path == "me":
|
||||
a.apiMe(rw, req)
|
||||
return
|
||||
case path == "me/tokens":
|
||||
a.apiMeTokens(rw, req)
|
||||
return
|
||||
case path == "me/password":
|
||||
a.apiMePassword(rw, req)
|
||||
return
|
||||
case path == "admin/users":
|
||||
if req.Method == http.MethodPost {
|
||||
a.apiAdminUserPost(rw, req)
|
||||
} else {
|
||||
a.apiAdminUsers(rw, req)
|
||||
}
|
||||
return
|
||||
case strings.HasPrefix(path, "admin/users/"):
|
||||
name := strings.TrimPrefix(path, "admin/users/")
|
||||
if name == "" {
|
||||
http.Error(rw, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if req.Method == http.MethodDelete {
|
||||
a.apiAdminUserDelete(rw, req, name)
|
||||
} else {
|
||||
a.apiAdminUserByName(rw, req, name)
|
||||
}
|
||||
return
|
||||
case path == "admin/settings":
|
||||
if req.Method == http.MethodGet {
|
||||
a.apiAdminSettingsGet(rw, req)
|
||||
} else {
|
||||
a.apiAdminSettingsPost(rw, req)
|
||||
}
|
||||
return
|
||||
case path == "admin/maps":
|
||||
a.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" {
|
||||
a.apiAdminMapToggleHidden(rw, req, idStr)
|
||||
return
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
a.apiAdminMapByID(rw, req, idStr)
|
||||
return
|
||||
}
|
||||
http.Error(rw, "not found", http.StatusNotFound)
|
||||
return
|
||||
case path == "admin/wipe":
|
||||
a.apiAdminWipe(rw, req)
|
||||
return
|
||||
case path == "admin/rebuildZooms":
|
||||
a.apiAdminRebuildZooms(rw, req)
|
||||
return
|
||||
case path == "admin/export":
|
||||
a.apiAdminExport(rw, req)
|
||||
return
|
||||
case path == "admin/merge":
|
||||
a.apiAdminMerge(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
// POST admin/users (create)
|
||||
if path == "admin/users" && req.Method == http.MethodPost {
|
||||
a.apiAdminUserPost(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(rw, "not found", http.StatusNotFound)
|
||||
}
|
||||
331
internal/app/app.go
Normal file
331
internal/app/app.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/webapp"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// App is the main application (map server) state.
|
||||
type App struct {
|
||||
gridStorage string
|
||||
frontendRoot string
|
||||
db *bbolt.DB
|
||||
|
||||
characters map[string]Character
|
||||
chmu sync.RWMutex
|
||||
|
||||
*webapp.WebApp
|
||||
|
||||
gridUpdates topic
|
||||
mergeUpdates mergeTopic
|
||||
}
|
||||
|
||||
// NewApp creates an App with the given storage paths and database.
|
||||
// frontendRoot is the directory for the map SPA (e.g. "frontend").
|
||||
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
|
||||
w, err := webapp.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &App{
|
||||
gridStorage: gridStorage,
|
||||
frontendRoot: frontendRoot,
|
||||
db: db,
|
||||
characters: make(map[string]Character),
|
||||
WebApp: w,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID string
|
||||
Username string
|
||||
Auths Auths `json:"-"`
|
||||
TempAdmin bool
|
||||
}
|
||||
|
||||
type Character struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Map int `json:"map"`
|
||||
Position Position `json:"position"`
|
||||
Type string `json:"type"`
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
type Marker struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
GridID string `json:"gridID"`
|
||||
Position Position `json:"position"`
|
||||
Image string `json:"image"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type FrontendMarker struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Map int `json:"map"`
|
||||
Position Position `json:"position"`
|
||||
Image string `json:"image"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type MapInfo struct {
|
||||
ID int
|
||||
Name string
|
||||
Hidden bool
|
||||
Priority bool
|
||||
}
|
||||
|
||||
type GridData struct {
|
||||
ID string
|
||||
Coord Coord
|
||||
NextUpdate time.Time
|
||||
Map int
|
||||
}
|
||||
|
||||
type Coord struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
func (c Coord) Name() string {
|
||||
return fmt.Sprintf("%d_%d", c.X, c.Y)
|
||||
}
|
||||
|
||||
func (c Coord) Parent() Coord {
|
||||
if c.X < 0 {
|
||||
c.X--
|
||||
}
|
||||
if c.Y < 0 {
|
||||
c.Y--
|
||||
}
|
||||
return Coord{
|
||||
X: c.X / 2,
|
||||
Y: c.Y / 2,
|
||||
}
|
||||
}
|
||||
|
||||
type Auths []string
|
||||
|
||||
func (a Auths) Has(auth string) bool {
|
||||
for _, v := range a {
|
||||
if v == auth {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
AUTH_ADMIN = "admin"
|
||||
AUTH_MAP = "map"
|
||||
AUTH_MARKERS = "markers"
|
||||
AUTH_UPLOAD = "upload"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Pass []byte
|
||||
Auths Auths
|
||||
Tokens []string
|
||||
}
|
||||
|
||||
func (a *App) getSession(req *http.Request) *Session {
|
||||
c, err := req.Cookie("session")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var s *Session
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
sessions := tx.Bucket([]byte("sessions"))
|
||||
if sessions == nil {
|
||||
return nil
|
||||
}
|
||||
session := sessions.Get([]byte(c.Value))
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal(session, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.TempAdmin {
|
||||
s.Auths = Auths{AUTH_ADMIN}
|
||||
return nil
|
||||
}
|
||||
users := tx.Bucket([]byte("users"))
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
raw := users.Get([]byte(s.Username))
|
||||
if raw == nil {
|
||||
s = nil
|
||||
return nil
|
||||
}
|
||||
u := User{}
|
||||
err = json.Unmarshal(raw, &u)
|
||||
if err != nil {
|
||||
s = nil
|
||||
return err
|
||||
}
|
||||
s.Auths = u.Auths
|
||||
return nil
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
func (a *App) deleteSession(s *Session) {
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sessions.Delete([]byte(s.ID))
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) saveSession(s *Session) {
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sessions.Put([]byte(s.ID), buf)
|
||||
})
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func (a *App) getPage(req *http.Request) Page {
|
||||
p := Page{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
c := tx.Bucket([]byte("config"))
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
p.Title = string(c.Get([]byte("title")))
|
||||
return nil
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
func (a *App) getUser(user, pass string) (u *User) {
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
users := tx.Bucket([]byte("users"))
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
raw := users.Get([]byte(user))
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
if bcrypt.CompareHashAndPassword(u.Pass, []byte(pass)) != nil {
|
||||
u = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
// serveMapFrontend serves the map SPA: static files from frontend, fallback to index.html for client-side routes.
|
||||
func (a *App) serveMapFrontend(rw http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
if len(path) <= len("/map/") {
|
||||
path = ""
|
||||
} else {
|
||||
path = path[len("/map/"):]
|
||||
}
|
||||
root := a.frontendRoot
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if path == "." || path == ".." || (len(path) >= 2 && path[:2] == "..") {
|
||||
http.NotFound(rw, req)
|
||||
return
|
||||
}
|
||||
tryPaths := []string{filepath.Join("map", path), path}
|
||||
var f http.File
|
||||
for _, p := range tryPaths {
|
||||
var err error
|
||||
f, err = http.Dir(root).Open(p)
|
||||
if err == nil {
|
||||
path = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if f == nil {
|
||||
http.ServeFile(rw, req, filepath.Join(root, "index.html"))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
stat, err := f.Stat()
|
||||
if err != nil || stat.IsDir() {
|
||||
http.ServeFile(rw, req, filepath.Join(root, "index.html"))
|
||||
return
|
||||
}
|
||||
http.ServeContent(rw, req, stat.Name(), stat.ModTime(), f)
|
||||
}
|
||||
|
||||
// CleanChars runs a background loop that removes stale character entries. Call once as a goroutine.
|
||||
func (a *App) CleanChars() {
|
||||
for range time.Tick(time.Second * 10) {
|
||||
a.chmu.Lock()
|
||||
for n, c := range a.characters {
|
||||
if c.updated.Before(time.Now().Add(-10 * time.Second)) {
|
||||
delete(a.characters, n)
|
||||
}
|
||||
}
|
||||
a.chmu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all HTTP handlers for the app.
|
||||
func (a *App) RegisterRoutes() {
|
||||
http.HandleFunc("/client/", a.client)
|
||||
|
||||
http.HandleFunc("/login", a.redirectLogin)
|
||||
http.HandleFunc("/logout", a.logout)
|
||||
http.HandleFunc("/", a.redirectRoot)
|
||||
http.HandleFunc("/generateToken", a.generateToken)
|
||||
http.HandleFunc("/password", a.changePassword)
|
||||
|
||||
http.HandleFunc("/admin/", a.admin)
|
||||
http.HandleFunc("/admin/user", a.adminUser)
|
||||
http.HandleFunc("/admin/deleteUser", a.deleteUser)
|
||||
http.HandleFunc("/admin/wipe", a.wipe)
|
||||
http.HandleFunc("/admin/setPrefix", a.setPrefix)
|
||||
http.HandleFunc("/admin/setDefaultHide", a.setDefaultHide)
|
||||
http.HandleFunc("/admin/setTitle", a.setTitle)
|
||||
http.HandleFunc("/admin/rebuildZooms", a.rebuildZooms)
|
||||
http.HandleFunc("/admin/export", a.export)
|
||||
http.HandleFunc("/admin/merge", a.merge)
|
||||
http.HandleFunc("/admin/map", a.adminMap)
|
||||
http.HandleFunc("/admin/mapic", a.adminICMap)
|
||||
|
||||
http.HandleFunc("/map/api/", a.apiRouter)
|
||||
http.HandleFunc("/map/updates", a.watchGridUpdates)
|
||||
http.HandleFunc("/map/grids/", a.gridTile)
|
||||
http.HandleFunc("/map/", a.serveMapFrontend)
|
||||
}
|
||||
706
internal/app/client.go
Normal file
706
internal/app/client.go
Normal file
@@ -0,0 +1,706 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var clientPath = regexp.MustCompile("client/([^/]+)/(.*)")
|
||||
|
||||
var UserInfo struct{}
|
||||
|
||||
const VERSION = "4"
|
||||
|
||||
func (a *App) client(rw http.ResponseWriter, req *http.Request) {
|
||||
matches := clientPath.FindStringSubmatch(req.URL.Path)
|
||||
if matches == nil {
|
||||
http.Error(rw, "Client token not found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
auth := false
|
||||
user := ""
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
tb := tx.Bucket([]byte("tokens"))
|
||||
if tb == nil {
|
||||
return nil
|
||||
}
|
||||
userName := tb.Get([]byte(matches[1]))
|
||||
if userName == nil {
|
||||
return nil
|
||||
}
|
||||
ub := tx.Bucket([]byte("users"))
|
||||
if ub == nil {
|
||||
return nil
|
||||
}
|
||||
userRaw := ub.Get(userName)
|
||||
if userRaw == nil {
|
||||
return nil
|
||||
}
|
||||
u := User{}
|
||||
json.Unmarshal(userRaw, &u)
|
||||
if u.Auths.Has(AUTH_UPLOAD) {
|
||||
user = string(userName)
|
||||
auth = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if !auth {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(req.Context(), UserInfo, user)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
switch matches[2] {
|
||||
case "locate":
|
||||
a.locate(rw, req)
|
||||
case "gridUpdate":
|
||||
a.gridUpdate(rw, req)
|
||||
case "gridUpload":
|
||||
a.gridUpload(rw, req)
|
||||
case "positionUpdate":
|
||||
a.updatePositions(rw, req)
|
||||
case "markerUpdate":
|
||||
a.uploadMarkers(rw, req)
|
||||
/*case "mapData":
|
||||
a.mapdataIndex(rw, req)*/
|
||||
case "":
|
||||
http.Redirect(rw, req, "/map/", 302)
|
||||
case "checkVersion":
|
||||
if req.FormValue("version") == VERSION {
|
||||
rw.WriteHeader(200)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
default:
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) updatePositions(rw http.ResponseWriter, req *http.Request) {
|
||||
defer req.Body.Close()
|
||||
craws := map[string]struct {
|
||||
Name string
|
||||
GridID string
|
||||
Coords struct {
|
||||
X, Y int
|
||||
}
|
||||
Type string
|
||||
}{}
|
||||
buf, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.Println("Error reading position update json: ", err)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(buf, &craws)
|
||||
if err != nil {
|
||||
log.Println("Error decoding position update json: ", err)
|
||||
log.Println("Original json: ", string(buf))
|
||||
return
|
||||
}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
a.chmu.Lock()
|
||||
defer a.chmu.Unlock()
|
||||
for id, craw := range craws {
|
||||
grid := grids.Get([]byte(craw.GridID))
|
||||
if grid == nil {
|
||||
return nil
|
||||
}
|
||||
gd := GridData{}
|
||||
json.Unmarshal(grid, &gd)
|
||||
idnum, _ := strconv.Atoi(id)
|
||||
c := Character{
|
||||
Name: craw.Name,
|
||||
ID: idnum,
|
||||
Map: gd.Map,
|
||||
Position: Position{
|
||||
X: craw.Coords.X + (gd.Coord.X * 100),
|
||||
Y: craw.Coords.Y + (gd.Coord.Y * 100),
|
||||
},
|
||||
Type: craw.Type,
|
||||
updated: time.Now(),
|
||||
}
|
||||
old, ok := a.characters[id]
|
||||
if !ok {
|
||||
a.characters[id] = c
|
||||
} else {
|
||||
if old.Type == "player" {
|
||||
if c.Type == "player" {
|
||||
a.characters[id] = c
|
||||
} else {
|
||||
old.Position = c.Position
|
||||
a.characters[id] = old
|
||||
}
|
||||
} else if old.Type != "unknown" {
|
||||
if c.Type != "unknown" {
|
||||
a.characters[id] = c
|
||||
} else {
|
||||
old.Position = c.Position
|
||||
a.characters[id] = old
|
||||
}
|
||||
} else {
|
||||
a.characters[id] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (a *App) uploadMarkers(rw http.ResponseWriter, req *http.Request) {
|
||||
defer req.Body.Close()
|
||||
markers := []struct {
|
||||
Name string
|
||||
GridID string
|
||||
X, Y int
|
||||
Image string
|
||||
Type string
|
||||
Color string
|
||||
}{}
|
||||
buf, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
log.Println("Error reading marker json: ", err)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(buf, &markers)
|
||||
if err != nil {
|
||||
log.Println("Error decoding marker json: ", err)
|
||||
log.Println("Original json: ", string(buf))
|
||||
return
|
||||
}
|
||||
err = a.db.Update(func(tx *bbolt.Tx) error {
|
||||
mb, err := tx.CreateBucketIfNotExists([]byte("markers"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grid, err := mb.CreateBucketIfNotExists([]byte("grid"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idB, err := mb.CreateBucketIfNotExists([]byte("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, mraw := range markers {
|
||||
key := []byte(fmt.Sprintf("%s_%d_%d", mraw.GridID, mraw.X, mraw.Y))
|
||||
if grid.Get(key) != nil {
|
||||
continue
|
||||
}
|
||||
if mraw.Image == "" {
|
||||
mraw.Image = "gfx/terobjs/mm/custom"
|
||||
}
|
||||
id, err := idB.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idKey := []byte(strconv.Itoa(int(id)))
|
||||
m := Marker{
|
||||
Name: mraw.Name,
|
||||
ID: int(id),
|
||||
GridID: mraw.GridID,
|
||||
Position: Position{
|
||||
X: mraw.X,
|
||||
Y: mraw.Y,
|
||||
},
|
||||
Image: mraw.Image,
|
||||
}
|
||||
raw, _ := json.Marshal(m)
|
||||
grid.Put(key, raw)
|
||||
idB.Put(idKey, key)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Error update db: ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
curRaw := grids.Get([]byte(grid))
|
||||
cur := GridData{}
|
||||
if curRaw == nil {
|
||||
return fmt.Errorf("grid not found")
|
||||
}
|
||||
err := json.Unmarshal(curRaw, &cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(rw, "%d;%d;%d", cur.Map, cur.Coord.X, cur.Coord.Y)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
rw.WriteHeader(404)
|
||||
}
|
||||
}
|
||||
|
||||
type GridUpdate struct {
|
||||
Grids [][]string `json:"grids"`
|
||||
}
|
||||
|
||||
type GridRequest struct {
|
||||
GridRequests []string `json:"gridRequests"`
|
||||
Map int `json:"map"`
|
||||
Coords Coord `json:"coords"`
|
||||
}
|
||||
|
||||
func (a *App) gridUpdate(rw http.ResponseWriter, req *http.Request) {
|
||||
defer req.Body.Close()
|
||||
dec := json.NewDecoder(req.Body)
|
||||
grup := GridUpdate{}
|
||||
err := dec.Decode(&grup)
|
||||
if err != nil {
|
||||
log.Println("Error decoding grid request json: ", err)
|
||||
http.Error(rw, "Error decoding request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.Println(grup)
|
||||
|
||||
ops := []struct {
|
||||
mapid int
|
||||
x, y int
|
||||
f string
|
||||
}{}
|
||||
|
||||
greq := GridRequest{}
|
||||
|
||||
err = a.db.Update(func(tx *bbolt.Tx) error {
|
||||
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mapB, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configb, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maps := map[int]struct{ X, Y int }{}
|
||||
for x, row := range grup.Grids {
|
||||
for y, grid := range row {
|
||||
gridRaw := grids.Get([]byte(grid))
|
||||
if gridRaw != nil {
|
||||
gd := GridData{}
|
||||
json.Unmarshal(gridRaw, &gd)
|
||||
maps[gd.Map] = struct{ X, Y int }{gd.Coord.X - x, gd.Coord.Y - y}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(maps) == 0 {
|
||||
seq, err := mapB.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mi := MapInfo{
|
||||
ID: int(seq),
|
||||
Name: strconv.Itoa(int(seq)),
|
||||
Hidden: configb.Get([]byte("defaultHide")) != nil,
|
||||
}
|
||||
raw, _ := json.Marshal(mi)
|
||||
err = mapB.Put([]byte(strconv.Itoa(int(seq))), raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("Client made mapid ", seq)
|
||||
for x, row := range grup.Grids {
|
||||
for y, grid := range row {
|
||||
|
||||
cur := GridData{}
|
||||
cur.ID = grid
|
||||
cur.Map = int(seq)
|
||||
cur.Coord.X = x - 1
|
||||
cur.Coord.Y = y - 1
|
||||
|
||||
raw, err := json.Marshal(cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grids.Put([]byte(grid), raw)
|
||||
greq.GridRequests = append(greq.GridRequests, grid)
|
||||
}
|
||||
}
|
||||
greq.Coords = Coord{0, 0}
|
||||
return nil
|
||||
}
|
||||
|
||||
mapid := -1
|
||||
offset := struct{ X, Y int }{}
|
||||
for id, off := range maps {
|
||||
mi := MapInfo{}
|
||||
mraw := mapB.Get([]byte(strconv.Itoa(id)))
|
||||
if mraw != nil {
|
||||
json.Unmarshal(mraw, &mi)
|
||||
}
|
||||
if mi.Priority {
|
||||
mapid = id
|
||||
offset = off
|
||||
break
|
||||
}
|
||||
if id < mapid || mapid == -1 {
|
||||
mapid = id
|
||||
offset = off
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Client in mapid ", mapid)
|
||||
|
||||
for x, row := range grup.Grids {
|
||||
for y, grid := range row {
|
||||
cur := GridData{}
|
||||
if curRaw := grids.Get([]byte(grid)); curRaw != nil {
|
||||
json.Unmarshal(curRaw, &cur)
|
||||
if time.Now().After(cur.NextUpdate) {
|
||||
greq.GridRequests = append(greq.GridRequests, grid)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
cur.ID = grid
|
||||
cur.Map = mapid
|
||||
cur.Coord.X = x + offset.X
|
||||
cur.Coord.Y = y + offset.Y
|
||||
raw, err := json.Marshal(cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grids.Put([]byte(grid), raw)
|
||||
greq.GridRequests = append(greq.GridRequests, grid)
|
||||
}
|
||||
}
|
||||
if len(grup.Grids) >= 2 && len(grup.Grids[1]) >= 2 {
|
||||
if curRaw := grids.Get([]byte(grup.Grids[1][1])); curRaw != nil {
|
||||
cur := GridData{}
|
||||
json.Unmarshal(curRaw, &cur)
|
||||
greq.Map = cur.Map
|
||||
greq.Coords = cur.Coord
|
||||
}
|
||||
}
|
||||
if len(maps) > 1 {
|
||||
grids.ForEach(func(k, v []byte) error {
|
||||
gd := GridData{}
|
||||
json.Unmarshal(v, &gd)
|
||||
if gd.Map == mapid {
|
||||
return nil
|
||||
}
|
||||
if merge, ok := maps[gd.Map]; ok {
|
||||
var td *TileData
|
||||
mapb, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(gd.Map)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zoom, err := mapb.CreateBucketIfNotExists([]byte(strconv.Itoa(0)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tileraw := zoom.Get([]byte(gd.Coord.Name()))
|
||||
if tileraw != nil {
|
||||
json.Unmarshal(tileraw, &td)
|
||||
}
|
||||
|
||||
gd.Map = mapid
|
||||
gd.Coord.X += offset.X - merge.X
|
||||
gd.Coord.Y += offset.Y - merge.Y
|
||||
raw, _ := json.Marshal(gd)
|
||||
if td != nil {
|
||||
ops = append(ops, struct {
|
||||
mapid int
|
||||
x int
|
||||
y int
|
||||
f string
|
||||
}{
|
||||
mapid: mapid,
|
||||
x: gd.Coord.X,
|
||||
y: gd.Coord.Y,
|
||||
f: td.File,
|
||||
})
|
||||
}
|
||||
grids.Put(k, raw)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
for mergeid, merge := range maps {
|
||||
if mapid == mergeid {
|
||||
continue
|
||||
}
|
||||
mapB.Delete([]byte(strconv.Itoa(mergeid)))
|
||||
log.Println("Reporting merge", mergeid, mapid)
|
||||
a.reportMerge(mergeid, mapid, Coord{X: offset.X - merge.X, Y: offset.Y - merge.Y})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
needProcess := map[zoomproc]struct{}{}
|
||||
for _, op := range ops {
|
||||
a.SaveTile(op.mapid, Coord{X: op.x, Y: op.y}, 0, op.f, time.Now().UnixNano())
|
||||
needProcess[zoomproc{c: Coord{X: op.x, Y: op.y}.Parent(), m: op.mapid}] = struct{}{}
|
||||
}
|
||||
for z := 1; z <= 5; z++ {
|
||||
process := needProcess
|
||||
needProcess = map[zoomproc]struct{}{}
|
||||
for p := range process {
|
||||
a.updateZoomLevel(p.m, p.c, z)
|
||||
needProcess[zoomproc{p.c.Parent(), p.m}] = struct{}{}
|
||||
}
|
||||
}
|
||||
log.Println(greq)
|
||||
json.NewEncoder(rw).Encode(greq)
|
||||
}
|
||||
|
||||
/*
|
||||
func (a *App) mapdataIndex(rw http.ResponseWriter, req *http.Request) {
|
||||
err := a.db.View(func(tx *bbolt.Tx) error {
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
if grids == nil {
|
||||
return fmt.Errorf("grid not found")
|
||||
}
|
||||
return grids.ForEach(func(k, v []byte) error {
|
||||
cur := GridData{}
|
||||
err := json.Unmarshal(v, &cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(rw, "%s,%d,%d,%d\n", cur.ID, cur.Map, cur.Coord.X, cur.Coord.Y)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
rw.WriteHeader(404)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
type ExtraData struct {
|
||||
Season int
|
||||
}
|
||||
|
||||
func (a *App) gridUpload(rw http.ResponseWriter, req *http.Request) {
|
||||
if strings.Count(req.Header.Get("Content-Type"), "=") >= 2 && strings.Count(req.Header.Get("Content-Type"), "\"") == 0 {
|
||||
parts := strings.SplitN(req.Header.Get("Content-Type"), "=", 2)
|
||||
req.Header.Set("Content-Type", parts[0]+"=\""+parts[1]+"\"")
|
||||
}
|
||||
|
||||
err := req.ParseMultipartForm(100000000)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
id := req.FormValue("id")
|
||||
|
||||
extraData := req.FormValue("extraData")
|
||||
if extraData != "" {
|
||||
ed := ExtraData{}
|
||||
json.Unmarshal([]byte(extraData), &ed)
|
||||
if ed.Season == 3 {
|
||||
needTile := false
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
curRaw := b.Get([]byte(id))
|
||||
if curRaw == nil {
|
||||
return fmt.Errorf("Unknown grid id: %s", id)
|
||||
}
|
||||
cur := GridData{}
|
||||
err = json.Unmarshal(curRaw, &cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
maps, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(cur.Map)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zooms, err := maps.CreateBucketIfNotExists([]byte("0"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tdRaw := zooms.Get([]byte(cur.Coord.Name()))
|
||||
if tdRaw == nil {
|
||||
needTile = true
|
||||
return nil
|
||||
}
|
||||
td := TileData{}
|
||||
err = json.Unmarshal(tdRaw, &td)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if td.File == "" {
|
||||
needTile = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if time.Now().After(cur.NextUpdate) {
|
||||
cur.NextUpdate = time.Now().Add(time.Minute * 30)
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Put([]byte(id), raw)
|
||||
|
||||
return nil
|
||||
})
|
||||
if !needTile {
|
||||
log.Println("ignoring tile upload: winter")
|
||||
return
|
||||
} else {
|
||||
log.Println("Missing tile, using winter version")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file, _, err := req.FormFile("file")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("map tile for ", id)
|
||||
|
||||
updateTile := false
|
||||
cur := GridData{}
|
||||
|
||||
mapid := 0
|
||||
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
curRaw := b.Get([]byte(id))
|
||||
if curRaw == nil {
|
||||
return fmt.Errorf("Unknown grid id: %s", id)
|
||||
}
|
||||
err = json.Unmarshal(curRaw, &cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateTile = time.Now().After(cur.NextUpdate)
|
||||
mapid = cur.Map
|
||||
|
||||
if updateTile {
|
||||
cur.NextUpdate = time.Now().Add(time.Minute * 30)
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(cur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Put([]byte(id), raw)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if updateTile {
|
||||
os.MkdirAll(fmt.Sprintf("%s/grids", a.gridStorage), 0600)
|
||||
f, err := os.Create(fmt.Sprintf("%s/grids/%s.png", a.gridStorage, cur.ID))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return
|
||||
}
|
||||
f.Close()
|
||||
|
||||
a.SaveTile(mapid, cur.Coord, 0, fmt.Sprintf("grids/%s.png", cur.ID), time.Now().UnixNano())
|
||||
|
||||
c := cur.Coord
|
||||
for z := 1; z <= 5; z++ {
|
||||
c = c.Parent()
|
||||
a.updateZoomLevel(mapid, c, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) updateZoomLevel(mapid int, c Coord, z int) {
|
||||
img := image.NewNRGBA(image.Rect(0, 0, 100, 100))
|
||||
draw.Draw(img, img.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||||
for x := 0; x <= 1; x++ {
|
||||
for y := 0; y <= 1; y++ {
|
||||
subC := c
|
||||
subC.X *= 2
|
||||
subC.Y *= 2
|
||||
subC.X += x
|
||||
subC.Y += y
|
||||
td := a.GetTile(mapid, subC, z-1)
|
||||
if td == nil || td.File == "" {
|
||||
continue
|
||||
}
|
||||
subf, err := os.Open(filepath.Join(a.gridStorage, td.File))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
subimg, _, err := image.Decode(subf)
|
||||
subf.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
draw.BiLinear.Scale(img, image.Rect(50*x, 50*y, 50*x+50, 50*y+50), subimg, subimg.Bounds(), draw.Src, nil)
|
||||
}
|
||||
}
|
||||
os.MkdirAll(fmt.Sprintf("%s/%d/%d", a.gridStorage, mapid, z), 0600)
|
||||
f, err := os.Create(fmt.Sprintf("%s/%d/%d/%s.png", a.gridStorage, mapid, z, c.Name()))
|
||||
a.SaveTile(mapid, c, z, fmt.Sprintf("%d/%d/%s.png", mapid, z, c.Name()), time.Now().UnixNano())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
f.Close()
|
||||
}()
|
||||
png.Encode(f, img)
|
||||
}
|
||||
175
internal/app/manage.go
Normal file
175
internal/app/manage.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (a *App) index(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if s == nil {
|
||||
http.Redirect(rw, req, "/login", 302)
|
||||
return
|
||||
}
|
||||
|
||||
tokens := []string{}
|
||||
prefix := "http://example.com"
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("users"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
uRaw := b.Get([]byte(s.Username))
|
||||
if uRaw == nil {
|
||||
return nil
|
||||
}
|
||||
u := User{}
|
||||
json.Unmarshal(uRaw, &u)
|
||||
tokens = u.Tokens
|
||||
|
||||
config := tx.Bucket([]byte("config"))
|
||||
if config != nil {
|
||||
prefix = string(config.Get([]byte("prefix")))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
a.ExecuteTemplate(rw, "index.tmpl", struct {
|
||||
Page Page
|
||||
Session *Session
|
||||
UploadTokens []string
|
||||
Prefix string
|
||||
}{
|
||||
Page: a.getPage(req),
|
||||
Session: s,
|
||||
UploadTokens: tokens,
|
||||
Prefix: prefix,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) login(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "POST" {
|
||||
u := a.getUser(req.FormValue("user"), req.FormValue("pass"))
|
||||
if u != nil {
|
||||
session := make([]byte, 32)
|
||||
rand.Read(session)
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "session",
|
||||
Expires: time.Now().Add(time.Hour * 24 * 7),
|
||||
Value: hex.EncodeToString(session),
|
||||
})
|
||||
s := &Session{
|
||||
ID: hex.EncodeToString(session),
|
||||
Username: req.FormValue("user"),
|
||||
TempAdmin: u.Auths.Has("tempadmin"),
|
||||
}
|
||||
a.saveSession(s)
|
||||
http.Redirect(rw, req, "/", 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
a.ExecuteTemplate(rw, "login.tmpl", struct {
|
||||
Page Page
|
||||
}{
|
||||
Page: a.getPage(req),
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) logout(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if s != nil {
|
||||
a.deleteSession(s)
|
||||
}
|
||||
http.Redirect(rw, req, "/login", 302)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *App) generateToken(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if s == nil || !s.Auths.Has(AUTH_UPLOAD) {
|
||||
http.Redirect(rw, req, "/", 302)
|
||||
return
|
||||
}
|
||||
tokenRaw := make([]byte, 16)
|
||||
_, err := rand.Read(tokenRaw)
|
||||
if err != nil {
|
||||
rw.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
token := hex.EncodeToString(tokenRaw)
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
ub, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uRaw := ub.Get([]byte(s.Username))
|
||||
if uRaw == nil {
|
||||
return nil
|
||||
}
|
||||
u := User{}
|
||||
err = json.Unmarshal(uRaw, &u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Tokens = append(u.Tokens, token)
|
||||
buf, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ub.Put([]byte(s.Username), buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("tokens"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(token), []byte(s.Username))
|
||||
})
|
||||
http.Redirect(rw, req, "/", 302)
|
||||
}
|
||||
|
||||
func (a *App) changePassword(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if s == nil {
|
||||
http.Redirect(rw, req, "/", 302)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
req.ParseForm()
|
||||
password := req.FormValue("pass")
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
users, err := tx.CreateBucketIfNotExists([]byte("users"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := User{}
|
||||
raw := users.Get([]byte(s.Username))
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
}
|
||||
if password != "" {
|
||||
u.Pass, _ = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
}
|
||||
raw, _ = json.Marshal(u)
|
||||
users.Put([]byte(s.Username), raw)
|
||||
return nil
|
||||
})
|
||||
http.Redirect(rw, req, "/", 302)
|
||||
}
|
||||
|
||||
a.ExecuteTemplate(rw, "password.tmpl", struct {
|
||||
Page Page
|
||||
Session *Session
|
||||
}{
|
||||
Page: a.getPage(req),
|
||||
Session: s,
|
||||
})
|
||||
}
|
||||
144
internal/app/map.go
Normal file
144
internal/app/map.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Title string `json:"title"`
|
||||
Auths []string `json:"auths"`
|
||||
}
|
||||
|
||||
func (a *App) canAccessMap(s *Session) bool {
|
||||
return s != nil && (s.Auths.Has(AUTH_MAP) || s.Auths.Has(AUTH_ADMIN))
|
||||
}
|
||||
|
||||
func (a *App) getChars(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if !a.canAccessMap(s) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if !s.Auths.Has(AUTH_MARKERS) && !s.Auths.Has(AUTH_ADMIN) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode([]interface{}{})
|
||||
return
|
||||
}
|
||||
chars := []Character{}
|
||||
a.chmu.RLock()
|
||||
defer a.chmu.RUnlock()
|
||||
for _, v := range a.characters {
|
||||
chars = append(chars, v)
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(chars)
|
||||
}
|
||||
|
||||
func (a *App) getMarkers(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if !a.canAccessMap(s) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if !s.Auths.Has(AUTH_MARKERS) && !s.Auths.Has(AUTH_ADMIN) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode([]interface{}{})
|
||||
return
|
||||
}
|
||||
markers := []FrontendMarker{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("markers"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
grid := b.Bucket([]byte("grid"))
|
||||
if grid == nil {
|
||||
return nil
|
||||
}
|
||||
grids := tx.Bucket([]byte("grids"))
|
||||
if grids == nil {
|
||||
return nil
|
||||
}
|
||||
return grid.ForEach(func(k, v []byte) error {
|
||||
marker := Marker{}
|
||||
json.Unmarshal(v, &marker)
|
||||
graw := grids.Get([]byte(marker.GridID))
|
||||
if graw == nil {
|
||||
return nil
|
||||
}
|
||||
g := GridData{}
|
||||
json.Unmarshal(graw, &g)
|
||||
markers = append(markers, FrontendMarker{
|
||||
Image: marker.Image,
|
||||
Hidden: marker.Hidden,
|
||||
ID: marker.ID,
|
||||
Name: marker.Name,
|
||||
Map: g.Map,
|
||||
Position: Position{
|
||||
X: marker.Position.X + g.Coord.X*100,
|
||||
Y: marker.Position.Y + g.Coord.Y*100,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(markers)
|
||||
}
|
||||
|
||||
func (a *App) getMaps(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if !a.canAccessMap(s) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
showHidden := s.Auths.Has(AUTH_ADMIN)
|
||||
maps := map[int]*MapInfo{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
mapB := tx.Bucket([]byte("maps"))
|
||||
if mapB == nil {
|
||||
return nil
|
||||
}
|
||||
return mapB.ForEach(func(k, v []byte) error {
|
||||
mapid, err := strconv.Atoi(string(k))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
mi := &MapInfo{}
|
||||
json.Unmarshal(v, &mi)
|
||||
if mi.Hidden && !showHidden {
|
||||
return nil
|
||||
}
|
||||
maps[mapid] = mi
|
||||
return nil
|
||||
})
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(maps)
|
||||
}
|
||||
|
||||
func (a *App) config(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if s == nil {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
config := Config{
|
||||
Auths: s.Auths,
|
||||
}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("config"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
title := b.Get([]byte("title"))
|
||||
config.Title = string(title)
|
||||
return nil
|
||||
})
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(config)
|
||||
}
|
||||
198
internal/app/migrations.go
Normal file
198
internal/app/migrations.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zoom, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(0)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return grids.ForEach(func(k, v []byte) error {
|
||||
g := GridData{}
|
||||
err := json.Unmarshal(v, &g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
td := &TileData{
|
||||
Coord: g.Coord,
|
||||
Zoom: 0,
|
||||
File: fmt.Sprintf("0/%s", g.Coord.Name()),
|
||||
Cache: time.Now().UnixNano(),
|
||||
}
|
||||
raw, err := json.Marshal(td)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return zoom.Put([]byte(g.Coord.Name()), raw)
|
||||
})
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
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"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket([]byte("tiles")) != nil {
|
||||
allTiles := map[string]map[string]TileData{}
|
||||
tiles := tx.Bucket([]byte("tiles"))
|
||||
err := tiles.ForEach(func(k, v []byte) error {
|
||||
zoom := tiles.Bucket(k)
|
||||
zoomTiles := map[string]TileData{}
|
||||
|
||||
allTiles[string(k)] = zoomTiles
|
||||
return zoom.ForEach(func(tk, tv []byte) error {
|
||||
td := TileData{}
|
||||
json.Unmarshal(tv, &td)
|
||||
zoomTiles[string(tk)] = td
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.DeleteBucket([]byte("tiles"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tiles, err = tx.CreateBucket([]byte("tiles"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
maptiles, err := tiles.CreateBucket([]byte("0"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range allTiles {
|
||||
zoom, err := maptiles.CreateBucket([]byte(k))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for tk, tv := range v {
|
||||
raw, _ := json.Marshal(tv)
|
||||
err = zoom.Put([]byte(strings.TrimSuffix(tk, ".png")), raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
err = tiles.SetSequence(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
if tx.Bucket([]byte("markers")) != nil {
|
||||
return tx.DeleteBucket([]byte("markers"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
highest := uint64(0)
|
||||
maps, err := tx.CreateBucketIfNotExists([]byte("maps"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grids, err := tx.CreateBucketIfNotExists([]byte("grids"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mapsFound := map[int]struct{}{}
|
||||
err = grids.ForEach(func(k, v []byte) error {
|
||||
gd := GridData{}
|
||||
err := json.Unmarshal(v, &gd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := mapsFound[gd.Map]; !ok {
|
||||
if uint64(gd.Map) > highest {
|
||||
highest = uint64(gd.Map)
|
||||
}
|
||||
mi := MapInfo{
|
||||
ID: gd.Map,
|
||||
Name: strconv.Itoa(gd.Map),
|
||||
Hidden: false,
|
||||
}
|
||||
raw, _ := json.Marshal(mi)
|
||||
return maps.Put([]byte(strconv.Itoa(gd.Map)), raw)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return maps.SetSequence(highest + 1)
|
||||
},
|
||||
func(tx *bbolt.Tx) error {
|
||||
users := tx.Bucket([]byte("users"))
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
return users.ForEach(func(k, v []byte) error {
|
||||
u := User{}
|
||||
json.Unmarshal(v, &u)
|
||||
if u.Auths.Has(AUTH_MAP) && !u.Auths.Has(AUTH_MARKERS) {
|
||||
u.Auths = append(u.Auths, AUTH_MARKERS)
|
||||
raw, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users.Put(k, raw)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// 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"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vraw := b.Get([]byte("version"))
|
||||
v, _ := strconv.Atoi(string(vraw))
|
||||
if v < len(migrations) {
|
||||
for _, f := range migrations[v:] {
|
||||
if err := f(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.Put([]byte("version"), []byte(strconv.Itoa(len(migrations))))
|
||||
})
|
||||
}
|
||||
269
internal/app/tile.go
Normal file
269
internal/app/tile.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var transparentPNG = []byte{
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
|
||||
0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41,
|
||||
0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
|
||||
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00,
|
||||
0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
|
||||
0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
type TileData struct {
|
||||
MapID int
|
||||
Coord Coord
|
||||
Zoom int
|
||||
File string
|
||||
Cache int64
|
||||
}
|
||||
|
||||
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"))
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
mapb := tiles.Bucket([]byte(strconv.Itoa(mapid)))
|
||||
if mapb == nil {
|
||||
return nil
|
||||
}
|
||||
zoom := mapb.Bucket([]byte(strconv.Itoa(z)))
|
||||
if zoom == nil {
|
||||
return nil
|
||||
}
|
||||
tileraw := zoom.Get([]byte(c.Name()))
|
||||
if tileraw == nil {
|
||||
return nil
|
||||
}
|
||||
json.Unmarshal(tileraw, &td)
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mapb, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(mapid)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zoom, err := mapb.CreateBucketIfNotExists([]byte(strconv.Itoa(z)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
td := &TileData{
|
||||
MapID: mapid,
|
||||
Coord: c,
|
||||
Zoom: z,
|
||||
File: f,
|
||||
Cache: t,
|
||||
}
|
||||
raw, err := json.Marshal(td)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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{
|
||||
From: from,
|
||||
To: to,
|
||||
Shift: shift,
|
||||
})
|
||||
}
|
||||
|
||||
type TileCache struct {
|
||||
M, X, Y, Z, T int
|
||||
}
|
||||
|
||||
func (a *App) watchGridUpdates(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if !a.canAccessMap(s) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "text/event-stream")
|
||||
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
rw.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := rw.(http.Flusher)
|
||||
|
||||
if !ok {
|
||||
http.Error(rw, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c := make(chan *TileData, 1000)
|
||||
mc := make(chan *Merge, 5)
|
||||
|
||||
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"))
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
return tiles.ForEach(func(mk, mv []byte) error {
|
||||
mapb := tiles.Bucket(mk)
|
||||
if mapb == nil {
|
||||
return nil
|
||||
}
|
||||
return mapb.ForEach(func(k, v []byte) error {
|
||||
zoom := mapb.Bucket(k)
|
||||
if zoom == nil {
|
||||
return nil
|
||||
}
|
||||
return zoom.ForEach(func(tk, tv []byte) error {
|
||||
td := TileData{}
|
||||
json.Unmarshal(tv, &td)
|
||||
tileCache = append(tileCache, TileCache{
|
||||
M: td.MapID,
|
||||
X: td.Coord.X,
|
||||
Y: td.Coord.Y,
|
||||
Z: td.Zoom,
|
||||
T: int(td.Cache),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
raw, _ := json.Marshal(tileCache)
|
||||
fmt.Fprint(rw, "data: ")
|
||||
rw.Write(raw)
|
||||
fmt.Fprint(rw, "\n\n")
|
||||
tileCache = tileCache[:0]
|
||||
flusher.Flush()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case e, ok := <-c:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for i := range tileCache {
|
||||
if tileCache[i].M == e.MapID && tileCache[i].X == e.Coord.X && tileCache[i].Y == e.Coord.Y && tileCache[i].Z == e.Zoom {
|
||||
tileCache[i].T = int(e.Cache)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
tileCache = append(tileCache, TileCache{
|
||||
M: e.MapID,
|
||||
X: e.Coord.X,
|
||||
Y: e.Coord.Y,
|
||||
Z: e.Zoom,
|
||||
T: int(e.Cache),
|
||||
})
|
||||
}
|
||||
case e, ok := <-mc:
|
||||
log.Println(e, ok)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
log.Println(string(raw))
|
||||
fmt.Fprint(rw, "event: merge\n")
|
||||
fmt.Fprint(rw, "data: ")
|
||||
rw.Write(raw)
|
||||
fmt.Fprint(rw, "\n\n")
|
||||
flusher.Flush()
|
||||
case <-ticker.C:
|
||||
raw, _ := json.Marshal(tileCache)
|
||||
fmt.Fprint(rw, "data: ")
|
||||
rw.Write(raw)
|
||||
fmt.Fprint(rw, "\n\n")
|
||||
tileCache = tileCache[:0]
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tileRegex = regexp.MustCompile("([0-9]+)/([0-9]+)/([-0-9]+)_([-0-9]+).png")
|
||||
|
||||
func (a *App) gridTile(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if !a.canAccessMap(s) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tile := tileRegex.FindStringSubmatch(req.URL.Path)
|
||||
if tile == nil || len(tile) < 5 {
|
||||
http.Error(rw, "invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mapid, err := strconv.Atoi(tile[1])
|
||||
if err != nil {
|
||||
http.Error(rw, "request parsing error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
z, err := strconv.Atoi(tile[2])
|
||||
if err != nil {
|
||||
http.Error(rw, "request parsing error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
x, err := strconv.Atoi(tile[3])
|
||||
if err != nil {
|
||||
http.Error(rw, "request parsing error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
y, err := strconv.Atoi(tile[4])
|
||||
if err != nil {
|
||||
http.Error(rw, "request parsing error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Map frontend Leaflet zoom (1…6) to storage level (0…5): z=6 → 0 (max detail), z=1..5 → same
|
||||
storageZ := z
|
||||
if storageZ == 6 {
|
||||
storageZ = 0
|
||||
}
|
||||
if storageZ < 0 || storageZ > 5 {
|
||||
storageZ = 0
|
||||
}
|
||||
td := a.GetTile(mapid, Coord{X: x, Y: y}, storageZ)
|
||||
if td == nil {
|
||||
rw.Header().Set("Content-Type", "image/png")
|
||||
rw.Header().Set("Cache-Control", "private, max-age=3600")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write(transparentPNG)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "image/png")
|
||||
rw.Header().Set("Cache-Control", "private immutable")
|
||||
http.ServeFile(rw, req, filepath.Join(a.gridStorage, td.File))
|
||||
}
|
||||
72
internal/app/topic.go
Normal file
72
internal/app/topic.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package app
|
||||
|
||||
import "sync"
|
||||
|
||||
type topic struct {
|
||||
c []chan *TileData
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *topic) watch(c chan *TileData) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.c = append(t.c, c)
|
||||
}
|
||||
|
||||
func (t *topic) send(b *TileData) {
|
||||
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 *topic) close() {
|
||||
for _, c := range t.c {
|
||||
close(c)
|
||||
}
|
||||
t.c = t.c[:0]
|
||||
}
|
||||
|
||||
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