All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor) - opensearch + edu-search-service in docker-compose.yml hinzugefuegt - voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core) - geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt) - CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt (Go lint, test mit go mod download, build, SBOM) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
3.5 KiB
Go
147 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|