From 69d79f6a54c57ad8aaebf7edde416aa116d18df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20D=C3=ADaz=20Marco?= Date: Sun, 3 Sep 2023 14:03:45 +0200 Subject: [PATCH] Add rate limit to authentication creation. --- .env.example | 5 + docker-compose.yml | 14 + go.mod | 4 + go.sum | 12 + internal/api/reporting/errors.go | 5 + internal/api/resolvers/authentication.go | 31 +- internal/api/resolvers/resolver.go | 15 +- .../resolvers_test/authentication_test.go | 28 ++ .../api/resolvers/resolvers_test/main_test.go | 11 +- internal/config/main.go | 7 +- internal/generated/container/container.go | 302 +++++++++++++++++- internal/generated/container/defs.go | 102 +++++- internal/services/limiter.go | 21 ++ internal/services/provider/main.go | 2 + internal/services/redis.go | 22 ++ internal/services/resolver.go | 12 +- 16 files changed, 556 insertions(+), 37 deletions(-) create mode 100755 internal/services/limiter.go create mode 100755 internal/services/redis.go diff --git a/.env.example b/.env.example index 6a4f3e1..c2b5b1a 100755 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ #DELVE_PORT=8001 #COCKROACHDB_CONSOLE_PORT=8080 +#REDIS_PORT=6379 # Application APP_ENV=development @@ -18,6 +19,10 @@ COCKROACHDB_PASSWORD= COCKROACHDB_DATABASE=defaultdb COCKROACHDB_TLS_MODE=disable +REDIS_ADDRESS=redis:6379 +#REDIS_PASSWORD= +#REDIS_DATABASE=0 + HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000 MAIL_SOURCE= diff --git a/docker-compose.yml b/docker-compose.yml index c5eddd5..ecbfe8f 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - ${DELVE_PORT:-8001}:${DELVE_PORT:-8001} depends_on: - cockroachdb + - redis tty: true cockroachdb: @@ -31,5 +32,18 @@ services: timeout: 3s retries: 3 + redis: + image: redis:7.2.0-alpine + command: + - redis-server + - --appendonly + - 'yes' + user: redis + volumes: + - redis:/data + ports: + - ${REDIS_PORT:-6379}:6379 + volumes: cockroachdb: + redis: diff --git a/go.mod b/go.mod index a795bc8..7694fdb 100755 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/brianvoe/gofakeit/v6 v6.23.1 github.com/caarlos0/env/v9 v9.0.0 github.com/criptalia/spanish_dni_validator v0.0.0-20230502125532-3278e5ffc050 + github.com/go-redis/redis_rate/v10 v10.0.1 github.com/google/uuid v1.3.1 github.com/hashicorp/go-multierror v1.1.1 github.com/kataras/hcaptcha v0.0.2 @@ -24,6 +25,7 @@ require ( github.com/oklog/ulid/v2 v2.1.0 github.com/pariz/gountries v0.1.6 github.com/realclientip/realclientip-go v1.0.0 + github.com/redis/go-redis/v9 v9.1.0 github.com/sarulabs/di/v2 v2.4.2 github.com/sarulabs/dingo/v4 v4.2.0 github.com/stretchr/testify v1.8.4 @@ -41,8 +43,10 @@ require ( github.com/andybalholm/cascadia v1.0.0 // indirect github.com/aokoli/goutils v1.0.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-openapi/inflect v0.19.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect diff --git a/go.sum b/go.sum index e5387d0..15fad86 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,14 @@ github.com/aws/aws-sdk-go v1.44.332 h1:Ze+98F41+LxoJUdsisAFThV+0yYYLYw17/Vt0++nF github.com/aws/aws-sdk-go v1.44.332/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/brianvoe/gofakeit/v6 v6.23.1 h1:k2gX0hQpJStvixDbbw8oJOvPBg0XmHJWbSOF5JkiUHw= github.com/brianvoe/gofakeit/v6 v6.23.1/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= +github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0= +github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/criptalia/spanish_dni_validator v0.0.0-20230502125532-3278e5ffc050 h1:r8iENXwfxhNFynMkd8oyLfOs6kDyyHG67FiOWDTrFS0= @@ -46,11 +52,15 @@ github.com/criptalia/spanish_dni_validator v0.0.0-20230502125532-3278e5ffc050/go github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-redis/redis_rate/v10 v10.0.1 h1:calPxi7tVlxojKunJwQ72kwfozdy25RjA0bCj1h0MUo= +github.com/go-redis/redis_rate/v10 v10.0.1/go.mod h1:EMiuO9+cjRkR7UvdvwMO7vbgqJkltQHtwbdIQvaBKIU= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -127,6 +137,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/realclientip/realclientip-go v1.0.0 h1:+yPxeC0mEaJzq1BfCt2h4BxlyrvIIBzR6suDc3BEF1U= github.com/realclientip/realclientip-go v1.0.0/go.mod h1:CXnUdVwFRcXFJIRb/dTYqbT7ud48+Pi2pFm80bxDmcI= +github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= +github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/internal/api/reporting/errors.go b/internal/api/reporting/errors.go index a12a272..39c4edc 100755 --- a/internal/api/reporting/errors.go +++ b/internal/api/reporting/errors.go @@ -22,6 +22,11 @@ var ErrInternal = &gqlerror.Error{ Extensions: map[string]any{"code": "INTERNAL"}, } +var ErrRateLimit = &gqlerror.Error{ + Message: "A limit of attempts per unit of time has been reached for this action.", + Extensions: map[string]any{"code": "RATE_LIMIT"}, +} + var ErrNotFound = &gqlerror.Error{ Message: "The specified resource does not exist.", Extensions: map[string]any{"code": "NOT_FOUND"}, diff --git a/internal/api/resolvers/authentication.go b/internal/api/resolvers/authentication.go index 864871f..876053e 100755 --- a/internal/api/resolvers/authentication.go +++ b/internal/api/resolvers/authentication.go @@ -6,6 +6,8 @@ package resolvers import ( "context" + "fmt" + "strings" "github.com/alexedwards/argon2id" "github.com/avptp/brain/internal/api/reporting" @@ -14,6 +16,7 @@ import ( "github.com/avptp/brain/internal/generated/data/person" "github.com/avptp/brain/internal/generated/data/privacy" "github.com/avptp/brain/internal/transport/request" + "github.com/go-redis/redis_rate/v10" ) // CreateAuthentication is the resolver for the createAuthentication field. @@ -21,21 +24,47 @@ func (r *mutationResolver) CreateAuthentication(ctx context.Context, input api.C d := data.FromContext(ctx) // transactional data client for mutations allowCtx := privacy.DecisionContext(ctx, privacy.Allow) + email := strings.ToLower(input.Email) + rlKey := fmt.Sprintf("createAuthentication:%s", email) + + // Rate limit by normalized email + // (to avoid exposing an email existence) + res, err := r.limiter.Allow(ctx, rlKey, redis_rate.PerHour(r.cfg.AuthenticationRateLimit)) + + if err != nil { + return nil, err + } + + if res.Allowed <= 0 { + return nil, reporting.ErrRateLimit + } + + // Retrieve person by email + // (it returns "wrong password" error to avoid exposing an email existence) person, err := d.Person. Query(). - Where(person.EmailEQ(input.Email)). + Where(person.EmailEQ(email)). First(allowCtx) if err != nil { return nil, reporting.ErrWrongPassword } + // Match the password match, err := argon2id.ComparePasswordAndHash(input.Password, person.Password) if err != nil || !match { return nil, reporting.ErrWrongPassword } + // Reset rate limit + err = r.limiter.Reset(ctx, rlKey) + + if err != nil { + return nil, err + } + + // Create authentication and return its token ip := request.IPFromCtx(ctx) a, err := d.Authentication. diff --git a/internal/api/resolvers/resolver.go b/internal/api/resolvers/resolver.go index c668b9f..70e781c 100644 --- a/internal/api/resolvers/resolver.go +++ b/internal/api/resolvers/resolver.go @@ -5,20 +5,29 @@ import ( "github.com/avptp/brain/internal/config" "github.com/avptp/brain/internal/generated/data" "github.com/avptp/brain/internal/messaging" + "github.com/go-redis/redis_rate/v10" ) type Resolver struct { - cfg *config.Config captcha auth.Captcha + cfg *config.Config data *data.Client + limiter *redis_rate.Limiter messenger messaging.Messenger } -func NewResolver(cfg *config.Config, captcha auth.Captcha, data *data.Client, messenger messaging.Messenger) *Resolver { +func NewResolver( + captcha auth.Captcha, + cfg *config.Config, + data *data.Client, + limiter *redis_rate.Limiter, + messenger messaging.Messenger, +) *Resolver { return &Resolver{ - cfg, captcha, + cfg, data, + limiter, messenger, } } diff --git a/internal/api/resolvers/resolvers_test/authentication_test.go b/internal/api/resolvers/resolvers_test/authentication_test.go index ded4c10..0653c6b 100755 --- a/internal/api/resolvers/resolvers_test/authentication_test.go +++ b/internal/api/resolvers/resolvers_test/authentication_test.go @@ -2,10 +2,12 @@ package resolvers_test import ( "encoding/base64" + "fmt" "github.com/99designs/gqlgen/client" "github.com/avptp/brain/internal/api/reporting" "github.com/avptp/brain/internal/generated/data/authentication" + "github.com/go-redis/redis_rate/v10" ) func (t *TestSuite) TestAuthentication() { @@ -55,6 +57,32 @@ func (t *TestSuite) TestAuthentication() { t.Equal(p.ID, atc.Edges.Person.ID) }) + t.Run("create_with_too_many_attempts", func() { + _, p, pf, _, _ := t.authenticate() + + rlKey := fmt.Sprintf("createAuthentication:%s", p.Email) + + res, err := t.limiter.AllowN( + t.allowCtx, + rlKey, + redis_rate.PerHour(t.cfg.AuthenticationRateLimit), + t.cfg.AuthenticationRateLimit, + ) + + t.NoError(err) + t.LessOrEqual(res.Remaining, 0) + + var response create + err = t.api.Post( + createMutation, + &response, + client.Var("email", pf.Email), + client.Var("password", pf.Password), + ) + + t.ErrorContains(err, reporting.ErrRateLimit.Message) + }) + t.Run("create_with_wrong_email", func() { input := t.factory.Person().Fields diff --git a/internal/api/resolvers/resolvers_test/main_test.go b/internal/api/resolvers/resolvers_test/main_test.go index fd33f03..8345ead 100755 --- a/internal/api/resolvers/resolvers_test/main_test.go +++ b/internal/api/resolvers/resolvers_test/main_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/avptp/brain/internal/auth/auth_test" + "github.com/avptp/brain/internal/config" "github.com/avptp/brain/internal/generated/container" "github.com/avptp/brain/internal/generated/data" "github.com/avptp/brain/internal/generated/data/factories" @@ -15,6 +16,7 @@ import ( "github.com/avptp/brain/internal/messaging/messaging_test" "github.com/avptp/brain/internal/services" "github.com/avptp/brain/internal/transport" + "github.com/go-redis/redis_rate/v10" "github.com/99designs/gqlgen/client" "github.com/brianvoe/gofakeit/v6" @@ -52,10 +54,11 @@ func init() { type TestSuite struct { suite.Suite - ctn *container.Container - data *data.Client - + ctn *container.Container captcha *auth_test.MockedCaptcha + cfg *config.Config + data *data.Client + limiter *redis_rate.Limiter messenger *messaging_test.MockedMessenger factory *factories.Factory @@ -87,7 +90,9 @@ func (t *TestSuite) SetupSuite() { ctn := builder.Build() t.ctn = ctn + t.cfg = ctn.GetConfig() t.data = ctn.GetData() + t.limiter = ctn.GetLimiter() t.factory = factories.New(t.data) t.api = client.New(transport.Mux(ctn)) diff --git a/internal/config/main.go b/internal/config/main.go index 855e4fa..18d5917 100755 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -17,6 +17,10 @@ type Config struct { CockroachDBTLSMode string `env:"COCKROACHDB_TLS_MODE" envDefault:"require"` CockroachDBTLSCA string `env:"COCKROACHDB_TLS_CA"` + RedisAddress string `env:"REDIS_ADDRESS"` + RedisPassword string `env:"REDIS_PASSWORD"` + RedisDatabase int `env:"REDIS_DATABASE"` + HcaptchaSecret string `env:"HCAPTCHA_SECRET"` MailSource string `env:"MAIL_SOURCE"` @@ -26,7 +30,8 @@ type Config struct { AwsKeyId string `env:"AWS_KEY_ID"` AwsKeySecret string `env:"AWS_KEY_SECRET"` - AuthorizationMaxAge time.Duration `env:"AUTHORIZATION_MAX_AGE"` + AuthenticationRateLimit int `env:"AUTHENTICATION_RATE_LIMIT" envDefault:"5"` // per person and hour + AuthorizationMaxAge time.Duration `env:"AUTHORIZATION_MAX_AGE"` FrontUrl string `env:"FRONT_URL"` FrontEmailAuthorizationPath string `env:"FRONT_EMAIL_AUTHORIZATION_PATH"` diff --git a/internal/generated/container/container.go b/internal/generated/container/container.go index 9d83d2b..937bf42 100644 --- a/internal/generated/container/container.go +++ b/internal/generated/container/container.go @@ -18,9 +18,11 @@ import ( data "github.com/avptp/brain/internal/generated/data" messaging "github.com/avptp/brain/internal/messaging" ses "github.com/aws/aws-sdk-go/service/ses" + v1 "github.com/go-redis/redis_rate/v10" tasks "github.com/madflojo/tasks" in "github.com/nicksnyder/go-i18n/v2/i18n" realclientipgo "github.com/realclientip/realclientip-go" + v "github.com/redis/go-redis/v9" ) // C retrieves a Container from an interface. @@ -865,6 +867,141 @@ func IpStrategy(i interface{}) realclientipgo.Strategy { return C(i).GetIpStrategy() } +// SafeGetLimiter retrieves the "limiter" object from the app scope. +// +// --------------------------------------------- +// +// name: "limiter" +// type: *v1.Limiter +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// - "1": Service(*v.Client) ["redis"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// If the object can not be retrieved, it returns an error. +func (c *Container) SafeGetLimiter() (*v1.Limiter, error) { + i, err := c.ctn.SafeGet("limiter") + if err != nil { + var eo *v1.Limiter + return eo, err + } + o, ok := i.(*v1.Limiter) + if !ok { + return o, errors.New("could get 'limiter' because the object could not be cast to *v1.Limiter") + } + return o, nil +} + +// GetLimiter retrieves the "limiter" object from the app scope. +// +// --------------------------------------------- +// +// name: "limiter" +// type: *v1.Limiter +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// - "1": Service(*v.Client) ["redis"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// If the object can not be retrieved, it panics. +func (c *Container) GetLimiter() *v1.Limiter { + o, err := c.SafeGetLimiter() + if err != nil { + panic(err) + } + return o +} + +// UnscopedSafeGetLimiter retrieves the "limiter" object from the app scope. +// +// --------------------------------------------- +// +// name: "limiter" +// type: *v1.Limiter +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// - "1": Service(*v.Client) ["redis"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// This method can be called even if app is a sub-scope of the container. +// If the object can not be retrieved, it returns an error. +func (c *Container) UnscopedSafeGetLimiter() (*v1.Limiter, error) { + i, err := c.ctn.UnscopedSafeGet("limiter") + if err != nil { + var eo *v1.Limiter + return eo, err + } + o, ok := i.(*v1.Limiter) + if !ok { + return o, errors.New("could get 'limiter' because the object could not be cast to *v1.Limiter") + } + return o, nil +} + +// UnscopedGetLimiter retrieves the "limiter" object from the app scope. +// +// --------------------------------------------- +// +// name: "limiter" +// type: *v1.Limiter +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// - "1": Service(*v.Client) ["redis"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// This method can be called even if app is a sub-scope of the container. +// If the object can not be retrieved, it panics. +func (c *Container) UnscopedGetLimiter() *v1.Limiter { + o, err := c.UnscopedSafeGetLimiter() + if err != nil { + panic(err) + } + return o +} + +// Limiter retrieves the "limiter" object from the app scope. +// +// --------------------------------------------- +// +// name: "limiter" +// type: *v1.Limiter +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// - "1": Service(*v.Client) ["redis"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// It tries to find the container with the C method and the given interface. +// If the container can be retrieved, it calls the GetLimiter method. +// If the container can not be retrieved, it panics. +func Limiter(i interface{}) *v1.Limiter { + return C(i).GetLimiter() +} + // SafeGetLogger retrieves the "logger" object from the main scope. // // --------------------------------------------- @@ -1135,6 +1272,136 @@ func Messenger(i interface{}) messaging.Messenger { return C(i).GetMessenger() } +// SafeGetRedis retrieves the "redis" object from the app scope. +// +// --------------------------------------------- +// +// name: "redis" +// type: *v.Client +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// If the object can not be retrieved, it returns an error. +func (c *Container) SafeGetRedis() (*v.Client, error) { + i, err := c.ctn.SafeGet("redis") + if err != nil { + var eo *v.Client + return eo, err + } + o, ok := i.(*v.Client) + if !ok { + return o, errors.New("could get 'redis' because the object could not be cast to *v.Client") + } + return o, nil +} + +// GetRedis retrieves the "redis" object from the app scope. +// +// --------------------------------------------- +// +// name: "redis" +// type: *v.Client +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// If the object can not be retrieved, it panics. +func (c *Container) GetRedis() *v.Client { + o, err := c.SafeGetRedis() + if err != nil { + panic(err) + } + return o +} + +// UnscopedSafeGetRedis retrieves the "redis" object from the app scope. +// +// --------------------------------------------- +// +// name: "redis" +// type: *v.Client +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// This method can be called even if app is a sub-scope of the container. +// If the object can not be retrieved, it returns an error. +func (c *Container) UnscopedSafeGetRedis() (*v.Client, error) { + i, err := c.ctn.UnscopedSafeGet("redis") + if err != nil { + var eo *v.Client + return eo, err + } + o, ok := i.(*v.Client) + if !ok { + return o, errors.New("could get 'redis' because the object could not be cast to *v.Client") + } + return o, nil +} + +// UnscopedGetRedis retrieves the "redis" object from the app scope. +// +// --------------------------------------------- +// +// name: "redis" +// type: *v.Client +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// This method can be called even if app is a sub-scope of the container. +// If the object can not be retrieved, it panics. +func (c *Container) UnscopedGetRedis() *v.Client { + o, err := c.UnscopedSafeGetRedis() + if err != nil { + panic(err) + } + return o +} + +// Redis retrieves the "redis" object from the app scope. +// +// --------------------------------------------- +// +// name: "redis" +// type: *v.Client +// scope: "app" +// build: func +// params: +// - "0": Service(*config.Config) ["config"] +// unshared: false +// close: false +// +// --------------------------------------------- +// +// It tries to find the container with the C method and the given interface. +// If the container can be retrieved, it calls the GetRedis method. +// If the container can not be retrieved, it panics. +func Redis(i interface{}) *v.Client { + return C(i).GetRedis() +} + // SafeGetResolver retrieves the "resolver" object from the app scope. // // --------------------------------------------- @@ -1144,10 +1411,11 @@ func Messenger(i interface{}) messaging.Messenger { // scope: "app" // build: func // params: -// - "0": Service(*config.Config) ["config"] -// - "1": Service(auth.Captcha) ["captcha"] +// - "0": Service(auth.Captcha) ["captcha"] +// - "1": Service(*config.Config) ["config"] // - "2": Service(*data.Client) ["data"] -// - "3": Service(messaging.Messenger) ["messenger"] +// - "3": Service(*v1.Limiter) ["limiter"] +// - "4": Service(messaging.Messenger) ["messenger"] // unshared: false // close: false // @@ -1176,10 +1444,11 @@ func (c *Container) SafeGetResolver() (*resolvers.Resolver, error) { // scope: "app" // build: func // params: -// - "0": Service(*config.Config) ["config"] -// - "1": Service(auth.Captcha) ["captcha"] +// - "0": Service(auth.Captcha) ["captcha"] +// - "1": Service(*config.Config) ["config"] // - "2": Service(*data.Client) ["data"] -// - "3": Service(messaging.Messenger) ["messenger"] +// - "3": Service(*v1.Limiter) ["limiter"] +// - "4": Service(messaging.Messenger) ["messenger"] // unshared: false // close: false // @@ -1203,10 +1472,11 @@ func (c *Container) GetResolver() *resolvers.Resolver { // scope: "app" // build: func // params: -// - "0": Service(*config.Config) ["config"] -// - "1": Service(auth.Captcha) ["captcha"] +// - "0": Service(auth.Captcha) ["captcha"] +// - "1": Service(*config.Config) ["config"] // - "2": Service(*data.Client) ["data"] -// - "3": Service(messaging.Messenger) ["messenger"] +// - "3": Service(*v1.Limiter) ["limiter"] +// - "4": Service(messaging.Messenger) ["messenger"] // unshared: false // close: false // @@ -1236,10 +1506,11 @@ func (c *Container) UnscopedSafeGetResolver() (*resolvers.Resolver, error) { // scope: "app" // build: func // params: -// - "0": Service(*config.Config) ["config"] -// - "1": Service(auth.Captcha) ["captcha"] +// - "0": Service(auth.Captcha) ["captcha"] +// - "1": Service(*config.Config) ["config"] // - "2": Service(*data.Client) ["data"] -// - "3": Service(messaging.Messenger) ["messenger"] +// - "3": Service(*v1.Limiter) ["limiter"] +// - "4": Service(messaging.Messenger) ["messenger"] // unshared: false // close: false // @@ -1264,10 +1535,11 @@ func (c *Container) UnscopedGetResolver() *resolvers.Resolver { // scope: "app" // build: func // params: -// - "0": Service(*config.Config) ["config"] -// - "1": Service(auth.Captcha) ["captcha"] +// - "0": Service(auth.Captcha) ["captcha"] +// - "1": Service(*config.Config) ["config"] // - "2": Service(*data.Client) ["data"] -// - "3": Service(messaging.Messenger) ["messenger"] +// - "3": Service(*v1.Limiter) ["limiter"] +// - "4": Service(messaging.Messenger) ["messenger"] // unshared: false // close: false // diff --git a/internal/generated/container/defs.go b/internal/generated/container/defs.go index 99e830d..06adf39 100644 --- a/internal/generated/container/defs.go +++ b/internal/generated/container/defs.go @@ -14,9 +14,11 @@ import ( data "github.com/avptp/brain/internal/generated/data" messaging "github.com/avptp/brain/internal/messaging" ses "github.com/aws/aws-sdk-go/service/ses" + v1 "github.com/go-redis/redis_rate/v10" tasks "github.com/madflojo/tasks" in "github.com/nicksnyder/go-i18n/v2/i18n" realclientipgo "github.com/realclientip/realclientip-go" + v "github.com/redis/go-redis/v9" ) func getDiDefs(provider dingo.Provider) []di.Def { @@ -156,6 +158,44 @@ func getDiDefs(provider dingo.Provider) []di.Def { }, Unshared: false, }, + { + Name: "limiter", + Scope: "app", + Build: func(ctn di.Container) (interface{}, error) { + d, err := provider.Get("limiter") + if err != nil { + var eo *v1.Limiter + return eo, err + } + pi0, err := ctn.SafeGet("config") + if err != nil { + var eo *v1.Limiter + return eo, err + } + p0, ok := pi0.(*config.Config) + if !ok { + var eo *v1.Limiter + return eo, errors.New("could not cast parameter 0 to *config.Config") + } + pi1, err := ctn.SafeGet("redis") + if err != nil { + var eo *v1.Limiter + return eo, err + } + p1, ok := pi1.(*v.Client) + if !ok { + var eo *v1.Limiter + return eo, errors.New("could not cast parameter 1 to *v.Client") + } + b, ok := d.Build.(func(*config.Config, *v.Client) (*v1.Limiter, error)) + if !ok { + var eo *v1.Limiter + return eo, errors.New("could not cast build function to func(*config.Config, *v.Client) (*v1.Limiter, error)") + } + return b(p0, p1) + }, + Unshared: false, + }, { Name: "logger", Scope: "", @@ -232,6 +272,34 @@ func getDiDefs(provider dingo.Provider) []di.Def { }, Unshared: false, }, + { + Name: "redis", + Scope: "app", + Build: func(ctn di.Container) (interface{}, error) { + d, err := provider.Get("redis") + if err != nil { + var eo *v.Client + return eo, err + } + pi0, err := ctn.SafeGet("config") + if err != nil { + var eo *v.Client + return eo, err + } + p0, ok := pi0.(*config.Config) + if !ok { + var eo *v.Client + return eo, errors.New("could not cast parameter 0 to *config.Config") + } + b, ok := d.Build.(func(*config.Config) (*v.Client, error)) + if !ok { + var eo *v.Client + return eo, errors.New("could not cast build function to func(*config.Config) (*v.Client, error)") + } + return b(p0) + }, + Unshared: false, + }, { Name: "resolver", Scope: "app", @@ -241,25 +309,25 @@ func getDiDefs(provider dingo.Provider) []di.Def { var eo *resolvers.Resolver return eo, err } - pi0, err := ctn.SafeGet("config") + pi0, err := ctn.SafeGet("captcha") if err != nil { var eo *resolvers.Resolver return eo, err } - p0, ok := pi0.(*config.Config) + p0, ok := pi0.(auth.Captcha) if !ok { var eo *resolvers.Resolver - return eo, errors.New("could not cast parameter 0 to *config.Config") + return eo, errors.New("could not cast parameter 0 to auth.Captcha") } - pi1, err := ctn.SafeGet("captcha") + pi1, err := ctn.SafeGet("config") if err != nil { var eo *resolvers.Resolver return eo, err } - p1, ok := pi1.(auth.Captcha) + p1, ok := pi1.(*config.Config) if !ok { var eo *resolvers.Resolver - return eo, errors.New("could not cast parameter 1 to auth.Captcha") + return eo, errors.New("could not cast parameter 1 to *config.Config") } pi2, err := ctn.SafeGet("data") if err != nil { @@ -271,22 +339,32 @@ func getDiDefs(provider dingo.Provider) []di.Def { var eo *resolvers.Resolver return eo, errors.New("could not cast parameter 2 to *data.Client") } - pi3, err := ctn.SafeGet("messenger") + pi3, err := ctn.SafeGet("limiter") + if err != nil { + var eo *resolvers.Resolver + return eo, err + } + p3, ok := pi3.(*v1.Limiter) + if !ok { + var eo *resolvers.Resolver + return eo, errors.New("could not cast parameter 3 to *v1.Limiter") + } + pi4, err := ctn.SafeGet("messenger") if err != nil { var eo *resolvers.Resolver return eo, err } - p3, ok := pi3.(messaging.Messenger) + p4, ok := pi4.(messaging.Messenger) if !ok { var eo *resolvers.Resolver - return eo, errors.New("could not cast parameter 3 to messaging.Messenger") + return eo, errors.New("could not cast parameter 4 to messaging.Messenger") } - b, ok := d.Build.(func(*config.Config, auth.Captcha, *data.Client, messaging.Messenger) (*resolvers.Resolver, error)) + b, ok := d.Build.(func(auth.Captcha, *config.Config, *data.Client, *v1.Limiter, messaging.Messenger) (*resolvers.Resolver, error)) if !ok { var eo *resolvers.Resolver - return eo, errors.New("could not cast build function to func(*config.Config, auth.Captcha, *data.Client, messaging.Messenger) (*resolvers.Resolver, error)") + return eo, errors.New("could not cast build function to func(auth.Captcha, *config.Config, *data.Client, *v1.Limiter, messaging.Messenger) (*resolvers.Resolver, error)") } - return b(p0, p1, p2, p3) + return b(p0, p1, p2, p3, p4) }, Unshared: false, }, diff --git a/internal/services/limiter.go b/internal/services/limiter.go new file mode 100755 index 0000000..06438ae --- /dev/null +++ b/internal/services/limiter.go @@ -0,0 +1,21 @@ +package services + +import ( + "github.com/avptp/brain/internal/config" + "github.com/go-redis/redis_rate/v10" + libredis "github.com/redis/go-redis/v9" + "github.com/sarulabs/di/v2" + "github.com/sarulabs/dingo/v4" +) + +const Limiter = "limiter" + +var LimiterDef = dingo.Def{ + Name: Limiter, + Scope: di.App, + Build: func(cfg *config.Config, redis *libredis.Client) (*redis_rate.Limiter, error) { + limiter := redis_rate.NewLimiter(redis) + + return limiter, nil + }, +} diff --git a/internal/services/provider/main.go b/internal/services/provider/main.go index efae5c1..8dda91c 100644 --- a/internal/services/provider/main.go +++ b/internal/services/provider/main.go @@ -16,8 +16,10 @@ func (p *Provider) Load() error { services.DataDef, services.I18nDef, services.IPStrategyDef, + services.LimiterDef, services.LoggerDef, services.MessengerDef, + services.RedisDef, services.ResolverDef, services.SchedulerDef, services.SesDef, diff --git a/internal/services/redis.go b/internal/services/redis.go new file mode 100755 index 0000000..5398896 --- /dev/null +++ b/internal/services/redis.go @@ -0,0 +1,22 @@ +package services + +import ( + "github.com/avptp/brain/internal/config" + "github.com/redis/go-redis/v9" + "github.com/sarulabs/di/v2" + "github.com/sarulabs/dingo/v4" +) + +const Redis = "redis" + +var RedisDef = dingo.Def{ + Name: Redis, + Scope: di.App, + Build: func(cfg *config.Config) (*redis.Client, error) { + return redis.NewClient(&redis.Options{ + Addr: cfg.RedisAddress, + Password: cfg.RedisPassword, + DB: cfg.RedisDatabase, + }), nil + }, +} diff --git a/internal/services/resolver.go b/internal/services/resolver.go index 63b2af9..3eb6794 100755 --- a/internal/services/resolver.go +++ b/internal/services/resolver.go @@ -6,6 +6,7 @@ import ( "github.com/avptp/brain/internal/config" "github.com/avptp/brain/internal/generated/data" "github.com/avptp/brain/internal/messaging" + "github.com/go-redis/redis_rate/v10" "github.com/sarulabs/di/v2" "github.com/sarulabs/dingo/v4" ) @@ -15,11 +16,18 @@ const Resolver = "resolver" var ResolverDef = dingo.Def{ Name: Resolver, Scope: di.App, - Build: func(cfg *config.Config, captcha auth.Captcha, data *data.Client, messenger messaging.Messenger) (*resolvers.Resolver, error) { + Build: func( + captcha auth.Captcha, + cfg *config.Config, + data *data.Client, + limiter *redis_rate.Limiter, + messenger messaging.Messenger, + ) (*resolvers.Resolver, error) { return resolvers.NewResolver( - cfg, captcha, + cfg, data, + limiter, messenger, ), nil },