|
|
|
|
@ -2,6 +2,7 @@ package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
@ -33,8 +34,8 @@ type AdminHandler struct {
|
|
|
|
|
hub *Hub
|
|
|
|
|
pool *pgxpool.Pool
|
|
|
|
|
logger *slog.Logger
|
|
|
|
|
adminUser string
|
|
|
|
|
adminPass string
|
|
|
|
|
adminUser string
|
|
|
|
|
adminPass string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewAdminHandler creates a new AdminHandler with all required dependencies.
|
|
|
|
|
@ -133,10 +134,18 @@ func buildAdminLiveMovementSnap(hm *game.HeroMovement) *adminLiveMovementJSON {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AdminHandler) writeAdminHeroDetail(w http.ResponseWriter, hero *model.Hero) {
|
|
|
|
|
if hero == nil {
|
|
|
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "nil hero"})
|
|
|
|
|
out, err := h.buildAdminHeroDetail(hero)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AdminHandler) buildAdminHeroDetail(hero *model.Hero) (adminHeroDetailResponse, error) {
|
|
|
|
|
if hero == nil {
|
|
|
|
|
return adminHeroDetailResponse{}, fmt.Errorf("nil hero")
|
|
|
|
|
}
|
|
|
|
|
now := time.Now()
|
|
|
|
|
hero.RefreshDerivedCombatStats(now)
|
|
|
|
|
out := adminHeroDetailResponse{Hero: *hero, TownPause: hero.TownPause}
|
|
|
|
|
@ -146,7 +155,7 @@ func (h *AdminHandler) writeAdminHeroDetail(w http.ResponseWriter, hero *model.H
|
|
|
|
|
out.TownPause = hm.Hero.TownPause
|
|
|
|
|
out.AdminLiveMovement = buildAdminLiveMovementSnap(hm)
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, out)
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListHeroes returns a paginated list of all heroes.
|
|
|
|
|
@ -1859,6 +1868,74 @@ func (h *AdminHandler) WSConnections(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AdminHeroSnapshotWS streams hero detail + movement snapshot for admin UI.
|
|
|
|
|
// GET /admin-ws/hero/{heroId}?auth=BASE64(user:pass)
|
|
|
|
|
func (h *AdminHandler) AdminHeroSnapshotWS(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !h.adminWSAuthorized(r) {
|
|
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Admin", charset="UTF-8"`)
|
|
|
|
|
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
heroID, err := parseHeroID(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid heroId"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.logger.Error("admin ws upgrade failed", "error", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
|
|
done := make(chan struct{})
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
|
|
|
|
for {
|
|
|
|
|
if _, _, err := conn.ReadMessage(); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
sendSnapshot := func() error {
|
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if hero == nil {
|
|
|
|
|
return fmt.Errorf("hero not found")
|
|
|
|
|
}
|
|
|
|
|
snap, err := h.buildAdminHeroDetail(hero)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
conn.SetWriteDeadline(time.Now().Add(writeWait))
|
|
|
|
|
return conn.WriteJSON(snap)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := sendSnapshot(); err != nil {
|
|
|
|
|
_ = conn.WriteJSON(map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ticker := time.NewTicker(time.Second)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-done:
|
|
|
|
|
return
|
|
|
|
|
case <-r.Context().Done():
|
|
|
|
|
return
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
if err := sendSnapshot(); err != nil {
|
|
|
|
|
_ = conn.WriteJSON(map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Server Info ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// ServerInfo returns general server diagnostics.
|
|
|
|
|
@ -2048,6 +2125,26 @@ func parseHeroID(r *http.Request) (int64, error) {
|
|
|
|
|
return strconv.ParseInt(chi.URLParam(r, "heroId"), 10, 64)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AdminHandler) adminWSAuthorized(r *http.Request) bool {
|
|
|
|
|
if user, pass, ok := r.BasicAuth(); ok {
|
|
|
|
|
if basicAuthCredentialsMatch(user, pass, h.adminUser, h.adminPass) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
q := r.URL.Query()
|
|
|
|
|
if raw := strings.TrimSpace(q.Get("auth")); raw != "" {
|
|
|
|
|
if decoded, err := base64.StdEncoding.DecodeString(raw); err == nil {
|
|
|
|
|
parts := strings.SplitN(string(decoded), ":", 2)
|
|
|
|
|
if len(parts) == 2 && basicAuthCredentialsMatch(parts[0], parts[1], h.adminUser, h.adminPass) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
user := q.Get("user")
|
|
|
|
|
pass := q.Get("pass")
|
|
|
|
|
return basicAuthCredentialsMatch(user, pass, h.adminUser, h.adminPass)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseQuestID(r *http.Request) (int64, error) {
|
|
|
|
|
return strconv.ParseInt(chi.URLParam(r, "questId"), 10, 64)
|
|
|
|
|
}
|
|
|
|
|
|