- Added optional HTTP server timeout configurations (`HNHMAP_READ_TIMEOUT`, `HNHMAP_WRITE_TIMEOUT`, `HNHMAP_IDLE_TIMEOUT`) to `.env.example` and updated the server initialization in `main.go` to utilize these settings. - Enhanced API documentation for the `rebuildZooms` endpoint to clarify its background processing and polling mechanism for status updates. - Updated `configuration.md` to include new timeout environment variables for better configuration guidance. - Improved error handling in the client for large request bodies, ensuring appropriate responses for oversized payloads.
451 lines
13 KiB
Go
451 lines
13 KiB
Go
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 !h.requireMethod(rw, req, http.MethodPost) {
|
|
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 !h.requireMethod(rw, req, http.MethodGet) {
|
|
return
|
|
}
|
|
if h.requireAdmin(rw, req) == nil {
|
|
return
|
|
}
|
|
auths, found, err := h.Admin.GetUser(req.Context(), name)
|
|
if err != nil {
|
|
HandleServiceError(rw, err)
|
|
return
|
|
}
|
|
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 !h.requireMethod(rw, req, http.MethodDelete) {
|
|
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 !h.requireMethod(rw, req, http.MethodGet) {
|
|
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 !h.requireMethod(rw, req, http.MethodPost) {
|
|
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 !h.requireMethod(rw, req, http.MethodGet) {
|
|
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 !h.requireMethod(rw, req, http.MethodPost) {
|
|
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 !h.requireMethod(rw, req, http.MethodPost) {
|
|
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 !h.requireMethod(rw, req, http.MethodPost) {
|
|
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.
|
|
// It starts the rebuild in the background and returns 202 Accepted immediately.
|
|
func (h *Handlers) APIAdminRebuildZooms(rw http.ResponseWriter, req *http.Request) {
|
|
if !h.requireMethod(rw, req, http.MethodPost) {
|
|
return
|
|
}
|
|
if h.requireAdmin(rw, req) == nil {
|
|
return
|
|
}
|
|
h.Admin.StartRebuildZooms()
|
|
rw.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
// APIAdminRebuildZoomsStatus handles GET /map/api/admin/rebuildZooms/status.
|
|
// Returns {"running": true|false} so the client can poll until the rebuild finishes.
|
|
func (h *Handlers) APIAdminRebuildZoomsStatus(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
|
|
}
|
|
running := h.Admin.RebuildZoomsRunning()
|
|
JSON(rw, http.StatusOK, map[string]bool{"running": running})
|
|
}
|
|
|
|
// APIAdminExport handles GET /map/api/admin/export.
|
|
func (h *Handlers) APIAdminExport(rw http.ResponseWriter, req *http.Request) {
|
|
if !h.requireMethod(rw, req, http.MethodGet) {
|
|
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 !h.requireMethod(rw, req, http.MethodPost) {
|
|
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 == "rebuildZooms/status":
|
|
h.APIAdminRebuildZoomsStatus(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")
|
|
}
|
|
}
|