package admin import ( "bytes" "net/http" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" "thehouseoficarus/internal/player" ) func (s *AdminServer) handlePlayers(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } charsDir := filepath.Join(s.dataDir, "players", "characters") entries, err := os.ReadDir(charsDir) if err != nil { failJSON(w, err.Error()) return } charToAccount := s.buildCharToAccountMap() type playerSummary struct { Name string `json:"name"` Level int `json:"level"` TotalLevel int `json:"total_level"` Room int `json:"room"` Account string `json:"account"` } var players []playerSummary for _, e := range entries { if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { continue } rawName := strings.TrimSuffix(e.Name(), ".yaml") p, err := s.accountStore.LoadCharacter(rawName) if err != nil { continue } players = append(players, playerSummary{ Name: rawName, Level: p.CombatLevel(), TotalLevel: p.TotalLevel(), Room: p.RoomID, Account: charToAccount[rawName], }) } writeJSON(w, players) } func (s *AdminServer) buildCharToAccountMap() map[string]string { m := make(map[string]string) names, err := s.accountStore.ListAccountNames() if err != nil { return m } for _, name := range names { acc, err := s.accountStore.LoadAccount(name) if err != nil { continue } for _, charName := range acc.Characters { m[charName] = acc.Name } } return m } func (s *AdminServer) handlePlayerByID(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/players/") if path == "" { http.Error(w, `{"error":"missing player id"}`, http.StatusBadRequest) return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if strings.HasPrefix(path, "account/") { s.getAccount(w, strings.TrimPrefix(path, "account/")) return } s.getCharacter(w, path) } 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) return } charPath := s.accountStore.CharPath(canonical) data, err := os.ReadFile(charPath) if err != nil { writeJSONError(w, "read error: "+err.Error(), http.StatusInternalServerError) return } m, err := yamlToMap(data) if err != nil { writeJSONError(w, "parse error: "+err.Error(), http.StatusInternalServerError) return } var p player.Player if err := yaml.Unmarshal(data, &p); err == nil { m["combat_level"] = p.CombatLevel() m["total_level"] = p.TotalLevel() m["max_hp"] = p.MaxHP() skillLevels := make(map[string]int) for _, skill := range player.AllSkills { skillLevels[string(skill)] = p.Level(skill) } m["skill_levels"] = skillLevels charToAccount := s.buildCharToAccountMap() if acc, ok := charToAccount[canonical]; ok { m["account"] = acc } } m["_path"] = charPath m["_raw"] = string(data) writeJSON(w, m) } 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) return } accPath := s.accountStore.AccountPath(canonical) data, err := os.ReadFile(accPath) if err != nil { writeJSONError(w, "read error: "+err.Error(), http.StatusInternalServerError) return } m, err := yamlToMap(data) if err != nil { writeJSONError(w, "parse error: "+err.Error(), http.StatusInternalServerError) return } delete(m, "password_hash") m["_path"] = accPath // _raw must not leak the password hash back over the API. Re-encode the // parsed document with the password_hash value replaced so multiline and // quoted values are redacted too — substring masking only covers the key's // own line. m["_raw"] = redactYAMLValue(data, "password_hash") writeJSON(w, m) } // redactYAMLValue returns data re-encoded with every top-level value for the // named key replaced by a fixed placeholder. Empty output falls back to the // original data so a parse failure never silently hides the document. func redactYAMLValue(data []byte, key string) string { var node yaml.Node if err := yaml.Unmarshal(data, &node); err != nil || len(node.Content) == 0 { return string(data) } root := node.Content[0] if root.Kind != yaml.MappingNode { return string(data) } changed := false for i := 0; i+1 < len(root.Content); i += 2 { if root.Content[i].Value == key { root.Content[i+1].Value = "[REDACTED]" root.Content[i+1].Tag = "!!str" root.Content[i+1].Style = 0 changed = true } } if !changed { return string(data) } var buf bytes.Buffer enc := yaml.NewEncoder(&buf) enc.SetIndent(2) if err := enc.Encode(&node); err != nil { return "[REDACTED]" } enc.Close() return buf.String() }