diff options
Diffstat (limited to 'internal/admin')
23 files changed, 234 insertions, 261 deletions
diff --git a/internal/admin/api_drops.go b/internal/admin/api_drops.go index 3724a6c..4cb0ae9 100644 --- a/internal/admin/api_drops.go +++ b/internal/admin/api_drops.go @@ -15,7 +15,7 @@ func (s *AdminServer) handleDrops(w http.ResponseWriter, r *http.Request) { case http.MethodGet: tree, err := listYAMLTree(s.dataDir, "drops") if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } if tree == nil { @@ -44,17 +44,17 @@ func (s *AdminServer) handleDropByID(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - s.getDrop(w, r, id) + s.getDrop(w, id) case http.MethodPut: s.updateDrop(w, r, id) case http.MethodDelete: - s.deleteDrop(w, r, id) + s.deleteDrop(w, id) default: http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } } -func (s *AdminServer) getDrop(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) getDrop(w http.ResponseWriter, id string) { data, path, err := readYAMLFile(s.dataDir, "drops", id) if err != nil { http.Error(w, `{"error":"drop not found"}`, http.StatusNotFound) @@ -108,7 +108,7 @@ func (s *AdminServer) updateDrop(w http.ResponseWriter, r *http.Request, id stri writeJSON(w, m) } -func (s *AdminServer) deleteDrop(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) deleteDrop(w http.ResponseWriter, id string) { _, path, err := readYAMLFile(s.dataDir, "drops", id) if err != nil { http.Error(w, `{"error":"drop not found"}`, http.StatusNotFound) diff --git a/internal/admin/api_files.go b/internal/admin/api_files.go index ea75249..d32bda4 100644 --- a/internal/admin/api_files.go +++ b/internal/admin/api_files.go @@ -57,18 +57,18 @@ func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) { NewName string `json:"new_name"` } if err := readJSON(r, &body); err != nil || body.Path == "" || body.NewName == "" { - writeJSON(w, map[string]any{"error": "missing path or new_name"}) + failJSON(w, "missing path or new_name") return } newName := strings.TrimSpace(body.NewName) if newName == "" || newName == "." || newName == ".." || strings.ContainsAny(newName, "/\\") { - writeJSON(w, map[string]any{"error": "invalid file name"}) + failJSON(w, "invalid file name") return } for _, c := range newName { if c < 32 || c == 127 { - writeJSON(w, map[string]any{"error": "file name contains invalid characters"}) + failJSON(w, "file name contains invalid characters") return } } @@ -77,28 +77,28 @@ func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) { relPath = strings.TrimPrefix(relPath, "data/") oldAbs, err := resolveDataPath(s.dataDir, relPath) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } oldContent, err := snapshotFile(oldAbs) if err != nil { - writeJSON(w, map[string]any{"error": "file not found: " + relPath}) + failJSON(w, "file not found: "+relPath) return } newAbs := filepath.Join(filepath.Dir(oldAbs), newName) if _, err := os.Stat(newAbs); err == nil { - writeJSON(w, map[string]any{"error": "a file named " + newName + " already exists"}) + failJSON(w, "a file named "+newName+" already exists") return } if err := os.WriteFile(newAbs, oldContent, 0644); err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } if err := os.Remove(oldAbs); err != nil { - writeJSON(w, map[string]any{"error": "remove error: " + err.Error()}) + failJSON(w, "remove error: "+err.Error()) return } @@ -122,7 +122,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { relPath = strings.TrimPrefix(relPath, "data/") fullPath, pathErr := resolveDataPath(s.dataDir, relPath) if pathErr != nil { - writeJSON(w, map[string]any{"error": pathErr.Error()}) + failJSON(w, pathErr.Error()) return } @@ -131,7 +131,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { if relPath == "" { entries, err := os.ReadDir(fullPath) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } var files []map[string]any @@ -148,14 +148,14 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { fullPath := filepath.Join(s.dataDir, relPath) info, err := os.Stat(fullPath) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + relPath}) + failJSON(w, "not found: "+relPath) return } if info.IsDir() { entries, err := os.ReadDir(fullPath) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } var files []map[string]any @@ -171,7 +171,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { data, err := os.ReadFile(fullPath) if err != nil { - writeJSON(w, map[string]any{"error": "read error: " + err.Error()}) + failJSON(w, "read error: "+err.Error()) return } writeJSON(w, map[string]any{ @@ -182,12 +182,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { case http.MethodPut, http.MethodPost: if relPath == "" { - writeJSON(w, map[string]any{"error": "missing path"}) + failJSON(w, "missing path") return } body, err := io.ReadAll(r.Body) if err != nil { - writeJSON(w, map[string]any{"error": "read body: " + err.Error()}) + failJSON(w, "read body: "+err.Error()) return } @@ -198,12 +198,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { - writeJSON(w, map[string]any{"error": "mkdir: " + err.Error()}) + failJSON(w, "mkdir: "+err.Error()) return } if err := os.WriteFile(fullPath, body, 0644); err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } @@ -223,12 +223,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: if relPath == "" { - writeJSON(w, map[string]any{"error": "missing path"}) + failJSON(w, "missing path") return } oldContent, _ := snapshotFile(fullPath) if err := os.Remove(fullPath); err != nil { - writeJSON(w, map[string]any{"error": "delete error: " + err.Error()}) + failJSON(w, "delete error: "+err.Error()) return } s.evictDeletedRoomFile(relPath) diff --git a/internal/admin/api_hazards.go b/internal/admin/api_hazards.go index fac68c4..89618e7 100644 --- a/internal/admin/api_hazards.go +++ b/internal/admin/api_hazards.go @@ -13,7 +13,7 @@ func (s *AdminServer) handleHazards(w http.ResponseWriter, r *http.Request) { case http.MethodGet: tree, err := listYAMLTree(s.dataDir, "hazards") if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } if tree == nil { @@ -42,17 +42,17 @@ func (s *AdminServer) handleHazardByID(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - s.getHazard(w, r, id) + s.getHazard(w, id) case http.MethodPut: s.updateHazard(w, r, id) case http.MethodDelete: - s.deleteHazard(w, r, id) + s.deleteHazard(w, id) default: http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } } -func (s *AdminServer) getHazard(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) getHazard(w http.ResponseWriter, id string) { data, path, err := readYAMLFile(s.dataDir, "hazards", id) if err != nil { http.Error(w, `{"error":"hazard not found"}`, http.StatusNotFound) @@ -106,7 +106,7 @@ func (s *AdminServer) updateHazard(w http.ResponseWriter, r *http.Request, id st writeJSON(w, m) } -func (s *AdminServer) deleteHazard(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) deleteHazard(w http.ResponseWriter, id string) { _, path, err := readYAMLFile(s.dataDir, "hazards", id) if err != nil { http.Error(w, `{"error":"hazard not found"}`, http.StatusNotFound) diff --git a/internal/admin/api_items.go b/internal/admin/api_items.go index 749e8ff..8798543 100644 --- a/internal/admin/api_items.go +++ b/internal/admin/api_items.go @@ -40,11 +40,11 @@ func (s *AdminServer) handleItemByID(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - s.getItem(w, r, id) + s.getItem(w, id) case http.MethodPut: s.updateItem(w, r, id) case http.MethodDelete: - s.deleteItem(w, r, id) + s.deleteItem(w, id) default: http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } @@ -66,7 +66,7 @@ func (s *AdminServer) findItemFile(id string) ([]byte, string, error) { return findYAMLFileInSubdirs(s.dataDir, "items", id) } -func (s *AdminServer) getItem(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) getItem(w http.ResponseWriter, id string) { data, path, err := s.findItemFile(id) if err != nil { http.Error(w, `{"error":"item not found"}`, http.StatusNotFound) @@ -120,7 +120,7 @@ func (s *AdminServer) updateItem(w http.ResponseWriter, r *http.Request, id stri writeJSON(w, m) } -func (s *AdminServer) deleteItem(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) deleteItem(w http.ResponseWriter, id string) { _, path, err := s.findItemFile(id) if err != nil { http.Error(w, `{"error":"item not found"}`, http.StatusNotFound) diff --git a/internal/admin/api_items_undo_test.go b/internal/admin/api_items_undo_test.go index 3ccbe10..d554bc6 100644 --- a/internal/admin/api_items_undo_test.go +++ b/internal/admin/api_items_undo_test.go @@ -31,7 +31,7 @@ func TestAccountAPIRedactsMultilinePasswordHash(t *testing.T) { s := &AdminServer{accountStore: player.NewAccountStore(dir)} rr := httptest.NewRecorder() - s.getAccount(rr, httptest.NewRequest("GET", "/api/players/account/alice", nil), "alice") + s.getAccount(rr, "alice") if strings.Contains(rr.Body.String(), "sensitive-hash-sentinel") { t.Fatalf("password hash leaked in response: %s", rr.Body.String()) diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go index 5b0464d..5eac385 100644 --- a/internal/admin/api_map.go +++ b/internal/admin/api_map.go @@ -463,10 +463,6 @@ func (s *AdminServer) handleNextRoomID(w http.ResponseWriter, r *http.Request) { if fromStr != "" { fromID, _ = strconv.Atoi(fromStr) } - id, _, err := s.nextRoomID(fromID) - if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) - return - } + id, _ := s.nextRoomID(fromID) writeJSON(w, map[string]any{"id": id}) } diff --git a/internal/admin/api_mobs.go b/internal/admin/api_mobs.go index 8531e40..c3e47bb 100644 --- a/internal/admin/api_mobs.go +++ b/internal/admin/api_mobs.go @@ -13,7 +13,7 @@ func (s *AdminServer) handleMobs(w http.ResponseWriter, r *http.Request) { case http.MethodGet: tree, err := listYAMLTree(s.dataDir, "mobs") if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } if tree == nil { @@ -42,11 +42,11 @@ func (s *AdminServer) handleMobByID(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - s.getMob(w, r, id) + s.getMob(w, id) case http.MethodPut: s.updateMob(w, r, id) case http.MethodDelete: - s.deleteMob(w, r, id) + s.deleteMob(w, id) default: http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } @@ -60,7 +60,7 @@ func (s *AdminServer) findMobFile(id string) ([]byte, string, error) { return findYAMLFileInSubdirs(s.dataDir, "mobs", id) } -func (s *AdminServer) getMob(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) getMob(w http.ResponseWriter, id string) { data, path, err := s.findMobFile(id) if err != nil { http.Error(w, `{"error":"mob not found"}`, http.StatusNotFound) @@ -110,7 +110,7 @@ func (s *AdminServer) updateMob(w http.ResponseWriter, r *http.Request, id strin writeJSON(w, m) } -func (s *AdminServer) deleteMob(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) deleteMob(w http.ResponseWriter, id string) { _, path, err := s.findMobFile(id) if err != nil { http.Error(w, `{"error":"mob not found"}`, http.StatusNotFound) diff --git a/internal/admin/api_modules.go b/internal/admin/api_modules.go index 074eeea..1d8c25b 100644 --- a/internal/admin/api_modules.go +++ b/internal/admin/api_modules.go @@ -12,7 +12,7 @@ func (s *AdminServer) handleModules(w http.ResponseWriter, r *http.Request) { case http.MethodGet: tree, err := listYAMLTree(s.dataDir, "modules") if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } if tree == nil { @@ -22,23 +22,23 @@ func (s *AdminServer) handleModules(w http.ResponseWriter, r *http.Request) { case http.MethodPost: var m map[string]any if err := readJSON(r, &m); err != nil { - writeJSON(w, map[string]any{"error": "invalid json"}) + failJSON(w, "invalid json") return } id, ok := m["id"].(string) if !ok || strings.TrimSpace(id) == "" { - writeJSON(w, map[string]any{"error": "missing id"}) + failJSON(w, "missing id") return } delete(m, "id") path := filepath.Join(s.dataDir, "modules", id+".yaml") if _, err := os.Stat(path); err == nil { - writeJSON(w, map[string]any{"error": "module already exists"}) + failJSON(w, "module already exists") return } newContent, err := writeMapAsYAML(path, m) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ @@ -68,12 +68,12 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) { case http.MethodGet: data, path, err := findYAMLFileInSubdirs(s.dataDir, "modules", id) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } raw, err := yamlToMap(data) if err != nil { - writeJSON(w, map[string]any{"error": "parse error: " + err.Error()}) + failJSON(w, "parse error: "+err.Error()) return } raw["id"] = id @@ -84,7 +84,7 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) { case http.MethodPost, http.MethodPut: var body map[string]any if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()}) + failJSON(w, "invalid JSON: "+err.Error()) return } delete(body, "id") @@ -99,7 +99,7 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) { newContent, err := writeMapAsYAML(path, body) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } @@ -120,12 +120,12 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: _, path, err := findYAMLFileInSubdirs(s.dataDir, "modules", id) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } oldContent, _ := snapshotFile(path) if err := os.Remove(path); err != nil { - writeJSON(w, map[string]any{"error": "delete error: " + err.Error()}) + failJSON(w, "delete error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ diff --git a/internal/admin/api_objects.go b/internal/admin/api_objects.go index 8f9eac5..4e1e621 100644 --- a/internal/admin/api_objects.go +++ b/internal/admin/api_objects.go @@ -40,11 +40,11 @@ func (s *AdminServer) handleObjectByID(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - s.getObject(w, r, id) + s.getObject(w, id) case http.MethodPut: s.updateObject(w, r, id) case http.MethodDelete: - s.deleteObject(w, r, id) + s.deleteObject(w, id) default: http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } @@ -58,7 +58,7 @@ func (s *AdminServer) findObjectFile(id string) ([]byte, string, error) { return findYAMLFileInSubdirs(s.dataDir, "objects", id) } -func (s *AdminServer) getObject(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) getObject(w http.ResponseWriter, id string) { data, path, err := s.findObjectFile(id) if err != nil { http.Error(w, `{"error":"object not found"}`, http.StatusNotFound) @@ -111,7 +111,7 @@ func (s *AdminServer) updateObject(w http.ResponseWriter, r *http.Request, id st writeJSON(w, m) } -func (s *AdminServer) deleteObject(w http.ResponseWriter, r *http.Request, id string) { +func (s *AdminServer) deleteObject(w http.ResponseWriter, id string) { _, path, err := s.findObjectFile(id) if err != nil { http.Error(w, `{"error":"object not found"}`, http.StatusNotFound) diff --git a/internal/admin/api_players.go b/internal/admin/api_players.go index f0bbe31..66142c5 100644 --- a/internal/admin/api_players.go +++ b/internal/admin/api_players.go @@ -21,7 +21,7 @@ func (s *AdminServer) handlePlayers(w http.ResponseWriter, r *http.Request) { charsDir := filepath.Join(s.dataDir, "players", "characters") entries, err := os.ReadDir(charsDir) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } @@ -87,14 +87,14 @@ func (s *AdminServer) handlePlayerByID(w http.ResponseWriter, r *http.Request) { } if strings.HasPrefix(path, "account/") { - s.getAccount(w, r, strings.TrimPrefix(path, "account/")) + s.getAccount(w, strings.TrimPrefix(path, "account/")) return } - s.getCharacter(w, r, path) + s.getCharacter(w, path) } -func (s *AdminServer) getCharacter(w http.ResponseWriter, r *http.Request, name string) { +func (s *AdminServer) getCharacter(w http.ResponseWriter, name string) { canonical, err := s.accountStore.FindCharacter(name) if err != nil { writeJSONError(w, "character not found: "+name, http.StatusNotFound) @@ -136,7 +136,7 @@ func (s *AdminServer) getCharacter(w http.ResponseWriter, r *http.Request, name writeJSON(w, m) } -func (s *AdminServer) getAccount(w http.ResponseWriter, r *http.Request, name string) { +func (s *AdminServer) getAccount(w http.ResponseWriter, name string) { canonical, err := s.accountStore.FindAccount(name) if err != nil { writeJSONError(w, "account not found: "+name, http.StatusNotFound) diff --git a/internal/admin/api_room_courses.go b/internal/admin/api_room_courses.go index b8f334e..bebacff 100644 --- a/internal/admin/api_room_courses.go +++ b/internal/admin/api_room_courses.go @@ -131,28 +131,28 @@ func (s *AdminServer) handleCourses(w http.ResponseWriter, r *http.Request) { case http.MethodPost: var body map[string]any if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid json"}) + failJSON(w, "invalid json") return } id, _ := body["id"].(string) id = strings.TrimSpace(id) if id == "" { - writeJSON(w, map[string]any{"error": "missing id"}) + failJSON(w, "missing id") return } if !validEntityID(id) { - writeJSON(w, map[string]any{"error": "invalid id"}) + failJSON(w, "invalid id") return } delete(body, "id") path := s.courseFilePath(id) if _, err := os.Stat(path); err == nil { - writeJSON(w, map[string]any{"error": "course already exists"}) + failJSON(w, "course already exists") return } newContent, err := writeMapAsYAML(path, body) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ @@ -174,11 +174,11 @@ func (s *AdminServer) handleCourses(w http.ResponseWriter, r *http.Request) { func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) { id := strings.TrimPrefix(r.URL.Path, "/api/courses/") if id == "" { - writeJSON(w, map[string]any{"error": "missing id"}) + failJSON(w, "missing id") return } if !validEntityID(id) { - writeJSON(w, map[string]any{"error": "invalid id"}) + failJSON(w, "invalid id") return } path := s.courseFilePath(id) @@ -187,7 +187,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) { case http.MethodGet: m, data, _, err := s.loadCourseMap(id) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } m["id"] = id @@ -197,7 +197,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) { case http.MethodPost, http.MethodPut: var body map[string]any if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()}) + failJSON(w, "invalid JSON: "+err.Error()) return } delete(body, "id") @@ -210,7 +210,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) { } newContent, err := writeMapAsYAML(path, body) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } isCreate := oldContent == nil @@ -236,11 +236,11 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: oldContent, err := snapshotFile(path) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } if err := os.Remove(path); err != nil { - writeJSON(w, map[string]any{"error": "delete error: " + err.Error()}) + failJSON(w, "delete error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ @@ -269,7 +269,7 @@ func (s *AdminServer) handleRoomCourse(w http.ResponseWriter, r *http.Request, r } ids, err := s.listCourseIDs() if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } for _, id := range ids { @@ -314,16 +314,16 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ Obstacle map[string]any `json:"obstacle"` } if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid json"}) + failJSON(w, "invalid json") return } if strings.TrimSpace(body.CourseID) == "" { - writeJSON(w, map[string]any{"error": "missing course_id"}) + failJSON(w, "missing course_id") return } m, _, path, err := s.loadCourseMap(body.CourseID) if err != nil { - writeJSON(w, map[string]any{"error": "course not found: " + body.CourseID}) + failJSON(w, "course not found: "+body.CourseID) return } oldContent, _ := snapshotFile(path) @@ -331,7 +331,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ obstacles := asMapList(m["obstacles"]) for i, o := range obstacles { if obstacleRoomID(o) == roomID { - writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d is already step %d of course %s", roomID, i+1, body.CourseID)}) + failJSON(w, fmt.Sprintf("room %d is already step %d of course %s", roomID, i+1, body.CourseID)) return } } @@ -379,7 +379,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ dir = "north" } if !isHorizontalDir(dir) { - writeJSON(w, map[string]any{"error": "invalid direction: " + dir}) + failJSON(w, "invalid direction: "+dir) return } @@ -396,7 +396,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ newContent, err := writeMapAsYAML(path, m) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ @@ -479,7 +479,7 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ } courseID, deleted := s.detachRoomFromCourse(roomID) if courseID == "" && !deleted { - writeJSON(w, map[string]any{"error": "room is not part of any course"}) + failJSON(w, "room is not part of any course") return } writeJSON(w, map[string]any{"ok": true, "course_id": courseID, "deleted": deleted}) diff --git a/internal/admin/api_room_insert_remove.go b/internal/admin/api_room_insert_remove.go index 812a3ae..45537d6 100644 --- a/internal/admin/api_room_insert_remove.go +++ b/internal/admin/api_room_insert_remove.go @@ -72,11 +72,7 @@ func (s *AdminServer) handleRoomInsert(w http.ResponseWriter, r *http.Request) { return } - newID, subdir, allocErr := s.nextRoomID(aID) - if allocErr != nil { - writeJSONError(w, fmt.Sprintf("could not allocate a room id: %v", allocErr), http.StatusInternalServerError) - return - } + newID, subdir := s.nextRoomID(aID) name := strings.TrimSpace(body.Name) if name == "" { diff --git a/internal/admin/api_room_insert_remove_test.go b/internal/admin/api_room_insert_remove_test.go index f613313..9fbc022 100644 --- a/internal/admin/api_room_insert_remove_test.go +++ b/internal/admin/api_room_insert_remove_test.go @@ -54,16 +54,18 @@ func roomExists(s *AdminServer, id int) bool { return err == nil } -func roomExitTarget(s *AdminServer, id int, dir world.ExitDir) (int, bool) { +// roomExitTarget returns the room an exit points at, or 0 when the room or +// exit does not exist (expected IDs are always >= 1, so 0 fails assertions). +func roomExitTarget(s *AdminServer, id int, dir world.ExitDir) int { r, err := s.world.LoadRoom(id) if err != nil { - return 0, false + return 0 } e, ok := r.Exits[dir] if !ok { - return 0, false + return 0 } - return e.Room, true + return e.Room } // postInsert drives the insert endpoint with the given body and returns the @@ -109,16 +111,16 @@ func TestInsertSuccess(t *testing.T) { } // A→east→new, new→east→2, new→west→1, B→west→new. - if tgt, _ := roomExitTarget(s, 1, world.East); tgt != newID { + if tgt := roomExitTarget(s, 1, world.East); tgt != newID { t.Errorf("A east: expected %d, got %d", newID, tgt) } - if tgt, _ := roomExitTarget(s, newID, world.East); tgt != 2 { + if tgt := roomExitTarget(s, newID, world.East); tgt != 2 { t.Errorf("new east: expected 2, got %d", tgt) } - if tgt, _ := roomExitTarget(s, newID, world.West); tgt != 1 { + if tgt := roomExitTarget(s, newID, world.West); tgt != 1 { t.Errorf("new west: expected 1, got %d", tgt) } - if tgt, _ := roomExitTarget(s, 2, world.West); tgt != newID { + if tgt := roomExitTarget(s, 2, world.West); tgt != newID { t.Errorf("B west: expected %d, got %d", newID, tgt) } @@ -131,10 +133,10 @@ func TestInsertSuccess(t *testing.T) { if roomExists(s, newID) { t.Error("undo: new room file should be gone") } - if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 { + if tgt := roomExitTarget(s, 1, world.East); tgt != 2 { t.Errorf("undo: A east should be 2, got %d", tgt) } - if tgt, _ := roomExitTarget(s, 2, world.West); tgt != 1 { + if tgt := roomExitTarget(s, 2, world.West); tgt != 1 { t.Errorf("undo: B west should be 1, got %d", tgt) } @@ -147,10 +149,10 @@ func TestInsertSuccess(t *testing.T) { if !roomExists(s, newID) { t.Error("redo: new room file should be back") } - if tgt, _ := roomExitTarget(s, 1, world.East); tgt != newID { + if tgt := roomExitTarget(s, 1, world.East); tgt != newID { t.Errorf("redo: A east should be %d, got %d", newID, tgt) } - if tgt, _ := roomExitTarget(s, 2, world.West); tgt != newID { + if tgt := roomExitTarget(s, 2, world.West); tgt != newID { t.Errorf("redo: B west should be %d, got %d", newID, tgt) } } @@ -202,7 +204,7 @@ func TestInsertGridConflict(t *testing.T) { t.Errorf("insert conflict: expected both colliding room ids (#3 and #8), got %q", msg) } // No partial writes: the world is unchanged. - if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 { + if tgt := roomExitTarget(s, 1, world.East); tgt != 2 { t.Errorf("insert conflict: A east should still be 2, got %d", tgt) } } @@ -228,10 +230,10 @@ func TestRemoveSuccess(t *testing.T) { if roomExists(s, 2) { t.Error("remove: B file should be gone") } - if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 3 { + if tgt := roomExitTarget(s, 1, world.East); tgt != 3 { t.Errorf("remove: A east should be 3, got %d", tgt) } - if tgt, _ := roomExitTarget(s, 3, world.West); tgt != 1 { + if tgt := roomExitTarget(s, 3, world.West); tgt != 1 { t.Errorf("remove: C west should be 1, got %d", tgt) } @@ -244,16 +246,16 @@ func TestRemoveSuccess(t *testing.T) { if !roomExists(s, 2) { t.Error("undo: B file should be back") } - if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 { + if tgt := roomExitTarget(s, 1, world.East); tgt != 2 { t.Errorf("undo: A east should be 2, got %d", tgt) } - if tgt, _ := roomExitTarget(s, 3, world.West); tgt != 2 { + if tgt := roomExitTarget(s, 3, world.West); tgt != 2 { t.Errorf("undo: C west should be 2, got %d", tgt) } - if tgt, _ := roomExitTarget(s, 2, world.West); tgt != 1 { + if tgt := roomExitTarget(s, 2, world.West); tgt != 1 { t.Errorf("undo: B west should be 1, got %d", tgt) } - if tgt, _ := roomExitTarget(s, 2, world.East); tgt != 3 { + if tgt := roomExitTarget(s, 2, world.East); tgt != 3 { t.Errorf("undo: B east should be 3, got %d", tgt) } @@ -266,7 +268,7 @@ func TestRemoveSuccess(t *testing.T) { if roomExists(s, 2) { t.Error("redo: B file should be gone again") } - if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 3 { + if tgt := roomExitTarget(s, 1, world.East); tgt != 3 { t.Errorf("redo: A east should be 3, got %d", tgt) } } @@ -386,7 +388,7 @@ func TestRemoveGridConflict(t *testing.T) { if !roomExists(s, 2) { t.Error("remove conflict: B file should still exist") } - if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 { + if tgt := roomExitTarget(s, 1, world.East); tgt != 2 { t.Errorf("remove conflict: A east should still be 2, got %d", tgt) } } @@ -434,10 +436,7 @@ func TestNextRoomIDNoCrossSubdirCollision(t *testing.T) { undoStack: NewUndoStack(dir), } // Allocating near room 1 (root) must skip 2 (which lives in zone/). - id, _, err := s.nextRoomID(1) - if err != nil { - t.Fatal(err) - } + id, _ := s.nextRoomID(1) if id == 2 { t.Fatalf("nextRoomID returned %d, which collides with the zone/ room", id) } diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go index 8ee16a5..16fc7a0 100644 --- a/internal/admin/api_rooms.go +++ b/internal/admin/api_rooms.go @@ -21,7 +21,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { case http.MethodGet: ids, err := listRoomIDs(s.dataDir) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } if ids == nil { @@ -33,13 +33,13 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) r.Body.Close() if err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", err)}) + failJSON(w, fmt.Sprintf("read body: %v", err)) return } var m map[string]any if err := json.Unmarshal(body, &m); err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)}) + failJSON(w, fmt.Sprintf("invalid json: %v", err)) return } if m == nil { @@ -93,11 +93,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { if linkFrom != nil { fromID = *linkFrom } - id, subdir, allocErr := s.nextRoomID(fromID) - if allocErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("alloc id: %v", allocErr)}) - return - } + id, subdir := s.nextRoomID(fromID) m["id"] = id if linkFrom != nil && linkDir != nil && *linkDir != "" { @@ -131,11 +127,11 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { conflicts++ }) if conflicts > 0 { - writeJSON(w, map[string]any{"error": "Add Next Step: that grid spot is already occupied — room would cause map overlap"}) + failJSON(w, "Add Next Step: that grid spot is already occupied — room would cause map overlap") return } } else { - writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid direction: %s", *linkDir)}) + failJSON(w, fmt.Sprintf("invalid direction: %s", *linkDir)) return } } @@ -153,7 +149,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { if opp, ok := world.OppositeExit[dir]; ok { srcRoom, loadErr := s.world.LoadRoom(*linkFrom) if loadErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("load link_from room %d: %v", *linkFrom, loadErr)}) + failJSON(w, fmt.Sprintf("load link_from room %d: %v", *linkFrom, loadErr)) return } @@ -166,7 +162,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { oldContent, _ := snapshotFile(srcPath) newContent, writeErr := writeYAMLFile(srcPath, srcRoom) if writeErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("write source room: %v", writeErr)}) + failJSON(w, fmt.Sprintf("write source room: %v", writeErr)) return } extraFiles = append(extraFiles, ExtraFile{ @@ -185,7 +181,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { path := filepath.Join(subdir, strconv.Itoa(id)+".yaml") newContent, writeErr := writeMapAsYAML(path, m) if writeErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("write room: %v", writeErr)}) + failJSON(w, fmt.Sprintf("write room: %v", writeErr)) return } @@ -214,7 +210,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { } id, err := strconv.Atoi(idStr) if err != nil { - writeJSON(w, map[string]any{"error": "invalid room id"}) + failJSON(w, "invalid room id") return } @@ -234,17 +230,17 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { case http.MethodGet: path, ok := s.world.GetRoomPath(id) if !ok { - writeJSON(w, map[string]any{"error": "room not found"}) + failJSON(w, "room not found") return } data, err := os.ReadFile(path) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } m, err := yamlToMap(data) if err != nil { - writeJSON(w, map[string]any{"error": "yaml parse error"}) + failJSON(w, "yaml parse error") return } m["id"] = id @@ -253,13 +249,13 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { case http.MethodPut: path, ok := s.world.GetRoomPath(id) if !ok { - writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)}) + failJSON(w, fmt.Sprintf("room %d not found", id)) return } oldContent, snapErr := snapshotFile(path) if snapErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room: %v", snapErr)}) + failJSON(w, fmt.Sprintf("snapshot room: %v", snapErr)) return } @@ -267,11 +263,11 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { body, readErr := io.ReadAll(r.Body) r.Body.Close() if readErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)}) + failJSON(w, fmt.Sprintf("read body: %v", readErr)) return } if err := json.Unmarshal(body, &m); err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)}) + failJSON(w, fmt.Sprintf("invalid json: %v", err)) return } if m == nil { @@ -285,7 +281,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { newContent, writeErr := writeMapAsYAML(path, m) if writeErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("write room: %v", writeErr)}) + failJSON(w, fmt.Sprintf("write room: %v", writeErr)) return } @@ -301,20 +297,20 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: path, ok := s.world.GetRoomPath(id) if !ok { - writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)}) + failJSON(w, fmt.Sprintf("room %d not found", id)) return } oldContent, backupErr := snapshotFile(path) if backupErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("backup room: %v", backupErr)}) + failJSON(w, fmt.Sprintf("backup room: %v", backupErr)) return } courseID, _ := s.detachRoomFromCourse(id) if err := os.Remove(path); err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room: %v", err)}) + failJSON(w, fmt.Sprintf("delete room: %v", err)) return } @@ -433,16 +429,16 @@ func (s *AdminServer) handleRenameRoomDir(w http.ResponseWriter, r *http.Request NewName string `json:"new_name"` } if err := readJSON(r, &body); err != nil || body.NewName == "" { - writeJSON(w, map[string]any{"error": "invalid"}) + failJSON(w, "invalid") return } if body.Dir == "" { - writeJSON(w, map[string]any{"error": "cannot rename root"}) + failJSON(w, "cannot rename root") return } newName := strings.TrimSpace(body.NewName) if newName == "" || strings.ContainsAny(newName, "/\\") || newName == "." || newName == ".." { - writeJSON(w, map[string]any{"error": "invalid directory name"}) + failJSON(w, "invalid directory name") return } base := filepath.Join(s.dataDir, "rooms") @@ -451,15 +447,15 @@ func (s *AdminServer) handleRenameRoomDir(w http.ResponseWriter, r *http.Request newRel := filepath.Join(parent, newName) newPath := filepath.Join(base, newRel) if _, err := os.Stat(oldPath); os.IsNotExist(err) { - writeJSON(w, map[string]any{"error": "directory not found"}) + failJSON(w, "directory not found") return } if _, err := os.Stat(newPath); err == nil { - writeJSON(w, map[string]any{"error": "directory already exists: " + newRel}) + failJSON(w, "directory already exists: "+newRel) return } if err := os.Rename(oldPath, newPath); err != nil { - writeJSON(w, map[string]any{"error": "rename failed: " + err.Error()}) + failJSON(w, "rename failed: "+err.Error()) return } s.world.RebuildRoomIndex(s.dataDir) @@ -475,21 +471,21 @@ func (s *AdminServer) handleCreateRoomDir(w http.ResponseWriter, r *http.Request Name string `json:"name"` } if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid"}) + failJSON(w, "invalid") return } name := strings.TrimSpace(body.Name) if name == "" || strings.ContainsAny(name, "/\\") || name == "." || name == ".." { - writeJSON(w, map[string]any{"error": "invalid directory name"}) + failJSON(w, "invalid directory name") return } newPath := filepath.Join(s.dataDir, "rooms", name) if _, err := os.Stat(newPath); err == nil { - writeJSON(w, map[string]any{"error": "directory already exists"}) + failJSON(w, "directory already exists") return } if err := os.Mkdir(newPath, 0755); err != nil { - writeJSON(w, map[string]any{"error": "create failed: " + err.Error()}) + failJSON(w, "create failed: "+err.Error()) return } writeJSON(w, map[string]any{"ok": true, "name": name}) @@ -802,37 +798,37 @@ func (s *AdminServer) handleRoomMove(w http.ResponseWriter, r *http.Request) { Dir string `json:"dir"` } if err := readJSON(r, &body); err != nil || body.ID <= 0 { - writeJSON(w, map[string]any{"error": "invalid"}) + failJSON(w, "invalid") return } oldPath, ok := s.world.GetRoomPath(body.ID) if !ok { - writeJSON(w, map[string]any{"error": "room not found"}) + failJSON(w, "room not found") return } dir := strings.TrimSpace(body.Dir) destDir := filepath.Join(s.dataDir, "rooms", dir) if _, err := os.Stat(destDir); os.IsNotExist(err) { - writeJSON(w, map[string]any{"error": "directory does not exist: " + dir}) + failJSON(w, "directory does not exist: "+dir) return } fileName := strconv.Itoa(body.ID) + ".yaml" newPath := filepath.Join(destDir, fileName) if _, err := os.Stat(newPath); err == nil { - writeJSON(w, map[string]any{"error": "room already exists in target directory"}) + failJSON(w, "room already exists in target directory") return } data, err := os.ReadFile(oldPath) if err != nil { - writeJSON(w, map[string]any{"error": "read: " + err.Error()}) + failJSON(w, "read: "+err.Error()) return } if err := os.WriteFile(newPath, data, 0644); err != nil { - writeJSON(w, map[string]any{"error": "write: " + err.Error()}) + failJSON(w, "write: "+err.Error()) return } if err := os.Remove(oldPath); err != nil { - writeJSON(w, map[string]any{"error": "remove old: " + err.Error()}) + failJSON(w, "remove old: "+err.Error()) return } s.world.RebuildRoomIndex(s.dataDir) @@ -1137,7 +1133,7 @@ func (s *AdminServer) handleRoomSwap(w http.ResponseWriter, r *http.Request) { func (s *AdminServer) handleDuplicateRooms(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { - writeJSON(w, map[string]any{"error": "method not allowed"}) + failJSON(w, "method not allowed") return } dups := behavior.CheckDuplicateIDs(filepath.Join(s.dataDir, "rooms")) @@ -1154,7 +1150,7 @@ func (s *AdminServer) handleDuplicateRooms(w http.ResponseWriter, r *http.Reques func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - writeJSON(w, map[string]any{"error": "method not allowed"}) + failJSON(w, "method not allowed") return } var body struct { @@ -1163,7 +1159,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ NewID int `json:"new_id"` } if err := readJSON(r, &body); err != nil || body.Path == "" || body.Action == "" { - writeJSON(w, map[string]any{"error": "invalid body"}) + failJSON(w, "invalid body") return } @@ -1171,7 +1167,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ absRooms, _ := filepath.Abs(roomsDir) absPath, err := filepath.Abs(body.Path) if err != nil || !strings.HasPrefix(absPath, absRooms+string(filepath.Separator)) { - writeJSON(w, map[string]any{"error": "invalid path"}) + failJSON(w, "invalid path") return } @@ -1179,7 +1175,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ case "delete": old, _ := snapshotFile(absPath) if err := os.Remove(absPath); err != nil { - writeJSON(w, map[string]any{"error": "delete failed: " + err.Error()}) + failJSON(w, "delete failed: "+err.Error()) return } s.world.RebuildRoomIndex(s.dataDir) @@ -1196,27 +1192,27 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ case "rename": if body.NewID <= 0 { - writeJSON(w, map[string]any{"error": "new_id must be positive"}) + failJSON(w, "new_id must be positive") return } if _, ok := s.world.GetRoomPath(body.NewID); ok { - writeJSON(w, map[string]any{"error": "room ID already exists"}) + failJSON(w, "room ID already exists") return } newRel := filepath.Join(filepath.Dir(absPath), strconv.Itoa(body.NewID)+".yaml") if _, err := os.Stat(newRel); err == nil { - writeJSON(w, map[string]any{"error": "target file already exists"}) + failJSON(w, "target file already exists") return } oldContent, err := os.ReadFile(absPath) if err != nil { - writeJSON(w, map[string]any{"error": "read failed: " + err.Error()}) + failJSON(w, "read failed: "+err.Error()) return } m, err := yamlToMap(oldContent) if err != nil { - writeJSON(w, map[string]any{"error": "yaml parse failed: " + err.Error()}) + failJSON(w, "yaml parse failed: "+err.Error()) return } // A duplicate file was never in the room index (only one of the pair @@ -1228,11 +1224,11 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ newContent, err := writeMapAsYAML(newRel, m) if err != nil { - writeJSON(w, map[string]any{"error": "write failed: " + err.Error()}) + failJSON(w, "write failed: "+err.Error()) return } if err := os.Remove(absPath); err != nil { - writeJSON(w, map[string]any{"error": "remove old failed: " + err.Error()}) + failJSON(w, "remove old failed: "+err.Error()) return } s.world.RebuildRoomIndex(s.dataDir) @@ -1246,7 +1242,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ writeJSON(w, map[string]any{"ok": true}) default: - writeJSON(w, map[string]any{"error": "unknown action: " + body.Action}) + failJSON(w, "unknown action: "+body.Action) } } @@ -1259,7 +1255,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque Dir string `json:"dir"` } if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid"}) + failJSON(w, "invalid") return } base := filepath.Join(s.dataDir, "rooms") @@ -1267,7 +1263,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque if body.Dir != "" { targetDir = filepath.Join(base, body.Dir) if _, err := os.Stat(targetDir); os.IsNotExist(err) { - writeJSON(w, map[string]any{"error": "directory not found"}) + failJSON(w, "directory not found") return } } @@ -1298,7 +1294,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque } newContent, writeErr := writeMapAsYAML(path, m) if writeErr != nil { - writeJSON(w, map[string]any{"error": "write room: " + writeErr.Error()}) + failJSON(w, "write room: "+writeErr.Error()) return } s.world.RebuildRoomIndex(s.dataDir) diff --git a/internal/admin/api_rooms_bulk.go b/internal/admin/api_rooms_bulk.go index a7fcee7..d10e0e3 100644 --- a/internal/admin/api_rooms_bulk.go +++ b/internal/admin/api_rooms_bulk.go @@ -44,23 +44,23 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) { body, readErr := io.ReadAll(r.Body) r.Body.Close() if readErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)}) + failJSON(w, fmt.Sprintf("read body: %v", readErr)) return } var req bulkEditRequest if err := json.Unmarshal(body, &req); err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)}) + failJSON(w, fmt.Sprintf("invalid json: %v", err)) return } if len(req.IDs) == 0 { - writeJSON(w, map[string]any{"error": "no room ids"}) + failJSON(w, "no room ids") return } if isEmptyPatch(req.Patch) { - writeJSON(w, map[string]any{"error": "no patch fields"}) + failJSON(w, "no patch fields") return } @@ -78,17 +78,17 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) { for _, id := range req.IDs { path, ok := s.world.GetRoomPath(id) if !ok { - writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)}) + failJSON(w, fmt.Sprintf("room %d not found", id)) return } oldContent, err := os.ReadFile(path) if err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("read room %d: %v", id, err)}) + failJSON(w, fmt.Sprintf("read room %d: %v", id, err)) return } m, yamlErr := yamlToMap(oldContent) if yamlErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("parse room %d: %v", id, yamlErr)}) + failJSON(w, fmt.Sprintf("parse room %d: %v", id, yamlErr)) return } @@ -110,13 +110,13 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) { if patch.Dir != nil && *patch.Dir != "" { destDir := filepath.Join(s.dataDir, "rooms", *patch.Dir) if _, statErr := os.Stat(destDir); os.IsNotExist(statErr) { - writeJSON(w, map[string]any{"error": fmt.Sprintf("directory does not exist: %s", *patch.Dir)}) + failJSON(w, fmt.Sprintf("directory does not exist: %s", *patch.Dir)) return } newPath = filepath.Join(destDir, strconv.Itoa(id)+".yaml") if newPath != path { if _, statErr := os.Stat(newPath); statErr == nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d already exists in target directory", id)}) + failJSON(w, fmt.Sprintf("room %d already exists in target directory", id)) return } } @@ -127,7 +127,7 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) { newContent, marshalErr := marshalMapYAML(m) if marshalErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("marshal room %d: %v", id, marshalErr)}) + failJSON(w, fmt.Sprintf("marshal room %d: %v", id, marshalErr)) return } writes = append(writes, roomWrite{id: id, oldPath: path, newPath: newPath, oldContent: oldContent, newContent: newContent}) @@ -138,17 +138,17 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) { for _, wr := range writes { if wr.newPath != wr.oldPath { if err := txn.Move(wr.oldPath, wr.newPath); err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("move room %d: %v", wr.id, err)}) + failJSON(w, fmt.Sprintf("move room %d: %v", wr.id, err)) return } } if err := txn.Replace(wr.newPath, wr.newContent, 0644); err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("write room %d: %v", wr.id, err)}) + failJSON(w, fmt.Sprintf("write room %d: %v", wr.id, err)) return } } if err := txn.Commit(); err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("commit bulk edit: %v", err)}) + failJSON(w, fmt.Sprintf("commit bulk edit: %v", err)) return } diff --git a/internal/admin/api_rooms_bulk_delete.go b/internal/admin/api_rooms_bulk_delete.go index 70c2eb6..5b525f6 100644 --- a/internal/admin/api_rooms_bulk_delete.go +++ b/internal/admin/api_rooms_bulk_delete.go @@ -24,18 +24,18 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) { body, readErr := io.ReadAll(r.Body) r.Body.Close() if readErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)}) + failJSON(w, fmt.Sprintf("read body: %v", readErr)) return } var req bulkDeleteRequest if err := json.Unmarshal(body, &req); err != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)}) + failJSON(w, fmt.Sprintf("invalid json: %v", err)) return } if len(req.IDs) == 0 { - writeJSON(w, map[string]any{"error": "no room ids"}) + failJSON(w, "no room ids") return } @@ -45,7 +45,7 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) { seenIDs := make(map[int]bool, len(req.IDs)) for _, id := range req.IDs { if seenIDs[id] { - writeJSON(w, map[string]any{"error": fmt.Sprintf("duplicate room id %d in request", id)}) + failJSON(w, fmt.Sprintf("duplicate room id %d in request", id)) return } seenIDs[id] = true @@ -61,12 +61,12 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) { for _, id := range req.IDs { path, ok := s.world.GetRoomPath(id) if !ok { - writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)}) + failJSON(w, fmt.Sprintf("room %d not found", id)) return } oldContent, snapErr := snapshotFile(path) if snapErr != nil { - writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room %d: %v", id, snapErr)}) + failJSON(w, fmt.Sprintf("snapshot room %d: %v", id, snapErr)) return } snapshots = append(snapshots, roomSnapshot{id: id, path: path, oldContent: oldContent}) @@ -85,7 +85,7 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) { } } } - writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room %d: %v", snap.id, err)}) + failJSON(w, fmt.Sprintf("delete room %d: %v", snap.id, err)) return } deletedSet[snap.id] = true diff --git a/internal/admin/api_rooms_bulk_delete_test.go b/internal/admin/api_rooms_bulk_delete_test.go index 083b890..66464a5 100644 --- a/internal/admin/api_rooms_bulk_delete_test.go +++ b/internal/admin/api_rooms_bulk_delete_test.go @@ -14,20 +14,20 @@ import ( "testing" ) -func postBulkDelete(t *testing.T, s *AdminServer, body string) (map[string]any, int) { +func postBulkDelete(t *testing.T, s *AdminServer, body string) map[string]any { t.Helper() req := httptest.NewRequest(http.MethodPost, "/api/rooms/bulk-delete", strings.NewReader(body)) rec := httptest.NewRecorder() s.handleBulkDelete(rec, req) var resp map[string]any _ = json.Unmarshal(rec.Body.Bytes(), &resp) - return resp, rec.Code + return resp } func TestBulkDeleteRejectsDuplicateIDs(t *testing.T) { s := newTestAdminServer(t, map[int]string{1: "id: 1\nname: Room\n"}) path, _ := s.world.GetRoomPath(1) - resp, _ := postBulkDelete(t, s, `{"ids":[1,1]}`) + resp := postBulkDelete(t, s, `{"ids":[1,1]}`) if errMsg, _ := resp["error"].(string); !strings.Contains(errMsg, "duplicate room id 1") { t.Fatalf("duplicate IDs not rejected: %v", resp) } @@ -60,7 +60,7 @@ func TestBulkDeleteFailureRollsBackDeletions(t *testing.T) { t.Cleanup(func() { _ = os.Chmod(sub, 0o755); os.RemoveAll(sub) }) path1, _ := s.world.GetRoomPath(1) - resp, _ := postBulkDelete(t, s, `{"ids":[1,3]}`) + resp := postBulkDelete(t, s, `{"ids":[1,3]}`) if errMsg, _ := resp["error"].(string); !strings.Contains(errMsg, "delete room 3") { t.Fatalf("expected room-3 delete failure: %v", resp) } diff --git a/internal/admin/api_search.go b/internal/admin/api_search.go index d54e7f1..b97f97e 100644 --- a/internal/admin/api_search.go +++ b/internal/admin/api_search.go @@ -18,7 +18,7 @@ func (s *AdminServer) handleSearch(w http.ResponseWriter, r *http.Request) { q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q"))) if q == "" { - writeJSON(w, map[string]any{"error": "missing query parameter 'q'"}) + failJSON(w, "missing query parameter 'q'") return } diff --git a/internal/admin/api_techs.go b/internal/admin/api_techs.go index ba3288a..91eb929 100644 --- a/internal/admin/api_techs.go +++ b/internal/admin/api_techs.go @@ -20,7 +20,7 @@ func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) { case http.MethodGet: tree, err := listYAMLTree(s.dataDir, "techs") if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } if tree == nil { @@ -30,23 +30,23 @@ func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) { case http.MethodPost: var m map[string]any if err := readJSON(r, &m); err != nil { - writeJSON(w, map[string]any{"error": "invalid json"}) + failJSON(w, "invalid json") return } id, ok := m["id"].(string) if !ok || strings.TrimSpace(id) == "" { - writeJSON(w, map[string]any{"error": "missing id"}) + failJSON(w, "missing id") return } delete(m, "id") if _, _, err := s.findTechFile(id); err == nil { - writeJSON(w, map[string]any{"error": "tech already exists"}) + failJSON(w, "tech already exists") return } path := filepath.Join(s.dataDir, "techs", id+".yaml") newContent, err := writeMapAsYAML(path, m) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ @@ -76,12 +76,12 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) { case http.MethodGet: data, path, err := s.findTechFile(id) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } raw, err := yamlToMap(data) if err != nil { - writeJSON(w, map[string]any{"error": "parse error: " + err.Error()}) + failJSON(w, "parse error: "+err.Error()) return } raw["id"] = id @@ -92,7 +92,7 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) { case http.MethodPost, http.MethodPut: var body map[string]any if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()}) + failJSON(w, "invalid JSON: "+err.Error()) return } delete(body, "id") @@ -114,7 +114,7 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) { newContent, err := writeMapAsYAML(path, body) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } @@ -134,12 +134,12 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: _, path, err := s.findTechFile(id) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } oldContent, _ := snapshotFile(path) if err := os.Remove(path); err != nil { - writeJSON(w, map[string]any{"error": "delete error: " + err.Error()}) + failJSON(w, "delete error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ diff --git a/internal/admin/api_triggers.go b/internal/admin/api_triggers.go index 87eb82e..5b82465 100644 --- a/internal/admin/api_triggers.go +++ b/internal/admin/api_triggers.go @@ -22,7 +22,7 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) { writeJSON(w, &YAMLTree{Root: []string{}, Dirs: map[string][]string{}}) return } - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } if tree == nil { @@ -32,16 +32,16 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) { case http.MethodPost: var m map[string]any if err := readJSON(r, &m); err != nil { - writeJSON(w, map[string]any{"error": "invalid json"}) + failJSON(w, "invalid json") return } id, ok := m["id"].(string) if !ok || strings.TrimSpace(id) == "" { - writeJSON(w, map[string]any{"error": "missing id"}) + failJSON(w, "missing id") return } if !validEntityID(id) { - writeJSON(w, map[string]any{"error": "invalid id"}) + failJSON(w, "invalid id") return } delete(m, "id") @@ -51,13 +51,13 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) { path := filepath.Join(s.dataDir, "triggers", subdir, id+".yaml") os.MkdirAll(filepath.Dir(path), 0755) if _, err := os.Stat(path); err == nil { - writeJSON(w, map[string]any{"error": "trigger already exists"}) + failJSON(w, "trigger already exists") return } newContent, err := writeMapAsYAML(path, m) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ @@ -87,17 +87,17 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request) case http.MethodGet: path, _, err := findTriggerFile(s.dataDir, id) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } data, err := os.ReadFile(path) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } raw, err := yamlToMap(data) if err != nil { - writeJSON(w, map[string]any{"error": "parse error: " + err.Error()}) + failJSON(w, "parse error: "+err.Error()) return } raw["id"] = id @@ -109,7 +109,7 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request) case http.MethodPut: var body map[string]any if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()}) + failJSON(w, "invalid JSON: "+err.Error()) return } delete(body, "id") @@ -134,12 +134,12 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request) triggerData, err := triggerMapToYAML(body) if err != nil { - writeJSON(w, map[string]any{"error": "serialize error: " + err.Error()}) + failJSON(w, "serialize error: "+err.Error()) return } if err := os.WriteFile(newPath, triggerData, 0644); err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) + failJSON(w, "write error: "+err.Error()) return } @@ -159,16 +159,16 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request) case http.MethodDelete: path, _, err := findTriggerFile(s.dataDir, id) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } oldContent, err := snapshotFile(path) if err != nil { - writeJSON(w, map[string]any{"error": "not found: " + id}) + failJSON(w, "not found: "+id) return } if err := os.Remove(path); err != nil { - writeJSON(w, map[string]any{"error": "delete error: " + err.Error()}) + failJSON(w, "delete error: "+err.Error()) return } s.undoStack.Push(ChangeDesc{ diff --git a/internal/admin/id_alloc.go b/internal/admin/id_alloc.go index 3a3c973..83ca89c 100644 --- a/internal/admin/id_alloc.go +++ b/internal/admin/id_alloc.go @@ -13,7 +13,7 @@ import ( // collide with an ID that lives in a different subdirectory. The starting point // is the local subdir's minimum existing ID (clustering), then the first // globally-free ID scanning upward. -func (s *AdminServer) nextRoomID(fromRoomID int) (int, string, error) { +func (s *AdminServer) nextRoomID(fromRoomID int) (int, string) { base := filepath.Join(s.dataDir, "rooms") subdir, err := findRoomSubdir(base, fromRoomID) if err != nil { @@ -46,7 +46,7 @@ func (s *AdminServer) nextRoomID(fromRoomID int) (int, string, error) { for used[id] { id++ } - return id, subdir, nil + return id, subdir } func findRoomSubdir(base string, roomID int) (string, error) { diff --git a/internal/admin/server.go b/internal/admin/server.go index fb0c53b..2c8681a 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -1,3 +1,6 @@ +// Package admin implements the HTTP admin server: authenticated routes for +// editing game data (rooms, items, mobs, drops, triggers, colors, …), the +// embedded web UI, and the undo stack backing it. package admin import ( @@ -516,6 +519,16 @@ func writeJSONError(w http.ResponseWriter, msg string, code int) { json.NewEncoder(w).Encode(map[string]any{"error": msg}) } +// failJSON writes a JSON error body without changing the HTTP status. This +// is the long-standing wire behavior most admin editors depend on: they read +// res.error out of a 200 response, while api wrappers in admin.js treat any +// non-200 as a thrown raw-text error. Unifying status codes requires a +// coordinated change across the static JS clients; do not silently switch +// call sites to non-200 codes here. +func failJSON(w http.ResponseWriter, msg string) { + writeJSONError(w, msg, http.StatusOK) +} + func readJSON(r *http.Request, v any) error { body, err := io.ReadAll(r.Body) if err != nil { @@ -580,20 +593,20 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc { NewName string `json:"new_name"` } if err := readJSON(r, &body); err != nil { - writeJSON(w, map[string]any{"error": "invalid"}) + failJSON(w, "invalid") return } switch body.Action { case "create": if err := createEntityDir(s.dataDir, entityType, body.Name); err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } writeJSON(w, map[string]any{"ok": true, "name": body.Name}) case "delete": changes, err := deleteEntityDir(s.dataDir, entityType, body.Dir) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } for _, ch := range changes { @@ -602,12 +615,12 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc { writeJSON(w, map[string]any{"ok": true, "moved": len(changes) - 1}) case "rename_dir": if err := renameEntityDir(s.dataDir, entityType, body.Dir, body.NewName); err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } writeJSON(w, map[string]any{"ok": true, "name": body.NewName}) default: - writeJSON(w, map[string]any{"error": "unknown action"}) + failJSON(w, "unknown action") } } } @@ -624,12 +637,12 @@ func (s *AdminServer) handleEntityRename(entityType string) http.HandlerFunc { } if err := readJSON(r, &body); err != nil || body.ID == "" || body.NewID == "" || !validEntityID(body.NewID) { - writeJSON(w, map[string]any{"error": "invalid"}) + failJSON(w, "invalid") return } oldPath, newPath, data, err := renameEntityFile(s.dataDir, entityType, body.ID, body.NewID) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } desc := fmt.Sprintf("rename %s %s to %s", entityType, body.ID, body.NewID) @@ -656,12 +669,12 @@ func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc { Dir string `json:"dir"` } if err := readJSON(r, &body); err != nil || body.ID == "" { - writeJSON(w, map[string]any{"error": "invalid"}) + failJSON(w, "invalid") return } oldPath, newPath, data, err := moveEntityFile(s.dataDir, entityType, body.ID, body.Dir) if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) + failJSON(w, err.Error()) return } desc := fmt.Sprintf("move %s %s to %s/", entityType, body.ID, body.Dir) diff --git a/internal/admin/xterm_to_css.go b/internal/admin/xterm_to_css.go deleted file mode 100644 index 6c1fda4..0000000 --- a/internal/admin/xterm_to_css.go +++ /dev/null @@ -1,27 +0,0 @@ -package admin - -import ( - "fmt" - "strconv" - "strings" - - "thehouseoficarus/internal/color" -) - -func XtermToCSS(xtermHex string) string { - xtermHex = strings.TrimPrefix(xtermHex, "#") - idx, err := strconv.ParseUint(xtermHex, 16, 8) - if err != nil || idx > 255 { - return "#808080" - } - r, g, b := color.Xterm256ToRGB(int(idx)) - return fmt.Sprintf("#%02x%02x%02x", r, g, b) -} - -func XtermColorForCSS(idx int) string { - if idx < 0 || idx > 255 { - return "#808080" - } - r, g, b := color.Xterm256ToRGB(idx) - return fmt.Sprintf("#%02x%02x%02x", r, g, b) -} |
