Add configuration files and update project documentation
- Introduced .editorconfig for consistent coding styles across the project. - Added .golangci.yml for Go linting configuration. - Updated AGENTS.md to clarify project structure and components. - Enhanced CONTRIBUTING.md with Makefile usage for common tasks. - Updated Dockerfiles to use Go 1.24 and improved build instructions. - Refined README.md and deployment documentation for clarity. - Added testing documentation in testing.md for backend and frontend tests. - Introduced Makefile for streamlined development commands and tasks.
This commit is contained in:
441
internal/app/handlers/admin.go
Normal file
441
internal/app/handlers/admin.go
Normal file
@@ -0,0 +1,441 @@
|
||||
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
|
||||
}
|
||||
h.Admin.RebuildZooms(req.Context())
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user