A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
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)
|
|
}
|
|
}
|