diff --git a/pkg/server/server.go b/pkg/server/server.go index 750f590..7483f00 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -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 }