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:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user