package handlers import ( "archive/zip" "encoding/json" "net/http" "strconv" "strings" "github.com/andyleap/hnh-map/internal/app" ) type mapInfoJSON struct { ID int `json:"ID"` Name string `json:"Name"` Hidden bool `json:"Hidden"` Priority bool `json:"Priority"` } // APIAdminUsers handles GET/POST /map/api/admin/users. func (h *Handlers) APIAdminUsers(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() if req.Method == http.MethodGet { if h.requireAdmin(rw, req) == nil { return } list, err := h.Admin.ListUsers(ctx) if err != nil { HandleServiceError(rw, err) return } JSON(rw, http.StatusOK, list) return } if req.Method != http.MethodPost { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } s := h.requireAdmin(rw, req) if s == nil { return } var body struct { User string `json:"user"` Pass string `json:"pass"` Auths []string `json:"auths"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil || body.User == "" { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } adminCreated, err := h.Admin.CreateOrUpdateUser(ctx, body.User, body.Pass, body.Auths) if err != nil { HandleServiceError(rw, err) return } if body.User == s.Username { s.Auths = body.Auths } if adminCreated && s.Username == "admin" { h.Auth.DeleteSession(ctx, s) } rw.WriteHeader(http.StatusOK) } // APIAdminUserByName handles GET /map/api/admin/users/:name. func (h *Handlers) APIAdminUserByName(rw http.ResponseWriter, req *http.Request, name string) { if req.Method != http.MethodGet { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } auths, found := h.Admin.GetUser(req.Context(), name) out := struct { Username string `json:"username"` Auths []string `json:"auths"` }{Username: name} if found { out.Auths = auths } JSON(rw, http.StatusOK, out) } // APIAdminUserDelete handles DELETE /map/api/admin/users/:name. func (h *Handlers) APIAdminUserDelete(rw http.ResponseWriter, req *http.Request, name string) { if req.Method != http.MethodDelete { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } s := h.requireAdmin(rw, req) if s == nil { return } ctx := req.Context() if err := h.Admin.DeleteUser(ctx, name); err != nil { HandleServiceError(rw, err) return } if name == s.Username { h.Auth.DeleteSession(ctx, s) } rw.WriteHeader(http.StatusOK) } // APIAdminSettingsGet handles GET /map/api/admin/settings. func (h *Handlers) APIAdminSettingsGet(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } prefix, defaultHide, title, err := h.Admin.GetSettings(req.Context()) if err != nil { HandleServiceError(rw, err) return } JSON(rw, http.StatusOK, struct { Prefix string `json:"prefix"` DefaultHide bool `json:"defaultHide"` Title string `json:"title"` }{Prefix: prefix, DefaultHide: defaultHide, Title: title}) } // APIAdminSettingsPost handles POST /map/api/admin/settings. func (h *Handlers) APIAdminSettingsPost(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } var body struct { Prefix *string `json:"prefix"` DefaultHide *bool `json:"defaultHide"` Title *string `json:"title"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } if err := h.Admin.UpdateSettings(req.Context(), body.Prefix, body.DefaultHide, body.Title); err != nil { HandleServiceError(rw, err) return } rw.WriteHeader(http.StatusOK) } // APIAdminMaps handles GET /map/api/admin/maps. func (h *Handlers) APIAdminMaps(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } maps, err := h.Admin.ListMaps(req.Context()) if err != nil { HandleServiceError(rw, err) return } out := make([]mapInfoJSON, len(maps)) for i, m := range maps { out[i] = mapInfoJSON{ID: m.ID, Name: m.Name, Hidden: m.Hidden, Priority: m.Priority} } JSON(rw, http.StatusOK, out) } // APIAdminMapByID handles POST /map/api/admin/maps/:id. func (h *Handlers) APIAdminMapByID(rw http.ResponseWriter, req *http.Request, idStr string) { id, err := strconv.Atoi(idStr) if err != nil { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } if req.Method != http.MethodPost { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } var body struct { Name string `json:"name"` Hidden bool `json:"hidden"` Priority bool `json:"priority"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } if err := h.Admin.UpdateMap(req.Context(), id, body.Name, body.Hidden, body.Priority); err != nil { HandleServiceError(rw, err) return } rw.WriteHeader(http.StatusOK) } // APIAdminMapToggleHidden handles POST /map/api/admin/maps/:id/toggle-hidden. func (h *Handlers) APIAdminMapToggleHidden(rw http.ResponseWriter, req *http.Request, idStr string) { id, err := strconv.Atoi(idStr) if err != nil { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } if req.Method != http.MethodPost { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } mi, err := h.Admin.ToggleMapHidden(req.Context(), id) if err != nil { HandleServiceError(rw, err) return } JSON(rw, http.StatusOK, mapInfoJSON{ ID: mi.ID, Name: mi.Name, Hidden: mi.Hidden, Priority: mi.Priority, }) } // APIAdminWipe handles POST /map/api/admin/wipe. func (h *Handlers) APIAdminWipe(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } if err := h.Admin.Wipe(req.Context()); err != nil { HandleServiceError(rw, err) return } rw.WriteHeader(http.StatusOK) } // APIAdminWipeTile handles POST /map/api/admin/wipeTile. func (h *Handlers) APIAdminWipeTile(rw http.ResponseWriter, req *http.Request) { if h.requireAdmin(rw, req) == nil { return } mapid, err := strconv.Atoi(req.FormValue("map")) if err != nil { JSONError(rw, http.StatusBadRequest, "coord parse failed", "BAD_REQUEST") return } x, err := strconv.Atoi(req.FormValue("x")) if err != nil { JSONError(rw, http.StatusBadRequest, "coord parse failed", "BAD_REQUEST") return } y, err := strconv.Atoi(req.FormValue("y")) if err != nil { JSONError(rw, http.StatusBadRequest, "coord parse failed", "BAD_REQUEST") return } if err := h.Admin.WipeTile(req.Context(), mapid, x, y); err != nil { HandleServiceError(rw, err) return } rw.WriteHeader(http.StatusOK) } // APIAdminSetCoords handles POST /map/api/admin/setCoords. func (h *Handlers) APIAdminSetCoords(rw http.ResponseWriter, req *http.Request) { if h.requireAdmin(rw, req) == nil { return } mapid, err := strconv.Atoi(req.FormValue("map")) if err != nil { JSONError(rw, http.StatusBadRequest, "coord parse failed", "BAD_REQUEST") return } fx, err := strconv.Atoi(req.FormValue("fx")) if err != nil { JSONError(rw, http.StatusBadRequest, "coord parse failed", "BAD_REQUEST") return } fy, err := strconv.Atoi(req.FormValue("fy")) if err != nil { JSONError(rw, http.StatusBadRequest, "coord parse failed", "BAD_REQUEST") return } tx, err := strconv.Atoi(req.FormValue("tx")) if err != nil { JSONError(rw, http.StatusBadRequest, "coord parse failed", "BAD_REQUEST") return } ty, err := strconv.Atoi(req.FormValue("ty")) if err != nil { JSONError(rw, http.StatusBadRequest, "coord parse failed", "BAD_REQUEST") return } if err := h.Admin.SetCoords(req.Context(), mapid, fx, fy, tx, ty); err != nil { HandleServiceError(rw, err) return } rw.WriteHeader(http.StatusOK) } // APIAdminHideMarker handles POST /map/api/admin/hideMarker. func (h *Handlers) APIAdminHideMarker(rw http.ResponseWriter, req *http.Request) { if h.requireAdmin(rw, req) == nil { return } markerID := req.FormValue("id") if markerID == "" { JSONError(rw, http.StatusBadRequest, "missing id", "BAD_REQUEST") return } if err := h.Admin.HideMarker(req.Context(), markerID); err != nil { HandleServiceError(rw, err) return } rw.WriteHeader(http.StatusOK) } // APIAdminRebuildZooms handles POST /map/api/admin/rebuildZooms. func (h *Handlers) APIAdminRebuildZooms(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } if err := h.Admin.RebuildZooms(req.Context()); err != nil { HandleServiceError(rw, err) return } rw.WriteHeader(http.StatusOK) } // APIAdminExport handles GET /map/api/admin/export. func (h *Handlers) APIAdminExport(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } rw.Header().Set("Content-Type", "application/zip") rw.Header().Set("Content-Disposition", `attachment; filename="griddata.zip"`) if err := h.Export.Export(req.Context(), rw); err != nil { HandleServiceError(rw, err) } } // APIAdminMerge handles POST /map/api/admin/merge. func (h *Handlers) APIAdminMerge(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED") return } if h.requireAdmin(rw, req) == nil { return } if err := req.ParseMultipartForm(app.MergeMaxMemory); err != nil { JSONError(rw, http.StatusBadRequest, "request error", "BAD_REQUEST") return } mergef, hdr, err := req.FormFile("merge") if err != nil { JSONError(rw, http.StatusBadRequest, "request error", "BAD_REQUEST") return } zr, err := zip.NewReader(mergef, hdr.Size) if err != nil { JSONError(rw, http.StatusBadRequest, "request error", "BAD_REQUEST") return } if err := h.Export.Merge(req.Context(), zr); err != nil { HandleServiceError(rw, err) return } rw.WriteHeader(http.StatusOK) } // APIAdminRoute routes /map/api/admin/* sub-paths. func (h *Handlers) APIAdminRoute(rw http.ResponseWriter, req *http.Request, path string) { switch { case path == "wipeTile": h.APIAdminWipeTile(rw, req) case path == "setCoords": h.APIAdminSetCoords(rw, req) case path == "hideMarker": h.APIAdminHideMarker(rw, req) case path == "users": h.APIAdminUsers(rw, req) case strings.HasPrefix(path, "users/"): name := strings.TrimPrefix(path, "users/") if name == "" { JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND") return } if req.Method == http.MethodDelete { h.APIAdminUserDelete(rw, req, name) } else { h.APIAdminUserByName(rw, req, name) } case path == "settings": if req.Method == http.MethodGet { h.APIAdminSettingsGet(rw, req) } else { h.APIAdminSettingsPost(rw, req) } case path == "maps": h.APIAdminMaps(rw, req) case strings.HasPrefix(path, "maps/"): rest := strings.TrimPrefix(path, "maps/") parts := strings.SplitN(rest, "/", 2) idStr := parts[0] if len(parts) == 2 && parts[1] == "toggle-hidden" { h.APIAdminMapToggleHidden(rw, req, idStr) return } if len(parts) == 1 { h.APIAdminMapByID(rw, req, idStr) return } JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND") case path == "wipe": h.APIAdminWipe(rw, req) case path == "rebuildZooms": h.APIAdminRebuildZooms(rw, req) case path == "export": h.APIAdminExport(rw, req) case path == "merge": h.APIAdminMerge(rw, req) default: JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND") } }