package api import ( "containr/internal/database" "context" "crypto/rand" "database/sql" "encoding/hex" "fmt" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "golang.org/x/crypto/bcrypt" ) // Constants for validation and limits const ( MaxServiceNameLength = 100 MaxRoutePrefixLength = 200 MaxUpstreamURLLength = 500 MaxAPIKeyNameLength = 100 MinAPIKeyLength = 20 MaxAPIKeyLength = 100 DefaultRPMLimit = 60 DefaultMonthlyQuota = 1000 MaxRPMLimit = 10000 MaxMonthlyQuota = 10000000 ) // Validator instance for request validation var validate = validator.New() // APIError represents a structured API error response type APIError struct { Code string `json:"code"` Message string `json:"message"` Details interface{} `json:"details,omitempty"` } // APIResponse represents a standardized API response type APIResponse struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error *APIError `json:"error,omitempty"` Meta *Meta `json:"meta,omitempty"` } // Meta contains pagination and metadata type Meta struct { Page int `json:"page,omitempty"` PerPage int `json:"per_page,omitempty"` Total int `json:"total,omitempty"` TotalPages int `json:"total_pages,omitempty"` } // ServiceRequest represents the request payload for creating/updating services type ServiceRequest struct { Name string `json:"name" binding:"required,max=100" validate:"required,min=1,max=100"` UpstreamURL string `json:"upstreamUrl" binding:"required,max=500" validate:"required,url,max=500"` RoutePrefix string `json:"routePrefix" binding:"required,max=200" validate:"required,min=1,max=200"` Enabled *bool `json:"enabled,omitempty"` RPMLimit *int `json:"rpmLimit,omitempty" validate:"omitempty,min=1,max=10000"` MonthlyQuota *int `json:"monthlyQuota,omitempty" validate:"omitempty,min=1,max=10000000"` } // APIKeyRequest represents the request payload for creating/updating API keys type APIKeyRequest struct { Name string `json:"name" binding:"required,max=100" validate:"required,min=1,max=100"` Plan string `json:"plan" validate:"omitempty,oneof=free pro business enterprise"` Enabled *bool `json:"enabled,omitempty"` RPMLimit *int `json:"rpmLimit,omitempty" validate:"omitempty,min=1,max=10000"` MonthlyQuota *int `json:"monthlyQuota,omitempty" validate:"omitempty,min=1,max=10000000"` } // generateServiceID generates a cryptographically secure service ID func generateServiceID() (string, error) { bytes := make([]byte, 16) if _, err := rand.Read(bytes); err != nil { return "", fmt.Errorf("failed to generate random bytes: %w", err) } return "svc_" + hex.EncodeToString(bytes), nil } // generateAPIKey generates a cryptographically secure API key func generateAPIKey() (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", fmt.Errorf("failed to generate random bytes: %w", err) } return "ap_" + hex.EncodeToString(bytes), nil } // hashAPIKey creates a bcrypt hash for the API key func hashAPIKey(key string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(key), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("failed to hash API key: %w", err) } return string(hash), nil } // validateAPIKeyPlan validates and returns default values for API key plans func validateAPIKeyPlan(plan string) (string, int, int, error) { if plan == "" { plan = "free" } switch plan { case "free": return plan, 60, 1000, nil case "pro": return plan, 600, 50000, nil case "business": return plan, 3000, 300000, nil case "enterprise": return plan, 10000, 10000000, nil default: return "", 0, 0, fmt.Errorf("invalid plan: %s", plan) } } // sendJSONResponse sends a standardized JSON response func sendJSONResponse(c *gin.Context, statusCode int, response APIResponse) { c.JSON(statusCode, response) } // sendErrorResponse sends a standardized error response func sendErrorResponse(c *gin.Context, statusCode int, code, message string, details interface{}) { response := APIResponse{ Success: false, Error: &APIError{ Code: code, Message: message, Details: details, }, } sendJSONResponse(c, statusCode, response) } // sendSuccessResponse sends a standardized success response func sendSuccessResponse(c *gin.Context, statusCode int, data interface{}) { response := APIResponse{ Success: true, Data: data, } sendJSONResponse(c, statusCode, response) } // validateRequest validates the request payload using the validator func validateRequest(c *gin.Context, req interface{}) error { if err := c.ShouldBindJSON(req); err != nil { return fmt.Errorf("invalid request body: %w", err) } if err := validate.Struct(req); err != nil { return fmt.Errorf("validation failed: %w", err) } return nil } // handleAPwhyServicesList returns a list of API services func handleAPwhyServicesList(c *gin.Context) { db := c.MustGet("db").(*database.DB) ctx := context.Background() // Query services from database rows, err := db.QueryContext(ctx, ` SELECT id, name, slug, upstream_url, route_prefix, enabled, created_at, updated_at FROM api_services ORDER BY created_at DESC `) if err != nil { sendErrorResponse(c, http.StatusInternalServerError, "DATABASE_ERROR", "Failed to query services", err.Error()) return } defer rows.Close() var services []map[string]interface{} for rows.Next() { var id, name, slug, upstreamURL, routePrefix, createdAt, updatedAt string var enabled bool err := rows.Scan(&id, &name, &slug, &upstreamURL, &routePrefix, &enabled, &createdAt, &updatedAt) if err != nil { continue // Skip malformed rows } services = append(services, map[string]interface{}{ "id": id, "name": name, "slug": slug, "upstreamUrl": upstreamURL, "routePrefix": routePrefix, "enabled": enabled, "createdAt": createdAt, "updatedAt": updatedAt, }) } sendSuccessResponse(c, http.StatusOK, map[string]interface{}{ "services": services, "count": len(services), }) } // handleAPwhyServicesCreate creates a new API service func handleAPwhyServicesCreate(c *gin.Context) { var req ServiceRequest if err := validateRequest(c, &req); err != nil { sendErrorResponse(c, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request parameters", err.Error()) return } db := c.MustGet("db").(*database.DB) ctx := context.Background() // Generate slug and ID id, err := generateServiceID() if err != nil { sendErrorResponse(c, http.StatusInternalServerError, "GENERATION_ERROR", "Failed to generate service ID", err.Error()) return } slug := strings.ToLower(strings.ReplaceAll(req.Name, " ", "-")) // Insert service into database query := ` INSERT INTO api_services ( id, name, slug, upstream_url, route_prefix, health_path, enabled, rpm_limit, monthly_quota, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, '/health', true, $6, $7, NOW(), NOW()) ` var rpmLimit, monthlyQuota int if req.RPMLimit != nil { rpmLimit = *req.RPMLimit } else { rpmLimit = DefaultRPMLimit } if req.MonthlyQuota != nil { monthlyQuota = *req.MonthlyQuota } else { monthlyQuota = DefaultMonthlyQuota } _, err = db.ExecContext(ctx, query, id, req.Name, slug, req.UpstreamURL, req.RoutePrefix, rpmLimit, monthlyQuota) if err != nil { sendErrorResponse(c, http.StatusInternalServerError, "DATABASE_ERROR", "Failed to create service", err.Error()) return } serviceData := map[string]interface{}{ "id": id, "name": req.Name, "slug": slug, "upstreamUrl": req.UpstreamURL, "routePrefix": req.RoutePrefix, "enabled": true, "rpmLimit": rpmLimit, "monthlyQuota": monthlyQuota, "createdAt": time.Now().UTC().Format(time.RFC3339), } sendSuccessResponse(c, http.StatusCreated, map[string]interface{}{ "service": serviceData, "message": "Service created successfully", }) } // handleAPwhyServicesPatch updates an existing API service func handleAPwhyServicesPatch(c *gin.Context) { serviceID := c.Param("id") var input struct { Enabled *bool `json:"enabled"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "ok": false, "error": "Invalid input: " + err.Error(), }) return } db := c.MustGet("db").(*database.DB) if input.Enabled != nil { _, err := db.ExecContext(context.Background(), "UPDATE api_services SET enabled = $1, updated_at = NOW() WHERE id = $2", *input.Enabled, serviceID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to update service: " + err.Error(), }) return } } c.JSON(http.StatusOK, gin.H{ "ok": true, "data": gin.H{"id": serviceID, "updated": true}, }) } // handleAPwhyKeysList returns a list of API keys func handleAPwhyKeysList(c *gin.Context) { db := c.MustGet("db").(*database.DB) rows, err := db.QueryContext(context.Background(), ` SELECT id, name, key_prefix, plan, enabled, rpm_limit, monthly_quota, created_at, updated_at FROM api_keys ORDER BY created_at DESC `) if err != nil { c.JSON(http.StatusOK, gin.H{ "ok": true, "data": []interface{}{}, }) return } defer rows.Close() var keys []map[string]interface{} for rows.Next() { var id, name, keyPrefix, plan, createdAt, updatedAt string var enabled bool var rpmLimit, monthlyQuota sql.NullInt64 err := rows.Scan(&id, &name, &keyPrefix, &plan, &enabled, &rpmLimit, &monthlyQuota, &createdAt, &updatedAt) if err != nil { continue } key := map[string]interface{}{ "id": id, "name": name, "keyPrefix": keyPrefix, "plan": plan, "enabled": enabled, "createdAt": createdAt, "updatedAt": updatedAt, } if rpmLimit.Valid { key["rpmLimit"] = rpmLimit.Int64 } if monthlyQuota.Valid { key["monthlyQuota"] = monthlyQuota.Int64 } keys = append(keys, key) } c.JSON(http.StatusOK, gin.H{ "ok": true, "data": keys, }) } // handleAPwhyKeysCreate creates a new API key func handleAPwhyKeysCreate(c *gin.Context) { var input struct { Name string `json:"name" binding:"required"` Plan string `json:"plan"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "ok": false, "error": "Invalid input: " + err.Error(), }) return } if input.Plan == "" { input.Plan = "free" } db := c.MustGet("db").(*database.DB) // Generate API key and hash apiKey, err := generateAPIKey() if err != nil { sendErrorResponse(c, http.StatusInternalServerError, "GENERATION_ERROR", "Failed to generate API key", err.Error()) return } keyHash, err := hashAPIKey(apiKey) if err != nil { sendErrorResponse(c, http.StatusInternalServerError, "HASH_ERROR", "Failed to hash API key", err.Error()) return } // Generate ID and prefix id, err := generateServiceID() if err != nil { sendErrorResponse(c, http.StatusInternalServerError, "GENERATION_ERROR", "Failed to generate key ID", err.Error()) return } keyPrefix := apiKey[:8] // Set default limits based on plan var rpmLimit, monthlyQuota int switch input.Plan { case "free": rpmLimit, monthlyQuota = 60, 1000 case "pro": rpmLimit, monthlyQuota = 600, 50000 case "business": rpmLimit, monthlyQuota = 3000, 300000 default: rpmLimit, monthlyQuota = 60, 1000 } // Insert key into database _, err = db.ExecContext(context.Background(), ` INSERT INTO api_keys ( id, name, key_hash, key_prefix, plan, allowed_service_ids, enabled, rpm_limit, monthly_quota, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, '[]', true, $6, $7, NOW(), NOW()) `, id, input.Name, keyHash, keyPrefix, input.Plan, rpmLimit, monthlyQuota) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to create key: " + err.Error(), }) return } c.JSON(http.StatusCreated, gin.H{ "ok": true, "data": gin.H{ "id": id, "name": input.Name, "plan": input.Plan, "key": apiKey, // Only return the actual key once "keyPrefix": keyPrefix, "enabled": true, "rpmLimit": rpmLimit, "monthlyQuota": monthlyQuota, }, }) } // handleAPwhyKeysPatch updates an existing API key func handleAPwhyKeysPatch(c *gin.Context) { keyID := c.Param("id") var input struct { Enabled *bool `json:"enabled"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "ok": false, "error": "Invalid input: " + err.Error(), }) return } db := c.MustGet("db").(*database.DB) if input.Enabled != nil { _, err := db.ExecContext(context.Background(), "UPDATE api_keys SET enabled = $1, updated_at = NOW() WHERE id = $2", *input.Enabled, keyID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to update key: " + err.Error(), }) return } } c.JSON(http.StatusOK, gin.H{ "ok": true, "data": gin.H{"id": keyID, "updated": true}, }) } // handleAPwhyAnalyticsOps returns operational analytics func handleAPwhyAnalyticsOps(c *gin.Context) { db := c.MustGet("db").(*database.DB) // Get counts from database var totalServices, totalKeys, totalUsers int if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM api_services").Scan(&totalServices); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to count services: " + err.Error(), }) return } if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM api_keys").Scan(&totalKeys); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to count api keys: " + err.Error(), }) return } if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM users").Scan(&totalUsers); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to count users: " + err.Error(), }) return } var totalRequests int if err := db.QueryRowContext(context.Background(), "SELECT COALESCE(SUM(request_count), 0) FROM usage_counters").Scan(&totalRequests); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to aggregate request counters: " + err.Error(), }) return } var requestsToday int if err := db.QueryRowContext(context.Background(), ` SELECT COALESCE(SUM(value), 0)::int FROM metrics_timeseries WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC('day', NOW()) `).Scan(&requestsToday); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to aggregate today's requests: " + err.Error(), }) return } var requestsThisMonth int if err := db.QueryRowContext(context.Background(), ` SELECT COALESCE(SUM(value), 0)::int FROM metrics_timeseries WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC('month', NOW()) `).Scan(&requestsThisMonth); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "ok": false, "error": "Failed to aggregate monthly requests: " + err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "ok": true, "data": gin.H{ "total_requests": totalRequests, "total_services": totalServices, "total_keys": totalKeys, "total_users": totalUsers, "requests_today": requestsToday, "requests_this_month": requestsThisMonth, }, }) } // handleAPwhyAnalyticsTraffic returns traffic analytics func handleAPwhyAnalyticsTraffic(c *gin.Context) { db := c.MustGet("db").(*database.DB) topServices := make([]map[string]interface{}, 0) serviceRows, err := db.QueryContext(context.Background(), ` SELECT s.id, s.name, COALESCE(SUM(u.request_count), 0) AS total_requests FROM api_services s LEFT JOIN usage_counters u ON u.service_id = s.id GROUP BY s.id, s.name ORDER BY total_requests DESC, s.name ASC LIMIT 10 `) if err == nil { defer serviceRows.Close() for serviceRows.Next() { var serviceID, serviceName string var totalRequests int if scanErr := serviceRows.Scan(&serviceID, &serviceName, &totalRequests); scanErr == nil { topServices = append(topServices, map[string]interface{}{ "service_id": serviceID, "name": serviceName, "requests": totalRequests, }) } } } requestsByDay := make([]map[string]interface{}, 0) trafficRows, err := db.QueryContext(context.Background(), ` SELECT TO_CHAR(DATE_TRUNC('day', occurred_at), 'YYYY-MM-DD') AS day_bucket, COUNT(*) AS total FROM metrics_timeseries WHERE metric = 'request_total' AND occurred_at >= NOW() - INTERVAL '7 days' GROUP BY DATE_TRUNC('day', occurred_at) ORDER BY DATE_TRUNC('day', occurred_at) ASC `) if err == nil { defer trafficRows.Close() for trafficRows.Next() { var day string var count int if scanErr := trafficRows.Scan(&day, &count); scanErr == nil { requestsByDay = append(requestsByDay, map[string]interface{}{ "day": day, "requests": count, }) } } } statusCodes := make([]map[string]interface{}, 0) statusRows, err := db.QueryContext(context.Background(), ` SELECT COALESCE(http_status, 0) AS status_code, COALESCE(SUM(count), 0) AS total FROM incident_events WHERE occurred_at >= NOW() - INTERVAL '7 days' GROUP BY COALESCE(http_status, 0) ORDER BY total DESC, status_code ASC `) if err == nil { defer statusRows.Close() for statusRows.Next() { var code int var total int if scanErr := statusRows.Scan(&code, &total); scanErr == nil { statusCodes = append(statusCodes, map[string]interface{}{ "status_code": code, "count": total, }) } } } clientEvents := make([]map[string]interface{}, 0) eventRows, err := db.QueryContext(context.Background(), ` SELECT COALESCE((labels_json::jsonb ->> 'path'), 'unknown') AS path, COUNT(*) AS total FROM metrics_timeseries WHERE metric = 'client_event' AND occurred_at >= NOW() - INTERVAL '7 days' GROUP BY COALESCE((labels_json::jsonb ->> 'path'), 'unknown') ORDER BY total DESC, path ASC LIMIT 20 `) if err == nil { defer eventRows.Close() for eventRows.Next() { var path string var total int if scanErr := eventRows.Scan(&path, &total); scanErr == nil { clientEvents = append(clientEvents, map[string]interface{}{ "path": path, "count": total, }) } } } c.JSON(http.StatusOK, gin.H{ "ok": true, "data": gin.H{ "top_services": topServices, "requests_by_day": requestsByDay, "status_codes": statusCodes, "client_events": clientEvents, }, }) }