feat: edu-search-service migriert, voice-service/geo-service entfernt
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
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>
This commit is contained in:
146
edu-search-service/internal/api/handlers/handlers.go
Normal file
146
edu-search-service/internal/api/handlers/handlers.go
Normal file
@@ -0,0 +1,146 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user