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