feat(server): cache /api/v1/greet responses + admin cache flush endpoint (#29)
Some checks failed
CI/CD Pipeline / Build Docker Cache (push) Successful in 13s
CI/CD Pipeline / Trigger Docker Push (push) Has been cancelled
CI/CD Pipeline / CI Pipeline (push) Has been cancelled

Extends cache service to /api/v1/greet (per-name 60s) and adds POST /api/admin/cache/flush. ~95% Mistral autonomous via ICM workspace, trainer finalized commit/PR (test scaffold did not compile).

Co-authored-by: Gabriel Radureau <arcodange@gmail.com>
Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #29.
This commit is contained in:
2026-05-03 16:33:02 +02:00
committed by arcodange
parent 93bd384ca8
commit 301471f728

View File

@@ -170,6 +170,12 @@ func (s *Server) setupRoutes() {
s.registerApiV1Routes(r) s.registerApiV1Routes(r)
}) })
// Admin routes
s.router.Route("/api/admin", func(r chi.Router) {
r.Use(s.getAllMiddlewares()...)
r.Post("/cache/flush", s.handleAdminCacheFlush)
})
// Register v2 routes if enabled // Register v2 routes if enabled
if s.config.GetV2Enabled() { if s.config.GetV2Enabled() {
s.router.Route("/api/v2", func(r chi.Router) { s.router.Route("/api/v2", func(r chi.Router) {
@@ -196,9 +202,6 @@ func (s *Server) setupRoutes() {
} }
func (s *Server) registerApiV1Routes(r chi.Router) { func (s *Server) registerApiV1Routes(r chi.Router) {
greetService := greet.NewService()
greetHandler := greet.NewApiV1GreetHandler(greetService)
// Create rate limit middleware // Create rate limit middleware
rateLimitMiddleware := middleware.NewRateLimiter(middleware.RateLimitConfig{ rateLimitMiddleware := middleware.NewRateLimiter(middleware.RateLimitConfig{
Enabled: s.config.GetRateLimitEnabled(), Enabled: s.config.GetRateLimitEnabled(),
@@ -219,7 +222,8 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
if authMiddleware != nil { if authMiddleware != nil {
r.Use(authMiddleware.Middleware) r.Use(authMiddleware.Middleware)
} }
greetHandler.RegisterRoutes(r) r.Get("/", s.handleGreetQuery)
r.Get("/{name}", s.handleGreetPath)
}) })
// Register user authentication routes // Register user authentication routes
@@ -445,6 +449,142 @@ func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(resp) json.NewEncoder(w).Encode(resp)
} }
// handleGreetQuery godoc
//
// @Summary Get greeting with cache
// @Description Returns greeting for name from query param with caching
// @Tags API/v1/Greeting
// @Accept json
// @Produce json
// @Param name query string false "Name to greet"
// @Success 200 {object} map[string]string "Greeting message"
// @Failure 400 {object} map[string]string "Invalid request"
// @Router /v1/greet [get]
func (s *Server) handleGreetQuery(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
cacheKey := "greet:v1:" + name
// Check cache if enabled
if s.cacheService != nil {
if cached, ok := s.cacheService.Get(cacheKey); ok {
log.Trace().Str("cache_key", cacheKey).Msg("Cache hit for greet")
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Cache", "HIT")
w.Write([]byte(cached.(string)))
return
}
}
// Compute response
greetService := greet.NewService()
message := greetService.Greet(r.Context(), name)
response, err := json.Marshal(map[string]string{"message": message})
if err != nil {
http.Error(w, `{"error":"server_error"}`, http.StatusInternalServerError)
return
}
// Cache the response for 60 seconds if cache is enabled
if s.cacheService != nil {
s.cacheService.Set(cacheKey, string(response), 60*time.Second)
w.Header().Set("X-Cache", "MISS")
log.Trace().Str("cache_key", cacheKey).Msg("Cached greet response")
}
w.Header().Set("Content-Type", "application/json")
w.Write(response)
}
// handleGreetPath godoc
//
// @Summary Get personalized greeting with cache
// @Description Returns greeting for name from path param with caching
// @Tags API/v1/Greeting
// @Accept json
// @Produce json
// @Param name path string true "Name to greet"
// @Success 200 {object} map[string]string "Greeting message"
// @Failure 400 {object} map[string]string "Invalid request"
// @Router /v1/greet/{name} [get]
func (s *Server) handleGreetPath(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
cacheKey := "greet:v1:" + name
// Check cache if enabled
if s.cacheService != nil {
if cached, ok := s.cacheService.Get(cacheKey); ok {
log.Trace().Str("cache_key", cacheKey).Msg("Cache hit for greet")
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Cache", "HIT")
w.Write([]byte(cached.(string)))
return
}
}
// Compute response
greetService := greet.NewService()
message := greetService.Greet(r.Context(), name)
response, err := json.Marshal(map[string]string{"message": message})
if err != nil {
http.Error(w, `{"error":"server_error"}`, http.StatusInternalServerError)
return
}
// Cache the response for 60 seconds if cache is enabled
if s.cacheService != nil {
s.cacheService.Set(cacheKey, string(response), 60*time.Second)
w.Header().Set("X-Cache", "MISS")
log.Trace().Str("cache_key", cacheKey).Msg("Cached greet response")
}
w.Header().Set("Content-Type", "application/json")
w.Write(response)
}
// handleAdminCacheFlush godoc
//
// @Summary Flush cache
// @Description Flushes the entire cache, requires admin authentication
// @Tags API/Admin
// @Accept json
// @Produce json
// @Param X-Admin-Password header string true "Admin master password"
// @Success 200 {object} map[string]interface{} "Cache flushed successfully"
// @Failure 401 {object} map[string]string "Unauthorized"
// @Failure 503 {object} map[string]string "Cache disabled"
// @Router /admin/cache/flush [post]
func (s *Server) handleAdminCacheFlush(w http.ResponseWriter, r *http.Request) {
if s.cacheService == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]string{"error": "cache_disabled"})
return
}
// Admin auth - check X-Admin-Password header
masterPassword := r.Header.Get("X-Admin-Password")
if masterPassword == "" {
http.Error(w, `{"error":"unauthorized","message":"Admin password required"}`, http.StatusUnauthorized)
return
}
_, err := s.userService.AdminAuthenticate(r.Context(), masterPassword)
if err != nil {
http.Error(w, `{"error":"unauthorized","message":"Invalid admin password"}`, http.StatusUnauthorized)
return
}
itemCount := s.cacheService.ItemCount()
s.cacheService.Flush()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"flushed": true,
"items_flushed": itemCount,
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}
func (s *Server) Router() http.Handler { func (s *Server) Router() http.Handler {
return s.router return s.router
} }