eq2go/internal/login/web_handlers.go

252 lines
7.1 KiB
Go

package login
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
)
// handleWebRoot handles the root web interface page
func (s *Server) handleWebRoot(w http.ResponseWriter, r *http.Request) {
if !s.authenticateWebRequest(w, r) {
return
}
html := `<!DOCTYPE html>
<html>
<head>
<title>EQ2Go Login Server</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.header { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; }
.stats { display: flex; gap: 20px; margin: 20px 0; }
.stat-card { background: #f8f9fa; padding: 15px; border-radius: 5px; flex: 1; }
.stat-value { font-size: 24px; font-weight: bold; color: #2c3e50; }
.table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.table th { background-color: #f2f2f2; }
.status-up { color: green; font-weight: bold; }
.status-down { color: red; font-weight: bold; }
.status-locked { color: orange; font-weight: bold; }
</style>
<script>
function refreshData() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
document.getElementById('clients').textContent = data.clients;
document.getElementById('worlds').textContent = data.worlds;
document.getElementById('uptime').textContent = data.uptime;
});
}
setInterval(refreshData, 5000); // Refresh every 5 seconds
</script>
</head>
<body>
<div class="header">
<h1>EQ2Go Login Server Administration</h1>
<p>Version: 1.0.0-dev</p>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="clients">-</div>
<div>Connected Clients</div>
</div>
<div class="stat-card">
<div class="stat-value" id="worlds">-</div>
<div>World Servers</div>
</div>
<div class="stat-card">
<div class="stat-value" id="uptime">-</div>
<div>Uptime</div>
</div>
</div>
<h2>API Endpoints</h2>
<ul>
<li><a href="/api/status">Server Status</a></li>
<li><a href="/api/clients">Connected Clients</a></li>
<li><a href="/api/worlds">World Servers</a></li>
</ul>
<script>refreshData();</script>
</body>
</html>`
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(html))
}
// handleAPIStatus handles the status API endpoint
func (s *Server) handleAPIStatus(w http.ResponseWriter, r *http.Request) {
if !s.authenticateWebRequest(w, r) {
return
}
status := map[string]interface{}{
"server_name": s.config.ServerName,
"version": "1.0.0-dev",
"running": s.IsRunning(),
"uptime": s.formatUptime(s.GetUptime()),
"clients": s.GetClientCount(),
"worlds": s.GetWorldCount(),
"timestamp": time.Now().Unix(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status)
}
// handleAPIClients handles the clients API endpoint
func (s *Server) handleAPIClients(w http.ResponseWriter, r *http.Request) {
if !s.authenticateWebRequest(w, r) {
return
}
clients := s.clientList.GetClients()
clientInfo := make([]map[string]interface{}, len(clients))
for i, client := range clients {
clientInfo[i] = map[string]interface{}{
"ip_address": client.GetIPAddress(),
"account_id": client.GetAccountID(),
"account_name": client.GetAccountName(),
"state": client.GetState().String(),
"connect_time": client.GetConnectTime().Unix(),
"last_activity": client.GetLastActivity().Unix(),
}
}
response := map[string]interface{}{
"total_clients": len(clients),
"clients": clientInfo,
"stats": s.clientList.GetStats(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// handleAPIWorlds handles the worlds API endpoint
func (s *Server) handleAPIWorlds(w http.ResponseWriter, r *http.Request) {
if !s.authenticateWebRequest(w, r) {
return
}
worlds := s.worldList.GetAllWorlds()
worldInfo := make([]map[string]interface{}, len(worlds))
for i, world := range worlds {
worldInfo[i] = map[string]interface{}{
"id": world.ID,
"name": world.Name,
"address": world.Address,
"port": world.Port,
"status": world.Status,
"population": world.Population,
"max_players": world.MaxPlayers,
"population_pct": world.GetPopulationPercentage(),
"population_level": world.GetPopulationLevel(),
"locked": world.IsLocked(),
"hidden": world.IsHidden(),
"last_heartbeat": world.LastHeartbeat,
"description": world.Description,
}
}
response := map[string]interface{}{
"total_worlds": len(worlds),
"worlds": worldInfo,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// authenticateWebRequest performs basic authentication for web requests
func (s *Server) authenticateWebRequest(w http.ResponseWriter, r *http.Request) bool {
// Skip authentication if no credentials are configured
if s.config.WebUser == "" || s.config.WebPassword == "" {
return true
}
username, password, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="EQ2Go Login Server"`)
w.WriteHeader(401)
w.Write([]byte("Unauthorized"))
return false
}
if username != s.config.WebUser || password != s.config.WebPassword {
w.Header().Set("WWW-Authenticate", `Basic realm="EQ2Go Login Server"`)
w.WriteHeader(401)
w.Write([]byte("Unauthorized"))
return false
}
return true
}
// formatUptime formats uptime duration into a readable string
func (s *Server) formatUptime(duration time.Duration) string {
if duration == 0 {
return "Not running"
}
days := int(duration.Hours()) / 24
hours := int(duration.Hours()) % 24
minutes := int(duration.Minutes()) % 60
seconds := int(duration.Seconds()) % 60
if days > 0 {
return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds)
} else if hours > 0 {
return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds)
} else if minutes > 0 {
return fmt.Sprintf("%dm %ds", minutes, seconds)
} else {
return fmt.Sprintf("%ds", seconds)
}
}
// handleKickClient handles kicking a client (admin endpoint)
func (s *Server) handleKickClient(w http.ResponseWriter, r *http.Request) {
if !s.authenticateWebRequest(w, r) {
return
}
if r.Method != "POST" {
w.WriteHeader(405)
w.Write([]byte("Method not allowed"))
return
}
accountIDStr := r.FormValue("account_id")
reason := r.FormValue("reason")
if reason == "" {
reason = "Kicked by administrator"
}
accountID, err := strconv.ParseInt(accountIDStr, 10, 32)
if err != nil {
w.WriteHeader(400)
w.Write([]byte("Invalid account ID"))
return
}
success := s.clientList.DisconnectByAccountID(int32(accountID), reason)
response := map[string]interface{}{
"success": success,
"message": "Client disconnected",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}