- Added a new AGENTS.md file to document the project structure and conventions. - Updated .gitignore to include node_modules and refined cursor rules. - Introduced new backend and frontend components for improved map interactions, including context menus and controls. - Enhanced API composables for better admin and authentication functionalities. - Refactored existing components for cleaner code and improved user experience. - Updated README.md to clarify production asset serving and user setup instructions.
585 lines
16 KiB
Go
585 lines
16 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/andyleap/hnh-map/internal/app"
|
|
"github.com/andyleap/hnh-map/internal/app/services"
|
|
)
|
|
|
|
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"`
|
|
}
|
|
|
|
// APILogin handles POST /map/api/login.
|
|
func (h *Handlers) 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 {
|
|
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
|
return
|
|
}
|
|
if u := h.Auth.GetUserByUsername(body.User); u != nil && u.Pass == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Use OAuth to sign in", "OAUTH_ONLY")
|
|
return
|
|
}
|
|
u := h.Auth.GetUser(body.User, body.Pass)
|
|
if u == nil {
|
|
if boot := h.Auth.BootstrapAdmin(body.User, body.Pass, services.GetBootstrapPassword()); boot != nil {
|
|
u = boot
|
|
} else {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
}
|
|
sessionID := h.Auth.CreateSession(body.User, u.Auths.Has("tempadmin"))
|
|
if sessionID == "" {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
http.SetCookie(rw, &http.Cookie{
|
|
Name: "session",
|
|
Value: sessionID,
|
|
Path: "/",
|
|
MaxAge: 24 * 7 * 3600,
|
|
HttpOnly: true,
|
|
Secure: req.TLS != nil,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
JSON(rw, http.StatusOK, meResponse{Username: body.User, Auths: u.Auths})
|
|
}
|
|
|
|
// APISetup handles GET /map/api/setup.
|
|
func (h *Handlers) APISetup(rw http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
JSON(rw, http.StatusOK, struct {
|
|
SetupRequired bool `json:"setupRequired"`
|
|
}{SetupRequired: h.Auth.SetupRequired()})
|
|
}
|
|
|
|
// APILogout handles POST /map/api/logout.
|
|
func (h *Handlers) APILogout(rw http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodPost {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
s := h.Auth.GetSession(req)
|
|
if s != nil {
|
|
h.Auth.DeleteSession(s)
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// APIMe handles GET /map/api/me.
|
|
func (h *Handlers) APIMe(rw http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
s := h.Auth.GetSession(req)
|
|
if s == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
out := meResponse{Username: s.Username, Auths: s.Auths}
|
|
out.Tokens, out.Prefix = h.Auth.GetUserTokensAndPrefix(s.Username)
|
|
JSON(rw, http.StatusOK, out)
|
|
}
|
|
|
|
// APIMeTokens handles POST /map/api/me/tokens.
|
|
func (h *Handlers) APIMeTokens(rw http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodPost {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
s := h.Auth.GetSession(req)
|
|
if s == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
if !s.Auths.Has(app.AUTH_UPLOAD) {
|
|
JSONError(rw, http.StatusForbidden, "Forbidden", "FORBIDDEN")
|
|
return
|
|
}
|
|
tokens := h.Auth.GenerateTokenForUser(s.Username)
|
|
if tokens == nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
JSON(rw, http.StatusOK, map[string][]string{"tokens": tokens})
|
|
}
|
|
|
|
type passwordRequest struct {
|
|
Pass string `json:"pass"`
|
|
}
|
|
|
|
// APIMePassword handles POST /map/api/me/password.
|
|
func (h *Handlers) APIMePassword(rw http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodPost {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
s := h.Auth.GetSession(req)
|
|
if s == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
var body passwordRequest
|
|
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
|
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
|
return
|
|
}
|
|
if err := h.Auth.SetUserPassword(s.Username, body.Pass); err != nil {
|
|
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
|
|
return
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// APIConfig handles GET /map/api/config.
|
|
func (h *Handlers) APIConfig(rw http.ResponseWriter, req *http.Request) {
|
|
s := h.Auth.GetSession(req)
|
|
if s == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
config, err := h.Map.GetConfig(s.Auths)
|
|
if err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
JSON(rw, http.StatusOK, config)
|
|
}
|
|
|
|
// APIGetChars handles GET /map/api/v1/characters.
|
|
func (h *Handlers) APIGetChars(rw http.ResponseWriter, req *http.Request) {
|
|
s := h.Auth.GetSession(req)
|
|
if !h.canAccessMap(s) {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
if !s.Auths.Has(app.AUTH_MARKERS) && !s.Auths.Has(app.AUTH_ADMIN) {
|
|
JSON(rw, http.StatusOK, []interface{}{})
|
|
return
|
|
}
|
|
chars := h.Map.GetCharacters()
|
|
JSON(rw, http.StatusOK, chars)
|
|
}
|
|
|
|
// APIGetMarkers handles GET /map/api/v1/markers.
|
|
func (h *Handlers) APIGetMarkers(rw http.ResponseWriter, req *http.Request) {
|
|
s := h.Auth.GetSession(req)
|
|
if !h.canAccessMap(s) {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
if !s.Auths.Has(app.AUTH_MARKERS) && !s.Auths.Has(app.AUTH_ADMIN) {
|
|
JSON(rw, http.StatusOK, []interface{}{})
|
|
return
|
|
}
|
|
markers, err := h.Map.GetMarkers()
|
|
if err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
JSON(rw, http.StatusOK, markers)
|
|
}
|
|
|
|
// APIGetMaps handles GET /map/api/maps.
|
|
func (h *Handlers) APIGetMaps(rw http.ResponseWriter, req *http.Request) {
|
|
s := h.Auth.GetSession(req)
|
|
if !h.canAccessMap(s) {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
showHidden := s.Auths.Has(app.AUTH_ADMIN)
|
|
maps, err := h.Map.GetMaps(showHidden)
|
|
if err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
JSON(rw, http.StatusOK, maps)
|
|
}
|
|
|
|
// --- Admin API ---
|
|
|
|
// APIAdminUsers handles GET/POST /map/api/admin/users.
|
|
func (h *Handlers) APIAdminUsers(rw http.ResponseWriter, req *http.Request) {
|
|
if req.Method == http.MethodGet {
|
|
if h.requireAdmin(rw, req) == nil {
|
|
return
|
|
}
|
|
list, err := h.Admin.ListUsers()
|
|
if err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
JSON(rw, http.StatusOK, list)
|
|
return
|
|
}
|
|
if req.Method != http.MethodPost {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
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(body.User, body.Pass, body.Auths)
|
|
if err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
if body.User == s.Username {
|
|
s.Auths = body.Auths
|
|
}
|
|
if adminCreated && s.Username == "admin" {
|
|
h.Auth.DeleteSession(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 {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if h.requireAdmin(rw, req) == nil {
|
|
return
|
|
}
|
|
auths, found := h.Admin.GetUser(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 {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
s := h.requireAdmin(rw, req)
|
|
if s == nil {
|
|
return
|
|
}
|
|
if err := h.Admin.DeleteUser(name); err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
if name == s.Username {
|
|
h.Auth.DeleteSession(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 {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if h.requireAdmin(rw, req) == nil {
|
|
return
|
|
}
|
|
prefix, defaultHide, title, err := h.Admin.GetSettings()
|
|
if err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
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 {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
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(body.Prefix, body.DefaultHide, body.Title); err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
type mapInfoJSON struct {
|
|
ID int `json:"ID"`
|
|
Name string `json:"Name"`
|
|
Hidden bool `json:"Hidden"`
|
|
Priority bool `json:"Priority"`
|
|
}
|
|
|
|
// APIAdminMaps handles GET /map/api/admin/maps.
|
|
func (h *Handlers) APIAdminMaps(rw http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if h.requireAdmin(rw, req) == nil {
|
|
return
|
|
}
|
|
maps, err := h.Admin.ListMaps()
|
|
if err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
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 {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
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(id, body.Name, body.Hidden, body.Priority); err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
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 {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if h.requireAdmin(rw, req) == nil {
|
|
return
|
|
}
|
|
mi, err := h.Admin.ToggleMapHidden(id)
|
|
if err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
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 {
|
|
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if h.requireAdmin(rw, req) == nil {
|
|
return
|
|
}
|
|
if err := h.Admin.Wipe(); err != nil {
|
|
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// APIRouter routes /map/api/* requests.
|
|
func (h *Handlers) APIRouter(rw http.ResponseWriter, req *http.Request) {
|
|
path := strings.TrimPrefix(req.URL.Path, "/map/api")
|
|
path = strings.TrimPrefix(path, "/")
|
|
|
|
switch path {
|
|
case "config":
|
|
h.APIConfig(rw, req)
|
|
return
|
|
case "v1/characters":
|
|
h.APIGetChars(rw, req)
|
|
return
|
|
case "v1/markers":
|
|
h.APIGetMarkers(rw, req)
|
|
return
|
|
case "maps":
|
|
h.APIGetMaps(rw, req)
|
|
return
|
|
}
|
|
if path == "admin/wipeTile" || path == "admin/setCoords" || path == "admin/hideMarker" {
|
|
switch path {
|
|
case "admin/wipeTile":
|
|
h.App.WipeTile(rw, req)
|
|
case "admin/setCoords":
|
|
h.App.SetCoords(rw, req)
|
|
case "admin/hideMarker":
|
|
h.App.HideMarker(rw, req)
|
|
}
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case path == "oauth/providers":
|
|
h.App.APIOAuthProviders(rw, req)
|
|
return
|
|
case strings.HasPrefix(path, "oauth/"):
|
|
rest := strings.TrimPrefix(path, "oauth/")
|
|
parts := strings.SplitN(rest, "/", 2)
|
|
if len(parts) != 2 {
|
|
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
|
return
|
|
}
|
|
provider := parts[0]
|
|
action := parts[1]
|
|
switch action {
|
|
case "login":
|
|
h.App.OAuthLogin(rw, req, provider)
|
|
case "callback":
|
|
h.App.OAuthCallback(rw, req, provider)
|
|
default:
|
|
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
|
}
|
|
return
|
|
case path == "setup":
|
|
h.APISetup(rw, req)
|
|
return
|
|
case path == "login":
|
|
h.APILogin(rw, req)
|
|
return
|
|
case path == "logout":
|
|
h.APILogout(rw, req)
|
|
return
|
|
case path == "me":
|
|
h.APIMe(rw, req)
|
|
return
|
|
case path == "me/tokens":
|
|
h.APIMeTokens(rw, req)
|
|
return
|
|
case path == "me/password":
|
|
h.APIMePassword(rw, req)
|
|
return
|
|
case path == "admin/users":
|
|
if req.Method == http.MethodPost {
|
|
h.APIAdminUsers(rw, req)
|
|
} else {
|
|
h.APIAdminUsers(rw, req)
|
|
}
|
|
return
|
|
case strings.HasPrefix(path, "admin/users/"):
|
|
name := strings.TrimPrefix(path, "admin/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)
|
|
}
|
|
return
|
|
case path == "admin/settings":
|
|
if req.Method == http.MethodGet {
|
|
h.APIAdminSettingsGet(rw, req)
|
|
} else {
|
|
h.APIAdminSettingsPost(rw, req)
|
|
}
|
|
return
|
|
case path == "admin/maps":
|
|
h.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" {
|
|
h.APIAdminMapToggleHidden(rw, req, idStr)
|
|
return
|
|
}
|
|
if len(parts) == 1 {
|
|
h.APIAdminMapByID(rw, req, idStr)
|
|
return
|
|
}
|
|
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
|
return
|
|
case path == "admin/wipe":
|
|
h.APIAdminWipe(rw, req)
|
|
return
|
|
case path == "admin/rebuildZooms":
|
|
h.App.APIAdminRebuildZooms(rw, req)
|
|
return
|
|
case path == "admin/export":
|
|
h.App.APIAdminExport(rw, req)
|
|
return
|
|
case path == "admin/merge":
|
|
h.App.APIAdminMerge(rw, req)
|
|
return
|
|
}
|
|
|
|
JSONError(rw, http.StatusNotFound, "not found", "NOT_FOUND")
|
|
}
|