feat: add /admin endpoint with Basic Auth and JSON stats
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
55
web/admin.go
Normal file
55
web/admin.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tolelom/catacombs/config"
|
||||||
|
"github.com/tolelom/catacombs/game"
|
||||||
|
"github.com/tolelom/catacombs/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminStats is the JSON response for the /admin endpoint.
|
||||||
|
type AdminStats struct {
|
||||||
|
OnlinePlayers int `json:"online_players"`
|
||||||
|
ActiveRooms int `json:"active_rooms"`
|
||||||
|
TodayRuns int `json:"today_runs"`
|
||||||
|
AvgFloorReach float64 `json:"avg_floor_reached"`
|
||||||
|
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminHandler returns an http.Handler for the /admin stats endpoint.
|
||||||
|
// It requires Basic Auth using credentials from config.
|
||||||
|
func AdminHandler(lobby *game.Lobby, db *store.DB, startTime time.Time) http.Handler {
|
||||||
|
cfg := lobby.Cfg()
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !checkAuth(r, cfg.Admin) {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="Catacombs Admin"`)
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todayRuns, _ := db.GetTodayRunCount()
|
||||||
|
avgFloor, _ := db.GetTodayAvgFloor()
|
||||||
|
|
||||||
|
stats := AdminStats{
|
||||||
|
OnlinePlayers: len(lobby.ListOnline()),
|
||||||
|
ActiveRooms: len(lobby.ListRooms()),
|
||||||
|
TodayRuns: todayRuns,
|
||||||
|
AvgFloorReach: avgFloor,
|
||||||
|
UptimeSeconds: int64(time.Since(startTime).Seconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(stats)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAuth(r *http.Request, cfg config.AdminConfig) bool {
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return username == cfg.Username && password == cfg.Password
|
||||||
|
}
|
||||||
64
web/admin_test.go
Normal file
64
web/admin_test.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tolelom/catacombs/config"
|
||||||
|
"github.com/tolelom/catacombs/game"
|
||||||
|
"github.com/tolelom/catacombs/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdminEndpoint(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := store.Open(filepath.Join(tmpDir, "test.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cfg, _ := config.Load("")
|
||||||
|
cfg.Admin = config.AdminConfig{Username: "admin", Password: "secret"}
|
||||||
|
lobby := game.NewLobby(cfg)
|
||||||
|
|
||||||
|
handler := AdminHandler(lobby, db, time.Now())
|
||||||
|
|
||||||
|
// Test without auth → 401
|
||||||
|
req := httptest.NewRequest("GET", "/admin", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("expected 401, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with wrong auth → 401
|
||||||
|
req = httptest.NewRequest("GET", "/admin", nil)
|
||||||
|
req.SetBasicAuth("admin", "wrong")
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("expected 401, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with correct auth → 200 + JSON
|
||||||
|
req = httptest.NewRequest("GET", "/admin", nil)
|
||||||
|
req.SetBasicAuth("admin", "secret")
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats AdminStats
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &stats); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats.OnlinePlayers != 0 {
|
||||||
|
t.Fatalf("expected 0 online, got %d", stats.OnlinePlayers)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user