package handlers import ( "net/http" "github.com/breakpilot/edu-search-service/internal/config" "github.com/breakpilot/edu-search-service/internal/indexer" "github.com/breakpilot/edu-search-service/internal/search" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // Handler contains all HTTP handlers type Handler struct { cfg *config.Config searchService *search.Service indexClient *indexer.Client } // NewHandler creates a new handler instance func NewHandler(cfg *config.Config, searchService *search.Service, indexClient *indexer.Client) *Handler { return &Handler{ cfg: cfg, searchService: searchService, indexClient: indexClient, } } // Health returns service health status func (h *Handler) Health(c *gin.Context) { status := "ok" // Check OpenSearch health osStatus, err := h.indexClient.Health(c.Request.Context()) if err != nil { status = "degraded" osStatus = "unreachable" } c.JSON(http.StatusOK, gin.H{ "status": status, "opensearch": osStatus, "service": "edu-search-service", "version": "0.1.0", }) } // Search handles /v1/search requests func (h *Handler) Search(c *gin.Context) { var req search.SearchRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } // Set defaults if req.Limit <= 0 || req.Limit > 100 { req.Limit = 10 } if req.Mode == "" { req.Mode = "keyword" // MVP: only BM25 } // Generate query ID queryID := uuid.New().String() // Execute search result, err := h.searchService.Search(c.Request.Context(), &req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Search failed", "details": err.Error()}) return } result.QueryID = queryID c.JSON(http.StatusOK, result) } // GetDocument retrieves a single document func (h *Handler) GetDocument(c *gin.Context) { docID := c.Query("doc_id") if docID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "doc_id parameter required"}) return } // TODO: Implement document retrieval c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"}) } // AuthMiddleware validates API keys func AuthMiddleware(apiKey string) gin.HandlerFunc { return func(c *gin.Context) { // Skip auth for health endpoint if c.Request.URL.Path == "/v1/health" { c.Next() return } // Check API key authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"}) return } // Extract Bearer token if len(authHeader) < 7 || authHeader[:7] != "Bearer " { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"}) return } token := authHeader[7:] if apiKey != "" && token != apiKey { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"}) return } c.Next() } } // RateLimitMiddleware implements basic rate limiting func RateLimitMiddleware() gin.HandlerFunc { // TODO: Implement proper rate limiting with Redis return func(c *gin.Context) { c.Next() } } // SetupRoutes configures all API routes func SetupRoutes(r *gin.Engine, h *Handler, apiKey string) { // Health endpoint (no auth) r.GET("/v1/health", h.Health) // API v1 group with auth v1 := r.Group("/v1") v1.Use(AuthMiddleware(apiKey)) v1.Use(RateLimitMiddleware()) { v1.POST("/search", h.Search) v1.GET("/document", h.GetDocument) // Admin routes SetupAdminRoutes(v1, h) } }