package server import ( "database/sql" "fmt" "net/http" "os" "strings" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "gophergate/internal/db" "gophergate/internal/models" "gophergate/internal/utils" "log/slog" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/load" "github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v3/process" ) type ApiResponse struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } func SuccessResponse(data interface{}) ApiResponse { return ApiResponse{Success: true, Data: data} } func ErrorResponse(err string) ApiResponse { return ApiResponse{Success: false, Error: err} } func (s *Server) adminAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") if token == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse("Not authenticated")) return } session, _, err := s.sessions.ValidateSession(token) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse("Session expired or invalid")) return } if session.Role != "admin" { c.AbortWithStatusJSON(http.StatusForbidden, ErrorResponse("Admin access required")) return } c.Set("session", session) c.Next() } } type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } func (s *Server) handleLogin(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request")) return } var user db.User err := s.database.Get(&user, "SELECT username, password_hash, display_name, role, must_change_password FROM users WHERE username = ?", req.Username) if err != nil { c.JSON(http.StatusUnauthorized, ErrorResponse("Invalid username or password")) return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, ErrorResponse("Invalid username or password")) return } displayName := user.Username if user.DisplayName != nil { displayName = *user.DisplayName } token, err := s.sessions.CreateSession(user.Username, displayName, user.Role) if err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse("Failed to create session")) return } c.JSON(http.StatusOK, SuccessResponse(gin.H{ "token": token, "must_change_password": user.MustChangePassword, "user": user, })) } func (s *Server) handleAuthStatus(c *gin.Context) { token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") session, _, err := s.sessions.ValidateSession(token) if err != nil { c.JSON(http.StatusUnauthorized, ErrorResponse("Not authenticated")) return } c.JSON(http.StatusOK, SuccessResponse(gin.H{ "authenticated": true, "user": gin.H{ "username": session.Username, "role": session.Role, "display_name": session.DisplayName, }, })) } type ChangePasswordRequest struct { CurrentPassword string `json:"current_password" binding:"required"` NewPassword string `json:"new_password" binding:"required"` } func (s *Server) handleChangePassword(c *gin.Context) { token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") session, _, err := s.sessions.ValidateSession(token) if err != nil { c.JSON(http.StatusUnauthorized, ErrorResponse("Not authenticated")) return } var req ChangePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request")) return } var user db.User err = s.database.Get(&user, "SELECT password_hash FROM users WHERE username = ?", session.Username) if err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse("User not found")) return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil { c.JSON(http.StatusUnauthorized, ErrorResponse("Current password incorrect")) return } newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 12) if err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse("Failed to hash new password")) return } _, err = s.database.Exec("UPDATE users SET password_hash = ?, must_change_password = 0 WHERE username = ?", string(newHash), session.Username) if err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse("Failed to update password")) return } c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Password updated successfully"})) } func (s *Server) handleLogout(c *gin.Context) { token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") if err := s.sessions.RevokeSession(token); err != nil { fmt.Printf("Error revoking session: %v\n", err) } c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Logged out"})) } type UsagePeriodFilter struct { Period string `form:"period"` From string `form:"from"` To string `form:"to"` } func (f *UsagePeriodFilter) ToSQL() (string, []interface{}) { period := f.Period if period == "" { period = "all" } if period == "custom" { var clauses []string var binds []interface{} if f.From != "" { clauses = append(clauses, "timestamp >= ?") binds = append(binds, f.From) } if f.To != "" { clauses = append(clauses, "timestamp <= ?") binds = append(binds, f.To) } if len(clauses) > 0 { return " AND " + strings.Join(clauses, " AND "), binds } return "", nil } now := time.Now().UTC() var cutoff time.Time switch period { case "today": cutoff = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) case "24h": cutoff = now.Add(-24 * time.Hour) case "7d": cutoff = now.Add(-7 * 24 * time.Hour) case "30d": cutoff = now.Add(-30 * 24 * time.Hour) default: return "", nil } return " AND timestamp >= ?", []interface{}{cutoff.Format(time.RFC3339)} } func (s *Server) handleUsageSummary(c *gin.Context) { var filter UsagePeriodFilter if err := c.ShouldBindQuery(&filter); err != nil { // ignore } clause, binds := filter.ToSQL() // Total stats var totalStats struct { TotalRequests int `db:"total_requests"` TotalTokens int `db:"total_tokens"` CacheReadTokens int `db:"total_cache_read_tokens"` CacheWriteTokens int `db:"total_cache_write_tokens"` TotalCost float64 `db:"total_cost"` ActiveClients int `db:"active_clients"` } err := s.database.Get(&totalStats, fmt.Sprintf(` SELECT COUNT(*) as total_requests, COALESCE(SUM(total_tokens), 0) as total_tokens, COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens, COALESCE(SUM(cache_write_tokens), 0) as total_cache_write_tokens, COALESCE(SUM(cost), 0.0) as total_cost, COUNT(DISTINCT client_id) as active_clients FROM llm_requests WHERE 1=1 %s `, clause), binds...) if err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) return } // Today stats var todayStats struct { TodayRequests int `db:"today_requests"` TodayCost float64 `db:"today_cost"` } today := time.Now().UTC().Format("2006-01-02") err = s.database.Get(&todayStats, ` SELECT COUNT(*) as today_requests, COALESCE(SUM(cost), 0.0) as today_cost FROM llm_requests WHERE timestamp LIKE ? `, today+"%") if err != nil { todayStats.TodayRequests = 0 todayStats.TodayCost = 0.0 } // Error rate & Avg response time var miscStats struct { ErrorRate float64 `db:"error_rate"` AvgResponseTime float64 `db:"avg_response_time"` } err = s.database.Get(&miscStats, ` SELECT CASE WHEN COUNT(*) = 0 THEN 0.0 ELSE (CAST(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*)) * 100.0 END as error_rate, COALESCE(AVG(duration_ms), 0.0) as avg_response_time FROM llm_requests `) if err != nil { miscStats.ErrorRate = 0.0 miscStats.AvgResponseTime = 0.0 } c.JSON(http.StatusOK, SuccessResponse(gin.H{ "total_requests": totalStats.TotalRequests, "total_tokens": totalStats.TotalTokens, "total_cache_read_tokens": totalStats.CacheReadTokens, "total_cache_write_tokens": totalStats.CacheWriteTokens, "total_cost": totalStats.TotalCost, "active_clients": totalStats.ActiveClients, "today_requests": todayStats.TodayRequests, "today_cost": todayStats.TodayCost, "error_rate": miscStats.ErrorRate, "avg_response_time": miscStats.AvgResponseTime, })) } func (s *Server) handleTimeSeries(c *gin.Context) { var filter UsagePeriodFilter if err := c.ShouldBindQuery(&filter); err != nil { // ignore } clause, binds := filter.ToSQL() if clause == "" { cutoff := time.Now().UTC().Add(-30 * 24 * time.Hour) clause = " AND timestamp >= ?" binds = []interface{}{cutoff.Format(time.RFC3339)} } query := fmt.Sprintf(` SELECT COALESCE(SUBSTR(timestamp, 1, 10), 'unknown') as bucket, COUNT(*) as requests, COALESCE(SUM(total_tokens), 0) as tokens, COALESCE(SUM(cost), 0.0) as cost FROM llm_requests WHERE 1=1 %s GROUP BY bucket ORDER BY bucket `, clause) rows, err := s.database.Queryx(query, binds...) if err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) return } defer rows.Close() var series []gin.H for rows.Next() { var bucket string var requests int var tokens int var cost float64 if err := rows.Scan(&bucket, &requests, &tokens, &cost); err != nil { continue } series = append(series, gin.H{ "time": bucket, "requests": requests, "tokens": tokens, "cost": cost, }) } granularity := "day" c.JSON(http.StatusOK, SuccessResponse(gin.H{ "series": series, "granularity": granularity, })) } func (s *Server) handleProvidersUsage(c *gin.Context) { var filter UsagePeriodFilter if err := c.ShouldBindQuery(&filter); err != nil { // ignore } clause, binds := filter.ToSQL() rows, err := s.database.Queryx(fmt.Sprintf(` SELECT COALESCE(provider, 'unknown') as provider, COUNT(*) as requests, COALESCE(SUM(cost), 0.0) as cost FROM llm_requests WHERE 1=1 %s GROUP BY provider `, clause), binds...) if err != nil { c.JSON(http.StatusOK, SuccessResponse([]interface{}{})) return } defer rows.Close() var results []gin.H for rows.Next() { var provider string var requests int var cost float64 if err := rows.Scan(&provider, &requests, &cost); err == nil { results = append(results, gin.H{"provider": provider, "requests": requests, "cost": cost}) } } c.JSON(http.StatusOK, SuccessResponse(results)) } func (s *Server) handleClientsUsage(c *gin.Context) { var filter UsagePeriodFilter if err := c.ShouldBindQuery(&filter); err != nil { // ignore } clause, binds := filter.ToSQL() rows, err := s.database.Queryx(fmt.Sprintf(` SELECT COALESCE(client_id, 'unknown') as client_id, COUNT(*) as requests FROM llm_requests WHERE 1=1 %s GROUP BY client_id `, clause), binds...) if err != nil { c.JSON(http.StatusOK, SuccessResponse([]interface{}{})) return } defer rows.Close() var results []gin.H for rows.Next() { var clientID string var requests int if err := rows.Scan(&clientID, &requests); err == nil { results = append(results, gin.H{"client_id": clientID, "requests": requests}) } } c.JSON(http.StatusOK, SuccessResponse(results)) } func (s *Server) handleAnalyticsBreakdown(c *gin.Context) { var filter UsagePeriodFilter if err := c.ShouldBindQuery(&filter); err != nil { // ignore } clause, binds := filter.ToSQL() // Models breakdown var models []struct { Label string `json:"label"` Value int `json:"value"` } mRows, err := s.database.Queryx(fmt.Sprintf("SELECT COALESCE(model, 'unknown') as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY model ORDER BY value DESC", clause), binds...) if err == nil { for mRows.Next() { var label string var value int if err := mRows.Scan(&label, &value); err == nil { models = append(models, struct { Label string `json:"label"` Value int `json:"value"` }{label, value}) } } mRows.Close() } // Clients breakdown var clients []struct { Label string `json:"label"` Value int `json:"value"` } cRows, err := s.database.Queryx(fmt.Sprintf("SELECT COALESCE(client_id, 'unknown') as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY client_id ORDER BY value DESC", clause), binds...) if err == nil { for cRows.Next() { var label string var value int if err := cRows.Scan(&label, &value); err == nil { clients = append(clients, struct { Label string `json:"label"` Value int `json:"value"` }{label, value}) } } cRows.Close() } c.JSON(http.StatusOK, SuccessResponse(gin.H{ "models": models, "clients": clients, })) } func (s *Server) handleDetailedUsage(c *gin.Context) { var filter UsagePeriodFilter if err := c.ShouldBindQuery(&filter); err != nil { // ignore } clause, binds := filter.ToSQL() query := fmt.Sprintf(` SELECT COALESCE(SUBSTR(timestamp, 1, 10), 'unknown') as date, COALESCE(client_id, 'unknown') as client, COALESCE(provider, 'unknown') as provider, COALESCE(model, 'unknown') as model, COUNT(*) as requests, COALESCE(SUM(total_tokens), 0) as tokens, COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens, COALESCE(SUM(cache_write_tokens), 0) as cache_write_tokens, COALESCE(SUM(cost), 0.0) as cost FROM llm_requests WHERE 1=1 %s GROUP BY date, client, provider, model ORDER BY date DESC, cost DESC `, clause) rows, err := s.database.Queryx(query, binds...) if err != nil { c.JSON(http.StatusOK, SuccessResponse([]interface{}{})) return } defer rows.Close() var results []gin.H for rows.Next() { var date, client, provider, model string var requests, tokens, cacheRead, cacheWrite int var cost float64 if err := rows.Scan(&date, &client, &provider, &model, &requests, &tokens, &cacheRead, &cacheWrite, &cost); err == nil { results = append(results, gin.H{ "date": date, "client": client, "provider": provider, "model": model, "requests": requests, "tokens": tokens, "cache_read_tokens": cacheRead, "cache_write_tokens": cacheWrite, "cost": cost, }) }