diff --git a/backend/go.mod b/backend/go.mod index ff93bfa..57fd9c9 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -32,6 +32,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-chi/httprate v0.15.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -45,6 +46,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -58,6 +60,7 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 95e37d6..365b39d 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -40,6 +40,8 @@ github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= +github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -103,6 +105,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -166,6 +170,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index 957e4e2..9d56736 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" + "github.com/go-chi/httprate" "github.com/jackc/pgx/v5/pgxpool" "github.com/forfarm/backend/internal/cache" @@ -130,18 +131,31 @@ func (a *api) Routes() *chi.Mux { router.Use(middleware.Logger) router.Use(cors.Handler(cors.Options{ - // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts - AllowedOrigins: []string{"https://*", "http://*", "http://localhost:3000"}, - // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedOrigins: []string{"http://localhost:3000", "http://localhost:5173", "http://127.0.0.1:3000"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, ExposedHeaders: []string{"Link"}, AllowCredentials: true, - MaxAge: 300, // Maximum value not ignored by any of major browsers + MaxAge: 300, })) - config := huma.DefaultConfig("ForFarm Public API", "v1.0.0") - api := humachi.New(router, config) + // --- Add Rate Limiter Middleware --- + if config.RATE_LIMIT_ENABLED { + a.logger.Info("Rate limiting enabled", + "rps", config.RATE_LIMIT_RPS, + "ttl", config.RATE_LIMIT_TTL) + + router.Use(httprate.Limit( + config.RATE_LIMIT_RPS, + config.RATE_LIMIT_TTL, + httprate.WithKeyFuncs(httprate.KeyByIP), + )) + } else { + a.logger.Info("Rate limiting is disabled") + } // --- End Rate Limiter Middleware --- + + humaConfig := huma.DefaultConfig("ForFarm Public API", "v1.0.0") + api := humachi.New(router, humaConfig) router.Group(func(r chi.Router) { a.registerAuthRoutes(r, api) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index bb7f232..2fcb3d2 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "log" + "time" "github.com/spf13/viper" ) @@ -21,6 +22,9 @@ var ( OPENWEATHER_CACHE_TTL string WEATHER_FETCH_INTERVAL string GEMINI_API_KEY string + RATE_LIMIT_ENABLED bool + RATE_LIMIT_RPS int + RATE_LIMIT_TTL time.Duration ) func Load() { @@ -38,6 +42,9 @@ func Load() { viper.SetDefault("OPENWEATHER_CACHE_TTL", "15m") viper.SetDefault("WEATHER_FETCH_INTERVAL", "15m") viper.SetDefault("GEMINI_API_KEY", "gemini_api_key") + viper.SetDefault("RATE_LIMIT_ENABLED", true) + viper.SetDefault("RATE_LIMIT_RPS", 10) + viper.SetDefault("RATE_LIMIT_TTL", 5*time.Minute) viper.SetConfigFile(".env") viper.AddConfigPath("../../.") @@ -62,4 +69,7 @@ func Load() { OPENWEATHER_CACHE_TTL = viper.GetString("OPENWEATHER_CACHE_TTL") WEATHER_FETCH_INTERVAL = viper.GetString("WEATHER_FETCH_INTERVAL") GEMINI_API_KEY = viper.GetString("GEMINI_API_KEY") + RATE_LIMIT_ENABLED = viper.GetBool("RATE_LIMIT_ENABLED") + RATE_LIMIT_RPS = viper.GetInt("RATE_LIMIT_RPS") + RATE_LIMIT_TTL = viper.GetDuration("RATE_LIMIT_TTL") }