all repos

onasty @ 643d597

a one-time notes service
10 files changed, 250 insertions(+), 47 deletions(-)
feat: implement caching (#40)

* chore(docker-compose): add redis

* connect to redis and do something

* refactor(usercache): build the redis key with string builder

* refactor(usercache): change naming

* feat(usersrv): add caching for  CheckIfUserIsActivated

* refactor(e2e): handle errors consistently while setting up dbs

* refactor(usercache): set custom ttl for cache

* fixup! refactor(usercache): set custom ttl for cache

* feat(usersrv): actually cache things

* fixup! feat(usersrv): actually cache things

* refactor: update redis connector

* fixup! refactor: update redis connector

* refactor(e2e): connect to redis via it's own helper function

* refactor(usersrv): make linter happy again

* refactor(e2e): refactor, naming mainly

* e2e: reformat

* fix(e2e): stop redis on tests end

* refactor(e2e): setup tests deps in less lines

* refactor(e2e): renaming
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2024-10-27 12:51:04 +0200
Parent: 9d9e251
M .env.example
···
        20
        20
         POSTGRESQL_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable"

      
        21
        21
         MIGRATION_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@localhost:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable"

      
        22
        22
         

      
        
        23
        +REDIS_ADDR="redis:6379"

      
        
        24
        +CACHE_USERS_TTL=1h

      
        
        25
        +

      
        23
        26
         MAILGUN_FROM=onasty@mail.com

      
        24
        27
         MAILGUN_DOMAI='<domain>'

      
        25
        28
         MAILGUN_API_KEY='<token>'

      
M cmd/server/main.go
···
        23
        23
         	"github.com/olexsmir/onasty/internal/store/psql/userepo"

      
        24
        24
         	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"

      
        25
        25
         	"github.com/olexsmir/onasty/internal/store/psqlutil"

      
        
        26
        +	"github.com/olexsmir/onasty/internal/store/rdb"

      
        
        27
        +	"github.com/olexsmir/onasty/internal/store/rdb/usercache"

      
        26
        28
         	httptransport "github.com/olexsmir/onasty/internal/transport/http"

      
        27
        29
         	"github.com/olexsmir/onasty/internal/transport/http/httpserver"

      
        28
        30
         	"github.com/olexsmir/onasty/internal/transport/http/ratelimit"

      ···
        57
        59
         

      
        58
        60
         	// app deps

      
        59
        61
         	psqlDB, err := psqlutil.Connect(ctx, cfg.PostgresDSN)

      
        
        62
        +	if err != nil {

      
        
        63
        +		return err

      
        
        64
        +	}

      
        
        65
        +

      
        
        66
        +	redisDB, err := rdb.Connect(ctx, cfg.RedisAddr, cfg.RedisPassword, cfg.RedisDB)

      
        60
        67
         	if err != nil {

      
        61
        68
         		return err

      
        62
        69
         	}

      ···
        69
        76
         	vertokrepo := vertokrepo.New(psqlDB)

      
        70
        77
         

      
        71
        78
         	userepo := userepo.New(psqlDB)

      
        
        79
        +	usercache := usercache.New(redisDB, cfg.CacheUsersTTL)

      
        72
        80
         	usersrv := usersrv.New(

      
        73
        81
         		userepo,

      
        74
        82
         		sessionrepo,

      ···
        76
        84
         		sha256Hasher,

      
        77
        85
         		jwtTokenizer,

      
        78
        86
         		mailGunMailer,

      
        
        87
        +		usercache,

      
        79
        88
         		cfg.JwtRefreshTokenTTL,

      
        80
        89
         		cfg.VerificationTokenTTL,

      
        81
        90
         		cfg.AppURL,

      ···
        127
        136
         

      
        128
        137
         	if err := psqlDB.Close(); err != nil {

      
        129
        138
         		return errors.Join(errors.New("failed to close postgres connection"), err)

      
        
        139
        +	}

      
        
        140
        +

      
        
        141
        +	if err := redisDB.Close(); err != nil {

      
        
        142
        +		return errors.Join(errors.New("failed to close redis connection"), err)

      
        130
        143
         	}

      
        131
        144
         

      
        132
        145
         	return nil

      
M docker-compose.yml
···
        22
        22
             ports:

      
        23
        23
               - 5432:5432

      
        24
        24
         

      
        
        25
        +  redis:

      
        
        26
        +    image: redis:7.4-alpine

      
        
        27
        +    container_name: onasty-redis

      
        
        28
        +    ports:

      
        
        29
        +      - 6379:6379

      
        
        30
        +

      
        25
        31
           prometheus:

      
        26
        32
             image: prom/prometheus

      
        27
        33
             container_name: onasty-prometheus

      
M e2e/e2e_test.go
···
        25
        25
         	"github.com/olexsmir/onasty/internal/store/psql/userepo"

      
        26
        26
         	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"

      
        27
        27
         	"github.com/olexsmir/onasty/internal/store/psqlutil"

      
        
        28
        +	"github.com/olexsmir/onasty/internal/store/rdb"

      
        
        29
        +	"github.com/olexsmir/onasty/internal/store/rdb/usercache"

      
        28
        30
         	httptransport "github.com/olexsmir/onasty/internal/transport/http"

      
        29
        31
         	"github.com/olexsmir/onasty/internal/transport/http/ratelimit"

      
        
        32
        +	"github.com/redis/go-redis/v9"

      
        30
        33
         	"github.com/stretchr/testify/require"

      
        31
        34
         	"github.com/stretchr/testify/suite"

      
        32
        35
         	"github.com/testcontainers/testcontainers-go"

      
        33
        
        -	"github.com/testcontainers/testcontainers-go/modules/postgres"

      
        
        36
        +	tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"

      
        
        37
        +	tcredis "github.com/testcontainers/testcontainers-go/modules/redis"

      
        34
        38
         	"github.com/testcontainers/testcontainers-go/wait"

      
        35
        39
         

      
        36
        40
         	_ "github.com/golang-migrate/migrate/v4/source/file"

      
        37
        41
         )

      
        38
        42
         

      
        39
        43
         type (

      
        40
        
        -	stopDBFunc   func()

      
        
        44
        +	stopFunc     func()

      
        41
        45
         	AppTestSuite struct {

      
        42
        46
         		suite.Suite

      
        43
        47
         

      ···
        45
        49
         		require *require.Assertions

      
        46
        50
         

      
        47
        51
         		postgresDB   *psqlutil.DB

      
        48
        
        -		stopPostgres stopDBFunc

      
        
        52
        +		stopPostgres stopFunc

      
        
        53
        +

      
        
        54
        +		redisDB   *rdb.DB

      
        
        55
        +		stopRedis stopFunc

      
        49
        56
         

      
        50
        57
         		router       http.Handler

      
        51
        58
         		hasher       hasher.Hasher

      ···
        72
        79
         	e.ctx = context.Background()

      
        73
        80
         	e.require = e.Require()

      
        74
        81
         

      
        75
        
        -	db, stop, err := e.prepPostgres()

      
        76
        
        -	e.require.NoError(err)

      
        77
        
        -

      
        78
        
        -	e.postgresDB = db

      
        79
        
        -	e.stopPostgres = stop

      
        
        82
        +	e.postgresDB, e.stopPostgres = e.prepPostgres()

      
        
        83
        +	e.redisDB, e.stopRedis = e.prepRedis()

      
        80
        84
         

      
        81
        85
         	e.initDeps()

      
        82
        86
         }

      
        83
        87
         

      
        84
        88
         func (e *AppTestSuite) TearDownSuite() {

      
        85
        89
         	e.stopPostgres()

      
        
        90
        +	e.stopRedis()

      
        86
        91
         }

      
        87
        92
         

      
        88
        93
         // initDeps initializes the dependencies for the app

      ···
        103
        108
         	vertokrepo := vertokrepo.New(e.postgresDB)

      
        104
        109
         

      
        105
        110
         	userepo := userepo.New(e.postgresDB)

      
        
        111
        +	usercache := usercache.New(e.redisDB, cfg.CacheUsersTTL)

      
        106
        112
         	usersrv := usersrv.New(

      
        107
        113
         		userepo,

      
        108
        114
         		sessionrepo,

      ···
        110
        116
         		e.hasher,

      
        111
        117
         		e.jwtTokenizer,

      
        112
        118
         		e.mailer,

      
        
        119
        +		usercache,

      
        113
        120
         		cfg.JwtRefreshTokenTTL,

      
        114
        121
         		cfg.VerificationTokenTTL,

      
        115
        122
         		cfg.AppURL,

      ···
        129
        136
         	e.router = handler.Handler()

      
        130
        137
         }

      
        131
        138
         

      
        132
        
        -func (e *AppTestSuite) prepPostgres() (*psqlutil.DB, stopDBFunc, error) {

      
        
        139
        +func (e *AppTestSuite) prepPostgres() (*psqlutil.DB, stopFunc) {

      
        133
        140
         	dbCredential := "testing"

      
        134
        
        -	postgresContainer, err := postgres.Run(e.ctx,

      
        
        141
        +	postgresContainer, err := tcpostgres.Run(e.ctx,

      
        135
        142
         		"postgres:16-alpine",

      
        136
        
        -		postgres.WithUsername(dbCredential),

      
        137
        
        -		postgres.WithPassword(dbCredential),

      
        138
        
        -		postgres.WithDatabase(dbCredential),

      
        
        143
        +		tcpostgres.WithUsername(dbCredential),

      
        
        144
        +		tcpostgres.WithPassword(dbCredential),

      
        
        145
        +		tcpostgres.WithDatabase(dbCredential),

      
        139
        146
         		testcontainers.WithWaitStrategy(wait.ForListeningPort("5432/tcp")))

      
        140
        147
         	e.require.NoError(err)

      
        141
        148
         

      
        142
        
        -	stop := func() {

      
        143
        
        -		err = postgresContainer.Terminate(e.ctx)

      
        144
        
        -		e.require.NoError(err)

      
        145
        
        -	}

      
        
        149
        +	stop := func() { e.require.NoError(postgresContainer.Terminate(e.ctx)) }

      
        146
        150
         

      
        147
        151
         	// connect to the db

      
        148
        152
         	host, err := postgresContainer.Host(e.ctx)

      ···
        151
        155
         	port, err := postgresContainer.MappedPort(e.ctx, "5432/tcp")

      
        152
        156
         	e.require.NoError(err)

      
        153
        157
         

      
        154
        
        -	db, err := psqlutil.Connect(

      
        155
        
        -		e.ctx,

      
        156
        
        -		fmt.Sprintf( //nolint:nosprintfhostport

      
        157
        
        -			"postgres://%s:%s@%s:%s/%s",

      
        158
        
        -			dbCredential,

      
        159
        
        -			dbCredential,

      
        160
        
        -			host,

      
        161
        
        -			port.Port(),

      
        162
        
        -			dbCredential,

      
        163
        
        -		),

      
        164
        
        -	)

      
        
        158
        +	db, err := psqlutil.Connect(e.ctx, fmt.Sprintf( //nolint:nosprintfhostport

      
        
        159
        +		"postgres://%s:%s@%s:%s/%s",

      
        
        160
        +		dbCredential,

      
        
        161
        +		dbCredential,

      
        
        162
        +		host,

      
        
        163
        +		port.Port(),

      
        
        164
        +		dbCredential,

      
        
        165
        +	))

      
        165
        166
         	e.require.NoError(err)

      
        166
        167
         

      
        167
        168
         	// run migrations

      ···
        175
        176
         	)

      
        176
        177
         	e.require.NoError(err)

      
        177
        178
         

      
        178
        
        -	err = m.Up()

      
        
        179
        +	e.require.NoError(m.Up())

      
        
        180
        +	e.require.NoError(driver.Close())

      
        
        181
        +

      
        
        182
        +	return db, stop

      
        
        183
        +}

      
        
        184
        +

      
        
        185
        +func (e *AppTestSuite) prepRedis() (*rdb.DB, stopFunc) {

      
        
        186
        +	redisContainer, err := tcredis.Run(e.ctx, "redis:7.4-alpine")

      
        179
        187
         	e.require.NoError(err)

      
        180
        188
         

      
        181
        
        -	return db, stop, driver.Close()

      
        
        189
        +	stop := func() { e.require.NoError(redisContainer.Terminate(e.ctx)) }

      
        
        190
        +

      
        
        191
        +	uri, err := redisContainer.ConnectionString(e.ctx)

      
        
        192
        +	e.require.NoError(err)

      
        
        193
        +

      
        
        194
        +	connOpts, err := redis.ParseURL(uri)

      
        
        195
        +	e.require.NoError(err)

      
        
        196
        +

      
        
        197
        +	redis, err := rdb.Connect(e.ctx, connOpts.Addr, connOpts.Password, connOpts.DB)

      
        
        198
        +	e.require.NoError(err)

      
        
        199
        +

      
        
        200
        +	return redis, stop

      
        182
        201
         }

      
        183
        202
         

      
        184
        203
         func (e *AppTestSuite) getConfig() *config.Config {

      ···
        194
        213
         		LogShowLine:          os.Getenv("LOG_SHOW_LINE") == "true",

      
        195
        214
         		LogFormat:            "text",

      
        196
        215
         		LogLevel:             "debug",

      
        
        216
        +		CacheUsersTTL:        time.Second,

      
        197
        217
         	}

      
        198
        218
         }

      
M go.mod
···
        12
        12
         	github.com/jackc/pgx/v5 v5.7.1

      
        13
        13
         	github.com/mailgun/mailgun-go/v4 v4.16.0

      
        14
        14
         	github.com/prometheus/client_golang v1.20.5

      
        
        15
        +	github.com/redis/go-redis/v9 v9.7.0

      
        15
        16
         	github.com/stretchr/testify v1.9.0

      
        16
        
        -	github.com/testcontainers/testcontainers-go v0.33.0

      
        17
        
        -	github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0

      
        
        17
        +	github.com/testcontainers/testcontainers-go v0.34.0

      
        
        18
        +	github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0

      
        
        19
        +	github.com/testcontainers/testcontainers-go/modules/redis v0.34.0

      
        18
        20
         	golang.org/x/time v0.7.0

      
        19
        21
         )

      
        20
        22
         

      ···
        32
        34
         	github.com/cloudwego/iasm v0.2.0 // indirect

      
        33
        35
         	github.com/containerd/log v0.1.0 // indirect

      
        34
        36
         	github.com/containerd/platforms v0.2.1 // indirect

      
        35
        
        -	github.com/cpuguy83/dockercfg v0.3.1 // indirect

      
        
        37
        +	github.com/cpuguy83/dockercfg v0.3.2 // indirect

      
        36
        38
         	github.com/davecgh/go-spew v1.1.1 // indirect

      
        
        39
        +	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect

      
        37
        40
         	github.com/distribution/reference v0.6.0 // indirect

      
        38
        41
         	github.com/docker/docker v27.2.0+incompatible // indirect

      
        39
        42
         	github.com/docker/go-connections v0.5.0 // indirect

      
M go.sum
···
        13
        13
         github.com/ahmetb/go-linq v3.0.0+incompatible/go.mod h1:PFffvbdbtw+QTB0WKRP0cNht7vnCfnGlEpak/DVg5cY=

      
        14
        14
         github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

      
        15
        15
         github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=

      
        
        16
        +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=

      
        
        17
        +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=

      
        
        18
        +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=

      
        
        19
        +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=

      
        16
        20
         github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=

      
        17
        21
         github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=

      
        18
        22
         github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=

      ···
        33
        37
         github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=

      
        34
        38
         github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=

      
        35
        39
         github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=

      
        36
        
        -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=

      
        37
        
        -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=

      
        
        40
        +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=

      
        
        41
        +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=

      
        38
        42
         github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=

      
        39
        43
         github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=

      
        40
        44
         github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=

      
        41
        45
         github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

      
        42
        46
         github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

      
        43
        47
         github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

      
        
        48
        +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=

      
        
        49
        +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

      
        44
        50
         github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0=

      
        45
        51
         github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs=

      
        46
        52
         github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=

      ···
        84
        90
         github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=

      
        85
        91
         github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=

      
        86
        92
         github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=

      
        
        93
        +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=

      
        
        94
        +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=

      
        87
        95
         github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=

      
        88
        96
         github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=

      
        89
        97
         github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=

      ···
        256
        264
         github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=

      
        257
        265
         github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=

      
        258
        266
         github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=

      
        
        267
        +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=

      
        
        268
        +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=

      
        259
        269
         github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

      
        260
        270
         github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=

      
        261
        271
         github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=

      ···
        281
        291
         github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=

      
        282
        292
         github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=

      
        283
        293
         github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

      
        
        294
        +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=

      
        284
        295
         github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=

      
        285
        296
         github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

      
        286
        297
         github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

      ···
        293
        304
         github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=

      
        294
        305
         github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=

      
        295
        306
         github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=

      
        296
        
        -github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=

      
        297
        
        -github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=

      
        298
        
        -github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0 h1:c+Gt+XLJjqFAejgX4hSpnHIpC9eAhvgI/TFWL/PbrFI=

      
        299
        
        -github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0/go.mod h1:I4DazHBoWDyf69ByOIyt3OdNjefiUx372459txOpQ3o=

      
        
        307
        +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo=

      
        
        308
        +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ=

      
        
        309
        +github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0 h1:c51aBXT3v2HEBVarmaBnsKzvgZjC5amn0qsj8Naqi50=

      
        
        310
        +github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0/go.mod h1:EWP75ogLQU4M4L8U+20mFipjV4WIR9WtlMXSB6/wiuc=

      
        
        311
        +github.com/testcontainers/testcontainers-go/modules/redis v0.34.0 h1:HkkKZPi6W2I+ywqplvnKOYRBKXQgpdxErBbdgx8F8nw=

      
        
        312
        +github.com/testcontainers/testcontainers-go/modules/redis v0.34.0/go.mod h1:iUkbN75F4E8WC5C1MfHbGOHOuKU7gOJfHjtwMT8G9QE=

      
        300
        313
         github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=

      
        301
        314
         github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=

      
        302
        315
         github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=

      
M internal/config/config.go
···
        8
        8
         )

      
        9
        9
         

      
        10
        10
         type Config struct {

      
        11
        
        -	AppEnv       string

      
        12
        
        -	AppURL       string

      
        13
        
        -	ServerPort   string

      
        
        11
        +	AppEnv     string

      
        
        12
        +	AppURL     string

      
        
        13
        +	ServerPort string

      
        
        14
        +

      
        14
        15
         	PostgresDSN  string

      
        15
        16
         	PasswordSalt string

      
        
        17
        +

      
        
        18
        +	RedisAddr     string

      
        
        19
        +	RedisPassword string

      
        
        20
        +	RedisDB       int

      
        
        21
        +

      
        
        22
        +	CacheUsersTTL time.Duration

      
        16
        23
         

      
        17
        24
         	JwtSigningKey      string

      
        18
        25
         	JwtAccessTokenTTL  time.Duration

      ···
        37
        44
         

      
        38
        45
         func NewConfig() *Config {

      
        39
        46
         	return &Config{

      
        40
        
        -		AppEnv:       getenvOrDefault("APP_ENV", "debug"),

      
        41
        
        -		AppURL:       getenvOrDefault("APP_URL", ""),

      
        42
        
        -		ServerPort:   getenvOrDefault("SERVER_PORT", "3000"),

      
        
        47
        +		AppEnv:     getenvOrDefault("APP_ENV", "debug"),

      
        
        48
        +		AppURL:     getenvOrDefault("APP_URL", ""),

      
        
        49
        +		ServerPort: getenvOrDefault("SERVER_PORT", "3000"),

      
        
        50
        +

      
        43
        51
         		PostgresDSN:  getenvOrDefault("POSTGRESQL_DSN", ""),

      
        44
        52
         		PasswordSalt: getenvOrDefault("PASSWORD_SALT", ""),

      
        
        53
        +

      
        
        54
        +		RedisAddr:     getenvOrDefault("REDIS_ADDR", ""),

      
        
        55
        +		RedisPassword: getenvOrDefault("REDIS_PASSWORD", ""),

      
        
        56
        +		RedisDB:       mustGetenvOrDefaultInt(getenvOrDefault("REDIS_DB", "0"), 0),

      
        
        57
        +

      
        
        58
        +		CacheUsersTTL: mustParseDuration(getenvOrDefault("CACHE_USERS_TTL", "1h")),

      
        45
        59
         

      
        46
        60
         		JwtSigningKey: getenvOrDefault("JWT_SIGNING_KEY", ""),

      
        47
        61
         		JwtAccessTokenTTL: mustParseDuration(

      
M internal/service/usersrv/usersrv.go
···
        3
        3
         import (

      
        4
        4
         	"context"

      
        5
        5
         	"errors"

      
        
        6
        +	"log/slog"

      
        6
        7
         	"time"

      
        7
        8
         

      
        8
        9
         	"github.com/gofrs/uuid/v5"

      ···
        14
        15
         	"github.com/olexsmir/onasty/internal/store/psql/sessionrepo"

      
        15
        16
         	"github.com/olexsmir/onasty/internal/store/psql/userepo"

      
        16
        17
         	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"

      
        
        18
        +	"github.com/olexsmir/onasty/internal/store/rdb/usercache"

      
        17
        19
         	"github.com/olexsmir/onasty/internal/transport/http/reqid"

      
        18
        20
         )

      
        19
        21
         

      ···
        43
        45
         	hasher       hasher.Hasher

      
        44
        46
         	jwtTokenizer jwtutil.JWTTokenizer

      
        45
        47
         	mailer       mailer.Mailer

      
        
        48
        +	cache        usercache.UserCacheer

      
        46
        49
         

      
        47
        50
         	refreshTokenTTL      time.Duration

      
        48
        51
         	verificationTokenTTL time.Duration

      ···
        56
        59
         	hasher hasher.Hasher,

      
        57
        60
         	jwtTokenizer jwtutil.JWTTokenizer,

      
        58
        61
         	mailer mailer.Mailer,

      
        
        62
        +	cache usercache.UserCacheer,

      
        59
        63
         	refreshTokenTTL, verificationTokenTTL time.Duration,

      
        60
        64
         	appURL string,

      
        61
        65
         ) *UserSrv {

      ···
        66
        70
         		hasher:               hasher,

      
        67
        71
         		jwtTokenizer:         jwtTokenizer,

      
        68
        72
         		mailer:               mailer,

      
        
        73
        +		cache:                cache,

      
        69
        74
         		refreshTokenTTL:      refreshTokenTTL,

      
        70
        75
         		verificationTokenTTL: verificationTokenTTL,

      
        71
        76
         		appURL:               appURL,

      ···
        227
        232
         }

      
        228
        233
         

      
        229
        234
         func (u UserSrv) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) {

      
        230
        
        -	return u.userstore.CheckIfUserExists(ctx, id)

      
        
        235
        +	if r, err := u.cache.GetIsExists(ctx, id.String()); err == nil {

      
        
        236
        +		return r, nil

      
        
        237
        +	} else { //nolint:revive

      
        
        238
        +		slog.ErrorContext(ctx, "usercache", "err", err)

      
        
        239
        +	}

      
        
        240
        +

      
        
        241
        +	isExists, err := u.userstore.CheckIfUserExists(ctx, id)

      
        
        242
        +	if err != nil {

      
        
        243
        +		return false, err

      
        
        244
        +	}

      
        
        245
        +

      
        
        246
        +	if err := u.cache.SetIsExists(ctx, id.String(), isExists); err != nil {

      
        
        247
        +		slog.Error("usercache", "err", err)

      
        
        248
        +	}

      
        
        249
        +

      
        
        250
        +	return isExists, nil

      
        231
        251
         }

      
        232
        252
         

      
        233
        253
         func (u UserSrv) CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) {

      
        234
        
        -	return u.userstore.CheckIfUserIsActivated(ctx, userID)

      
        
        254
        +	if r, err := u.cache.GetIsActivated(ctx, userID.String()); err == nil {

      
        
        255
        +		return r, nil

      
        
        256
        +	} else { //nolint:revive

      
        
        257
        +		slog.ErrorContext(ctx, "usercache", "err", err)

      
        
        258
        +	}

      
        
        259
        +

      
        
        260
        +	isActivated, err := u.userstore.CheckIfUserExists(ctx, userID)

      
        
        261
        +	if err != nil {

      
        
        262
        +		return false, err

      
        
        263
        +	}

      
        
        264
        +

      
        
        265
        +	if err := u.cache.SetIsActivated(ctx, userID.String(), isActivated); err != nil {

      
        
        266
        +		slog.Error("usercache", "err", err)

      
        
        267
        +	}

      
        
        268
        +

      
        
        269
        +	return isActivated, nil

      
        235
        270
         }

      
        236
        271
         

      
        237
        272
         func (u UserSrv) getTokens(userID uuid.UUID) (dtos.TokensDTO, error) {

      
A internal/store/rdb/rdb.go
···
        
        1
        +package rdb

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +

      
        
        6
        +	"github.com/redis/go-redis/v9"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +type DB struct{ *redis.Client }

      
        
        10
        +

      
        
        11
        +func Connect(ctx context.Context, addr, password string, db int) (*DB, error) {

      
        
        12
        +	client := redis.NewClient(&redis.Options{ //nolint:exhaustruct

      
        
        13
        +		Addr:     addr,

      
        
        14
        +		Password: password,

      
        
        15
        +		DB:       db,

      
        
        16
        +	})

      
        
        17
        +

      
        
        18
        +	_, err := client.Ping(ctx).Result()

      
        
        19
        +	if err != nil {

      
        
        20
        +		return nil, err

      
        
        21
        +	}

      
        
        22
        +

      
        
        23
        +	return &DB{Client: client}, nil

      
        
        24
        +}

      
A internal/store/rdb/usercache/usercache.go
···
        
        1
        +package usercache

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"strings"

      
        
        6
        +	"time"

      
        
        7
        +

      
        
        8
        +	"github.com/olexsmir/onasty/internal/store/rdb"

      
        
        9
        +)

      
        
        10
        +

      
        
        11
        +type UserCacheer interface {

      
        
        12
        +	SetIsExists(ctx context.Context, userID string, isExists bool) error

      
        
        13
        +	GetIsExists(ctx context.Context, userID string) (isExists bool, err error)

      
        
        14
        +

      
        
        15
        +	SetIsActivated(ctx context.Context, userID string, isActivated bool) error

      
        
        16
        +	GetIsActivated(ctx context.Context, userID string) (isActivated bool, err error)

      
        
        17
        +}

      
        
        18
        +

      
        
        19
        +var _ UserCacheer = (*UserCache)(nil)

      
        
        20
        +

      
        
        21
        +type UserCache struct {

      
        
        22
        +	rdb *rdb.DB

      
        
        23
        +	ttl time.Duration

      
        
        24
        +}

      
        
        25
        +

      
        
        26
        +func New(rdb *rdb.DB, ttl time.Duration) *UserCache {

      
        
        27
        +	return &UserCache{

      
        
        28
        +		rdb: rdb,

      
        
        29
        +		ttl: ttl,

      
        
        30
        +	}

      
        
        31
        +}

      
        
        32
        +

      
        
        33
        +func (u *UserCache) SetIsExists(ctx context.Context, userID string, val bool) error {

      
        
        34
        +	_, err := u.rdb.

      
        
        35
        +		Set(ctx, getKey("exists", userID), val, u.ttl).

      
        
        36
        +		Result()

      
        
        37
        +	return err

      
        
        38
        +}

      
        
        39
        +

      
        
        40
        +func (u *UserCache) GetIsExists(ctx context.Context, userID string) (bool, error) {

      
        
        41
        +	res, err := u.rdb.Get(ctx, getKey(userID, "exists")).Bool()

      
        
        42
        +	if err != nil {

      
        
        43
        +		return false, err

      
        
        44
        +	}

      
        
        45
        +

      
        
        46
        +	return res, nil

      
        
        47
        +}

      
        
        48
        +

      
        
        49
        +func (u *UserCache) SetIsActivated(ctx context.Context, userID string, val bool) error {

      
        
        50
        +	_, err := u.rdb.

      
        
        51
        +		Set(ctx, getKey("activated", userID), val, u.ttl).

      
        
        52
        +		Result()

      
        
        53
        +	return err

      
        
        54
        +}

      
        
        55
        +

      
        
        56
        +func (u *UserCache) GetIsActivated(ctx context.Context, userID string) (bool, error) {

      
        
        57
        +	res, err := u.rdb.Get(ctx, getKey(userID, "activated")).Bool()

      
        
        58
        +	if err != nil {

      
        
        59
        +		return false, err

      
        
        60
        +	}

      
        
        61
        +	return res, nil

      
        
        62
        +}

      
        
        63
        +

      
        
        64
        +// getKey return a key for redis in this format user:<userID>:<key>

      
        
        65
        +func getKey(userID, key string) string {

      
        
        66
        +	var sb strings.Builder

      
        
        67
        +	sb.WriteString("user:")

      
        
        68
        +	sb.WriteString(userID)

      
        
        69
        +	sb.WriteString(":")

      
        
        70
        +	sb.WriteString(key)

      
        
        71
        +	return sb.String()

      
        
        72
        +}