✨ feat(server): cache /api/v1/greet responses + admin cache flush endpoint #29
@@ -170,6 +170,12 @@ func (s *Server) setupRoutes() {
|
||||
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
|
||||
if s.config.GetV2Enabled() {
|
||||
s.router.Route("/api/v2", func(r chi.Router) {
|
||||
@@ -196,9 +202,6 @@ func (s *Server) setupRoutes() {
|
||||
}
|
||||
|
||||
func (s *Server) registerApiV1Routes(r chi.Router) {
|
||||
greetService := greet.NewService()
|
||||
greetHandler := greet.NewApiV1GreetHandler(greetService)
|
||||
|
||||
// Create rate limit middleware
|
||||
rateLimitMiddleware := middleware.NewRateLimiter(middleware.RateLimitConfig{
|
||||
Enabled: s.config.GetRateLimitEnabled(),
|
||||
@@ -219,7 +222,8 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
|
||||
if authMiddleware != nil {
|
||||
r.Use(authMiddleware.Middleware)
|
||||
}
|
||||
greetHandler.RegisterRoutes(r)
|
||||
r.Get("/", s.handleGreetQuery)
|
||||
r.Get("/{name}", s.handleGreetPath)
|
||||
})
|
||||
|
||||
// Register user authentication routes
|
||||
@@ -445,6 +449,142 @@ func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
return s.router
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user