@@ -20,6 +20,9 @@ POSTGRES_DATABASE=onasty
POSTGRESQL_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable" MIGRATION_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@localhost:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable" +REDIS_ADDR="redis:6379" +CACHE_USERS_TTL=1h + MAILGUN_FROM=onasty@mail.com MAILGUN_DOMAI='<domain>' MAILGUN_API_KEY='<token>'
@@ -23,6 +23,8 @@ "github.com/olexsmir/onasty/internal/store/psql/sessionrepo"
"github.com/olexsmir/onasty/internal/store/psql/userepo" "github.com/olexsmir/onasty/internal/store/psql/vertokrepo" "github.com/olexsmir/onasty/internal/store/psqlutil" + "github.com/olexsmir/onasty/internal/store/rdb" + "github.com/olexsmir/onasty/internal/store/rdb/usercache" httptransport "github.com/olexsmir/onasty/internal/transport/http" "github.com/olexsmir/onasty/internal/transport/http/httpserver" "github.com/olexsmir/onasty/internal/transport/http/ratelimit"@@ -57,6 +59,11 @@ }
// app deps psqlDB, err := psqlutil.Connect(ctx, cfg.PostgresDSN) + if err != nil { + return err + } + + redisDB, err := rdb.Connect(ctx, cfg.RedisAddr, cfg.RedisPassword, cfg.RedisDB) if err != nil { return err }@@ -69,6 +76,7 @@ sessionrepo := sessionrepo.New(psqlDB)
vertokrepo := vertokrepo.New(psqlDB) userepo := userepo.New(psqlDB) + usercache := usercache.New(redisDB, cfg.CacheUsersTTL) usersrv := usersrv.New( userepo, sessionrepo,@@ -76,6 +84,7 @@ vertokrepo,
sha256Hasher, jwtTokenizer, mailGunMailer, + usercache, cfg.JwtRefreshTokenTTL, cfg.VerificationTokenTTL, cfg.AppURL,@@ -127,6 +136,10 @@ }
if err := psqlDB.Close(); err != nil { return errors.Join(errors.New("failed to close postgres connection"), err) + } + + if err := redisDB.Close(); err != nil { + return errors.Join(errors.New("failed to close redis connection"), err) } return nil
@@ -22,6 +22,12 @@ - .docker/postgres:/var/lib/postgresql/data
ports: - 5432:5432 + redis: + image: redis:7.4-alpine + container_name: onasty-redis + ports: + - 6379:6379 + prometheus: image: prom/prometheus container_name: onasty-prometheus
@@ -25,19 +25,23 @@ "github.com/olexsmir/onasty/internal/store/psql/sessionrepo"
"github.com/olexsmir/onasty/internal/store/psql/userepo" "github.com/olexsmir/onasty/internal/store/psql/vertokrepo" "github.com/olexsmir/onasty/internal/store/psqlutil" + "github.com/olexsmir/onasty/internal/store/rdb" + "github.com/olexsmir/onasty/internal/store/rdb/usercache" httptransport "github.com/olexsmir/onasty/internal/transport/http" "github.com/olexsmir/onasty/internal/transport/http/ratelimit" + "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/postgres" + tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" + tcredis "github.com/testcontainers/testcontainers-go/modules/redis" "github.com/testcontainers/testcontainers-go/wait" _ "github.com/golang-migrate/migrate/v4/source/file" ) type ( - stopDBFunc func() + stopFunc func() AppTestSuite struct { suite.Suite@@ -45,7 +49,10 @@ ctx context.Context
require *require.Assertions postgresDB *psqlutil.DB - stopPostgres stopDBFunc + stopPostgres stopFunc + + redisDB *rdb.DB + stopRedis stopFunc router http.Handler hasher hasher.Hasher@@ -72,17 +79,15 @@ func (e *AppTestSuite) SetupSuite() {
e.ctx = context.Background() e.require = e.Require() - db, stop, err := e.prepPostgres() - e.require.NoError(err) - - e.postgresDB = db - e.stopPostgres = stop + e.postgresDB, e.stopPostgres = e.prepPostgres() + e.redisDB, e.stopRedis = e.prepRedis() e.initDeps() } func (e *AppTestSuite) TearDownSuite() { e.stopPostgres() + e.stopRedis() } // initDeps initializes the dependencies for the app@@ -103,6 +108,7 @@ sessionrepo := sessionrepo.New(e.postgresDB)
vertokrepo := vertokrepo.New(e.postgresDB) userepo := userepo.New(e.postgresDB) + usercache := usercache.New(e.redisDB, cfg.CacheUsersTTL) usersrv := usersrv.New( userepo, sessionrepo,@@ -110,6 +116,7 @@ vertokrepo,
e.hasher, e.jwtTokenizer, e.mailer, + usercache, cfg.JwtRefreshTokenTTL, cfg.VerificationTokenTTL, cfg.AppURL,@@ -129,20 +136,17 @@ handler := httptransport.NewTransport(usersrv, notesrv, ratelimitCfg)
e.router = handler.Handler() } -func (e *AppTestSuite) prepPostgres() (*psqlutil.DB, stopDBFunc, error) { +func (e *AppTestSuite) prepPostgres() (*psqlutil.DB, stopFunc) { dbCredential := "testing" - postgresContainer, err := postgres.Run(e.ctx, + postgresContainer, err := tcpostgres.Run(e.ctx, "postgres:16-alpine", - postgres.WithUsername(dbCredential), - postgres.WithPassword(dbCredential), - postgres.WithDatabase(dbCredential), + tcpostgres.WithUsername(dbCredential), + tcpostgres.WithPassword(dbCredential), + tcpostgres.WithDatabase(dbCredential), testcontainers.WithWaitStrategy(wait.ForListeningPort("5432/tcp"))) e.require.NoError(err) - stop := func() { - err = postgresContainer.Terminate(e.ctx) - e.require.NoError(err) - } + stop := func() { e.require.NoError(postgresContainer.Terminate(e.ctx)) } // connect to the db host, err := postgresContainer.Host(e.ctx)@@ -151,17 +155,14 @@
port, err := postgresContainer.MappedPort(e.ctx, "5432/tcp") e.require.NoError(err) - db, err := psqlutil.Connect( - e.ctx, - fmt.Sprintf( //nolint:nosprintfhostport - "postgres://%s:%s@%s:%s/%s", - dbCredential, - dbCredential, - host, - port.Port(), - dbCredential, - ), - ) + db, err := psqlutil.Connect(e.ctx, fmt.Sprintf( //nolint:nosprintfhostport + "postgres://%s:%s@%s:%s/%s", + dbCredential, + dbCredential, + host, + port.Port(), + dbCredential, + )) e.require.NoError(err) // run migrations@@ -175,10 +176,28 @@ "pgxv5", driver,
) e.require.NoError(err) - err = m.Up() + e.require.NoError(m.Up()) + e.require.NoError(driver.Close()) + + return db, stop +} + +func (e *AppTestSuite) prepRedis() (*rdb.DB, stopFunc) { + redisContainer, err := tcredis.Run(e.ctx, "redis:7.4-alpine") e.require.NoError(err) - return db, stop, driver.Close() + stop := func() { e.require.NoError(redisContainer.Terminate(e.ctx)) } + + uri, err := redisContainer.ConnectionString(e.ctx) + e.require.NoError(err) + + connOpts, err := redis.ParseURL(uri) + e.require.NoError(err) + + redis, err := rdb.Connect(e.ctx, connOpts.Addr, connOpts.Password, connOpts.DB) + e.require.NoError(err) + + return redis, stop } func (e *AppTestSuite) getConfig() *config.Config {@@ -194,5 +213,6 @@ VerificationTokenTTL: 24 * time.Hour,
LogShowLine: os.Getenv("LOG_SHOW_LINE") == "true", LogFormat: "text", LogLevel: "debug", + CacheUsersTTL: time.Second, } }
@@ -12,9 +12,11 @@ github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2
github.com/jackc/pgx/v5 v5.7.1 github.com/mailgun/mailgun-go/v4 v4.16.0 github.com/prometheus/client_golang v1.20.5 + github.com/redis/go-redis/v9 v9.7.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 - github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0 + github.com/testcontainers/testcontainers-go/modules/redis v0.34.0 golang.org/x/time v0.7.0 )@@ -32,8 +34,9 @@ github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.2.0+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect
@@ -13,6 +13,10 @@ github.com/ahmetb/go-linq v3.0.0+incompatible h1:qQkjjOXKrKOTy83X8OpRmnKflXKQIL/mC/gMVVDMhOA=
github.com/ahmetb/go-linq v3.0.0+incompatible/go.mod h1:PFffvbdbtw+QTB0WKRP0cNht7vnCfnGlEpak/DVg5cY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=@@ -33,14 +37,16 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=@@ -84,6 +90,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=@@ -256,6 +264,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=@@ -281,6 +291,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=@@ -293,10 +304,12 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= -github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= -github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0 h1:c+Gt+XLJjqFAejgX4hSpnHIpC9eAhvgI/TFWL/PbrFI= -github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0/go.mod h1:I4DazHBoWDyf69ByOIyt3OdNjefiUx372459txOpQ3o= +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= +github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0 h1:c51aBXT3v2HEBVarmaBnsKzvgZjC5amn0qsj8Naqi50= +github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0/go.mod h1:EWP75ogLQU4M4L8U+20mFipjV4WIR9WtlMXSB6/wiuc= +github.com/testcontainers/testcontainers-go/modules/redis v0.34.0 h1:HkkKZPi6W2I+ywqplvnKOYRBKXQgpdxErBbdgx8F8nw= +github.com/testcontainers/testcontainers-go/modules/redis v0.34.0/go.mod h1:iUkbN75F4E8WC5C1MfHbGOHOuKU7gOJfHjtwMT8G9QE= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -8,11 +8,18 @@ "time"
) type Config struct { - AppEnv string - AppURL string - ServerPort string + AppEnv string + AppURL string + ServerPort string + PostgresDSN string PasswordSalt string + + RedisAddr string + RedisPassword string + RedisDB int + + CacheUsersTTL time.Duration JwtSigningKey string JwtAccessTokenTTL time.Duration@@ -37,11 +44,18 @@ }
func NewConfig() *Config { return &Config{ - AppEnv: getenvOrDefault("APP_ENV", "debug"), - AppURL: getenvOrDefault("APP_URL", ""), - ServerPort: getenvOrDefault("SERVER_PORT", "3000"), + AppEnv: getenvOrDefault("APP_ENV", "debug"), + AppURL: getenvOrDefault("APP_URL", ""), + ServerPort: getenvOrDefault("SERVER_PORT", "3000"), + PostgresDSN: getenvOrDefault("POSTGRESQL_DSN", ""), PasswordSalt: getenvOrDefault("PASSWORD_SALT", ""), + + RedisAddr: getenvOrDefault("REDIS_ADDR", ""), + RedisPassword: getenvOrDefault("REDIS_PASSWORD", ""), + RedisDB: mustGetenvOrDefaultInt(getenvOrDefault("REDIS_DB", "0"), 0), + + CacheUsersTTL: mustParseDuration(getenvOrDefault("CACHE_USERS_TTL", "1h")), JwtSigningKey: getenvOrDefault("JWT_SIGNING_KEY", ""), JwtAccessTokenTTL: mustParseDuration(
@@ -3,6 +3,7 @@
import ( "context" "errors" + "log/slog" "time" "github.com/gofrs/uuid/v5"@@ -14,6 +15,7 @@ "github.com/olexsmir/onasty/internal/models"
"github.com/olexsmir/onasty/internal/store/psql/sessionrepo" "github.com/olexsmir/onasty/internal/store/psql/userepo" "github.com/olexsmir/onasty/internal/store/psql/vertokrepo" + "github.com/olexsmir/onasty/internal/store/rdb/usercache" "github.com/olexsmir/onasty/internal/transport/http/reqid" )@@ -43,6 +45,7 @@ vertokrepo vertokrepo.VerificationTokenStorer
hasher hasher.Hasher jwtTokenizer jwtutil.JWTTokenizer mailer mailer.Mailer + cache usercache.UserCacheer refreshTokenTTL time.Duration verificationTokenTTL time.Duration@@ -56,6 +59,7 @@ vertokrepo vertokrepo.VerificationTokenStorer,
hasher hasher.Hasher, jwtTokenizer jwtutil.JWTTokenizer, mailer mailer.Mailer, + cache usercache.UserCacheer, refreshTokenTTL, verificationTokenTTL time.Duration, appURL string, ) *UserSrv {@@ -66,6 +70,7 @@ vertokrepo: vertokrepo,
hasher: hasher, jwtTokenizer: jwtTokenizer, mailer: mailer, + cache: cache, refreshTokenTTL: refreshTokenTTL, verificationTokenTTL: verificationTokenTTL, appURL: appURL,@@ -227,11 +232,41 @@ return u.jwtTokenizer.Parse(token)
} func (u UserSrv) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) { - return u.userstore.CheckIfUserExists(ctx, id) + if r, err := u.cache.GetIsExists(ctx, id.String()); err == nil { + return r, nil + } else { //nolint:revive + slog.ErrorContext(ctx, "usercache", "err", err) + } + + isExists, err := u.userstore.CheckIfUserExists(ctx, id) + if err != nil { + return false, err + } + + if err := u.cache.SetIsExists(ctx, id.String(), isExists); err != nil { + slog.Error("usercache", "err", err) + } + + return isExists, nil } func (u UserSrv) CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) { - return u.userstore.CheckIfUserIsActivated(ctx, userID) + if r, err := u.cache.GetIsActivated(ctx, userID.String()); err == nil { + return r, nil + } else { //nolint:revive + slog.ErrorContext(ctx, "usercache", "err", err) + } + + isActivated, err := u.userstore.CheckIfUserExists(ctx, userID) + if err != nil { + return false, err + } + + if err := u.cache.SetIsActivated(ctx, userID.String(), isActivated); err != nil { + slog.Error("usercache", "err", err) + } + + return isActivated, nil } func (u UserSrv) getTokens(userID uuid.UUID) (dtos.TokensDTO, error) {
@@ -0,0 +1,24 @@
+package rdb + +import ( + "context" + + "github.com/redis/go-redis/v9" +) + +type DB struct{ *redis.Client } + +func Connect(ctx context.Context, addr, password string, db int) (*DB, error) { + client := redis.NewClient(&redis.Options{ //nolint:exhaustruct + Addr: addr, + Password: password, + DB: db, + }) + + _, err := client.Ping(ctx).Result() + if err != nil { + return nil, err + } + + return &DB{Client: client}, nil +}
@@ -0,0 +1,72 @@
+package usercache + +import ( + "context" + "strings" + "time" + + "github.com/olexsmir/onasty/internal/store/rdb" +) + +type UserCacheer interface { + SetIsExists(ctx context.Context, userID string, isExists bool) error + GetIsExists(ctx context.Context, userID string) (isExists bool, err error) + + SetIsActivated(ctx context.Context, userID string, isActivated bool) error + GetIsActivated(ctx context.Context, userID string) (isActivated bool, err error) +} + +var _ UserCacheer = (*UserCache)(nil) + +type UserCache struct { + rdb *rdb.DB + ttl time.Duration +} + +func New(rdb *rdb.DB, ttl time.Duration) *UserCache { + return &UserCache{ + rdb: rdb, + ttl: ttl, + } +} + +func (u *UserCache) SetIsExists(ctx context.Context, userID string, val bool) error { + _, err := u.rdb. + Set(ctx, getKey("exists", userID), val, u.ttl). + Result() + return err +} + +func (u *UserCache) GetIsExists(ctx context.Context, userID string) (bool, error) { + res, err := u.rdb.Get(ctx, getKey(userID, "exists")).Bool() + if err != nil { + return false, err + } + + return res, nil +} + +func (u *UserCache) SetIsActivated(ctx context.Context, userID string, val bool) error { + _, err := u.rdb. + Set(ctx, getKey("activated", userID), val, u.ttl). + Result() + return err +} + +func (u *UserCache) GetIsActivated(ctx context.Context, userID string) (bool, error) { + res, err := u.rdb.Get(ctx, getKey(userID, "activated")).Bool() + if err != nil { + return false, err + } + return res, nil +} + +// getKey return a key for redis in this format user:<userID>:<key> +func getKey(userID, key string) string { + var sb strings.Builder + sb.WriteString("user:") + sb.WriteString(userID) + sb.WriteString(":") + sb.WriteString(key) + return sb.String() +}