Skip to content

feat: add redis session driver#696

Open
henrywhitaker3 wants to merge 3 commits intosteveiliop56:mainfrom
henrywhitaker3:feat/redis-sessions
Open

feat: add redis session driver#696
henrywhitaker3 wants to merge 3 commits intosteveiliop56:mainfrom
henrywhitaker3:feat/redis-sessions

Conversation

@henrywhitaker3
Copy link

@henrywhitaker3 henrywhitaker3 commented Mar 7, 2026

Adds a new session driver option to add redis as a backend for sessions. To use it, set the following vars:

  • TINYAUTH_DATABASE_REDIS_URL=redis.redis.svc:6379
  • TINYAUTH_SESSION_DRIVER=redis

Summary by CodeRabbit

  • New Features
    • Configurable Redis session support (URL/password/DB) with default driver remaining "database".
    • Redis-backed session store: create, retrieve, update, and delete sessions in Redis.
    • Authentication now uses the configured session backend so existing auth flows work with Redis sessions.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

Adds rueidis dependency and Redis configuration; initializes a Rueidis client when session driver is "redis"; introduces a Redis-backed SessionRepository and a SessionRepository interface; AuthService now depends on the session abstraction and bootstrap selects DB or Redis session storage.

Changes

Cohort / File(s) Summary
Dependency & Config
go.mod, internal/config/config.go
Add github.com/redis/rueidis v1.0.72. Introduce RedisConfig and SessionConfig, add Session to top-level Config (default driver "database") and include Redis in DatabaseConfig.
Bootstrap / Init
internal/bootstrap/app_bootstrap.go, internal/bootstrap/db_bootstrap.go, internal/bootstrap/service_bootstrap.go
Add SetupRedis(url,password,db) returning rueidis.Client. Bootstrap conditionally initializes Redis when Session.Driver == "redis", update initServices signature to accept the redis client and choose session repository accordingly.
Repository: Redis-backed sessions
internal/repository/redis_session_queries.go
New RedisSessionRepository with GetSession, CreateSession, UpdateSession, DeleteSession implemented using rueidis and JSON serialization; constructor NewRedisSessionRepository.
Service: Auth abstraction
internal/service/auth_service.go
Add SessionRepository interface. AuthService now depends on SessionRepository (constructor/signature updated). Treat rueidis nil responses as "session not found."

Sequence Diagram(s)

sequenceDiagram
    participant App as App Bootstrap
    participant Config as Configuration
    participant Redis as Redis
    participant Services as Service Init
    participant SessionRepo as Session Repository
    participant AuthSvc as Auth Service

    App->>Config: load config (Session.Driver)
    Config-->>App: config

    alt Session.Driver == "redis"
        App->>Redis: SetupRedis(url, password, db)
        Redis-->>App: rueidis.Client
    else
        App-->>App: skip Redis setup
    end

    App->>Services: initServices(queries, redisClient)
    alt Session.Driver == "redis"
        Services->>SessionRepo: NewRedisSessionRepository(redisClient)
        SessionRepo-->>Services: RedisSessionRepository
    else
        Services->>SessionRepo: use DB-backed session repo (queries)
        SessionRepo-->>Services: DB Session Repository
    end

    Services->>AuthSvc: NewAuthService(..., SessionRepository)
    AuthSvc-->>Services: AuthService ready
    Services-->>App: services initialized
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I hopped through config, nose to the key,
Found Redis roots where session seeds be,
JSON crumbs stored beneath a TTL moon,
DB or cache — both hum a new tune,
A rabbit claps paws, refactor complete 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add redis session driver' directly summarizes the main change—adding Redis as a new session driver option. It aligns with the PR objectives and the comprehensive changes across configuration, bootstrap, repository, and service files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
internal/config/config.go (1)

99-112: Consider adding PasswordFile option for Redis password.

For consistency with other secret handling in this codebase (e.g., ClientSecretFile for OAuth, PasswordFile for basic auth), consider adding a PasswordFile option for the Redis password. This allows users to manage secrets via files rather than environment variables.

Suggested addition
 type RedisConfig struct {
 	URL      string `description:"The url of the redis instance to connect to" yaml:"url"`
 	Password string `description:"The password to connect to redis with" yaml:"password"`
+	PasswordFile string `description:"Path to file containing the redis password" yaml:"passwordFile"`
 	DB       int    `description:"The DB index to use" yaml:"db"`
 }

Then in app_bootstrap.go, use utils.GetSecret(app.config.Database.Redis.Password, app.config.Database.Redis.PasswordFile) similar to OAuth secret handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/config.go` around lines 99 - 112, Add a PasswordFile field to
the RedisConfig struct so users can supply Redis password via a file (consistent
with other secret handling); update RedisConfig (and any usage in
DatabaseConfig) to include PasswordFile string `yaml:"passwordFile"` and then in
app_bootstrap.go replace direct uses of app.config.Database.Redis.Password with
utils.GetSecret(app.config.Database.Redis.Password,
app.config.Database.Redis.PasswordFile) (or equivalent) where the Redis password
is read; ensure any documentation/validation that checks Redis password
considers the new PasswordFile option.
internal/repository/redis_session_queries.go (1)

10-18: Consider adding a key prefix to avoid collisions in shared Redis instances.

Using raw UUIDs as keys could conflict with other applications sharing the same Redis instance. A prefix like tinyauth:session: would provide namespace isolation.

Suggested approach
const sessionKeyPrefix = "tinyauth:session:"

func sessionKey(uuid string) string {
	return sessionKeyPrefix + uuid
}

// Then use sessionKey(cookie) instead of cookie directly
cmd := r.c.B().Get().Key(sessionKey(cookie)).Build()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repository/redis_session_queries.go` around lines 10 - 18, Redis
keys use raw UUIDs which can collide in shared Redis; add a namespace prefix
constant (e.g. sessionKeyPrefix) and helper function sessionKey(uuid string)
string to build keys, then update RedisSessionRepository methods (methods on
RedisSessionRepository that call r.c.B().Get().Key(...), Set, Del, etc.) to use
sessionKey(cookie) instead of the raw cookie/uuid; keep constructor
NewRedisSessionRepository unchanged but ensure all internal key usages are
replaced to avoid cross-application collisions.
internal/bootstrap/db_bootstrap.go (1)

60-70: Add explicit connection timeout and TLS configuration to the Redis client setup.

The current implementation lacks resilience and production-readiness:

  1. Connection timeout: Relying on implicit defaults (5s) is fragile. Explicitly configure timeout via Dialer.Timeout to handle unreachable Redis servers gracefully.

  2. TLS support: Production deployments typically require TLS. The current config has no TLSConfig field, preventing secure Redis connections.

Suggested improvement with timeout and TLS
 func (app *BootstrapApp) SetupRedis(url, password string, db int) (rueidis.Client, error) {
 	client, err := rueidis.NewClient(rueidis.ClientOption{
 		InitAddress: []string{url},
 		Password:    password,
 		SelectDB:    db,
+		Dialer: net.Dialer{
+			Timeout:   5 * time.Second,
+			KeepAlive: 30 * time.Second,
+		},
+		TLSConfig: nil, // Set to *tls.Config for production Redis
 	})

Alternatively, use URL-based configuration: rueidis.MustParseURL("rediss://host:port?dial_timeout=5s") for automatic TLS and timeout setup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/bootstrap/db_bootstrap.go` around lines 60 - 70, The SetupRedis
function currently calls rueidis.NewClient with minimal ClientOption; update it
to explicitly set connection timeout and TLS by populating the
ClientOption.Dialer.Timeout and ClientOption.TLSConfig (or use
rueidis.MustParseURL with a rediss:// URL and dial_timeout query) so unreachable
servers and secure connections are handled; modify the SetupRedis implementation
(where rueidis.NewClient and rueidis.ClientOption are used) to accept/create a
tls.Config when TLS is required and set Dialer.Timeout to a sensible duration
(e.g., 5s) before returning the client or error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/bootstrap/app_bootstrap.go`:
- Around line 133-144: The bootstrap currently creates a rueidis.Client via
app.SetupRedis (assigned to variable redisClient) but never closes it and
Redis-backed sessions lack an expiry; modify app bootstrap to persist the redis
client reference (e.g., store on app struct) and ensure Close() is called during
graceful shutdown alongside other resources (refer to where dbCleanup or
shutdown sequence runs), and update CreateSession in redis_session_queries.go to
include an expiry/TTL on the SET ... NX command using the session's expiry time
(set EX seconds) so Redis auto-evicts expired sessions instead of relying solely
on DeleteExpiredSessions.

In `@internal/repository/redis_session_queries.go`:
- Around line 41-70: CreateSession is currently using
r.c.B().Set().Key(...).Value(...).Nx().Build() without any TTL so Redis sessions
never expire; compute TTLSeconds = params.Expiry - time.Now().Unix(), ensure
TTLSeconds > 0 (return an error or treat as immediate expiry if non-positive),
and include that TTL when building the Redis SET command (use the builder's
expiry option, e.g., Ex/Px/Expire-at equivalent) instead of the current call so
the stored session respects params.Expiry; update the command built in
CreateSession accordingly and keep the Nx() behavior.
- Around line 72-103: UpdateSession currently does a non-atomic GET→modify→SET
via GetSession and then r.c.B().Set().Key(...).Value(...).Build(), which can
lose concurrent updates and also doesn't refresh the Redis TTL when Expiry
changes; replace this with an atomic write that sets both the new session JSON
and the key TTL in one command. Either: 1) avoid the initial GetSession and
construct the updated Session from UpdateSessionParams then use a single atomic
SET with an expiry (EX/PX) so the value and TTL are applied together (use the
r.c builder SET with TTL args or SETEX equivalent), or 2) use a small Redis EVAL
Lua script that GETs, merges the stored session with params, and SETs the new
JSON plus TTL in one atomic operation; ensure the code paths referencing
UpdateSession, GetSession, r.c.B().Set().Key(...).Value(...).Build() are updated
so the TTL reflects params.Expiry and no read-modify-write window remains.
- Around line 20-39: GetSession in RedisSessionRepository currently returns
res.Error() directly when a key is missing, which yields a rueidis nil error
incompatible with auth service checks; add a sentinel error (e.g.
ErrSessionNotFound) in the repository package and update
RedisSessionRepository.GetSession to detect a nil-key using
rueidis.IsRedisNil(err) and return ErrSessionNotFound for missing sessions,
otherwise return the original error; then update callers (or auth service) to
compare against repository.ErrSessionNotFound instead of sql.ErrNoRows (or add a
handling branch) so "session not found" is recognized consistently.

---

Nitpick comments:
In `@internal/bootstrap/db_bootstrap.go`:
- Around line 60-70: The SetupRedis function currently calls rueidis.NewClient
with minimal ClientOption; update it to explicitly set connection timeout and
TLS by populating the ClientOption.Dialer.Timeout and ClientOption.TLSConfig (or
use rueidis.MustParseURL with a rediss:// URL and dial_timeout query) so
unreachable servers and secure connections are handled; modify the SetupRedis
implementation (where rueidis.NewClient and rueidis.ClientOption are used) to
accept/create a tls.Config when TLS is required and set Dialer.Timeout to a
sensible duration (e.g., 5s) before returning the client or error.

In `@internal/config/config.go`:
- Around line 99-112: Add a PasswordFile field to the RedisConfig struct so
users can supply Redis password via a file (consistent with other secret
handling); update RedisConfig (and any usage in DatabaseConfig) to include
PasswordFile string `yaml:"passwordFile"` and then in app_bootstrap.go replace
direct uses of app.config.Database.Redis.Password with
utils.GetSecret(app.config.Database.Redis.Password,
app.config.Database.Redis.PasswordFile) (or equivalent) where the Redis password
is read; ensure any documentation/validation that checks Redis password
considers the new PasswordFile option.

In `@internal/repository/redis_session_queries.go`:
- Around line 10-18: Redis keys use raw UUIDs which can collide in shared Redis;
add a namespace prefix constant (e.g. sessionKeyPrefix) and helper function
sessionKey(uuid string) string to build keys, then update RedisSessionRepository
methods (methods on RedisSessionRepository that call r.c.B().Get().Key(...),
Set, Del, etc.) to use sessionKey(cookie) instead of the raw cookie/uuid; keep
constructor NewRedisSessionRepository unchanged but ensure all internal key
usages are replaced to avoid cross-application collisions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b610ed60-d0da-4114-8fca-6000bdd7f3b5

📥 Commits

Reviewing files that changed from the base of the PR and between 1d76cb8 and f2008dd.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • go.mod
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/db_bootstrap.go
  • internal/bootstrap/service_bootstrap.go
  • internal/config/config.go
  • internal/repository/redis_session_queries.go
  • internal/service/auth_service.go

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
internal/repository/redis_session_queries.go (2)

65-67: ⚠️ Potential issue | 🟠 Major

Guard EX against zero or negative TTL.

params.Expiry - time.Now().Unix() can evaluate to <= 0 under misconfiguration or timing edges, and Redis rejects SET ... EX 0/-n. That turns an already-expired session into a write error during create/refresh instead of expiring it cleanly. Clamp the TTL or return a domain error before building the command.

Also applies to: 100-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repository/redis_session_queries.go` around lines 65 - 67, The SET
command uses params.Expiry - time.Now().Unix() directly which can be <= 0 and
causes Redis to reject the EX argument; compute a TTL variable before building
the command (ttl := params.Expiry - time.Now().Unix()), then either return a
domain error when ttl <= 0 or clamp it to a minimum positive value (e.g., ttl =
1) and pass that to ExSeconds(ttl) on the r.c.B().Set() call; apply the same
change to the other occurrences referenced (around lines 100-103) so all uses of
params.Expiry subtracting time.Now().Unix() are guarded.

79-103: ⚠️ Potential issue | 🔴 Critical

Don't let UpdateSession recreate a deleted session.

This is still a GET → mutate → unconditional SET. If logout deletes the key after Line 79 but before Line 100, the final SET writes the session back and effectively undoes the logout. At minimum, make the write conditional with XX and treat a nil reply as "not found"; if you want to remove the full race window, move this to a Lua script / transaction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repository/redis_session_queries.go` around lines 79 - 103,
UpdateSession currently does a GetSession -> mutate -> unconditional Set which
can recreate a session deleted concurrently (e.g., logout); change the Set to
use the Redis "XX" option so it only updates existing keys (use the builder's XX
flag or method on r.c.B().Set() to require existing key), and treat a nil/empty
reply from the Set as "not found" (return a not-found error) instead of assuming
success; alternatively consider moving the read-modify-write into a Lua
script/transaction to eliminate the race—apply this change around the
UpdateSession code that calls r.GetSession(...) and builds the Set command
(r.c.B().Set().Key(...).Value(...).ExSeconds(...).Build()).
🧹 Nitpick comments (1)
internal/service/auth_service.go (1)

49-54: Keep backend-specific miss handling out of AuthService.

SessionRepository is storage-agnostic, but GetSessionCookie still has to know about both sql.ErrNoRows and rueidis.IsRedisNil. That leaks backend details through the interface and leaves other callers to remember the same translation. Prefer a shared sentinel such as repository.ErrSessionNotFound, and have each repository implementation map its own miss to that error.

Also applies to: 363-367

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/auth_service.go` around lines 49 - 54, Add a
repository-level sentinel error (e.g., repository.ErrSessionNotFound) and update
each storage implementation of SessionRepository (the SQL-based GetSession and
the Redis-based GetSession) to translate their backend-specific "not found"
results (sql.ErrNoRows, rueidis.IsRedisNil, etc.) into that sentinel; then
change AuthService.GetSessionCookie (and other callers around the other
referenced call site) to check for repository.ErrSessionNotFound instead of
backend-specific errors so the storage details are hidden behind the
SessionRepository interface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/repository/redis_session_queries.go`:
- Around line 65-67: The SET command uses params.Expiry - time.Now().Unix()
directly which can be <= 0 and causes Redis to reject the EX argument; compute a
TTL variable before building the command (ttl := params.Expiry -
time.Now().Unix()), then either return a domain error when ttl <= 0 or clamp it
to a minimum positive value (e.g., ttl = 1) and pass that to ExSeconds(ttl) on
the r.c.B().Set() call; apply the same change to the other occurrences
referenced (around lines 100-103) so all uses of params.Expiry subtracting
time.Now().Unix() are guarded.
- Around line 79-103: UpdateSession currently does a GetSession -> mutate ->
unconditional Set which can recreate a session deleted concurrently (e.g.,
logout); change the Set to use the Redis "XX" option so it only updates existing
keys (use the builder's XX flag or method on r.c.B().Set() to require existing
key), and treat a nil/empty reply from the Set as "not found" (return a
not-found error) instead of assuming success; alternatively consider moving the
read-modify-write into a Lua script/transaction to eliminate the race—apply this
change around the UpdateSession code that calls r.GetSession(...) and builds the
Set command (r.c.B().Set().Key(...).Value(...).ExSeconds(...).Build()).

---

Nitpick comments:
In `@internal/service/auth_service.go`:
- Around line 49-54: Add a repository-level sentinel error (e.g.,
repository.ErrSessionNotFound) and update each storage implementation of
SessionRepository (the SQL-based GetSession and the Redis-based GetSession) to
translate their backend-specific "not found" results (sql.ErrNoRows,
rueidis.IsRedisNil, etc.) into that sentinel; then change
AuthService.GetSessionCookie (and other callers around the other referenced call
site) to check for repository.ErrSessionNotFound instead of backend-specific
errors so the storage details are hidden behind the SessionRepository interface.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dbf44334-719a-4da0-b753-3aed69e4e2a1

📥 Commits

Reviewing files that changed from the base of the PR and between f2008dd and 0303ec8.

📒 Files selected for processing (2)
  • internal/repository/redis_session_queries.go
  • internal/service/auth_service.go

Signed-off-by: Henry Whitaker <henrywhitaker3@outlook.com>
Signed-off-by: Henry Whitaker <henrywhitaker3@outlook.com>
Signed-off-by: Henry Whitaker <henrywhitaker3@outlook.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
internal/repository/redis_session_queries.go (2)

100-103: ⚠️ Potential issue | 🟡 Minor

Same TTL edge case applies here.

Similar to CreateSession, if params.Expiry - time.Now().Unix() is zero or negative, the SET command may fail or behave unexpectedly.

Proposed fix
+	ttl := params.Expiry - time.Now().Unix()
+	if ttl <= 0 {
+		return session, fmt.Errorf("session expiry is in the past")
+	}
+
 	cmd := r.c.B().Set().Key(params.UUID).
 		Value(string(raw)).
-		ExSeconds(params.Expiry - time.Now().Unix()).
+		ExSeconds(ttl).
 		Build()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repository/redis_session_queries.go` around lines 100 - 103, The TTL
calculation passed to ExSeconds can be zero or negative (params.Expiry -
time.Now().Unix()), causing the SET command to fail; update the session save
logic around the
r.c.B().Set().Key(params.UUID).Value(string(raw)).ExSeconds(...) call to compute
ttl := params.Expiry - time.Now().Unix() and only call ExSeconds(ttl) when ttl >
0, otherwise call Set without ExSeconds (or return an error/handle as your
CreateSession does) so you avoid passing non-positive expiry values to the
builder.

65-67: ⚠️ Potential issue | 🟡 Minor

Handle edge case when computed TTL is non-positive.

If params.Expiry is set to a past timestamp or very close to time.Now().Unix(), the computed TTL could be zero or negative. Redis SET ... EX 0 will fail, and negative values may cause unexpected behavior.

Add a guard to ensure TTL is at least 1 second or return an error for already-expired sessions.

Proposed fix
+	ttl := params.Expiry - time.Now().Unix()
+	if ttl <= 0 {
+		return out, fmt.Errorf("session expiry is in the past")
+	}
+
 	cmd := r.c.B().Set().Key(params.UUID).Value(string(raw)).
-		Nx().ExSeconds(params.Expiry - time.Now().Unix()).
+		Nx().ExSeconds(ttl).
 		Build()

You'll also need to add "fmt" to the imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repository/redis_session_queries.go` around lines 65 - 67, Compute
TTL before building the Redis command by doing ttl := params.Expiry -
time.Now().Unix(); if ttl <= 0 return an appropriate error (e.g., "session
already expired") to avoid issuing SET ... EX 0/negative, otherwise use ttl (or
ttl := max(1, ttl) if you prefer to allow a 1s minimum) when calling ExSeconds;
update the builder call
(r.c.B().Set().Key(params.UUID).Value(string(raw)).Nx().ExSeconds(ttl).Build())
and add "fmt" to imports for the error formatting if needed.
🧹 Nitpick comments (5)
internal/bootstrap/db_bootstrap.go (2)

60-70: Consider adding TLS support for secure Redis connections.

The current implementation doesn't expose TLS configuration. For production environments using Redis over TLS (e.g., Redis Cloud, AWS ElastiCache with encryption), users would need additional options like TLSConfig.

This could be a follow-up enhancement to add TLS bool and InsecureTLS bool fields to RedisConfig.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/bootstrap/db_bootstrap.go` around lines 60 - 70, The SetupRedis
function currently calls rueidis.NewClient without TLS options; update it to
accept TLS settings from your RedisConfig (add fields like TLS bool and
InsecureTLS bool) and pass a configured tls.Config into rueidis.ClientOption
when TLS is enabled; specifically, modify SetupRedis to read RedisConfig.TLS and
RedisConfig.InsecureTLS, build a tls.Config (e.g., InsecureSkipVerify when
InsecureTLS is true), and set the appropriate TLS fields on rueidis.ClientOption
so the rueidis.NewClient call uses TLS for secure Redis connections.

60-70: Consider adding a connection health check.

Rueidis creates connections lazily. Adding a simple ping after client creation would provide earlier feedback if Redis is misconfigured or unreachable.

Proposed enhancement
 func (app *BootstrapApp) SetupRedis(url, password string, db int) (rueidis.Client, error) {
 	client, err := rueidis.NewClient(rueidis.ClientOption{
 		InitAddress: []string{url},
 		Password:    password,
 		SelectDB:    db,
 	})
 	if err != nil {
 		return nil, fmt.Errorf("init redis client: %w", err)
 	}
+
+	// Verify connectivity
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	if err := client.Do(ctx, client.B().Ping().Build()).Error(); err != nil {
+		client.Close()
+		return nil, fmt.Errorf("ping redis: %w", err)
+	}
+
 	return client, nil
 }

This requires adding "context" and "time" to imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/bootstrap/db_bootstrap.go` around lines 60 - 70, The SetupRedis
function creates a rueidis client lazily but doesn't verify connectivity; modify
SetupRedis to perform an immediate health check by creating a context with
timeout (e.g., context.WithTimeout(ctx, 2*time.Second)), sending a PING command
via the created client (using the client’s request builder/Do method, e.g.,
client.Do(ctx, client.B().Ping().Build())), and returning a wrapped error if the
ping fails; also add the "context" and "time" imports so the timeout context is
available.
internal/config/config.go (1)

85-85: Minor: Missing period in description tag.

For consistency with other fields in the Config struct (e.g., Database, Analytics), the Session field's description should end with a period.

-	Session      SessionConfig      `description:"Session configuration" yaml:"session"`
+	Session      SessionConfig      `description:"Session configuration." yaml:"session"`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/config.go` at line 85, The description tag for the Config
struct's Session field is missing a trailing period; update the struct field
"Session SessionConfig `description:\"Session configuration\" yaml:\"session\"`"
so the description ends with a period (i.e., `description:"Session
configuration."`) to match the style of other fields like Database and
Analytics.
internal/repository/redis_session_queries.go (1)

75-109: Non-atomic read-modify-write pattern remains.

The UpdateSession method fetches the session, modifies it in memory, and writes it back. If two concurrent requests update the same session, one update may be lost. For session refresh operations this is likely acceptable (both would extend the session), but be aware this differs from the database implementation which uses a single UPDATE statement.

Consider documenting this behavior or using a Lua script for full atomicity if stricter guarantees are needed in the future.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repository/redis_session_queries.go` around lines 75 - 109,
UpdateSession currently does a non-atomic read-modify-write (calls GetSession
then Set) which can lose concurrent updates; either document this concurrency
caveat on RedisSessionRepository/UpdateSession or make the update atomic by
replacing the RMW with a Redis EVAL Lua script that: GETs the key, unmarshals
JSON, applies the same field updates (UUID, Email, Username, Name, Provider,
TotpPending, OAuthName, OAuthGroups, OAuthSub, Expiry), re-marshals and SETs the
key with EX seconds computed from params.Expiry - time.Now().Unix(), and returns
the final JSON (or error); locate UpdateSession and GetSession to implement or
reference the Lua-based atomic path.
internal/service/auth_service.go (1)

13-13: Consider extracting the "not found" check to avoid coupling.

Importing rueidis in the auth service creates a compile-time dependency on the Redis library even when using the database backend. While this is minor (the library is already a dependency), you could consider having the Redis repository return a common sentinel error (e.g., repository.ErrSessionNotFound) to keep the service layer backend-agnostic.

This is a minor architectural consideration and the current approach works correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/auth_service.go` at line 13, Extract the Redis-specific "not
found" check into a repository-level sentinel error so auth_service.go no longer
imports rueidis; add a package-level error like repository.ErrSessionNotFound in
the repository package, update the Redis implementation (the redis repository
type / its GetSession/FindSession method) to translate rueidis.NilReply (or
other Redis nil) into repository.ErrSessionNotFound, and change auth_service.go
to check against repository.ErrSessionNotFound instead of referencing
rueidis.NilReply so the service layer stays backend-agnostic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/repository/redis_session_queries.go`:
- Around line 100-103: The TTL calculation passed to ExSeconds can be zero or
negative (params.Expiry - time.Now().Unix()), causing the SET command to fail;
update the session save logic around the
r.c.B().Set().Key(params.UUID).Value(string(raw)).ExSeconds(...) call to compute
ttl := params.Expiry - time.Now().Unix() and only call ExSeconds(ttl) when ttl >
0, otherwise call Set without ExSeconds (or return an error/handle as your
CreateSession does) so you avoid passing non-positive expiry values to the
builder.
- Around line 65-67: Compute TTL before building the Redis command by doing ttl
:= params.Expiry - time.Now().Unix(); if ttl <= 0 return an appropriate error
(e.g., "session already expired") to avoid issuing SET ... EX 0/negative,
otherwise use ttl (or ttl := max(1, ttl) if you prefer to allow a 1s minimum)
when calling ExSeconds; update the builder call
(r.c.B().Set().Key(params.UUID).Value(string(raw)).Nx().ExSeconds(ttl).Build())
and add "fmt" to imports for the error formatting if needed.

---

Nitpick comments:
In `@internal/bootstrap/db_bootstrap.go`:
- Around line 60-70: The SetupRedis function currently calls rueidis.NewClient
without TLS options; update it to accept TLS settings from your RedisConfig (add
fields like TLS bool and InsecureTLS bool) and pass a configured tls.Config into
rueidis.ClientOption when TLS is enabled; specifically, modify SetupRedis to
read RedisConfig.TLS and RedisConfig.InsecureTLS, build a tls.Config (e.g.,
InsecureSkipVerify when InsecureTLS is true), and set the appropriate TLS fields
on rueidis.ClientOption so the rueidis.NewClient call uses TLS for secure Redis
connections.
- Around line 60-70: The SetupRedis function creates a rueidis client lazily but
doesn't verify connectivity; modify SetupRedis to perform an immediate health
check by creating a context with timeout (e.g., context.WithTimeout(ctx,
2*time.Second)), sending a PING command via the created client (using the
client’s request builder/Do method, e.g., client.Do(ctx,
client.B().Ping().Build())), and returning a wrapped error if the ping fails;
also add the "context" and "time" imports so the timeout context is available.

In `@internal/config/config.go`:
- Line 85: The description tag for the Config struct's Session field is missing
a trailing period; update the struct field "Session SessionConfig
`description:\"Session configuration\" yaml:\"session\"`" so the description
ends with a period (i.e., `description:"Session configuration."`) to match the
style of other fields like Database and Analytics.

In `@internal/repository/redis_session_queries.go`:
- Around line 75-109: UpdateSession currently does a non-atomic
read-modify-write (calls GetSession then Set) which can lose concurrent updates;
either document this concurrency caveat on RedisSessionRepository/UpdateSession
or make the update atomic by replacing the RMW with a Redis EVAL Lua script
that: GETs the key, unmarshals JSON, applies the same field updates (UUID,
Email, Username, Name, Provider, TotpPending, OAuthName, OAuthGroups, OAuthSub,
Expiry), re-marshals and SETs the key with EX seconds computed from
params.Expiry - time.Now().Unix(), and returns the final JSON (or error); locate
UpdateSession and GetSession to implement or reference the Lua-based atomic
path.

In `@internal/service/auth_service.go`:
- Line 13: Extract the Redis-specific "not found" check into a repository-level
sentinel error so auth_service.go no longer imports rueidis; add a package-level
error like repository.ErrSessionNotFound in the repository package, update the
Redis implementation (the redis repository type / its GetSession/FindSession
method) to translate rueidis.NilReply (or other Redis nil) into
repository.ErrSessionNotFound, and change auth_service.go to check against
repository.ErrSessionNotFound instead of referencing rueidis.NilReply so the
service layer stays backend-agnostic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: daa5a97c-173f-4ee9-a2a2-1d2dcb3dd259

📥 Commits

Reviewing files that changed from the base of the PR and between 0303ec8 and e93fb77.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • go.mod
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/db_bootstrap.go
  • internal/bootstrap/service_bootstrap.go
  • internal/config/config.go
  • internal/repository/redis_session_queries.go
  • internal/service/auth_service.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • go.mod
  • internal/bootstrap/app_bootstrap.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant