Skip to content

Conversation

@capcom6
Copy link
Member

@capcom6 capcom6 commented Nov 18, 2025

Summary by CodeRabbit

  • New Features

    • Full JWT support: issue, validate and revoke tokens; new 3rd‑party token endpoints and auth handler; user management APIs (create, login, change password).
  • Configuration

    • New JWT config block (secret, ttl, issuer) with sensible defaults.
  • Security

    • JWT auth added alongside Basic; scope-based permission checks enforced across 3rd‑party and mobile APIs.
  • Observability

    • JWT metrics, dashboard and alerting rules added.
  • Documentation

    • OpenAPI and example requests updated for JWTAuth and token endpoints.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 18, 2025

Walkthrough

Adds a JWT authentication subsystem and a users service, plus cache and cache backends, many handler/middleware updates to use JWT/users, OpenAPI and observability artifacts, DB migrations for tokens, and fx module wiring/config types.

Changes

Cohort / File(s) Summary
Build deps & config
go.mod, internal/config/config.go, internal/config/types.go, internal/config/module.go
Add github.com/golang-jwt/jwt/v5, bump client dependency, introduce config.Duration type and jwt config block with fx provider mapping to jwt.Config.
JWT package (API, impl, models, migration, metrics, wiring)
internal/sms-gateway/jwt/*.go, internal/sms-gateway/jwt/models.go, internal/sms-gateway/jwt/module.go, internal/sms-gateway/jwt/repository.go, internal/sms-gateway/jwt/service.go, internal/sms-gateway/jwt/metrics.go, internal/sms-gateway/jwt/errors.go, internal/sms-gateway/jwt/config.go, internal/sms-gateway/jwt/disabled.go, internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
New jwt package: Config+validation, error vars, Service interface, Claims/TokenInfo, GORM token model + migration, repository (Insert/Revoke/IsRevoked), disabled/real service implementation, metrics, and fx module.
Third‑party auth handler & module
internal/sms-gateway/handlers/thirdparty/auth.go, internal/sms-gateway/handlers/thirdparty/module.go, internal/sms-gateway/handlers/thirdparty/permissions.go
Add AuthHandler with POST/DELETE token endpoints, error mapping, fx provider, and tokens:manage scope constant.
Users domain, repo, service & module
internal/sms-gateway/users/domain.go, internal/sms-gateway/users/models.go, internal/sms-gateway/users/repository.go, internal/sms-gateway/users/service.go, internal/sms-gateway/users/cache.go, internal/sms-gateway/users/errors.go, internal/sms-gateway/users/module.go
New users package: public User, GORM userModel + migration, repository (Exists/GetByID/Insert/UpdatePassword), login cache, Service (Create/GetByID/Login/ChangePassword), errors, and fx wiring.
Auth module refactor
internal/sms-gateway/modules/auth/*.go, internal/sms-gateway/modules/auth/repository.go (removed)
Auth module now depends on users.Service; removed old auth repo/provider; updated service constructor and methods to use users.Service and public users.User.
Handlers & middleware (JWT, permissions, userauth changes)
internal/sms-gateway/handlers/**, internal/sms-gateway/handlers/middlewares/*, internal/sms-gateway/handlers/3rdparty.go, internal/sms-gateway/handlers/module.go, internal/sms-gateway/handlers/thirdparty/module.go
Add jwtauth and permissions middleware; userauth now uses users.Service and stores users.User; many handlers updated to accept users.User, apply scope checks, register JWT middleware and thirdparty module wiring.
Specific handler updates (devices/messages/logs/settings/webhooks/mobile/third‑party)
internal/sms-gateway/handlers/*/3rdparty.go, internal/sms-gateway/handlers/mobile.go, internal/sms-gateway/handlers/thirdparty.go
Replace models.User with users.User in handlers, add permissions.RequireScope wrappers, add JWT OpenAPI annotations, and adapt mobile/device flows to use users.Service.
Models & migrations
internal/sms-gateway/models/models.go, internal/sms-gateway/models/migration.go, internal/sms-gateway/modules/*/models.go, internal/sms-gateway/models/migrations/mysql/*
Remove legacy models.User type and auth repo; adjust migrations (devices only), update DeviceSettings/Webhook to reference users.User, add tokens migration and token model.
Cache core & implementations
pkg/cache/*.go, pkg/cache/typed.go, pkg/cache/memory.go, pkg/cache/redis.go, pkg/cache/options.go, pkg/cache/errors.go
Add generic typed cache wrapper, expanded GetOption API (AndSetTTL/AndUpdateTTL/AndDelete/AndSetValidUntil/AndDefaultTTL), full in-memory TTL cache, Redis-backed cache (RedisConfig/NewRedis), and docs/errors enhancements.
Permission constants
internal/sms-gateway/handlers/*/permissions.go (multiple)
Add scope constants for messages, devices, logs, settings, webhooks, thirdparty, etc., used by permissions middleware.
Handler helpers & tests
internal/sms-gateway/handlers/base/handler.go, internal/sms-gateway/handlers/base/handler_test.go
Change parser/validation error creation to fmt.Errorf wrapping (from fiber.NewError) and update tests to expect revised error/status behavior.
Observability & docs
internal/sms-gateway/openapi/docs.go, deployments/grafana/dashboards/jwt.json, deployments/prometheus/alerts/jwt-alerts.yml, cmd/sms-gateway/main.go, configs/config.example.yml, api/requests.http, api/mobile.http
Add JWT metrics, Grafana dashboard, Prometheus alerts, OpenAPI JWTAuth security and token schemas, update example config and HTTP request examples.
Misc: golangci config
.golangci.yml
Update exhaustruct exclusions to include JWT and GoFiber types adjustments.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant AuthHandler as "3rdparty Auth Handler"
    participant JWTSvc as "JWT Service"
    participant Repo as "Token Repo"
    participant DB as "Database"

    Client->>AuthHandler: POST /3rdparty/v1/auth/token (Basic)
    AuthHandler->>JWTSvc: GenerateToken(ctx, userID, scopes, ttl)
    JWTSvc->>JWTSvc: build claims, sign HS256
    JWTSvc->>Repo: Insert(ctx, tokenModel)
    Repo->>DB: INSERT tokens
    Repo-->>JWTSvc: OK
    JWTSvc-->>AuthHandler: TokenInfo
    AuthHandler-->>Client: 201 Created + token
Loading
sequenceDiagram
    autonumber
    actor Client
    participant JWTMw as "JWT Middleware"
    participant JWTSvc as "JWT Service"
    participant Repo as "Token Repo"
    participant UsersSvc as "Users Service"
    participant Handler as "Protected Handler"

    Client->>JWTMw: Request (Authorization: Bearer <token>)
    JWTMw->>JWTSvc: ParseToken(ctx, token)
    JWTSvc->>Repo: IsRevoked(ctx, jti)
    Repo-->>JWTSvc: false
    JWTSvc-->>JWTMw: Claims(userID, scopes)
    JWTMw->>UsersSvc: GetByID(ctx, userID)
    UsersSvc-->>JWTMw: *User
    JWTMw->>JWTMw: SetUser + SetScopes
    JWTMw->>Handler: Next()
    Handler-->>Client: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Focus areas requiring extra attention:
    • internal/sms-gateway/jwt/* — token signing, TTL enforcement, claim validation, revocation and persistence, metric labels.
    • internal/sms-gateway/users/* — bcrypt usage, cache key derivation/TTL semantics, repository error mapping, migrations.
    • pkg/cache/* — generic Typed[T] instantiation/reflection correctness, memory concurrency and TTL semantics, Redis Lua scripts and pipelining.
    • Handlers/middlewares and fx wiring — many signature changes across handlers, permission enforcement, and OpenAPI consistency.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title '[jwt] add module and config' accurately describes the main changes: JWT module and configuration setup have been added throughout the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jwt/introduce-jwt

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.

Copy link

@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: 1

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

130-132: Consider a shorter default JWT TTL for better security.

The default TTL of 1 year is unusually long for JWT tokens. Industry best practice typically ranges from minutes to hours (e.g., 1 hour to 24 hours). Long-lived tokens increase the window of opportunity for token theft and misuse.

If this extended TTL is intentional for the SMS gateway use case, consider adding a comment explaining the rationale.

Consider a more conservative default:

 	JWT: JWT{
-		TTL: Duration(time.Hour * 24 * 365), // 1 year
+		TTL: Duration(time.Hour * 24), // 24 hours
 	},
internal/sms-gateway/jwt/config.go (1)

8-22: Guard against negative TTL values in Config.Validate

Right now only TTL == 0 is rejected, so a negative duration would pass validation and later produce already-expired tokens, which is almost certainly a misconfiguration. Consider tightening the check to reject non‑positive values:

 func (c Config) Validate() error {
 	if c.Secret == "" {
 		return fmt.Errorf("%w: secret is required", ErrInvalidConfig)
 	}

-	if c.TTL == 0 {
-		return fmt.Errorf("%w: ttl is required", ErrInvalidConfig)
-	}
+	if c.TTL <= 0 {
+		return fmt.Errorf("%w: ttl must be positive", ErrInvalidConfig)
+	}

 	return nil
 }
internal/sms-gateway/jwt/service.go (3)

20-45: Consider validating revoked dependency in New

New assumes revoked is non‑nil; if wiring ever passes nil, IsRevoked/Revoke will panic at runtime. Either validate here and fail fast or document the non‑nil contract explicitly; validation could look like:

 func New(config Config, revoked *revokedStorage) (Service, error) {
 	if err := config.Validate(); err != nil {
 		return nil, err
 	}
+
+	if revoked == nil {
+		return nil, fmt.Errorf("%w: revoked storage is required", ErrInvalidConfig)
+	}

47-72: Tighten TTL handling and consider making issuer configurable

Two small points in GenerateToken:

  • A negative ttl argument will currently produce an already‑expired token via now.Add(min(ttl, s.config.TTL)). If that’s not a valid use case, consider rejecting negative values early:
 func (s *service) GenerateToken(userID string, scopes []string, ttl time.Duration) (string, error) {
-	if ttl == 0 {
+	if ttl < 0 {
+		return "", fmt.Errorf("ttl must be non-negative")
+	}
+
+	if ttl == 0 {
 		ttl = s.config.TTL
 	}
  • The issuer is hardcoded as "sms-gate.app". If you have multiple environments or brands, you may want this to come from configuration instead of being fixed in code.

74-103: Align algorithm checks and error signaling in ParseToken

ParseToken is generally solid, but you might want to tighten and standardize behavior:

  • You currently accept any SigningMethodHMAC. To strictly mirror GenerateToken (which always uses HS256), consider restricting to that method:
 parsedToken, err := jwt.ParseWithClaims(token, new(Claims), func(t *jwt.Token) (any, error) {
-	if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
-		return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, t.Header["alg"])
-	}
+	if t.Method != jwt.SigningMethodHS256 {
+		return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, t.Header["alg"])
+	}
  • For the !ok || !parsedToken.Valid case you return a plain "invalid token" error. If callers need to distinguish invalid/expired tokens from other failures (similar to how ErrTokenRevoked is used), consider introducing an ErrInvalidToken sentinel in errors.go and returning that here.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1759faf and 2a31e3d.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • go.mod (1 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (2 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/revoked.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
internal/sms-gateway/jwt/module.go (6)
internal/config/module.go (1)
  • Module (23-134)
internal/sms-gateway/app.go (1)
  • Module (34-58)
internal/sms-gateway/cache/factory.go (1)
  • Factory (16-18)
internal/config/config.go (2)
  • Cache (83-85)
  • Config (12-23)
internal/sms-gateway/jwt/service.go (2)
  • New (28-45)
  • Service (14-18)
internal/sms-gateway/jwt/config.go (1)
  • Config (8-11)
internal/config/config.go (1)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/jwt/errors.go (1)
internal/sms-gateway/jwt/service.go (1)
  • New (28-45)
internal/config/module.go (3)
internal/config/config.go (2)
  • Config (12-23)
  • JWT (91-94)
internal/sms-gateway/jwt/config.go (1)
  • Config (8-11)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/jwt/config.go (2)
internal/config/config.go (1)
  • Config (12-23)
internal/sms-gateway/jwt/errors.go (1)
  • ErrInvalidConfig (7-7)
internal/sms-gateway/jwt/service.go (3)
internal/sms-gateway/jwt/jwt.go (1)
  • Claims (5-10)
internal/sms-gateway/jwt/config.go (1)
  • Config (8-11)
internal/sms-gateway/jwt/errors.go (2)
  • ErrUnexpectedSigningMethod (9-9)
  • ErrTokenRevoked (8-8)
internal/sms-gateway/jwt/revoked.go (2)
internal/config/config.go (1)
  • Cache (83-85)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (7-7)
internal/sms-gateway/app.go (2)
internal/config/module.go (1)
  • Module (23-134)
internal/sms-gateway/jwt/module.go (1)
  • Module (9-25)
internal/sms-gateway/jwt/disabled.go (3)
internal/sms-gateway/jwt/service.go (1)
  • Service (14-18)
internal/sms-gateway/jwt/errors.go (1)
  • ErrDisabled (6-6)
internal/sms-gateway/jwt/jwt.go (1)
  • Claims (5-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test
  • GitHub Check: Lint
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (8)
internal/config/types.go (1)

1-48: LGTM! Well-designed Duration type.

The implementation is clean and robust:

  • Nil-safe accessors prevent potential panics
  • Proper error wrapping with context
  • Clean delegation from YAML to text unmarshalling
  • Interface assertions ensure compile-time compatibility
internal/config/module.go (1)

128-133: LGTM! Config provider follows established patterns.

The JWT config provider correctly maps configuration fields and follows the same pattern as other providers in this module.

internal/sms-gateway/app.go (1)

10-10: LGTM! Clean module integration.

JWT module is properly integrated into the application's dependency graph following the established pattern.

Also applies to: 57-57

internal/sms-gateway/jwt/jwt.go (1)

1-10: LGTM! Clean Claims structure.

The Claims struct properly embeds RegisteredClaims and adds custom fields with appropriate JSON tags, following JWT best practices.

internal/sms-gateway/jwt/module.go (1)

9-25: LGTM! Well-structured FX module.

The module properly:

  • Provides a named logger for JWT operations
  • Creates private cache and revoked storage dependencies
  • Conditionally enables JWT based on configuration (empty Secret → disabled service)
  • Follows established FX patterns
go.mod (1)

18-18: Dependency verified: v5.3.0 is the latest stable version and includes the security fix for the HIGH severity memory allocation vulnerability (CVE affecting versions < 5.2.2).

No action required.

internal/sms-gateway/jwt/errors.go (1)

5-10: Error sentinel set looks appropriate

The exported error values cover the key JWT states (disabled, invalid config, revoked, bad signing method) and are well named for use as sentinels in higher layers.

internal/sms-gateway/jwt/disabled.go (1)

8-27: Disabled Service implementation is clear and consistent

The disabled implementation cleanly satisfies Service and consistently returns ErrDisabled for all operations, which should make wiring and feature‑flagging straightforward for callers.

Copy link

@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: 1

🧹 Nitpick comments (5)
internal/config/config.go (2)

22-23: Clarify semantics of JWT.Secret and potential enable/disable behavior

Right now JWT.Secret has no default and an empty string is a valid in-memory value. Depending on how jwt.Config and the JWT module consume this, an empty secret could either:

  • silently weaken security (e.g., tokens effectively unsigned or trivially forgeable), or
  • cause late failures when signing/verifying tokens.

It’s usually safer to make the configuration explicit:

  • Either treat “JWT enabled” as a separate boolean in JWT and validate that Secret is non-empty when enabled, or
  • Fail fast during config loading if Secret is empty whenever the JWT module is wired in.

This keeps the security posture obvious and avoids ambiguous “empty secret means disabled?” semantics.

Also applies to: 91-95


3-4: JWT defaults look reasonable; consider deriving or requiring Issuer for self‑hosted setups

The default TTL of 24 hours and a non-empty Issuer help produce usable tokens out of the box. For self-hosted deployments, though, hard-coding "sms-gate.app" could lead to iss claims that don’t match the actual public host.

You might consider either:

  • deriving the default Issuer from existing HTTP/API config if present, or
  • leaving JWT.Issuer empty by default and requiring it (with validation) when JWT is enabled.

Not critical, but it can reduce surprises in multi-tenant or custom-domain environments.

Also applies to: 131-134

internal/config/types.go (2)

11-27: Duration wrapper is solid; minor naming/style nit

The wrapper around time.Duration with nil‑safe Duration() / String() is straightforward and fits the config use case.

Very minor: Duration.Duration() reads a bit stuttery at call sites (e.g., cfg.JWT.TTL.Duration()). If you touch this again before it spreads widely, consider a name like AsDuration() or Value() for slightly clearer usage. Not a blocker.


29-45: Add nil‑guards in unmarshallers to avoid panics on misuse

UnmarshalText and UnmarshalYAML assume d is non‑nil. In normal struct-field usage that’s fine, but a manually used *Duration (or a future optional *Duration config field) could end up nil and panic when these methods are called.

A small defensive check would make the type more robust with negligible cost. For example:

 func (d *Duration) UnmarshalText(text []byte) error {
-	t, err := time.ParseDuration(string(text))
+	if d == nil {
+		return fmt.Errorf("can't unmarshal duration into nil *Duration")
+	}
+
+	t, err := time.ParseDuration(string(text))
 	if err != nil {
 		return fmt.Errorf("can't parse duration: %w", err)
 	}
 	*d = Duration(t)
 	return nil
 }

 func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
+	if d == nil {
+		return fmt.Errorf("can't unmarshal duration into nil *Duration")
+	}
+
 	var s string
 	if err := value.Decode(&s); err != nil {
 		return fmt.Errorf("can't unmarshal duration: %w", err)
 	}

 	return d.UnmarshalText([]byte(s))
 }

This keeps behavior the same for normal config paths while hardening against accidental nil usage.

internal/sms-gateway/jwt/config.go (1)

14-24: Consider adding minimum secret length validation.

For HMAC-based JWT signing, weak secrets can compromise security. Consider enforcing a minimum secret length (e.g., 32 bytes for HS256).

Example validation:

const minSecretLength = 32

func (c Config) Validate() error {
	if c.Secret == "" {
		return fmt.Errorf("%w: secret is required", ErrInvalidConfig)
	}

	if len(c.Secret) < minSecretLength {
		return fmt.Errorf("%w: secret must be at least %d bytes", ErrInvalidConfig, minSecretLength)
	}

	if c.TTL <= 0 {
		return fmt.Errorf("%w: ttl must be positive", ErrInvalidConfig)
	}

	return nil
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a31e3d and 9a89acb.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • go.mod (1 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (2 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/revoked.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • go.mod
  • internal/sms-gateway/app.go
  • internal/sms-gateway/jwt/module.go
  • internal/config/module.go
  • internal/sms-gateway/jwt/disabled.go
  • internal/sms-gateway/jwt/service.go
  • internal/sms-gateway/jwt/revoked.go
  • internal/sms-gateway/jwt/jwt.go
🧰 Additional context used
🧬 Code graph analysis (3)
internal/config/config.go (1)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/jwt/config.go (2)
internal/config/config.go (1)
  • Config (12-23)
internal/sms-gateway/jwt/errors.go (1)
  • ErrInvalidConfig (8-8)
internal/sms-gateway/jwt/errors.go (1)
internal/sms-gateway/jwt/service.go (1)
  • New (28-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (3)
internal/sms-gateway/jwt/errors.go (1)

5-11: LGTM!

The error definitions are clear, follow Go conventions, and align well with their usage across the JWT module.

internal/sms-gateway/jwt/config.go (2)

8-12: LGTM!

The Config struct is well-defined with appropriate types for JWT configuration.


15-17: Secret validation is correct.

The empty string check properly ensures a secret is provided.

Copy link

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/sms-gateway/handlers/3rdparty.go (2)

50-73: Critical bug: nil pointer panic and auth routes incorrectly placed behind authentication.

Two critical issues:

  1. Nil pointer dereference at Line 60: The authHandler field is never initialized in the constructor (see lines 75-86), so calling h.authHandler.Register() will panic.

  2. Authentication chicken-and-egg problem: The auth routes are registered at Line 60 after the authentication middleware is applied (lines 55-58). This means the /token endpoint requires authentication to obtain an authentication token, which is logically impossible.

The /auth routes should be registered before the userauth middleware is applied, typically right after the health handler registration.

Apply this diff to fix both issues:

 func (h *thirdPartyHandler) Register(router fiber.Router) {
 	router = router.Group("/3rdparty/v1")
 
 	h.healthHandler.Register(router)
+	h.authHandler.Register(router.Group("/auth"))
 
 	router.Use(
 		userauth.NewBasic(h.authSvc),
 		userauth.UserRequired(),
 	)
 
-	h.authHandler.Register(router.Group("/auth"))
-
 	h.messagesHandler.Register(router.Group("/message")) // TODO: remove after 2025-12-31

75-86: Critical bug: authHandler field not initialized.

The constructor receives params.AuthHandler but never assigns it to the authHandler field. This will cause a nil pointer dereference panic when Register() is called.

Apply this diff to fix:

 func newThirdPartyHandler(params ThirdPartyHandlerParams) *thirdPartyHandler {
 	return &thirdPartyHandler{
 		Handler:         base.Handler{Logger: params.Logger.Named("ThirdPartyHandler"), Validator: params.Validator},
 		healthHandler:   params.HealthHandler,
 		messagesHandler: params.MessagesHandler,
 		webhooksHandler: params.WebhooksHandler,
 		devicesHandler:  params.DevicesHandler,
 		settingsHandler: params.SettingsHandler,
 		logsHandler:     params.LogsHandler,
+		authHandler:     params.AuthHandler,
 		authSvc:         params.AuthSvc,
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a89acb and 7ae4f08.

📒 Files selected for processing (5)
  • internal/sms-gateway/handlers/3rdparty.go (4 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T23:45:45.502Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: internal/sms-gateway/online/module.go:14-16
Timestamp: 2025-09-10T23:45:45.502Z
Learning: fx.Decorate in uber-go/fx creates module-scoped decorated dependencies, not global modifications. Using fx.Decorate(func(log *zap.Logger) *zap.Logger { return log.Named("module_name") }) is a correct pattern for creating module-specific loggers that are scoped only to that module's dependency graph.

Applied to files:

  • internal/sms-gateway/handlers/thirdparty/module.go
🧬 Code graph analysis (4)
internal/sms-gateway/handlers/thirdparty/module.go (2)
internal/sms-gateway/handlers/module.go (1)
  • Module (16-41)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • NewAuthHandler (14-21)
internal/sms-gateway/jwt/config.go (2)
internal/config/config.go (1)
  • Config (12-23)
internal/sms-gateway/jwt/errors.go (1)
  • ErrInvalidConfig (8-8)
internal/sms-gateway/handlers/module.go (1)
internal/sms-gateway/handlers/thirdparty/module.go (1)
  • Module (8-16)
internal/sms-gateway/handlers/3rdparty.go (1)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (10-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: Analyze (go)
🔇 Additional comments (12)
internal/sms-gateway/jwt/config.go (3)

1-6: LGTM!

Package declaration and imports are appropriate. Both fmt and time are used correctly throughout the file.


8-10: LGTM!

The minimum secret length of 32 bytes (256 bits) is appropriate for JWT secrets and aligns with security best practices for HMAC-SHA256.


18-32: LGTM! Previous concern has been addressed.

The validation logic is comprehensive and correct:

  • Secret validation checks for both emptiness and minimum length
  • TTL validation correctly rejects both zero and negative values (line 27: c.TTL <= 0)

This addresses the previous review comment about negative TTL values.

internal/sms-gateway/handlers/thirdparty/module.go (1)

8-16: LGTM!

The module structure follows the established fx patterns in the codebase. The logger configuration and provider setup are correct.

internal/sms-gateway/handlers/module.go (2)

9-9: LGTM!

Import correctly added to support the new thirdparty module registration.


40-40: LGTM!

Module registration follows the established pattern and correctly wires the thirdparty module into the handlers module.

internal/sms-gateway/handlers/thirdparty/auth.go (3)

10-21: LGTM!

The AuthHandler structure and constructor follow the established patterns in the codebase. The embedded base.Handler is correctly initialized with logger and validator dependencies.


23-26: LGTM!

Route registration follows the standard pattern. The endpoints are appropriately defined for JWT token generation and revocation.


28-34: Stub implementation as expected for draft PR.

The NotImplemented placeholders are appropriate for the current draft state. These handlers will need proper implementation before the PR is ready for merge.

internal/sms-gateway/handlers/3rdparty.go (3)

10-10: LGTM!

Import correctly added for the thirdparty.AuthHandler dependency.


19-34: LGTM!

The AuthHandler dependency is correctly added to the parameter struct for fx injection.


36-48: LGTM!

The authHandler field is correctly added to the internal handler struct.

Copy link

@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: 1

🧹 Nitpick comments (2)
internal/sms-gateway/jwt/service.go (2)

28-49: Consider unexported constructor or exported storage type.

The exported New function accepts an unexported *revokedStorage parameter, making it unusable by external packages. If New is intended only for internal module wiring, consider making it unexported (new). Alternatively, if external packages should construct the service, export the revokedStorage type or accept an interface.


98-101: Consider wrapping error for better context.

Line 100 returns a generic "invalid token" error. Consider using fmt.Errorf to wrap or provide additional context about the validation failure, which aids debugging.

-	if !ok || !parsedToken.Valid {
-		return nil, fmt.Errorf("invalid token")
-	}
+	if !ok {
+		return nil, fmt.Errorf("invalid token claims type")
+	}
+	if !parsedToken.Valid {
+		return nil, fmt.Errorf("token validation failed")
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ae4f08 and a03ff64.

📒 Files selected for processing (3)
  • api/requests.http (1 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (5 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
internal/sms-gateway/jwt/service.go (3)
internal/sms-gateway/jwt/jwt.go (1)
  • Claims (5-10)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/errors.go (3)
  • ErrInitFailed (7-7)
  • ErrInvalidConfig (8-8)
  • ErrTokenRevoked (9-9)
internal/sms-gateway/handlers/3rdparty.go (3)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (10-12)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/handlers/health.go (1)
  • HealthHandler (13-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (5)
internal/sms-gateway/handlers/3rdparty.go (3)

10-10: LGTM: AuthHandler integration follows existing patterns.

The import, field declarations, and initialization of AuthHandler are consistent with how other handlers (messages, webhooks, devices, etc.) are integrated into the third-party handler.

Also applies to: 28-28, 45-45, 84-84


60-60: Verify middleware ordering for auth endpoints.

The auth routes are registered after the userauth.NewBasic and userauth.UserRequired middleware (lines 55-58), which means Basic authentication is required to access /auth/token and /auth/token/revoke.

This appears to implement a token exchange pattern where clients trade Basic credentials for JWT tokens. However, please verify:

  1. Token generation: Is Basic auth → JWT exchange the intended flow?
  2. Token revocation: Should the revoke endpoint accept a Bearer token in the Authorization header instead of requiring Basic auth?

Consider whether token revocation should be accessible with Bearer tokens:

// Potential alternative: separate middleware for revoke endpoint
authRouter := router.Group("/auth")
h.authHandler.Register(authRouter)  // Register first
// Then apply selective middleware within AuthHandler.Register()

77-77: Logger initialization is consistent with all other handlers.

All main handlers in the module (3rdparty, upstream, mobile) initialize their loggers identically using params.Logger directly without a name qualifier. Sub-handlers appropriately use Named() for distinguishing their logs. No inconsistency exists.

api/requests.http (1)

195-208: These test requests are for unimplemented endpoints—revisit once handlers are complete.

The auth endpoint handlers in internal/sms-gateway/handlers/thirdparty/auth.go currently return fiber.ErrNotImplemented (lines 28-30 and 32-34). The test requests in api/requests.http (lines 195-208) are placeholders and cannot be meaningfully validated against non-existent logic.

Once the token() and tokenRevoke() handlers are implemented, return here to verify and update these test requests with appropriate sample request bodies based on the actual implementation requirements.

internal/sms-gateway/jwt/service.go (1)

82-112: Strong security validation in ParseToken.

The implementation correctly enforces expiration, validates the issuer, restricts signing methods to HS256 (preventing algorithm confusion attacks), and checks token revocation. These are essential security practices for JWT handling.

Copy link

@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: 0

🧹 Nitpick comments (3)
internal/sms-gateway/jwt/revoked.go (1)

22-35: Revocation storage correctly uses cache TTL; consider clarifying naming

IsRevoked/Revoke correctly treat presence in the cache as revocation and now apply a TTL via cache.WithTTL, which avoids unbounded growth in both Redis and in‑memory caches (where TTL is tracked by the app). Based on learnings.

You might optionally rename the token parameter (and related local naming) to jti or key to better reflect that you’re storing the token ID, not the full JWT string.

internal/sms-gateway/jwt/service.go (2)

51-76: Consider avoiding reliance on builtin min for broader Go version support

ExpiresAt uses now.Add(min(ttl, s.config.TTL)). If the project targets Go versions without the builtin min for time.Duration, this will not compile; a small local helper keeps behavior explicit and portable:

func minDuration(a, b time.Duration) time.Duration {
	if a < b {
		return a
	}
	return b
}

and then:

ExpiresAt: jwt.NewNumericDate(now.Add(minDuration(ttl, s.config.TTL))),

Functionally the capping behavior (never exceeding configured TTL) is sound; this is mainly about portability and clarity.


90-124: Normalize ParseToken error mapping around ErrInvalidToken / ErrUnexpectedSigningMethod

Right now:

  • Any jwt.ParseWithClaims error is wrapped as fmt.Errorf("failed to parse token: %w", err).
  • ErrInvalidToken is only returned when the claims type assertion fails or parsedToken.Valid is false.
  • ErrUnexpectedSigningMethod is defined but not used; signing-method mismatches will surface as generic parse errors.

For callers, it may be more convenient if all “token is syntactically/cryptographically/temporally invalid” cases (bad signature, expired, issuer mismatch, invalid method, etc.) are surfaced under ErrInvalidToken, with the underlying jwt error still wrapped for logging/debugging. For example:

parsedToken, err := jwt.ParseWithClaims(/* ... */)
if err != nil {
	return nil, fmt.Errorf("%w: %v", ErrInvalidToken, err)
}

claims, ok := parsedToken.Claims.(*Claims)
if !ok || !parsedToken.Valid {
	return nil, ErrInvalidToken
}

Optionally, if you want to distinguish signing‑method issues, you could inspect the error returned by the library and wrap it with ErrUnexpectedSigningMethod. This would give a clearer and more stable public error contract while still using ErrTokenRevoked for the revocation path.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a03ff64 and 4c6d149.

📒 Files selected for processing (4)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/revoked.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • internal/sms-gateway/jwt/revoked.go
🧬 Code graph analysis (4)
internal/sms-gateway/jwt/module.go (5)
internal/sms-gateway/app.go (1)
  • Module (34-58)
internal/sms-gateway/cache/factory.go (1)
  • Factory (16-18)
internal/sms-gateway/jwt/service.go (2)
  • New (28-49)
  • Service (14-18)
internal/sms-gateway/jwt/revoked.go (2)
  • NewRevokedStorage (16-20)
  • RevokedStorage (12-14)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/errors.go (1)
internal/sms-gateway/jwt/service.go (1)
  • New (28-49)
internal/sms-gateway/jwt/service.go (4)
internal/sms-gateway/jwt/jwt.go (1)
  • Claims (5-10)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/revoked.go (1)
  • RevokedStorage (12-14)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInitFailed (7-7)
  • ErrInvalidConfig (8-8)
  • ErrInvalidToken (9-9)
  • ErrTokenRevoked (10-10)
internal/sms-gateway/jwt/revoked.go (3)
internal/config/config.go (1)
  • Cache (83-85)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (7-7)
pkg/cache/options.go (1)
  • WithTTL (22-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (3)
internal/sms-gateway/jwt/errors.go (1)

5-12: Centralized JWT error definitions look good

Error values are clear, specific, and align with the usages in the service (config/init/token/revocation), which should keep downstream error handling consistent.

internal/sms-gateway/jwt/module.go (1)

9-24: FX wiring and conditional disablement are coherent

The module cleanly encapsulates its own cache and RevokedStorage as private providers and exposes only Service, with a straightforward disabled path when Config.Secret is empty. This keeps JWT concerns modular without leaking cache internals.

internal/sms-gateway/jwt/service.go (1)

28-48: Constructor enforces config and dependencies appropriately

New validates Config, rejects a nil RevokedStorage, and wires an idFactory from nanoid, so misconfigurations or missing dependencies fail fast at startup.

Copy link

@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: 0

🧹 Nitpick comments (3)
internal/sms-gateway/jwt/revoked.go (3)

12-20: Add godoc comments for exported types and methods.

The exported RevokedStorage type and NewRevokedStorage constructor lack documentation. Adding godoc comments will improve API discoverability and maintainability.

Apply this diff to add documentation:

+// RevokedStorage manages revoked JWT tokens using a cache backend.
+// Revoked tokens are stored with a TTL matching their expiration time
+// to prevent unbounded cache growth.
 type RevokedStorage struct {
 	storage cache.Cache
 }
 
+// NewRevokedStorage creates a new RevokedStorage backed by the given cache.
 func NewRevokedStorage(storage cache.Cache) *RevokedStorage {
 	return &RevokedStorage{
 		storage: storage,
 	}
 }

Optionally, add a nil check for defensive coding:

 func NewRevokedStorage(storage cache.Cache) *RevokedStorage {
+	if storage == nil {
+		panic("storage cannot be nil")
+	}
 	return &RevokedStorage{
 		storage: storage,
 	}
 }

22-32: Add godoc comment; implementation is correct.

The IsRevoked method correctly handles all three cases: token not found (not revoked), token found (revoked), and errors. Error wrapping provides helpful context.

Add documentation:

+// IsRevoked checks whether a JWT with the given ID has been revoked.
+// Returns (false, nil) if the token is not revoked, (true, nil) if revoked,
+// or (false, error) on lookup failure.
 func (r *RevokedStorage) IsRevoked(ctx context.Context, jti string) (bool, error) {
 	_, err := r.storage.Get(ctx, jti)
 	if errors.Is(err, cache.ErrKeyNotFound) {

34-36: TTL implementation looks good; consider error wrapping for consistency.

The addition of the ttl parameter and cache.WithTTL(ttl) correctly addresses the previous concern about unbounded cache growth. Revoked tokens will now expire automatically.

Add godoc and optionally wrap errors for consistency with IsRevoked:

+// Revoke marks a JWT with the given ID as revoked until the specified TTL expires.
+// The TTL should typically match the token's remaining lifetime.
 func (r *RevokedStorage) Revoke(ctx context.Context, jti string, ttl time.Duration) error {
-	return r.storage.Set(ctx, jti, nil, cache.WithTTL(ttl))
+	if err := r.storage.Set(ctx, jti, nil, cache.WithTTL(ttl)); err != nil {
+		return fmt.Errorf("failed to revoke token: %w", err)
+	}
+	return nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6d149 and 9f3e7f7.

📒 Files selected for processing (1)
  • internal/sms-gateway/jwt/revoked.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • internal/sms-gateway/jwt/revoked.go
🧬 Code graph analysis (1)
internal/sms-gateway/jwt/revoked.go (2)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (7-7)
pkg/cache/options.go (1)
  • WithTTL (22-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: Analyze (go)

Copy link

@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: 1

🧹 Nitpick comments (3)
configs/config.example.yml (1)

41-44: Document that empty secret disables JWT.

The jwt.secret field is empty by default, which will cause the JWT service to operate in disabled mode (returning ErrDisabled for all operations). Consider adding a comment to make this behavior explicit for users configuring the system.

Apply this diff to clarify the behavior:

 jwt:
-  secret: # jwt secret [JWT__SECRET]
+  secret: # jwt secret (leave empty to disable JWT functionality) [JWT__SECRET]
   ttl: 24h # jwt ttl [JWT__TTL]
   issuer: # jwt issuer [JWT__ISSUER]
internal/sms-gateway/handlers/thirdparty/auth.go (1)

70-72: Token revocation endpoint is not implemented.

The deleteToken handler returns ErrNotImplemented. According to the JWT service interface, this should call jwtSvc.RevokeToken(ctx, jti) to revoke the token.

Would you like me to generate the implementation? The basic logic would be:

 func (h *AuthHandler) deleteToken(user users.User, c *fiber.Ctx) error {
-	return fiber.ErrNotImplemented
+	jti := c.Params("jti")
+	if jti == "" {
+		return fiber.NewError(fiber.StatusBadRequest, "JTI is required")
+	}
+
+	if err := h.jwtSvc.RevokeToken(c.Context(), jti); err != nil {
+		return err
+	}
+
+	return c.SendStatus(fiber.StatusNoContent)
 }
internal/sms-gateway/users/repository.go (1)

43-45: Consider wrapping error for consistency.

Unlike GetByID and Insert, this method returns the raw GORM error. For consistency, consider wrapping it:

 func (r *repository) UpdatePassword(ID string, passwordHash string) error {
-	return r.db.Model((*User)(nil)).Where("id = ?", ID).Update("password_hash", passwordHash).Error
+	if err := r.db.Model((*User)(nil)).Where("id = ?", ID).Update("password_hash", passwordHash).Error; err != nil {
+		return fmt.Errorf("can't update password: %w", err)
+	}
+	return nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f3e7f7 and 9d5cf91.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • api/requests.http (1 hunks)
  • configs/config.example.yml (1 hunks)
  • go.mod (1 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (2 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (5 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (5 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3 hunks)
  • internal/sms-gateway/handlers/mobile.go (3 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (4 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (4 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/revoked.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (2 hunks)
  • internal/sms-gateway/modules/auth/service.go (8 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (1 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
💤 Files with no reviewable changes (1)
  • internal/sms-gateway/models/models.go
🚧 Files skipped from review as they are similar to previous changes (17)
  • internal/config/module.go
  • internal/sms-gateway/users/errors.go
  • internal/sms-gateway/users/models.go
  • go.mod
  • internal/sms-gateway/handlers/settings/3rdparty.go
  • internal/sms-gateway/modules/webhooks/models.go
  • internal/sms-gateway/jwt/jwt.go
  • internal/sms-gateway/users/module.go
  • internal/sms-gateway/handlers/messages/3rdparty.go
  • internal/sms-gateway/handlers/devices/3rdparty.go
  • internal/sms-gateway/jwt/errors.go
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/modules/messages/service.go
  • api/requests.http
  • internal/sms-gateway/jwt/config.go
  • internal/sms-gateway/handlers/thirdparty/module.go
  • internal/sms-gateway/handlers/module.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • internal/sms-gateway/jwt/revoked.go
🧬 Code graph analysis (16)
internal/sms-gateway/jwt/module.go (7)
internal/config/module.go (1)
  • Module (23-135)
internal/sms-gateway/app.go (1)
  • Module (34-58)
internal/sms-gateway/cache/factory.go (1)
  • Factory (16-18)
internal/sms-gateway/jwt/service.go (1)
  • New (22-43)
internal/sms-gateway/jwt/revoked.go (2)
  • NewRevokedStorage (16-20)
  • RevokedStorage (12-14)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/jwt.go (1)
  • Service (10-14)
internal/sms-gateway/jwt/disabled.go (3)
internal/sms-gateway/jwt/jwt.go (3)
  • Service (10-14)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/jwt/errors.go (1)
  • ErrDisabled (6-6)
internal/sms-gateway/app.go (2)
internal/sms-gateway/jwt/module.go (1)
  • Module (9-25)
internal/config/module.go (1)
  • Module (23-135)
internal/sms-gateway/handlers/mobile.go (1)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/sms-gateway/jwt/revoked.go (2)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (7-7)
pkg/cache/options.go (1)
  • WithTTL (22-30)
internal/sms-gateway/handlers/3rdparty.go (3)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (15-19)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/handlers/health.go (1)
  • HealthHandler (13-17)
internal/sms-gateway/handlers/logs/3rdparty.go (1)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/config/config.go (1)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/handlers/thirdparty/auth.go (3)
internal/sms-gateway/jwt/jwt.go (1)
  • Service (10-14)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (121-125)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/sms-gateway/handlers/webhooks/3rdparty.go (1)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/sms-gateway/jwt/service.go (4)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/revoked.go (1)
  • RevokedStorage (12-14)
internal/sms-gateway/jwt/jwt.go (3)
  • Service (10-14)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInitFailed (7-7)
  • ErrInvalidConfig (8-8)
  • ErrInvalidToken (9-9)
  • ErrTokenRevoked (10-10)
internal/sms-gateway/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
  • Device (17-28)
internal/sms-gateway/modules/auth/service.go (1)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/sms-gateway/modules/auth/repository.go (1)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/sms-gateway/users/errors.go (1)
  • ErrNotFound (6-6)
🪛 GitHub Actions: Go
internal/sms-gateway/users/repository.go

[error] 10-10: golangci-lint: type repository is unused (unused).

🪛 GitHub Check: Lint
internal/sms-gateway/users/repository.go

[failure] 43-43:
func (*repository).UpdatePassword is unused (unused)


[failure] 35-35:
func (*repository).Insert is unused (unused)


[failure] 22-22:
func (*repository).GetByID is unused (unused)


[failure] 15-15:
func newRepository is unused (unused)


[failure] 10-10:
type repository is unused (unused)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build / Docker image (linux/arm64)
  • GitHub Check: Build / Docker image (linux/amd64)
🔇 Additional comments (28)
internal/sms-gateway/models/migration.go (1)

12-13: Confirm users migrations are now handled outside models.Migrate

Restricting AutoMigrate here to Device only matches the move of User into the users package, so this function’s scope is now clear and focused. Just make sure:

  • There is a corresponding users.Migrate (or similar) that migrates the users schema.
  • Whatever initializes the DB now calls both migration entry points in a deterministic order (typically users before devices, if you ever add FKs).

If both are true, this change looks good.

internal/sms-gateway/jwt/disabled.go (1)

8-28: LGTM! Clean null object pattern implementation.

The disabled service implementation correctly returns ErrDisabled for all JWT operations when the service is not configured. This provides graceful degradation when config.Secret is empty.

internal/config/types.go (1)

11-48: LGTM! Well-implemented Duration type with proper nil-safety.

The custom Duration type correctly implements YAML and text unmarshaling with nil-safe accessors. Error wrapping is appropriate, and compile-time interface checks ensure conformance.

internal/sms-gateway/handlers/3rdparty.go (2)

10-10: LGTM! AuthHandler integration is correct.

The third-party authentication handler is properly wired through FX dependency injection and registered at the /auth route group.

Also applies to: 28-28, 45-45, 60-60, 84-84


77-77: Verify logger naming change is intentional.

The logger no longer uses .Named("ThirdPartyHandler"), which changes the logger identifier from "ThirdPartyHandler" to the parent logger name. This may affect log filtering and observability. Please confirm this change is intentional.

Previous pattern (based on other handlers):

Logger: params.Logger.Named("ThirdPartyHandler")

Current pattern:

Logger: params.Logger
internal/sms-gateway/users/domain.go (1)

1-1: Question: Purpose of empty domain.go file?

This file contains only a package declaration with no types, functions, or imports. Is this a placeholder for future domain logic, or was content intended to be added in this PR?

internal/sms-gateway/handlers/mobile.go (1)

19-19: LGTM! Clean refactoring to use the users package.

The User type references have been correctly updated from models.User to users.User, aligning with the new package structure. No logic changes were introduced.

Also applies to: 104-104, 179-179

internal/sms-gateway/jwt/module.go (1)

9-25: LGTM! Clean FX module with conditional service activation.

The JWT module correctly provides a disabled service when config.Secret is empty (line 18) and a fully functional service otherwise. The private cache and revoked storage are properly scoped within the module.

internal/sms-gateway/app.go (1)

10-10: LGTM! JWT module correctly integrated into the application.

The JWT module is properly imported and registered in the FX module list, following the established pattern for other modules.

Also applies to: 57-57

internal/sms-gateway/handlers/logs/3rdparty.go (1)

6-6: LGTM! Clean refactor to the new users package.

The type migration from models.User to users.User is consistent with the broader refactoring across the codebase.

Also applies to: 38-38

internal/sms-gateway/modules/auth/repository.go (1)

4-4: LGTM! Consistent type migration across repository methods.

All repository methods have been updated to use users.User consistently, aligning with the new users package structure.

Also applies to: 19-36

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

8-8: LGTM! Middleware properly updated to use new users package.

The middleware functions and comments have been consistently updated to use users.User throughout.

Also applies to: 95-99, 121-121

internal/sms-gateway/jwt/revoked.go (2)

22-32: LGTM! IsRevoked implementation is correct.

The logic properly distinguishes between not-found (not revoked) and error cases, with appropriate error wrapping.


34-36: LGTM! Revoke correctly includes TTL to prevent unbounded cache growth.

The implementation now properly sets a TTL for revoked tokens, addressing the concern from the previous review. This ensures that revoked tokens are automatically cleaned up after the JWT expires.

internal/sms-gateway/handlers/thirdparty/auth.go (2)

39-42: LGTM! Token request validation is properly configured.

The validation rules correctly enforce that at least one scope is provided, with TTL being optional.


51-68: LGTM! Token generation handler is correctly implemented.

The handler properly validates input, generates the token, and returns a 201 status with the token details.

internal/sms-gateway/users/repository.go (2)

22-33: LGTM! GetByID properly handles errors and maps GORM's not found error.

The implementation correctly uses Take for single-record queries and properly wraps errors.


35-41: LGTM! Insert method is correctly implemented.

The method properly creates the user record with appropriate error wrapping.

internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

10-10: LGTM! Webhooks handler consistently updated to use new users package.

All three handler methods (get, post, delete) have been consistently updated to use users.User.

Also applies to: 43-43, 66-66, 96-96

internal/config/config.go (1)

91-95: The codebase properly validates empty JWT secrets. The Config.Validate() method in internal/sms-gateway/jwt/config.go (line 19) explicitly checks for empty secrets and returns an error, with an additional minimum length requirement (line 23). Additionally, the module initialization in internal/sms-gateway/jwt/module.go (line 18) provides a defensive check that returns a disabled service if the secret is empty. No action required.

internal/sms-gateway/jwt/service.go (5)

22-43: LGTM! Constructor properly validates dependencies.

The constructor correctly validates the config, ensures revoked storage is not nil, and initializes the nanoid-based ID factory with appropriate error handling.


46-48: Excellent! UserID validation now implemented.

The addition of userID validation prevents generating JWT tokens with an empty Subject claim, addressing the concern from the previous review.


45-82: Verify TTL capping behavior is documented for API consumers.

Line 69 caps the token expiry at min(ttl, s.config.TTL), which enforces a maximum TTL from configuration. When a caller requests a TTL larger than the configured maximum, it will be silently reduced. This is a reasonable design to prevent excessively long-lived tokens, but ensure this behavior is documented in the API so callers understand their requested TTL may not be honored.


84-114: LGTM! ParseToken has comprehensive validation.

The token parsing includes proper security validations (expiration, issuer, signing method) and correctly integrates revocation checks. The error handling appropriately wraps errors for context while returning sentinel errors for revoked/invalid tokens.


116-118: LGTM! Clean delegation to revocation storage.

The method appropriately delegates to the revocation storage layer with the configured TTL, ensuring revocation records are retained for the appropriate duration.

internal/sms-gateway/modules/auth/service.go (3)

15-15: LGTM! Import added for user type migration.

The addition of the users package import supports the migration of the User type from models to users, while the models import remains for Device and other types.


45-45: LGTM! Cache type updated consistently.

The usersCache field type and initialization correctly reflect the migration to users.User, maintaining the existing cache configuration.

Also applies to: 67-67


99-114: Type migration verified successfully—all call sites properly updated.

All public methods in the auth service that return or accept users.User have been consistently updated. Verification confirms:

  • RegisterUser return values assigned to users.User variables (mobile.go:104)
  • RegisterDevice receives users.User parameters correctly (mobile.go:123)
  • AuthorizeUser return values assigned to users.User variables (userauth.go:48)
  • AuthorizeUserByCode return values assigned to users.User variables (userauth.go:75)
  • No remaining models.User references outside the auth module

The type migration is complete and consistent throughout the codebase.

Copy link

@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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d5cf91 and 2aecb55.

📒 Files selected for processing (3)
  • configs/config.example.yml (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • configs/config.example.yml
  • internal/sms-gateway/users/module.go
🧰 Additional context used
🧬 Code graph analysis (1)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/models.go (1)
  • User (10-16)
internal/sms-gateway/users/errors.go (1)
  • ErrNotFound (6-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (3)
internal/sms-gateway/users/repository.go (3)

21-33: Good error handling for record not found.

The method correctly distinguishes between "record not found" and other database errors, returning the appropriate custom error.


35-41: LGTM!

The insert operation is correctly implemented with proper error wrapping.


10-51: Repository is properly provided to the DI container—concern resolved.

The repository is correctly registered in users/module.go at line 13 via fx.Provide(newRepository, fx.Private), which addresses the previous review concern. The fx.Private scope appropriately restricts visibility to the module while making it available for injection to internal consumers.

Copy link

@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: 3

🧹 Nitpick comments (6)
api/mobile.http (1)

71-72: Test credentials in version control require caution.

The static analysis tool flagged these values as potential generic API keys. While these are clearly placeholder test credentials (not production secrets), it's generally best practice to avoid committing any credentials—even test ones—to version control.

Consider moving these to environment variables or a gitignored .http.local file that developers can customize locally without risk of accidental exposure.

internal/sms-gateway/handlers/3rdparty.go (1)

36-83: authSvc is unused; confirm intended auth coverage for /3rdparty/v1/auth

  • The authSvc *auth.Service field (and corresponding constructor parameter) is no longer used anywhere in this handler. It can be safely dropped to keep the dependency surface clean.
  • With router.Use(userauth.NewBasic(h.usersSvc), userauth.UserRequired()) applied before h.authHandler.Register(router.Group("/auth")), all /3rdparty/v1/auth/** routes now require Basic auth as well. That’s probably what you want (e.g. exchanging Basic for JWT), but it’s a behavior change worth double‑checking against the API contract.
 type thirdPartyHandler struct {
 	base.Handler
 
-	authSvc  *auth.Service
 	usersSvc *users.Service
@@
-func newThirdPartyHandler(
-	authSvc *auth.Service,
+func newThirdPartyHandler(
 	usersSvc *users.Service,
@@
-		authSvc:  authSvc,
 		usersSvc: usersSvc,
internal/sms-gateway/users/service.go (1)

33-108: Don’t let cache failures block password changes

The service logic looks good overall (bcrypt usage, error wrapping, and login cache behavior). One place that’s a bit harsh is ChangePassword:

if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
	return err
}

Given that Login already treats the cache as best‑effort (miss or failure just falls back to DB) and Set only logs on error, making password changes fail when the cache is unavailable is inconsistent and can hurt reliability.

Consider aligning it with Login:

-	if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
-		return err
-	}
+	if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
+		s.logger.Error("failed to delete login cache entry", zap.String("username", username), zap.Error(err))
+	}

You may also want to be aware that Create’s ExistsInsert sequence is still racy under concurrent creation attempts; the DB primary key will save you, but the resulting error won’t be ErrExists.

internal/sms-gateway/modules/auth/service.go (1)

24-36: Auth service integration with users.Service and code-based auth looks sane

  • Injecting usersSvc *users.Service and delegating RegisterDevice and AuthorizeUserByCode to it is a clean separation of concerns.
  • AuthorizeUserByCode’s flow (codesCache.GetAndDeleteusersSvc.GetByUsername) is straightforward; with the userauth middleware storing a value into Locals, types now line up end‑to‑end.

No issues from these changes; just consider renaming userID in GenerateUserCode/codesCache to username for clarity, since it’s passed to GetByUsername.

Also applies to: 38-59, 90-97, 133-145

internal/sms-gateway/handlers/mobile.go (2)

28-41: Remove commented-out code.

The old fx.In dependency injection structure is no longer used. Consider removing this commented code to improve maintainability.


142-144: Add defensive nil check after GetUser.

While HasUser checks GetUser(c) != nil, adding an explicit nil check improves defensive coding and prevents potential panics if the middleware implementation changes.

Apply this diff:

 	if userauth.HasUser(c) {
 		user = userauth.GetUser(c)
+		if user == nil {
+			return fmt.Errorf("user authentication inconsistency")
+		}
 		username = user.ID
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2aecb55 and eef9522.

📒 Files selected for processing (17)
  • api/mobile.http (1 hunks)
  • api/requests.http (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
  • pkg/cache/typed.go (1 hunks)
💤 Files with no reviewable changes (2)
  • internal/sms-gateway/modules/auth/module.go
  • internal/sms-gateway/modules/auth/repository.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/sms-gateway/users/module.go
  • internal/sms-gateway/users/errors.go
  • api/requests.http
  • internal/sms-gateway/users/domain.go
  • internal/sms-gateway/users/repository.go
🧰 Additional context used
🧬 Code graph analysis (9)
internal/sms-gateway/users/cache.go (3)
pkg/cache/typed.go (2)
  • Typed (13-15)
  • NewTyped (17-21)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/options.go (2)
  • AndSetTTL (64-68)
  • WithTTL (22-30)
internal/sms-gateway/users/service.go (3)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/errors.go (1)
  • ErrExists (7-7)
pkg/crypto/passwords.go (2)
  • MakeBCryptHash (14-20)
  • CompareBCryptHash (22-27)
internal/sms-gateway/app.go (3)
internal/sms-gateway/users/module.go (1)
  • Module (12-27)
internal/sms-gateway/jwt/module.go (1)
  • Module (9-25)
internal/config/module.go (1)
  • Module (23-135)
pkg/cache/typed.go (1)
pkg/cache/options.go (1)
  • GetOption (48-48)
internal/sms-gateway/users/models.go (3)
internal/sms-gateway/models/models.go (1)
  • SoftDeletableModel (12-15)
internal/sms-gateway/models/migration.go (1)
  • Migrate (12-14)
internal/sms-gateway/modules/settings/models.go (1)
  • Migrate (20-25)
internal/sms-gateway/handlers/mobile.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/service.go (1)
  • Service (11-17)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
  • HasUser (89-91)
  • GetUser (96-103)
  • NewBasic (20-57)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
internal/sms-gateway/users/service.go (1)
  • Service (11-17)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/3rdparty.go (5)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/service.go (1)
  • Service (11-17)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (24-36)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (15-19)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • NewBasic (20-57)
  • UserRequired (108-116)
internal/sms-gateway/modules/auth/service.go (4)
internal/sms-gateway/users/service.go (1)
  • Service (11-17)
internal/sms-gateway/online/service.go (1)
  • Service (14-17)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/models.go (1)
  • Device (17-28)
🪛 Gitleaks (8.29.0)
api/mobile.http

[high] 71-71: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 72-72: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (3)
internal/sms-gateway/users/models.go (1)

10-27: User persistence model & migration look consistent

Schema and Migrate implementation match existing patterns (soft-deletable model, explicit table name, wrapped migration error). No issues from this diff alone.

internal/sms-gateway/users/cache.go (1)

14-79: Login cache behavior is reasonable given the TTL and keying strategy

Using a 1‑hour TTL and deriving the cache key as sha256(username || 0x00 || password) keeps only successful logins cached and avoids storing raw credentials. With the Typed cache fixed, Get/Set/Delete here look correct and errors are surfaced cleanly to the service layer.

internal/sms-gateway/app.go (1)

7-60: JWT and users modules are wired correctly into the Fx graph

Adding users.Module() and jwt.Module() to the top‑level Module is consistent with how other modules are registered, and should give both services access to config/logger/cache as intended. No issues from this diff.

Copy link

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/cache/redis.go (1)

41-44: Fix inconsistent HEXPIRE syntax on line 44.

HEXPIRE requires FIELDS count field syntax. Line 40 correctly uses 'FIELDS', '1', field, but line 44 omits the FIELDS keyword. Line 44 must be updated to match:

-  redis.call('HExpire', KEYS[1], newTtl, field)
+  redis.call('HExpire', KEYS[1], newTtl, 'FIELDS', '1', field)
♻️ Duplicate comments (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

128-131: Guard WithUser against missing user for a clearer failure mode

WithUser currently does:

return handler(*GetUser(c), c)

If it’s ever used without a preceding auth middleware (or if that middleware fails to set the user), this will panic with a nil dereference, which is harder to diagnose.

You already document that it will panic when the user is missing; consider adding an explicit check to produce a clearer panic message:

func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler {
	return func(c *fiber.Ctx) error {
-		return handler(*GetUser(c), c)
+		user := GetUser(c)
+		if user == nil {
+			panic("userauth.WithUser called without an authenticated user in context")
+		}
+		return handler(*user, c)
	}
}

This keeps the contract (“panics if user is missing”) but makes debugging much easier if the middleware chain is misconfigured.

internal/sms-gateway/handlers/mobile.go (1)

43-74: Handle nanoid.Standard error to avoid nil idGen panics

At Line 56 you ignore the error from nanoid.Standard(21):

idGen, _ := nanoid.Standard(21)

If initialization fails, idGen will be nil and any later h.idGen() call will panic at runtime. This was already flagged in a previous review; it’s still a critical correctness issue.

A minimal fix is to fail fast during handler construction:

 func newMobileHandler(
@@
-	idGen, _ := nanoid.Standard(21)
+	idGen, err := nanoid.Standard(21)
+	if err != nil {
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}

This guarantees idGen is never nil in live handlers.

🧹 Nitpick comments (4)
internal/sms-gateway/users/service.go (1)

96-112: Avoid failing password changes on missing/expired cache entries

ChangePassword currently treats any error from s.cache.Delete as fatal. If the login entry has already expired or was never cached, Delete will likely return cache.ErrKeyNotFound, which would cause a valid password change to fail purely due to cache state, even though you’ve already authenticated via Login.

You already treat cache.ErrKeyNotFound as benign in Login; consider doing the same here so cache misses don’t block updates:

 func (s *Service) ChangePassword(ctx context.Context, username, currentPassword, newPassword string) error {
@@
-	if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
-		return err
-	}
+	if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
+		if !errors.Is(err, cache.ErrKeyNotFound) {
+			return err
+		}
+	}

The rest of the flow (authenticate via Login, hash new password, then UpdatePassword) looks good.

internal/sms-gateway/handlers/mobile.go (2)

114-152: Clarify response semantics for existing vs new users in postDevice

The flow for creating/registering a device with the new users.Service looks consistent (reusing an authenticated user when present and creating a new one otherwise). One detail to double‑check is the response payload:

  • For new users, you set both Login and Password.
  • For already authenticated users, you set Login = username but leave Password empty.

The docstring says “Returns user credentials only for new users”, which might suggest that both fields should be omitted/empty for existing users, not just Password. If clients interpret any non‑empty Login as “new credentials”, this could be confusing.

If the intent is truly “credentials only for new users”, consider leaving Login empty (or documenting that only Password is conditional) in the authenticated‑user branch.


223-236: Differentiate invalid password from internal failures in changePassword

changePassword currently treats any error from h.usersSvc.ChangePassword as “Invalid current password” and returns 401:

if err := h.usersSvc.ChangePassword(...); err != nil {
    h.Logger.Error("failed to change password", zap.Error(err))
    return fiber.NewError(fiber.StatusUnauthorized, "Invalid current password")
}

But ChangePassword can fail for reasons other than a bad current password (e.g. DB error, cache backend failure, hashing error). Mapping all of those to 401 hides server‑side issues and misleads clients.

Consider distinguishing:

  • Invalid current password → 401 Unauthorized with the current message.
  • Other errors → propagate or wrap as 500 Internal Server Error.

You can do this by checking errors.Is(err, crypto.ErrPasswordInvalid) (the error returned from CompareBCryptHash) versus other error types.

internal/sms-gateway/handlers/3rdparty.go (1)

64-87: Confirm that protecting /3rdparty/v1/auth with Basic auth is intentional

In Register you add:

router.Use(
    userauth.NewBasic(h.usersSvc),
    userauth.UserRequired(),
)

h.authHandler.Register(router.Group("/auth"))

Given this order, the /3rdparty/v1/auth endpoints registered by authHandler will also go through NewBasic + UserRequired, effectively requiring basic authentication for the auth routes themselves.

If the design is “exchange Basic credentials for JWT tokens” then this is fine; if the /auth endpoints are meant to be usable without prior Basic auth (e.g. by presenting some other credential or for login itself), you may want to:

  • Apply the Basic middleware only to specific sub‑groups (messages/devices/settings/etc.), or
  • Register /auth before calling router.Use.

Please verify that the current middleware scope matches the desired API behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eef9522 and 0557862.

📒 Files selected for processing (7)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
  • pkg/cache/memory.go (2 hunks)
  • pkg/cache/redis.go (1 hunks)
  • pkg/cache/typed.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/cache/typed.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • pkg/cache/redis.go
📚 Learning: 2025-09-10T23:54:16.828Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:118-121
Timestamp: 2025-09-10T23:54:16.828Z
Learning: Redis 7.4.0 introduced HEXPIRE, HEXPIREAT, and related commands that support per-field TTL on hash data structures. go-redis v9.6.0+ includes support for these commands with methods like HExpireAt.

Applied to files:

  • pkg/cache/redis.go
🧬 Code graph analysis (4)
internal/sms-gateway/handlers/3rdparty.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (15-19)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • NewBasic (20-57)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/service.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/errors.go (1)
  • ErrExists (7-7)
pkg/crypto/passwords.go (2)
  • MakeBCryptHash (14-20)
  • CompareBCryptHash (22-27)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (7-7)
internal/sms-gateway/handlers/mobile.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • GetUser (100-107)
  • NewBasic (20-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (3)
pkg/cache/memory.go (1)

30-30: LGTM! Good refactoring.

The rename from newItem to newMemoryItem improves clarity by making the function's purpose more explicit.

Also applies to: 155-155

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

15-54: User storage in context is now consistent across Basic and Code auth

The switch to users.Service in NewBasic plus the new setUser/GetUser pairing makes the user storage consistent:

  • Both NewBasic and NewCode now store a users.User value via setUser.
  • GetUser type‑asserts to users.User and returns a pointer, and HasUser just checks for non‑nil.

This removes the earlier pointer/value mismatch between Basic and Code flows and should make GetUser/HasUser reliable for all callers.

Also applies to: 80-107

internal/sms-gateway/handlers/3rdparty.go (1)

18-62: Constructor and dependency wiring for third‑party handler look consistent

The new thirdPartyHandler shape and newThirdPartyHandler constructor wire in:

  • usersSvc *users.Service for basic auth via userauth.NewBasic.
  • authHandler *thirdparty.AuthHandler for JWT/3rd‑party auth endpoints.
  • base.Handler with logger and validator.

The explicit constructor keeps dependencies clear and avoids fx‑specific parameter structs here. No issues from a correctness or maintainability standpoint.

Copy link

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/sms-gateway/handlers/devices/3rdparty.go (1)

46-89: Delete route path is missing required leading slash—this is a functional bug

Fiber's grouped routers require leading slashes in route paths, as routes are flattened to their full path.

Current code at line 88:

router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))

This should be:

router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))

This aligns with the webhooks handler (line 113 of internal/sms-gateway/handlers/webhooks/3rdparty.go) and the documented path /3rdparty/v1/devices/{id}.

♻️ Duplicate comments (1)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

16-133: User storage bug is fixed; consider making WithUser’s failure mode clearer

Nice improvement: both NewBasic and NewCode now call SetUser(c, *user), which matches GetUser’s .(users.User) assertion, so the previous pointer/value mismatch is resolved. HasUser now correctly delegates to GetUser.

One remaining sharp edge is WithUser:

func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler {
	return func(c *fiber.Ctx) error {
		return handler(*GetUser(c), c)
	}
}

If a route accidentally uses WithUser without any prior auth middleware setting the user, GetUser(c) returns nil and this will panic with a generic nil‑pointer deref, even though the comment says “it will panic”.

For a clearer failure mode, you could guard this:

 func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler {
 	return func(c *fiber.Ctx) error {
-		return handler(*GetUser(c), c)
+		user := GetUser(c)
+		if user == nil {
+			panic("WithUser called without authenticated user in context")
+		}
+		return handler(*user, c)
 	}
 }

This keeps the contract (it still panics) but makes misconfiguration easier to diagnose.

🧹 Nitpick comments (3)
internal/sms-gateway/handlers/messages/permissions.go (1)

3-7: LGTM! Consider adding documentation.

The permission scope constants are well-named and follow the standard resource:action pattern, which aligns with JWT/OAuth2 best practices. The granularity (send, read, list) appropriately separates concerns.

Consider adding godoc comments to clarify each scope's purpose:

+// ScopeSend grants permission to send new messages.
 const (
 	ScopeSend = "messages:send"
+	// ScopeRead grants permission to read individual messages.
 	ScopeRead = "messages:read"
+	// ScopeList grants permission to list messages.
 	ScopeList = "messages:list"
 )
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

28-31: Consider adding error logging for debugging.

When user lookup fails, the middleware returns a generic 401 without logging. While this is good for security (not leaking information), consider adding debug-level logging to help diagnose authentication issues during development.

Example enhancement:

 user, err := usersSvc.GetByUsername(claims.UserID)
 if err != nil {
+	// Optional: Add debug logging here
+	// logger.Debug("JWT auth failed: user not found", zap.String("userID", claims.UserID))
 	return fiber.ErrUnauthorized
 }
internal/sms-gateway/handlers/3rdparty.go (1)

35-67: Consider a parameter struct if constructor complexity grows.

The constructor currently accepts 8 parameters, which is at the upper limit of what's manageable. While acceptable for explicit dependency injection, consider introducing a parameter struct if more dependencies are added in the future.

Example refactor:

type ThirdPartyHandlerParams struct {
	UsersSvc   *users.Service
	JwtService jwt.Service
	
	HealthHandler   *HealthHandler
	MessagesHandler *messages.ThirdPartyController
	// ... other handlers
	
	Logger    *zap.Logger
	Validator *validator.Validate
}

func newThirdPartyHandler(params ThirdPartyHandlerParams) *thirdPartyHandler {
	// ...
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0557862 and 661a6f1.

📒 Files selected for processing (19)
  • api/requests.http (4 hunks)
  • cmd/sms-gateway/main.go (1 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (10 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/openapi/docs.go (20 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/sms-gateway/handlers/messages/3rdparty.go
🧰 Additional context used
🧬 Code graph analysis (8)
internal/sms-gateway/handlers/thirdparty/auth.go (5)
internal/sms-gateway/jwt/jwt.go (1)
  • Service (10-14)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/thirdparty/permissions.go (1)
  • ScopeTokensManage (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/settings/permissions.go (2)
  • ScopeRead (4-4)
  • ScopeWrite (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (24-36)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
  • SetScopes (15-17)
  • ScopeAll (10-10)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • SetUser (88-90)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
internal/sms-gateway/handlers/webhooks/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/webhooks/permissions.go (3)
  • ScopeList (4-4)
  • ScopeWrite (5-5)
  • ScopeDelete (6-6)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/3rdparty.go (5)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (16-20)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • NewBasic (21-59)
  • UserRequired (114-122)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
  • NewJWT (13-38)
internal/sms-gateway/handlers/logs/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/logs/permissions.go (1)
  • ScopeRead (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (19)
internal/sms-gateway/handlers/logs/permissions.go (1)

3-5: LGTM!

Clean permission scope definition following the established pattern.

internal/sms-gateway/handlers/devices/permissions.go (1)

3-6: LGTM!

Permission scopes are clearly defined and follow the established naming convention.

api/requests.http (2)

4-4: LGTM!

JWT token variable added to support Bearer authentication alongside Basic auth.


38-39: Verify Bearer token authentication works with JWT service.

The Authorization header now uses Bearer token instead of Basic auth. Ensure the JWT middleware and service are properly wired to handle this authentication method.

internal/sms-gateway/handlers/webhooks/permissions.go (1)

3-7: LGTM!

Permission scopes are well-defined. Note that the HTTP request examples use "webhooks:read" which doesn't match ScopeList = "webhooks:list" - this was flagged in the api/requests.http review.

cmd/sms-gateway/main.go (1)

13-16: LGTM!

JWTAuth security definition properly added to Swagger documentation, following the same pattern as existing security definitions.

internal/sms-gateway/handlers/thirdparty/permissions.go (1)

3-5: LGTM!

Permission scope for token management is clearly defined. Note that this scope isn't included in the example token request in api/requests.http - this may be intentional for security reasons (requiring separate privileged access for token management).

internal/sms-gateway/handlers/settings/permissions.go (1)

3-6: LGTM!

Permission scopes correctly defined and align with the token request examples in api/requests.http.

internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (3)

13-19: LGTM - Proper Bearer token detection and fallback.

The middleware correctly:

  • Uses case-insensitive comparison for "Bearer " (per HTTP specs)
  • Validates minimum token length (> 7 characters for "Bearer " + token)
  • Falls through to next handler for non-Bearer auth, allowing other authentication methods

33-36: LGTM - Context properly populated.

The middleware correctly:

  • Sets the authenticated user in request context
  • Applies JWT token scopes for permission checks
  • Continues to the next handler

The scope propagation integrates well with the permission middleware system.


23-26: Review comment is incorrect—ParseToken already implements revocation checks.

The code in internal/sms-gateway/jwt/service.go (lines 105–111) explicitly validates tokens against the revocation list within ParseToken. The method calls s.revoked.IsRevoked(ctx, claims.ID) and returns ErrTokenRevoked if the token is revoked. The RevokedStorage dependency is properly initialized and required by the service constructor.

Likely an incorrect or invalid review comment.

internal/sms-gateway/handlers/logs/3rdparty.go (1)

3-46: Logs 3rd‑party handler wiring looks consistent

Switch to users.User plus permissions.RequireScope(ScopeRead) + userauth.WithUser matches the new auth model; Swagger JWTAuth annotation also aligns. No issues from this change.

internal/sms-gateway/handlers/settings/3rdparty.go (1)

8-129: Settings 3rd‑party handlers align with new user/scope model

Migration to users.User plus permissions.RequireScope(ScopeRead/ScopeWrite) and userauth.WithUser is consistent and preserves existing business logic around settingsSvc. No issues spotted.

internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)

1-36: Permission middleware is straightforward and consistent

SetScopes, HasScope, and RequireScope are simple and do what the handlers expect (including honoring ScopeAll). The locals key is kept internal, which is good for encapsulation.

internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

8-113: Webhooks 3rd‑party handlers: user/scopes wiring looks good

The switch to users.User plus per‑route scopes (ScopeList, ScopeWrite, ScopeDelete) and userauth.WithUser is coherent, and the delete route path "/:id" matches the documented /3rdparty/v1/webhooks/{id}. Implementation looks solid.

internal/sms-gateway/handlers/3rdparty.go (4)

8-8: LGTM! Import additions align with new dependencies.

The new imports for JWT authentication, users service, and third-party auth handler are all utilized in the updated code.

Also applies to: 11-11, 13-14


23-25: LGTM! Service and handler fields properly typed.

The new fields for users service, JWT service, and auth handler are correctly typed and well-organized.

Also applies to: 32-32


74-78: LGTM! Authentication middleware chain is correctly ordered.

The middleware sequence (basic auth → JWT auth → user required) properly allows both authentication methods while enforcing that at least one succeeds.


80-80: Auth handler registration is correct.

The /auth endpoints are for token management only (POST /token to create managed tokens, DELETE /token/:jti to revoke). Both are already protected by userauth.WithUser() middleware in their handlers, and requiring group-level authentication is the correct design. No issues detected.

Copy link

@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: 1

♻️ Duplicate comments (1)
internal/sms-gateway/handlers/mobile.go (1)

56-56: Handle the error from nanoid.Standard.

This issue was already flagged in a previous review. The error from nanoid.Standard(21) is still being ignored, which can cause a nil pointer panic when h.idGen() is called at line 131.

🧹 Nitpick comments (4)
internal/sms-gateway/handlers/thirdparty/auth.go (1)

87-89: Token revocation endpoint is stubbed.

The DELETE /token/:jti endpoint is registered but returns ErrNotImplemented. Given this is a draft PR, this is expected.

When ready to implement this handler, it should:

  1. Extract the jti parameter from the route
  2. Call h.jwtSvc.RevokeToken(c.Context(), jti)
  3. Return appropriate success/error responses

Would you like me to generate the implementation or open a tracking issue?

internal/sms-gateway/users/cache.go (2)

15-15: Consider making the TTL configurable.

A hardcoded 1-hour TTL may be too long in security-sensitive environments. Consider allowing this to be configured via environment variables or application config, especially since this cache is keyed by authentication credentials.


47-58: Review the sliding expiration strategy.

The cache.AndSetTTL(loginCacheTTL) call extends the cache TTL on every access, implementing a sliding expiration window. While this improves user experience by keeping frequently-accessed credentials cached, it also means:

  • Compromised credentials could remain cached indefinitely if accessed regularly
  • Cache entries persist longer than the initial 1-hour TTL suggests

Consider whether absolute expiration (without TTL extension on reads) would be more appropriate for security-sensitive credential caching.

Additionally, consider adding observability for cache hit/miss rates to monitor effectiveness and detect anomalies.

internal/sms-gateway/handlers/mobile.go (1)

132-133: Clarify the slicing logic that skips index 6.

The generated ID is 21 characters, but username uses id[:6] (indices 0-5) and password uses id[7:] (indices 7+), deliberately skipping the character at index 6. While this might be intentional (e.g., as a separator or to avoid ambiguous characters), the logic isn't documented and could confuse future maintainers.

Consider adding a comment explaining why index 6 is skipped:

+	// Generate credentials from nanoid: first 6 chars for username, chars after index 6 for password
 	username = strings.ToUpper(id[:6])
 	password = strings.ToLower(id[7:])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 661a6f1 and 6f1f9af.

📒 Files selected for processing (6)
  • api/requests.http (5 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • pkg/cache/redis.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/sms-gateway/handlers/messages/permissions.go
  • pkg/cache/redis.go
  • api/requests.http
🧰 Additional context used
🧬 Code graph analysis (3)
internal/sms-gateway/users/cache.go (4)
pkg/cache/typed.go (2)
  • Typed (14-16)
  • NewTyped (18-22)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/options.go (2)
  • AndSetTTL (64-68)
  • WithTTL (22-30)
pkg/cache/errors.go (2)
  • ErrKeyNotFound (7-7)
  • ErrKeyExpired (9-9)
internal/sms-gateway/handlers/thirdparty/auth.go (5)
internal/sms-gateway/jwt/jwt.go (1)
  • Service (10-14)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/thirdparty/permissions.go (1)
  • ScopeTokensManage (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/mobile.go (5)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (24-36)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • GetUser (102-109)
  • NewBasic (21-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (10)
internal/sms-gateway/handlers/thirdparty/auth.go (3)

35-38: Routes properly secured with scope-based access control.

Both endpoints correctly require the tokens:manage scope and user authentication via middleware. The past review concern about missing @Security annotations in the OpenAPI documentation has been addressed (see lines 54-55).


52-85: Security annotations properly added per previous review.

The OpenAPI documentation now includes @Security ApiAuth and @Security JWTAuth annotations (lines 54-55), correctly reflecting the authentication and scope requirements enforced by the middleware. The handler implementation is straightforward and correct.


40-43: ****

The code already handles zero-value TTL appropriately. When ttl is omitted from the request, GenerateToken (line 58–60 of internal/sms-gateway/jwt/service.go) explicitly defaults it to s.config.TTL, which is guaranteed to be positive by configuration validation. Tokens will always receive a valid expiration time.

Likely an incorrect or invalid review comment.

internal/sms-gateway/users/cache.go (3)

17-30: LGTM!

The wrapper struct correctly mirrors the User domain model and implements the required cache.Item interface using standard JSON serialization.


60-72: LGTM!

The Set method correctly converts the domain User to the cache wrapper and stores it with the configured TTL. Error handling is appropriate.


74-81: Cache invalidation confirmed in password change flow

The Delete method implementation is correct and properly integrated. Cache invalidation occurs at internal/sms-gateway/users/service.go:102 within the ChangePassword method, where the old password credentials are removed from cache immediately after password validation and before updating the password hash. This prevents stale login cache entries from persisting after a password change.

internal/sms-gateway/handlers/mobile.go (4)

197-207: LGTM! Clean migration to users.User.

The signature change from models.User to users.User is consistent with the broader migration. The function correctly uses user.ID which exists in the new type.


230-232: LGTM! Correct delegation to users service.

The change properly delegates password changes to usersSvc.ChangePassword with the context and user ID from the authenticated device. The updated error message is appropriately generic.

Note: Line 275 has a pre-existing comment indicating this endpoint should eventually use userauth protection instead of deviceauth, but that's outside the scope of this change.


242-243: LGTM! Consistent authentication middleware migration.

The routes correctly use userauth.NewBasic(h.usersSvc) for user authentication, which is consistent with the migration from auth service to users service for user-related authentication flows.

Also applies to: 260-260


127-150: The review comment's concern is incorrect and should be disregarded.

The verification confirms that users.User.ID is always equal to the login username:

  • In internal/sms-gateway/users/service.go:51, the Create method explicitly sets ID: username, using the provided username parameter as the user's ID.
  • The User struct contains only an ID field with no separate Login or Username field.
  • For new users, the generated 6-character string is passed as username to Create(), which becomes the user.ID.
  • For existing users, user.ID IS the login username.

Both code paths return the correct login username in the response.

Likely an incorrect or invalid review comment.

Copy link

@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: 0

♻️ Duplicate comments (1)
api/requests.http (1)

4-223: Re‑verify token scopes against permission constants (especially devices:write).

The addition of @jwtToken and the JWT‑secured examples for messages and devices look good, and the new /3rdparty/v1/auth/token and /3rdparty/v1/auth/token/{id} endpoints make sense.

However, the scopes in the token request body still deserve another pass:

"scopes": [
  "messages:send",
  "messages:read",
  "devices:list",
  "devices:write",
  "webhooks:list",
  "webhooks:write",
  "settings:read",
  "settings:write",
  "logs:read"
]

A previous review found that the defined device permissions were "devices:list" and "devices:delete" (not "devices:write"), and that webhooks scopes needed to be "webhooks:list" / "webhooks:write" / "webhooks:delete". You’ve already aligned the webhooks scopes here, but it’s unclear whether "devices:write" now exists as a constant or still mismatches the code.

To avoid hard‑to‑debug authorization failures, please re‑check that every scope string in this sample exactly matches the corresponding permission constants (devices, webhooks, messages, settings, logs) and update either the constants or this request accordingly.

You can re‑run a quick scan like this to confirm:

#!/bin/bash
# List all defined permission scopes
rg -n 'Scope[A-Za-z0-9_]*\s*=\s*".*:.*"' --type go

# Show where scopes are enforced in handlers/middleware
rg -n 'RequireScope\(' --type go -A2
🧹 Nitpick comments (10)
internal/sms-gateway/jwt/models.go (1)

11-37: Keep GORM model and SQL migration in sync (and consider FK definition here).

The tokenModel shape and tags line up with the tokens DDL in 20251121071748_add_tokens.sql, but the SQL migration also defines the fk_tokens_user foreign key while the GORM model does not. Since Migrate uses AutoMigrate and you also ship an explicit SQL migration, you now have two schema sources of truth.

To avoid future drift (e.g., if columns or indexes change) and to keep FKs consistent across environments that only run Migrate, consider either:

  • documenting that goose SQL is authoritative and Migrate is only for tests/dev, or
  • adding equivalent FK metadata to the GORM model (or dropping one of the mechanisms).
internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1)

3-18: Consider indexing expires_at if you’ll query/cleanup by expiry.

The tokens schema looks consistent with the Go model and has good indexes on user_id and revoked_at. If you plan to frequently select or delete tokens based on expires_at (e.g., cleanup of expired tokens), consider adding an index on expires_at (or a composite like (user_id, expires_at)) to avoid future table scans.

internal/sms-gateway/jwt/module.go (1)

10-30: JWT module wiring looks correct; double‑check need for the private cache provider.

The FX wiring for Service (real vs newDisabled() depending on Config.Secret) and the migration registration via db.RegisterMigration(Migrate) both look good.

One minor thing: the private cache.Cache provider created with factory.New("jwt") isn’t consumed by any other provider in this module as shown. If nothing in jwt currently depends on cache.Cache, you could drop this provider to reduce noise; if you plan to use it for revocation caching or similar, leaving it is fine but maybe add a brief comment for intent.

internal/sms-gateway/jwt/repository.go (1)

28-46: Token revocation semantics and timestamp portability

Revoke always returns nil even when no row matches (id,user_id,revoked_at is null), and it uses gorm.Expr("NOW()") for the timestamp. If you ever need to distinguish “no such token / belongs to another user / already revoked” from “successfully revoked”, consider checking the update’s RowsAffected and surfacing a domain error. Also, using a Go time.Now() value (e.g., Update("revoked_at", time.Now())) would avoid tying this to dialects that don’t support NOW().

internal/sms-gateway/handlers/thirdparty/auth.go (3)

36-40: Middleware wiring is sound; consider renaming errorHanlder and clarifying responsibilities

The route setup with router.Use(h.errorHanlder) plus permissions.RequireScope and userauth.WithUser gives a clear protection story. Two small polish points:

  • The method name errorHanlder looks like a typo; renaming to errorHandler will make usages easier to grep.
  • This middleware currently only maps ErrDisabled, ErrInitFailed, and ErrInvalidConfig. If other JWT‑related errors need consistent HTTP mapping (e.g. always 401 vs 500), consider documenting whether they’re expected to be handled here or by a shared/global error handler.

Also applies to: 111-128


42-52: Clarify TTL behavior in docs to match implementation

tokenRequest.TTL is optional (omitempty) and GenerateToken falls back to config.TTL when it’s zero while also capping by the configured maximum. The Swagger description currently just says “with specified scopes and ttl”, which reads as if TTL were always required.

Consider adjusting the comment above postToken to explicitly state that:

  • scopes are required, and
  • ttl is optional and, when omitted or zero, defaults to the configured maximum TTL.

This will make the behavior more obvious to API consumers.

Also applies to: 54-87


89-109: Confirm desired behavior for revoking non‑existent or foreign tokens

deleteToken delegates to jwtSvc.RevokeToken, which in turn calls the repository’s Revoke without surfacing whether any row was actually updated. As a result, a request with a non‑existent jti (or one that belongs to another user) will still return 204.

If you want idempotent “best‑effort revoke” semantics, this is fine; if instead clients should get feedback (e.g. 404 when no matching token exists), you’ll need the repository to report that distinction and translate it here.

internal/sms-gateway/openapi/docs.go (1)

1016-1050: TTL / time.Duration schema may not match actual JSON shape

internal_sms-gateway_handlers_thirdparty.tokenRequest.ttl references time.Duration, which this spec models as an integer with nanosecond‑based enum values. In Go, time.Duration is typically marshaled as a string (e.g. "1h", "30s") in JSON, so clients relying on the schema may send integers while the server expects duration strings.

If the intended wire format is the default Go duration string, consider:

  • Using a string alias type for TTL and annotating it accordingly in the handler/model, or
  • Overriding the schema for time.Duration via swag annotations/custom type configuration,

and then regenerating this file rather than editing it directly.

Also applies to: 1822-1844

internal/sms-gateway/jwt/service.go (2)

45-86: TTL validation/capping is good; watch Go version for builtin min

The validation around userID, scopes, and non‑negative TTL plus the “0 means default config TTL” behavior all look solid, and capping expiry with min(ttl, s.config.TTL) enforces a max TTL.

The only caveat is that the generic builtin min requires Go 1.21+. If your go.mod still targets an older Go version, this won’t compile. In that case you could replace it with an explicit clamp, e.g.:

-	now := time.Now()
-	claims := &Claims{
-		RegisteredClaims: jwt.RegisteredClaims{
-			ID:        s.idFactory(),
-			Issuer:    s.config.Issuer,
-			Subject:   userID,
-			IssuedAt:  jwt.NewNumericDate(now),
-			ExpiresAt: jwt.NewNumericDate(now.Add(min(ttl, s.config.TTL))),
-		},
-		UserID: userID,
-		Scopes: scopes,
-	}
+	now := time.Now()
+	effectiveTTL := ttl
+	if effectiveTTL > s.config.TTL {
+		effectiveTTL = s.config.TTL
+	}
+	claims := &Claims{
+		RegisteredClaims: jwt.RegisteredClaims{
+			ID:        s.idFactory(),
+			Issuer:    s.config.Issuer,
+			Subject:   userID,
+			IssuedAt:  jwt.NewNumericDate(now),
+			ExpiresAt: jwt.NewNumericDate(now.Add(effectiveTTL)),
+		},
+		UserID: userID,
+		Scopes: scopes,
+	}

88-118: Make all token‑validation failures surface as ErrInvalidToken for callers

Right now, parse/validation failures from jwt.ParseWithClaims are wrapped as fmt.Errorf("failed to parse token: %w", err), while only the !ok || !parsedToken.Valid branch returns ErrInvalidToken. That means callers can’t reliably use errors.Is(err, ErrInvalidToken) to detect “invalid/expired token” in all cases and may end up treating many invalid tokens as generic server errors.

If you want a consistent contract for downstream code (e.g., jwtauth middleware mapping all invalid tokens to 401), consider mapping all token‑validation failures to ErrInvalidToken (possibly logging the wrapped error separately) instead of returning a wrapped error here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f1f9af and 63ef342.

📒 Files selected for processing (11)
  • api/requests.http (5 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/models.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/repository.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1 hunks)
  • internal/sms-gateway/openapi/docs.go (17 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/sms-gateway/jwt/errors.go
🧰 Additional context used
🧬 Code graph analysis (5)
internal/sms-gateway/jwt/models.go (1)
internal/sms-gateway/models/models.go (1)
  • TimedModel (7-10)
internal/sms-gateway/jwt/module.go (8)
internal/sms-gateway/app.go (1)
  • Module (35-60)
internal/config/module.go (1)
  • Module (23-135)
internal/sms-gateway/cache/factory.go (1)
  • Factory (16-18)
internal/sms-gateway/jwt/service.go (1)
  • New (22-43)
internal/sms-gateway/jwt/repository.go (2)
  • NewRepository (14-18)
  • Repository (10-12)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/jwt.go (1)
  • Service (10-14)
internal/sms-gateway/jwt/models.go (1)
  • Migrate (32-37)
internal/sms-gateway/handlers/thirdparty/auth.go (6)
internal/sms-gateway/jwt/jwt.go (1)
  • Service (10-14)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/thirdparty/permissions.go (1)
  • ScopeTokensManage (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/jwt/errors.go (3)
  • ErrDisabled (6-6)
  • ErrInitFailed (7-7)
  • ErrInvalidConfig (8-8)
internal/sms-gateway/jwt/disabled.go (2)
internal/sms-gateway/jwt/jwt.go (3)
  • Service (10-14)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/sms-gateway/jwt/errors.go (1)
  • ErrDisabled (6-6)
internal/sms-gateway/jwt/service.go (4)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/repository.go (1)
  • Repository (10-12)
internal/sms-gateway/jwt/jwt.go (3)
  • Service (10-14)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInitFailed (7-7)
  • ErrInvalidConfig (8-8)
  • ErrInvalidToken (9-9)
  • ErrTokenRevoked (10-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
internal/sms-gateway/jwt/disabled.go (1)

8-27: Disabled service implementation looks solid.

The disabled implementation cleanly satisfies Service and consistently returns ErrDisabled from all methods, giving a predictable behavior when JWT is turned off. No state or concurrency concerns here.

internal/sms-gateway/jwt/jwt.go (1)

10-27: JWT service interface and types look well‑shaped

The Service contract plus Claims/TokenInfo structs are coherent with the implementation in service.go and the third‑party handlers; fields and JSON tags give consumers what they need without overexposing internals.

Copy link

@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: 8

♻️ Duplicate comments (1)
api/requests.http (1)

201-219: Verify scope name "devices:write" matches defined permission constants.

The past review flagged that "devices:write" (line 212) does not match the permission constants defined in devices/permissions.go. According to the previous review, the available device permissions are "devices:list" and "devices:delete".

Run the following script to confirm the available device permission scopes:

#!/bin/bash
# Find all device permission scope constants
rg -nP 'Scope.*=.*\"devices:' --type go

Update line 212 to use a valid device permission constant.

🧹 Nitpick comments (4)
deployments/grafana/dashboards/jwt.json (1)

38-38: Consider using a variable for the datasource UID.

The datasource UID "edqp0a73uh2bka" is hardcoded throughout the dashboard. If the datasource changes in different environments, this will require manual updates.

Consider using ${DS_PROMETHEUS} or a similar variable instead of hardcoding the UID, or ensure the UID is documented in deployment instructions.

Also applies to: 85-85, 149-149

internal/sms-gateway/jwt/service.go (2)

78-88: Consider removing the redundant UserID field from Claims.

The Claims struct stores UserID separately (line 86) even though RegisteredClaims.Subject already holds the same value (line 82). This duplication may cause maintenance issues if they ever diverge.

If the custom UserID field serves no distinct purpose, consider removing it from the Claims struct in jwt.go and using only Subject throughout the codebase.


63-66: Consider validating that scopes are from an allowed set.

The function requires len(scopes) > 0 but does not validate that the provided scopes are valid permission constants. Invalid scopes will be accepted and stored, potentially causing authorization failures later.

Consider adding a scope validation step that checks each scope against the defined permission constants (e.g., messages:send, devices:list, etc.) to catch configuration errors early.

internal/sms-gateway/jwt/metrics.go (1)

99-103: Consider adding panic recovery in duration observation helpers.

If the callback f() panics, timer.ObserveDuration() is never called, which could skew latency metrics. This applies to all three Observe* methods.

Apply this pattern to all three observation methods:

 func (m *Metrics) ObserveIssuance(f func()) {
 	timer := prometheus.NewTimer(m.issuanceDurationHistogram)
+	defer timer.ObserveDuration()
 	f()
-	timer.ObserveDuration()
 }

Using defer ensures the duration is recorded even if f() panics.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63ef342 and edcbf50.

📒 Files selected for processing (10)
  • api/requests.http (5 hunks)
  • deployments/grafana/dashboards/jwt.json (1 hunks)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/metrics.go (1 hunks)
  • internal/sms-gateway/jwt/models.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/sms-gateway/jwt/module.go
  • internal/sms-gateway/handlers/thirdparty/auth.go
  • internal/sms-gateway/jwt/errors.go
  • internal/sms-gateway/jwt/models.go
🧰 Additional context used
🧬 Code graph analysis (1)
internal/sms-gateway/jwt/service.go (5)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/repository.go (1)
  • Repository (10-12)
internal/sms-gateway/jwt/metrics.go (3)
  • Metrics (24-31)
  • StatusError (20-20)
  • StatusSuccess (19-19)
internal/sms-gateway/jwt/jwt.go (3)
  • Service (10-14)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInitFailed (7-7)
  • ErrInvalidParams (9-9)
  • ErrInvalidToken (10-10)
  • ErrTokenRevoked (11-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
internal/sms-gateway/jwt/service.go (1)

58-61: Good fix—userID validation now prevents empty Subject claims.

The validation added here addresses the past review concern about empty "sub" claims. This ensures JWTs are spec-compliant.

internal/sms-gateway/jwt/metrics.go (1)

23-31: LGTM—Metrics structure is well-organized.

The metrics cover all three core JWT operations (issuance, validation, revocation) with both counters and duration histograms. The use of a single status label keeps cardinality low.

Note: The absence of a reason label for granular error classification is consistent with the simplified error tracking approach, though it limits observability of specific failure modes (expired vs. revoked vs. malformed tokens).

Copy link

@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: 1

♻️ Duplicate comments (2)
deployments/prometheus/alerts/jwt-alerts.yml (2)

24-32: Critical: Label mismatch persists—metric does not expose a "reason" label.

This issue was previously identified but remains unresolved. The alert queries reason="database_error", but tokensRevokedCounter in internal/sms-gateway/jwt/metrics.go (line 50-55) only exposes a status label. The alert will never fire.


34-42: Critical: Label mismatch persists, and inconsistent division protection.

Two issues:

  1. Label mismatch (same as previously identified): The alert queries reason=~"expired|revoked", but tokensValidatedCounter in internal/sms-gateway/jwt/metrics.go (lines 43-48) only exposes a status label. This alert will never fire.

  2. Inconsistent division protection: This alert uses max(..., 0.00001) while line 5 uses clamp_min(..., 1e-9). Both work, but the inconsistency makes the codebase harder to maintain.

Consider standardizing on clamp_min for consistency:

-        expr: rate(sms_auth_jwt_tokens_validated_total{status="error",reason=~"expired|revoked"}[5m]) / max(rate(sms_auth_jwt_tokens_validated_total[5m]), 0.00001) > 0.1
+        expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / clamp_min(rate(sms_auth_jwt_tokens_validated_total[5m]), 1e-9) > 0.1

Note: This also removes the non-existent reason label filter. If you need per-reason alerting, add the reason label to the metric in metrics.go first.

🧹 Nitpick comments (1)
internal/sms-gateway/jwt/metrics.go (1)

57-79: Consider extracting shared histogram buckets constant.

All three histograms use identical bucket definitions. For maintainability, consider extracting to a shared constant:

+var jwtDurationBuckets = []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1}
+
 func NewMetrics() *Metrics {
 	return &Metrics{
 		...
 		issuanceDurationHistogram: promauto.NewHistogram(prometheus.HistogramOpts{
 			Namespace: "sms",
 			Subsystem: "auth",
 			Name:      MetricIssuanceDurationSeconds,
 			Help:      "JWT issuance duration in seconds",
-			Buckets:   []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1},
+			Buckets:   jwtDurationBuckets,
 		}),

This makes it easier to adjust bucket ranges consistently across all JWT operations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edcbf50 and 70cf6c0.

📒 Files selected for processing (3)
  • deployments/grafana/dashboards/jwt.json (1 hunks)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • internal/sms-gateway/jwt/metrics.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (17)
deployments/prometheus/alerts/jwt-alerts.yml (2)

4-12: LGTM! Division-by-zero protection correctly implemented.

The alert correctly uses clamp_min to prevent division by zero and queries the status label that exists in the metrics definition.


14-22: LGTM! Latency alert correctly configured.

The histogram quantile query correctly references the issuance duration metric, and the 0.5s threshold is appropriate given the histogram buckets defined in metrics.go.

deployments/grafana/dashboards/jwt.json (12)

82-92: LGTM! Panel correctly queries the tokens issued metric.

The query correctly references sms_auth_jwt_tokens_issued_total, which matches the metric defined in internal/sms-gateway/jwt/metrics.go.


146-156: LGTM! Validation success rate correctly calculated.

The panel correctly uses sms_auth_jwt_tokens_validated_total with proper division-by-zero protection via clamp_min.


208-218: LGTM! Revocation rate panel correctly configured.

The query correctly references sms_auth_jwt_tokens_revoked_total.


308-318: LGTM! Issuance rate timeseries correctly configured.

The query correctly aggregates by status, which will display separate series for success and error rates.


383-396: LGTM! Panel correctly uses the existing metric with status filter.

The query correctly filters sms_auth_jwt_tokens_issued_total by status="error", which is the proper approach given the metric definition in metrics.go. The past concern about a non-existent metric has been resolved.


483-513: LGTM! Latency distribution panel correctly configured.

The histogram quantile queries correctly reference sms_auth_jwt_issuance_duration_seconds_bucket for p99, p95, and p50 percentiles.


603-613: LGTM! Validation success rate timeseries correctly configured.

The query correctly uses sms_auth_jwt_tokens_validated_total with proper division protection.


667-681: LGTM! Panel correctly uses the existing metric aggregated by status.

The query correctly aggregates sms_auth_jwt_tokens_validated_total by status, which will show the breakdown in a pie chart. The past concern about a non-existent metric has been resolved.


770-800: LGTM! Validation latency panel correctly configured.

The histogram quantile queries correctly reference sms_auth_jwt_validation_duration_seconds_bucket.


890-900: LGTM! Revocation rate timeseries correctly configured.

The query correctly aggregates by status to show success and error rates separately.


990-1000: LGTM! Panel correctly uses the existing metric with status filter.

The query correctly filters sms_auth_jwt_tokens_revoked_total by status="error" and includes proper division protection with clamp_min. The past concern about a non-existent metric has been resolved.


1090-1120: LGTM! Revocation latency panel correctly configured.

The histogram quantile queries correctly reference sms_auth_jwt_revocation_duration_seconds_bucket.

internal/sms-gateway/jwt/metrics.go (3)

23-31: LGTM! Metrics struct is well-designed.

The struct appropriately uses CounterVec for labeled counters and Histogram for duration tracking. Field names are clear and follow Go conventions.


83-96: LGTM! Increment methods correctly implemented.

The increment methods properly delegate to the underlying counter's WithLabelValues and Inc methods.


98-117: LGTM! Observation methods correctly use timer pattern.

The observe methods correctly use prometheus.NewTimer with deferred ObserveDuration to measure operation duration, which ensures timing is captured even if the operation panics.

Copy link

@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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70cf6c0 and cb98e8c.

📒 Files selected for processing (1)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
deployments/prometheus/alerts/jwt-alerts.yml (1)

5-5: PromQL expressions look correct with proper safeguards.

The past division-by-zero and label-mismatch issues have been addressed: expressions now use clamp_min() / max() to guard against zero denominators, and label selectors use the correct status label that exists in the metrics.

Also applies to: 15-15, 25-25, 35-35

Copy link

@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: 0

🧹 Nitpick comments (1)
deployments/prometheus/alerts/jwt-alerts.yml (1)

5-5: Harmonize zero-protection thresholds for consistency.

Alert 1 uses clamp_min(..., 1e-9) while Alert 4 uses max(..., 0.00001). Both prevent division-by-zero, but the minimum thresholds differ by several orders of magnitude. For consistency across alerts with similar semantics, consider using the same minimum value.

Apply this diff to align both to the same minimum:

-        expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / clamp_min(rate(sms_auth_jwt_tokens_validated_total[5m]), 1e-9) > 0.05
+        expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / clamp_min(rate(sms_auth_jwt_tokens_validated_total[5m]), 0.00001) > 0.05

Or update Alert 4 to match Alert 1:

-        expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / max(rate(sms_auth_jwt_tokens_validated_total[5m]), 0.00001) > 0.1
+        expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / clamp_min(rate(sms_auth_jwt_tokens_validated_total[5m]), 0.00001) > 0.1

Also applies to: 35-35

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb98e8c and c533392.

📒 Files selected for processing (1)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
deployments/prometheus/alerts/jwt-alerts.yml (2)

1-42: Excellent work addressing previous critical issues.

All four alerts now correctly use the status="error" label (fixed from non-existent reason label references in earlier commits) and both division operations are protected against zero denominators via clamp_min() and max().


24-32: Verify Alert 3 sensitivity for production operations.

The JWT_Revocation_Failures alert fires on rate(...) > 0—any revocation error within a 5m window with critical severity. Over 5 minutes, even a single error will trigger. Depending on your operational baseline, this may cause alert fatigue or may be intentional given the security criticality of revocation.

Confirm this threshold aligns with expected error rates and operational oncall procedures.

Copy link

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
pkg/cache/memory.go (1)

177-241: Fix data race on memoryItem.validUntil when updating TTL in Get

In the Get path with options, item.validUntil is mutated under m.mux.Lock, but getItem later calls item.isExpired(time.Time) without holding any lock. Other goroutines can concurrently call Get with TTL‑updating options on the same key, leading to unsynchronized reads/writes of validUntil and a data race under the Go memory model (and likely flagged by the race detector).

A minimal fix is to avoid mutating memoryItem in place and instead create a new memoryItem with the updated TTL and swap it into the map while holding the write lock, so the structs themselves become immutable after creation. That way, readers may see either the old or new item pointer, but no field is ever written concurrently with being read.

One way to adjust Get is:

 func (m *memoryCache) Get(_ context.Context, key string, opts ...GetOption) ([]byte, error) {
 	return m.getValue(func() (*memoryItem, bool) {
 		if len(opts) == 0 {
 			m.mux.RLock()
 			item, ok := m.items[key]
 			m.mux.RUnlock()
 
 			return item, ok
 		}
 
 		o := getOptions{}
 		o.apply(opts...)
 
-		m.mux.Lock()
-		item, ok := m.items[key]
-		if !ok {
-			// item not found, nothing to do
-		} else if o.delete {
-			delete(m.items, key)
-		} else if !item.isExpired(time.Now()) {
-			if o.validUntil != nil {
-				item.validUntil = *o.validUntil
-			} else if o.setTTL != nil {
-				item.validUntil = time.Now().Add(*o.setTTL)
-			} else if o.updateTTL != nil {
-				item.validUntil = item.validUntil.Add(*o.updateTTL)
-			} else if o.defaultTTL {
-				item.validUntil = time.Now().Add(m.ttl)
-			}
-		}
-		m.mux.Unlock()
-
-		return item, ok
+		m.mux.Lock()
+		defer m.mux.Unlock()
+
+		item, ok := m.items[key]
+		if !ok {
+			return nil, false
+		}
+
+		if o.delete {
+			delete(m.items, key)
+			return item, true
+		}
+
+		now := time.Now()
+		if item.isExpired(now) {
+			// Let getItem report the expired state; cleanup() will remove it later.
+			return item, true
+		}
+
+		// Avoid mutating shared state in-place; create a new item with updated TTL.
+		updated := &memoryItem{
+			value:      item.value,
+			validUntil: item.validUntil,
+		}
+
+		if o.validUntil != nil {
+			updated.validUntil = *o.validUntil
+		} else if o.setTTL != nil {
+			updated.validUntil = now.Add(*o.setTTL)
+		} else if o.updateTTL != nil {
+			updated.validUntil = updated.validUntil.Add(*o.updateTTL)
+		} else if o.defaultTTL {
+			updated.validUntil = now.Add(m.ttl)
+		}
+
+		m.items[key] = updated
+		return updated, true
 	})
 }

This preserves the existing API and semantics while removing the shared‑state mutation that causes the race.

Also applies to: 366-400

pkg/cache/options.go (1)

25-48: WithTTL implementation contradicts its own documentation

Docs say “A TTL of zero or negative means the item will not expire”, but the implementation always sets validUntil = time.Now().Add(ttl). For ttl <= 0 this yields an immediate or past expiration instead of “no expiry”.

You probably want to return early when ttl <= 0:

 func WithTTL(ttl time.Duration) Option {
 	return func(o *options) {
-		if ttl <= 0 {
-			o.validUntil = time.Time{}
-		}
-
-		o.validUntil = time.Now().Add(ttl)
+		if ttl <= 0 {
+			// Zero value means "no expiration"
+			o.validUntil = time.Time{}
+			return
+		}
+
+		o.validUntil = time.Now().Add(ttl)
 	}
 }
internal/sms-gateway/handlers/mobile.go (1)

197-233: Change‑password handler conflates auth failures with server errors

Switching getUserCode to use users.User is aligned with the new users module and userauth.WithUser, that looks fine.

In changePassword, every error from usersSvc.ChangePassword is logged and then returned as 401 Unauthorized:

if err := h.usersSvc.ChangePassword(...); err != nil {
    h.Logger.Error("failed to change password", zap.Error(err))
    return fiber.NewError(fiber.StatusUnauthorized, "failed to change password")
}

This lumps together invalid current password, missing user, cache issues, and database failures as “unauthorized”. It would be more accurate (and easier to debug) to distinguish credential errors from internal ones (e.g., map a known “login failed”/“bad credentials” sentinel to 401, and everything else to 500 while keeping the generic message).

♻️ Duplicate comments (5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

130-133: Make WithUser panic message explicit instead of a nil‑pointer deref

WithUser still does return handler(*GetUser(c), c), so if it’s ever used without a preceding auth middleware that calls SetUser, it will panic with a generic nil‑pointer error. Since the comment already promises a panic, you can make the failure mode clearer:

 func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler {
     return func(c *fiber.Ctx) error {
-        return handler(*GetUser(c), c)
+        user := GetUser(c)
+        if user == nil {
+            panic("WithUser called without authenticated user in context")
+        }
+        return handler(*user, c)
     }
 }

This keeps the contract (panic on missing user) but makes debugging mis‑wired routes much easier.

internal/sms-gateway/users/cache.go (1)

42-45: Reconsider credential-based cache keys for login cache

makeKey hashes username and password directly into the cache key. This keeps deterministic, unsalted credential material in the cache layer and makes the cache usable as an authentication oracle. It also complicates safe invalidation on password changes (especially admin resets where the old password is unknown), potentially allowing old passwords to remain valid until TTL expiry.

Consider instead:

  • Keying by a stable, non-secret identifier (e.g., user ID) and caching whatever you actually want to reuse (e.g., user record, precomputed scopes), or
  • If you must depend on credentials, deriving keys with an HMAC that includes a server-side secret and a per-user salt, and wiring explicit invalidation on password changes.

At minimum, document how password-change flows guarantee invalidation of old-credential entries.

internal/sms-gateway/handlers/mobile.go (1)

31-74: Still ignoring nanoid initialization error; can panic if idGen is nil

nanoid.Standard(21)’s error is ignored and idGen is used later (e.g., in postDevice). If initialization fails and returns a nil function, calling h.idGen() will panic.

Handle the error and fail fast (or propagate it) instead of constructing a handler with a potentially nil generator, e.g.:

-	idGen, _ := nanoid.Standard(21)
+	idGen, err := nanoid.Standard(21)
+	if err != nil {
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}

(or return the error from the constructor, depending on your DI pattern).

internal/sms-gateway/jwt/config.go (1)

12-16: Consider validating Issuer (or documenting that it is optional)

Config.Validate doesn’t currently enforce that Issuer is non‑empty, even though it’s written into tokens and is later required in ParseToken via jwt.WithIssuer. If your design expects a meaningful issuer, consider adding a similar non‑empty check here; otherwise, documenting that an empty issuer is explicitly supported would avoid ambiguity.

Also applies to: 18-31

internal/sms-gateway/users/repository.go (1)

54-62: Still need to handle RowsAffected == 0 in UpdatePassword

UpdatePassword wraps only result.Error and treats “no rows updated” as success, so a non‑existent user ID silently passes, unlike GetByID which returns ErrNotFound. A past review already suggested checking RowsAffected; that feedback still applies here.

You can adopt the earlier diff to make the behavior consistent:

 func (r *repository) UpdatePassword(ID string, passwordHash string) error {
-	if err := r.db.Model((*userModel)(nil)).
-		Where("id = ?", ID).
-		Update("password_hash", passwordHash).Error; err != nil {
-		return fmt.Errorf("can't update password: %w", err)
-	}
+	result := r.db.Model((*userModel)(nil)).
+		Where("id = ?", ID).
+		Update("password_hash", passwordHash)
+
+	if err := result.Error; err != nil {
+		return fmt.Errorf("can't update password: %w", err)
+	}
+	if result.RowsAffected == 0 {
+		return ErrNotFound
+	}
 
 	return nil
 }

Please confirm this matches your expectations for GORM’s Update semantics in your current version (specifically that RowsAffected == 0 indicates “no matching rows” and not a soft‑error case):

GORM Update method: meaning of RowsAffected and handling non-existent rows
🧹 Nitpick comments (17)
pkg/cache/redis.go (1)

423-446: Consider atomic SetOrFail with TTL.

The current implementation has a race condition: HSetNX (line 424) and HExpireAt (line 440) are separate operations. If HSetNX succeeds but HExpireAt fails, the key will be set without TTL, potentially never expiring.

While the error is returned to the caller, the key remains in Redis without expiration, which could lead to memory issues over time.

Consider implementing this atomically using a Lua script:

local key = KEYS[1]
local field = ARGV[1]
local value = ARGV[2]
local ttl = tonumber(ARGV[3]) or 0

local exists = redis.call('HEXISTS', key, field)
if exists == 1 then
  return {err = 'key exists'}
end

redis.call('HSET', key, field, value)
if ttl > 0 then
  redis.call('HEXPIREAT', key, ttl, 'FIELDS', '1', field)
end
return {ok = 'OK'}

Then update the Go code:

 func (r *redisCache) SetOrFail(ctx context.Context, key string, value []byte, opts ...Option) error {
+	options := new(options)
+	if r.ttl > 0 {
+		options.validUntil = time.Now().Add(r.ttl)
+	}
+	options.apply(opts...)
+
+	var ttl int64
+	if !options.validUntil.IsZero() {
+		ttl = options.validUntil.Unix()
+	}
+
+	result, err := r.client.Eval(ctx, setOrFailScript, []string{r.key}, key, value, ttl).Result()
-	val, err := r.client.HSetNX(ctx, r.key, key, value).Result()
 	if err != nil {
+		if strings.Contains(err.Error(), "key exists") {
+			return ErrKeyExists
+		}
 		return fmt.Errorf("can't set cache item: %w", err)
 	}
-
-	if !val {
-		return ErrKeyExists
-	}
-
-	options := new(options)
-	if r.ttl > 0 {
-		options.validUntil = time.Now().Add(r.ttl)
-	}
-	options.apply(opts...)
-
-	if !options.validUntil.IsZero() {
-		if err := r.client.HExpireAt(ctx, r.key, options.validUntil, key).Err(); err != nil {
-			return fmt.Errorf("can't set cache item ttl: %w", err)
-		}
-	}
-
 	return nil
 }
internal/config/types.go (1)

29-48: Clarify semantics for empty / missing duration strings (optional)

UnmarshalText currently feeds the raw string into time.ParseDuration, which will fail on an empty string. If you expect configs like ttl: "" or ttl: null (or want “missing or empty” to mean “0 / disabled”), you may want to treat empty input specially, e.g. map it to 0 instead of returning an error. If you want it to be strictly required, this behavior is fine but should be documented in the surrounding config docs.

internal/sms-gateway/jwt/disabled.go (1)

1-28: Disabled JWT service behavior is clear and consistent

Returning ErrDisabled from all Service methods is a clean way to represent a globally disabled JWT subsystem, and newDisabled hiding the concrete type behind the Service interface keeps the wiring simple. If you want an extra guard, you could add:

var _ Service = (*disabled)(nil)

but the current code is functionally fine as‑is.

internal/sms-gateway/users/domain.go (1)

5-19: User domain model and mapper are straightforward

The User struct is minimal and the newUser helper cleanly maps from userModel to the domain type. One thing to decide explicitly: if model should never be nil, it may be worth documenting that contract; if it’s possible for repository code to pass nil, adding a quick guard (or returning nil/error higher up) would prevent a panic.

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

66-83: Consider assigning scopes for Code‑based auth as well

NewCode now correctly uses AuthorizeUserByCode and SetUser, but it doesn’t set any scopes. Any routes guarded only by permissions.RequireScope(...) (like the updated 3rd‑party devices handlers) will deny requests authenticated via the "Code …" header unless some other middleware also sets scopes.

If Code‑based auth is meant to have the same effective permissions as Basic/JWT on those endpoints, consider mirroring the Basic behavior:

 func NewCode(authSvc *auth.Service) fiber.Handler {
@@
-        user, err := authSvc.AuthorizeUserByCode(code)
+        user, err := authSvc.AuthorizeUserByCode(code)
         if err != nil {
             return fiber.ErrUnauthorized
         }
 
-        SetUser(c, *user)
+        SetUser(c, *user)
+        // Give code-based auth full access, like Basic.
+        permissions.SetScopes(c, []string{permissions.ScopeAll})

If instead Code auth is intentionally restricted from scope‑protected endpoints, it would help to document that explicitly.

internal/sms-gateway/handlers/logs/3rdparty.go (1)

25-46: JWT security + scope middleware look good; consider documenting scope and unused param

The added @Security JWTAuth and permissions.RequireScope(ScopeRead) plus userauth.WithUser are consistent and tighten access control without changing the 501 behavior. The user users.User argument in get is currently unused; if that’s intentional for future use, consider naming it _ users.User to keep linters quiet. Also, if your OpenAPI tooling supports it, documenting that this route requires the ScopeRead scope (not just the JWT scheme) would make the spec match the runtime behavior more closely.

internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)

9-36: Scope middleware is clear and correct; consider adding observability later

SetScopes, HasScope, and RequireScope correctly wire scopes via Fiber locals, and ScopeAll behaves as expected via slices.ContainsFunc. For typical small scope lists this is perfectly fine. Longer‑term, you might want to add logging/metrics on forbidden responses in RequireScope (or at a higher layer) to aid debugging permission issues.

internal/sms-gateway/handlers/thirdparty/auth.go (1)

89-131: Align errorHandler status codes with documented responses

errorHandler can return 501 Not Implemented for jwt.ErrDisabled, and potentially 400 Bad Request for jwt.ErrInvalidParams. The swagger for both /3rdparty/v1/auth/token and /3rdparty/v1/auth/token/{jti} doesn’t currently list 501 (and DELETE doesn’t list 400), so generated OpenAPI will under‑document possible outcomes.

If you rely on these statuses, consider adding the corresponding @Failure 400 / @Failure 501 entries to the comments so API consumers see the complete set of responses.

internal/sms-gateway/handlers/mobile.go (1)

241-277: Basic auth now correctly wired through usersSvc; consider revisiting password route TODO

Using userauth.NewBasic(h.usersSvc) for /mobile/v1/device and /mobile/v1/user/code aligns these endpoints with the new users service and the users.User domain type, which looks good.

There’s still a TODO noting that /mobile/v1/user/password “Should be under userauth.NewBasic protection instead of deviceauth”. Now that usersSvc is available here, it may be a good time in a follow‑up to revisit that route’s protection model if you intend password changes to require user credentials rather than device identity.

internal/sms-gateway/users/service.go (1)

71-112: Login/password caching and Delete semantics deserve a closer look

The Login flow (cache → DB → cache.Set) is sensible for performance, but two aspects are worth tightening:

  • You’re passing the raw password into the loginCache API (Get, Set, Delete). That’s fine if loginCache only uses it to derive an in‑memory key and never logs or persists the raw value, but it’s unusual enough that it’s worth double‑checking the implementation to ensure passwords aren’t stored or emitted anywhere.
  • In ChangePassword, a failure from cache.Delete(ctx, username, currentPassword) aborts the password change, even though the cache is only an optimization. It may be more robust to treat cache.ErrKeyNotFound (and possibly other transient cache errors) as non‑fatal: log a warning but still proceed to update the stored password, mirroring the tolerant behavior in Login.

Adjusting these would improve both security posture and resilience around the cache layer.

internal/sms-gateway/openapi/docs.go (2)

129-137: Clarify intended auth semantics for combined ApiAuth and JWTAuth

In Swagger 2.0, a security array like:

"security": [
  { "ApiAuth": [] },
  { "JWTAuth": [] }
]

means either ApiAuth or JWTAuth is sufficient (logical OR of the objects), not that both are required. Please verify that the runtime middleware for these endpoints also allows either mechanism; if the intent is to require both, the OpenAPI should instead use a single object with both schemes.

Also applies to: 180-188, 264-273, 330-339, 421-429, 509-515, 569-577, 625-633, 665-672, 724-731, 785-792, 827-833, 887-895


1016-1050: TTL / time.Duration representation may be confusing to API consumers

internal_sms-gateway_handlers_thirdparty.tokenRequest.ttl is modeled as time.Duration with an underlying integer and enum values like nanosecond, second, minute, hour. For external clients this can be non‑obvious (nanoseconds vs seconds, human durations, etc.). Consider adjusting the handler type or swagger annotations so the docs clearly state the accepted format and units for ttl (e.g., seconds as integer, or Go‑style duration strings), then regenerate this file.

Also applies to: 1822-1844

internal/sms-gateway/jwt/service.go (1)

77-85: Ensure Go toolchain version supports min on time.Duration

This code relies on min(ttl, s.config.TTL) over time.Duration. If you are using the Go 1.21+ builtin min, confirm that go.mod and CI are set to a compatible Go version; otherwise, provide a local helper func minDuration(a, b time.Duration) time.Duration instead.

internal/sms-gateway/modules/auth/service.go (2)

62-88: GenerateUserCode loop depends on Go’s integer range and could use a clearer retry pattern

The for range 3 construct is valid only on newer Go versions that support ranging over integers. Please ensure your go.mod and CI toolchain target a compatible Go release; otherwise, rewrite as an explicit for i := 0; i < 3; i++ { ... } loop. The retry logic itself (up to 3 attempts on rand.Read / SetOrFail) looks fine.


90-97: User lookup via AuthorizeUserByCode relies on usernames; consider clarifying naming

AuthorizeUserByCode pulls a string from codesCache and passes it to usersSvc.GetByUsername, while GenerateUserCode’s parameter is named userID. If the stored value is actually the username (which seems likely), renaming the parameter and local variable to username would better reflect the behavior and avoid confusion for future maintainers.

Also applies to: 132-145

pkg/cache/typed.go (1)

89-122: Clarify and surface the pointer-type requirement for T

The newItem[T] helper correctly enforces that T is a pointer type and fixes the earlier nil‑pointer Unmarshal issue, but that requirement is only enforced at runtime and only on read paths (Get, GetAndDelete, Drain). A misconfigured NewTyped[SomeStruct] will happily compile and even Set, but will start failing with "cache: type ... must be a pointer" once something calls Get.

To make this easier to discover for callers, consider either:

  • Explicitly documenting in the exported comments for Typed / NewTyped that T must be a pointer type implementing Item (e.g. NewTyped[*User], not NewTyped[User]), or
  • Adding a cheap validation in NewTyped (e.g. calling newItem[T]() once and panicking/logging on error) so misconfiguration is caught at construction time rather than on first cache read.

This doesn’t require changing the current behavior, but would make the API safer to use for new call sites.

Also applies to: 361-387

internal/sms-gateway/handlers/3rdparty.go (1)

23-33: Verify auth middleware interplay and /auth route protection

The new wiring makes all /3rdparty/v1 routes (including /auth) go through:

router.Use(
    userauth.NewBasic(h.usersSvc),
    jwtauth.NewJWT(h.jwtSvc, h.usersSvc),
    userauth.UserRequired(),
)

This is a reasonable composition (Basic OR JWT must set a user, then UserRequired enforces it), but it relies on both userauth.NewBasic and jwtauth.NewJWT behaving as no‑ops when their respective credentials are absent (e.g. no Authorization header for JWT, no Basic header for non‑login calls). If either middleware actively returns 401 on “no credentials”, it could unintentionally block the other mechanism.

In particular:

  • /3rdparty/v1/auth/... endpoints are now behind this chain as well. If the intention is “obtain tokens via Basic, use them via JWT”, this is fine as long as missing JWT on those endpoints doesn’t cause failures while Basic succeeds.
  • For purely JWT‑protected endpoints, missing Basic headers must not cause errors from NewBasic.

It’d be good to confirm these contracts in the respective middlewares (and ideally with tests) so that adding JWT auth here doesn’t regress existing Basic‑auth flows.

If you’d like, I can sketch table‑driven tests that assert the desired behavior for combinations of {Basic, JWT} presence on representative endpoints.

Also applies to: 35-67, 69-81

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c533392 and a66a972.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (62)
  • api/mobile.http (1 hunks)
  • api/requests.http (5 hunks)
  • cmd/sms-gateway/main.go (1 hunks)
  • configs/config.example.yml (1 hunks)
  • deployments/grafana/dashboards/jwt.json (1 hunks)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • go.mod (1 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (10 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/metrics.go (1 hunks)
  • internal/sms-gateway/jwt/models.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/repository.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (1 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/openapi/docs.go (17 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
  • pkg/cache/cache.go (1 hunks)
  • pkg/cache/errors.go (1 hunks)
  • pkg/cache/memory.go (13 hunks)
  • pkg/cache/options.go (5 hunks)
  • pkg/cache/redis.go (10 hunks)
  • pkg/cache/typed.go (1 hunks)
💤 Files with no reviewable changes (3)
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/module.go
  • internal/sms-gateway/modules/auth/repository.go
✅ Files skipped from review due to trivial changes (2)
  • pkg/cache/errors.go
  • pkg/cache/cache.go
🚧 Files skipped from review as they are similar to previous changes (25)
  • internal/config/module.go
  • internal/sms-gateway/users/models.go
  • internal/config/config.go
  • configs/config.example.yml
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • internal/sms-gateway/jwt/models.go
  • internal/sms-gateway/users/errors.go
  • deployments/prometheus/alerts/jwt-alerts.yml
  • internal/sms-gateway/jwt/module.go
  • api/mobile.http
  • internal/sms-gateway/handlers/settings/permissions.go
  • internal/sms-gateway/handlers/messages/3rdparty.go
  • internal/sms-gateway/handlers/devices/permissions.go
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/handlers/messages/permissions.go
  • internal/sms-gateway/handlers/logs/permissions.go
  • internal/sms-gateway/handlers/webhooks/3rdparty.go
  • internal/sms-gateway/handlers/settings/3rdparty.go
  • cmd/sms-gateway/main.go
  • api/requests.http
  • internal/sms-gateway/users/module.go
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
  • go.mod
  • deployments/grafana/dashboards/jwt.json
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-10T23:45:45.502Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: internal/sms-gateway/online/module.go:14-16
Timestamp: 2025-09-10T23:45:45.502Z
Learning: fx.Decorate in uber-go/fx creates module-scoped decorated dependencies, not global modifications. Using fx.Decorate(func(log *zap.Logger) *zap.Logger { return log.Named("module_name") }) is a correct pattern for creating module-specific loggers that are scoped only to that module's dependency graph.

Applied to files:

  • internal/sms-gateway/handlers/thirdparty/module.go
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • pkg/cache/memory.go
  • pkg/cache/redis.go
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.

Applied to files:

  • pkg/cache/memory.go
  • pkg/cache/redis.go
📚 Learning: 2025-09-10T23:54:16.828Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:118-121
Timestamp: 2025-09-10T23:54:16.828Z
Learning: Redis 7.4.0 introduced HEXPIRE, HEXPIREAT, and related commands that support per-field TTL on hash data structures. go-redis v9.6.0+ includes support for these commands with methods like HExpireAt.

Applied to files:

  • pkg/cache/redis.go
🔇 Additional comments (29)
internal/sms-gateway/handlers/webhooks/permissions.go (1)

3-7: LGTM! Clean permission scope definitions.

The permission constants follow a clear resource:action pattern and Go naming conventions. The three scopes (list, write, delete) provide a sensible access control model for webhook operations.

One optional consideration: verify that ScopeWrite appropriately covers both create and update operations in your API design. If you need granular control between creating new webhooks versus updating existing ones, you might consider separate scopes in the future.

internal/sms-gateway/models/migration.go (2)

9-10: Unused embedded filesystem.

The migrations variable is declared but never used in this file. If it's not used elsewhere in the package, consider removing it.

Run the following script to check if migrations is referenced elsewhere:


13-13: Verify User migration is handled elsewhere and check Device dependencies.

The removal of User from the migration aligns with the broader refactor, but please confirm:

  1. User migration is properly handled in the new jwt or users module
  2. The Device model doesn't have foreign key dependencies on User that could break

Run the following script to verify:

pkg/cache/redis.go (1)

40-44: Previous critical issue resolved.

All Lua script Redis commands (HExpireAt, HTTL, HExpire) now correctly use the Redis 7.4.0+ format with 'FIELDS', '1', field parameters. The migration is complete and consistent.

internal/config/types.go (1)

11-27: Duration wrapper and nil‑safe accessors look good

The Duration alias and its Duration() / String() methods are straightforward, handle nil safely, and are appropriate for config use as a thin wrapper over time.Duration. No issues from a correctness or API‑shape standpoint.

pkg/cache/memory.go (2)

9-28: Overall memory cache structure and TTL model look solid

The memoryCache / memoryItem design, default TTL handling in NewMemory and newItem, and the isExpired helper form a clear, easy‑to‑reason‑about in‑memory implementation. Zero TTL meaning “no expiration by default” is consistent with the docs, and ErrKeyNotFound vs ErrKeyExpired separation via getItem/getValue is clean.

Also applies to: 58-92, 331-353


93-176: Cleanup/Drain semantics correctly preserve non‑expired items only

Cleanup removes expired entries first and only then invokes the callback, and Drain uses cleanup to snapshot m.items and swap in a fresh map before copying out values. This matches the prior contract that:

  • expired items are dropped before Drain runs, and
  • Drain returns only non‑expired items and leaves the cache empty afterward.

This keeps the behavior aligned with the existing Redis cache behavior and previous memoryCache.cleanup semantics. Based on learnings.

Also applies to: 401-421

internal/sms-gateway/handlers/thirdparty/module.go (1)

3-16: FX module wiring and named logger usage look correct

Module() follows the established pattern: an fx.Module with a package‑specific name, a scoped named logger via logger.WithNamedLogger("3rdparty"), and fx.Provide(NewAuthHandler) to expose the handler constructor. This matches the existing fx/logger conventions in the project and should compose cleanly with the top‑level handlers module. Based on learnings.

internal/sms-gateway/modules/webhooks/models.go (1)

6-21: Confirm users.User GORM mapping matches previous models.User

Swapping the association type to users.User is consistent with the new users module and should work as long as users.User keeps the same table name/PK type that UserID points to. Please double‑check that the new model’s GORM config (table name, primary key column/type) still matches the existing webhooks FK/constraints so AutoMigrate doesn’t unexpectedly rewrite them.

internal/sms-gateway/handlers/module.go (1)

9-10: thirdparty handlers module wiring looks consistent

Importing handlers/thirdparty and adding thirdparty.Module() to the handlers fx.Module cleanly integrates the new submodule; this aligns with the existing newThirdPartyHandler usage and keeps composition idiomatic.

Also applies to: 40-41

internal/sms-gateway/app.go (1)

10-11: JWT and users modules are wired appropriately into the Fx app

Adding users.Module() and jwt.Module() to the top‑level Module is consistent with the rest of the DI setup and should make the new services available to handlers and middlewares. Please ensure you run the app (or fx.ValidateApp/tests) to confirm there are no missing bindings or new dependency cycles introduced by these modules.

Also applies to: 24-25, 44-45, 59-60

internal/sms-gateway/handlers/devices/3rdparty.go (1)

9-13: Devices 3rd‑party handlers now depend on scopes and users.User – verify upstream auth pipeline

Switching the handler signatures to users.User and wrapping the routes with permissions.RequireScope(ScopeList|ScopeDelete) plus userauth.WithUser is consistent with the new auth/users design. This does mean:

  • These endpoints now require scopes to be present in the context (set by Basic/JWT middleware via permissions.SetScopes).
  • They also assume a user has been stored via userauth.SetUser before WithUser runs; otherwise WithUser will panic.

Please double‑check the 3rd‑party router setup (newThirdPartyHandler and JWT/basic middlewares) to ensure that, for every security scheme advertised here (ApiAuth, JWTAuth), both the user and appropriate scopes are always set before these handlers are hit.

Also applies to: 36-37, 46-48, 60-61, 72-76, 87-88

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

7-8: Basic auth → users.Service + ScopeAll wiring looks correct

NewBasic now logs in via users.Service, stores a users.User via SetUser, and assigns permissions.ScopeAll, while SetUser/GetUser consistently use a value type in Locals. This resolves the earlier pointer/value mismatch and aligns Basic auth with the new permissions model.

Also applies to: 16-21, 49-56, 88-90, 96-97, 99-109

internal/sms-gateway/modules/messages/service.go (1)

124-168: Switch to users.User looks consistent and non-breaking

Updating SelectStates and GetState to accept users.User while still keying everything off user.ID keeps behavior unchanged and aligns this service with the new users domain. Callers should be updated by the compiler; no further changes needed here.

internal/sms-gateway/jwt/errors.go (1)

5-12: Error set is clear and well-scoped

The exported error variables cover the main JWT failure modes cleanly and are suitable for use with errors.Is across the JWT subsystem and callers.

internal/sms-gateway/jwt/jwt.go (1)

10-27: JWT service interface and models are well-shaped

The Service interface and the Claims/TokenInfo types define a minimal, cohesive JWT API (issue/parse/revoke with user ID and scopes) that should integrate cleanly with handlers and middleware.

internal/sms-gateway/handlers/thirdparty/auth.go (1)

17-40: Auth handler wiring looks consistent with the rest of the stack

The AuthHandler constructor and Register method are wired cleanly: shared base.Handler, per‑router errorHandler, and per‑route permissions.RequireScope(ScopeTokensManage) plus userauth.WithUser match the security model implied by the swagger comments. I don’t see functional issues in this wiring.

internal/sms-gateway/handlers/mobile.go (1)

121-152: Device registration flow with usersSvc looks consistent

The refactor to:

  • Prefer userauth.GetUser(c) when present, and
  • Fall back to generating a username/password and creating a user via usersSvc.Create,

is consistent with the new users service design. Passing *user into authSvc.RegisterDevice and returning Login/Password only for the newly created‑user path matches the endpoint’s description. I don’t see functional problems in this section (aside from the idGen initialization issue called out separately).

internal/sms-gateway/users/service.go (2)

13-33: Service wiring is straightforward and idiomatic

The Service struct and NewService constructor cleanly encapsulate the repository, login cache, and logger. Dependency injection here looks consistent with the rest of the codebase.


35-69: User creation and lookup logic look solid

Create correctly checks for existing users, hashes the password with BCrypt, and wraps repository errors with useful context. GetByUsername is a simple and appropriate wrapper over GetByID. These pieces look good as‑is.

internal/sms-gateway/openapi/docs.go (3)

21-87: JWT token issue/ revoke endpoints now correctly documented with security and schemas

The /3rdparty/v1/auth/token POST and /3rdparty/v1/auth/token/{jti} DELETE operations now include both ApiAuth and JWTAuth security plus concrete request/response/error schemas, which aligns the OpenAPI spec with the new token handlers and resolves the earlier missing‑security problem.


936-1013: Health endpoints and response schema look consistent

The new /health/live, /health/ready, and /health/startup endpoints, together with smsgateway.HealthResponse and related types, provide clear liveness/readiness/startup probes with 200/503 semantics. This is a solid addition for observability and Kubernetes-style health checks.

Also applies to: 1230-1288


1847-1855: JWTAuth security definition and updated title

Defining JWTAuth as an apiKey in the Authorization header and updating the Swagger title to “SMSGate API” both look appropriate and consistent with the rest of the spec and new JWT functionality.

Also applies to: 1879-1884

internal/sms-gateway/jwt/config.go (1)

12-32: Config validation for secret and TTL looks solid

Validate correctly enforces a non-empty secret with a minimum length and rejects non‑positive TTL values, which prevents misconfigured JWT services from starting.

internal/sms-gateway/jwt/service.go (2)

24-51: JWT token generation flow is well-validated and side-effect safe

Constructor validation, userID/scopes/TTL checks, default TTL handling, max‑TTL capping via min(ttl, s.config.TTL), signing with HS256, and persisting the token through the repository all look correct. Error paths are wrapped with context, and issuance metrics are updated consistently based on success vs error.

Also applies to: 53-112


114-161: Token parsing, validation, and revocation checks look correct

ParseToken enforces expiry, issued-at, issuer, and algorithm, then consults the repository to fail revoked JTIs, with validation metrics updated accordingly. This matches typical JWT best practices and integrates cleanly with your revocation store.

internal/sms-gateway/jwt/repository.go (1)

10-47: Repository methods correctly encapsulate token persistence and revocation checks

Insert, Revoke, and IsRevoked use WithContext, narrow queries on tokenModel, and wrap underlying GORM errors with descriptive messages. The revocation update is idempotent and the IsRevoked check is simple and efficient.

internal/sms-gateway/modules/auth/service.go (1)

24-36: Auth service wiring to users/devices/online modules looks clean

The updated Service struct and New constructor neatly inject usersSvc, devicesSvc, onlineSvc, and logger, and keep the one-time-code cache localized. This simplifies auth to rely on the new users module without extra repositories or caches.

Also applies to: 38-60

internal/sms-gateway/jwt/metrics.go (1)

8-81: JWT metrics wiring looks solid and idiomatic

The metrics setup here is clean:

  • Namespaces/subsystem are consistent (sms/auth).
  • Counters and histograms are clearly separated per operation (issuance/validation/revocation).
  • Helper methods (Increment*, Observe*) keep call sites simple and reduce copy‑paste of metric logic.

Assuming the alerting and dashboards are now aligned with the single status label, this is good to go.

Please confirm that your Prometheus alert rules and dashboards reference these exact metric names and the status label only (or update them accordingly). For example:

Also applies to: 83-117

Copy link

@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: 0

🧹 Nitpick comments (2)
pkg/cache/options.go (2)

5-73: Set options (Option, WithTTL, WithValidUntil) look correct; maybe clarify “no expiry” semantics

The data model and helpers are straightforward and match the comments: WithTTL using time.Now().Add(ttl) on apply and treating ttl <= 0 as “no expiry” via time.Time{} is consistent with the documentation, and WithValidUntil is a simple override.

One potential improvement: callers only see that “TTL <= 0 means the item will not expire,” but the implementation encodes that specifically as validUntil.IsZero(). It would help to call that out explicitly in the docstring (and ensure the concrete caches consistently interpret time.Time{} as “no expiration”), and perhaps mention that WithValidUntil cannot currently express “no expiry” and that WithTTL(0) is the supported way to get that behavior.


107-208: GetOption helpers are implemented correctly; consider documenting interactions and edge cases

The AndSetTTL / AndUpdateTTL / AndSetValidUntil / AndDefaultTTL / AndDelete helpers are all thin, correct setters on getOptions and the pointer‑capture pattern for ttl/validUntil is idiomatic in Go (the closure causes escape to heap, so there’s no lifetime issue).

Two minor API/contract points you might want to tighten up:

  • The docs for AndSetTTL and AndUpdateTTL don’t mention how non‑positive TTLs are interpreted, even though WithTTL clearly defines ttl <= 0 as “no expiry.” If the underlying Get path uses the same convention, mirroring that in the comments (or explicitly stating if they are rejected/treated differently) would avoid surprises.
  • When multiple GetOptions are combined (e.g., AndSetTTL + AndSetValidUntil + AndDefaultTTL or AndDelete plus a TTL update), the effective precedence is delegated to the consumers of getOptions. It might be worth documenting the intended precedence or combination rules somewhere near GetOption so callers know what to expect.

Aside from those clarifications, the surface looks clean and composable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a66a972 and 70e753c.

📒 Files selected for processing (1)
  • pkg/cache/options.go (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Lint
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
pkg/cache/options.go (1)

75-105: getOptions plumbing and isEmpty helper are solid

The getOptions struct and apply/isEmpty helpers are minimal and correctly model optional behavior via pointer fields and booleans. isEmpty checks all flags, so consumers can cheaply skip post‑processing when no GetOptions were supplied.

No functional issues here from what’s visible.

Copy link

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

33-42: Document the new 403 “forbidden” responses introduced by scope checks

Each endpoint now uses permissions.RequireScope(...), which will return 403 Forbidden when the user lacks the required scope. The Swagger annotations for list/post/delete webhooks still only declare 401 and 500 failures.

Consider adding a 403 error response to all three endpoints’ annotations so the OpenAPI spec matches the actual behavior.

Also applies to: 54-66, 87-97

internal/sms-gateway/handlers/messages/3rdparty.go (1)

243-258: /messages/inbox/export lacks a permission scope guard unlike other message endpoints

In Register, all 3rd‑party message routes except inbox export are wrapped with permissions.RequireScope(...):

  • GET ""ScopeList
  • POST ""ScopeSend
  • GET ":id"ScopeRead
  • POST "inbox/export" → only userauth.WithUser(h.postInboxExport)

So /3rdparty/v1/messages/inbox/export is authenticated (ApiAuth/JWTAuth) but not scope‑restricted, which is inconsistent with the rest of the messages API and could allow users without the expected permission to trigger exports.

Consider guarding this route with an appropriate scope (e.g., ScopeRead or a dedicated export scope) to keep the authorization model consistent:

 func (h *ThirdPartyController) Register(router fiber.Router) {
-    router.Post("inbox/export", userauth.WithUser(h.postInboxExport))
+    router.Post("inbox/export", permissions.RequireScope(ScopeRead), userauth.WithUser(h.postInboxExport))
 }

(or whatever scope name best matches your intent).

Also applies to: 280-286

pkg/cache/memory.go (1)

177-241: Fix TTL update semantics for items without TTL and caches with zero default TTL

In memoryCache.Get, the TTL update block has two problematic edge cases:

  • When an item has no TTL (item.validUntil.IsZero()), AndUpdateTTL does item.validUntil = item.validUntil.Add(*o.updateTTL). This computes a timestamp relative to the zero time, so the entry becomes effectively “always expired” and getItem will return ErrKeyExpired instead of extending its lifetime.
  • When the cache was created with NewMemory(0) (documented as “no expiration by default”), AndDefaultTTL still executes item.validUntil = time.Now().Add(m.ttl). With m.ttl == 0, that sets validUntil to “now”, making the entry expire almost immediately, which contradicts the “no expiration” default.

A minimal fix is to special‑case these two situations:

  • Treat AndUpdateTTL on an item with no existing TTL as “set a fresh TTL from now”.
  • Treat AndDefaultTTL with m.ttl <= 0 as “clear the TTL” (no expiration), matching NewMemory and WithTTL semantics.

For example:

 func (m *memoryCache) Get(_ context.Context, key string, opts ...GetOption) ([]byte, error) {
   return m.getValue(func() (*memoryItem, bool) {
@@
-    o := getOptions{}
-    o.apply(opts...)
+    o := getOptions{}
+    o.apply(opts...)
@@
-    m.mux.Lock()
+    m.mux.Lock()
     item, ok := m.items[key]
@@
-    } else if o.delete {
+    } else if o.delete {
       delete(m.items, key)
-    } else if !item.isExpired(time.Now()) {
-      if o.validUntil != nil {
-        item.validUntil = *o.validUntil
-      } else if o.setTTL != nil {
-        item.validUntil = time.Now().Add(*o.setTTL)
-      } else if o.updateTTL != nil {
-        item.validUntil = item.validUntil.Add(*o.updateTTL)
-      } else if o.defaultTTL {
-        item.validUntil = time.Now().Add(m.ttl)
-      }
-    }
+    } else if !item.isExpired(time.Now()) {
+      now := time.Now()
+      if o.validUntil != nil {
+        item.validUntil = *o.validUntil
+      } else if o.setTTL != nil {
+        item.validUntil = now.Add(*o.setTTL)
+      } else if o.updateTTL != nil {
+        if item.validUntil.IsZero() {
+          // No existing TTL: treat as setting a fresh TTL from now.
+          item.validUntil = now.Add(*o.updateTTL)
+        } else {
+          item.validUntil = item.validUntil.Add(*o.updateTTL)
+        }
+      } else if o.defaultTTL {
+        if m.ttl > 0 {
+          item.validUntil = now.Add(m.ttl)
+        } else {
+          // Zero/negative default TTL means "no expiration".
+          item.validUntil = time.Time{}
+        }
+      }
+    }
@@
   })
 }

This keeps the fast path unchanged, preserves existing behavior for items that already have a TTL, and makes AndUpdateTTL/AndDefaultTTL behave sensibly for “no TTL” entries and NewMemory(0) caches. Based on learnings, this also keeps cleanup/Drain semantics intact because expiration is still driven solely by validUntil.

pkg/cache/redis.go (1)

24-48: Fix Redis TTL edge cases: AndDefaultTTL with zero TTL and AndUpdateTTL without existing TTL

Two TTL bugs confirmed in the Redis cache implementation:

  1. AndDefaultTTL with RedisConfig{TTL: 0} (lines 306–307)
    Setting ttlTimestamp = time.Now().Add(r.ttl).Unix() when r.ttl == 0 produces the current Unix timestamp. This causes HExpireAt to set the field expiration to "now", immediately deleting it on next access—opposite of the intended "no default TTL" behavior.

  2. AndUpdateTTL when field has no TTL (lines 34–36 in Lua script)
    When HTTL returns -1 (field exists but has no TTL), newTtl = -1 + ttlDelta produces ttlDelta - 1 or a negative value. This is unintuitive; when a field has no TTL, extending by ttlDelta should set a fresh TTL of ttlDelta seconds, not treat it as offset arithmetic.

  3. Missing Redis version requirement (type comment)
    The redisCache type comment does not document that Redis 7.4+ is required for per-field hash TTL commands.

Apply the fixes from the original comment, which correctly handle both edge cases.

♻️ Duplicate comments (5)
internal/sms-gateway/users/repository.go (1)

32-62: Fix GORM pointer usage in GetByID and detect missing users in UpdatePassword

Two important issues here, both previously flagged:

  1. Incorrect pointer passed to GORM in GetByID
user := new(userModel)
if err := r.db.Where("id = ?", id).Take(&user).Error; err != nil {

user is already of type *userModel, so &user is **userModel. GORM expects a pointer to the destination struct (*userModel), not a pointer to a pointer. This can lead to incorrect behavior depending on the GORM version.

Change Take(&user) to Take(user):

 func (r *repository) GetByID(id string) (*userModel, error) {
 	user := new(userModel)
 
-	if err := r.db.Where("id = ?", id).Take(&user).Error; err != nil {
+	if err := r.db.Where("id = ?", id).Take(user).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, ErrNotFound
 		}
 		return nil, fmt.Errorf("can't get user: %w", err)
 	}
 
 	return user, nil
 }
  1. UpdatePassword silently succeeds when no rows are updated

GORM’s Update does not return an error when zero rows are affected (e.g. missing user ID). To keep behavior consistent with GetByID returning ErrNotFound, you should check RowsAffected:

 func (r *repository) UpdatePassword(ID string, passwordHash string) error {
-	if err := r.db.Model((*userModel)(nil)).
-		Where("id = ?", ID).
-		Update("password_hash", passwordHash).Error; err != nil {
-		return fmt.Errorf("can't update password: %w", err)
-	}
-
-	return nil
+	result := r.db.Model((*userModel)(nil)).
+		Where("id = ?", ID).
+		Update("password_hash", passwordHash)
+
+	if err := result.Error; err != nil {
+		return fmt.Errorf("can't update password: %w", err)
+	}
+	if result.RowsAffected == 0 {
+		return ErrNotFound
+	}
+
+	return nil
 }

These fixes align repository behavior with the rest of the users module and avoid hard-to-debug inconsistencies.

Please confirm against your GORM version’s docs that Take should receive *userModel (not **userModel) and that RowsAffected is the correct way to detect missing rows after Update.

internal/sms-gateway/jwt/config.go (1)

12-16: Optional: decide whether Issuer should be validated or explicitly documented as optional

Config.Issuer is present but not validated in Validate(). Since the service uses this when building JWT claims, consider either:

  • Adding a non-empty check (similar to Secret) if an issuer is required for your tokens, or
  • Adding a brief comment/doc note indicating that an empty issuer is intentionally allowed.

This mirrors a previous comment and is mostly about making the configuration contract explicit.

internal/sms-gateway/handlers/thirdparty/auth.go (1)

42-45: Clarify ttl semantics, add bounds, and align with OpenAPI definition

tokenRequest.TTL is a uint64 in (implicit) seconds and is passed as:

time.Duration(req.TTL) * time.Second

There are a few problems here:

  • Very large ttl values can overflow time.Duration (int64) during the cast/multiply, silently producing incorrect expiries.
  • ttl == 0 is accepted but its meaning (“no expiry”, “use default”, or “immediately expired”) isn’t defined or validated.
  • The generated OpenAPI currently models ttl as time.Duration (nanoseconds) via #/definitions/time.Duration, while the handler interprets it as seconds, so clients and server disagree on units.

Consider:

  • Adding explicit validation on TTL (e.g., reject 0/negative-equivalent and enforce an upper bound before casting), and
  • Making the units and behavior explicit and consistent between handler and docs (either keep seconds everywhere or switch the field type to time.Duration and treat it as such, then regenerate Swagger).

Also applies to: 76-76

internal/sms-gateway/handlers/mobile.go (1)

43-74: Still ignoring nanoid.Standard error; handle init failure explicitly

idGen, _ := nanoid.Standard(21) ignores the returned error. If initialization ever fails and returns a nil function, subsequent calls to h.idGen() will panic at runtime.

You should either fail fast or log+exit if ID generation can’t be initialized, rather than constructing a handler with a broken generator. For example:

 func newMobileHandler(
@@
-	idGen, _ := nanoid.Standard(21)
+	idGen, err := nanoid.Standard(21)
+	if err != nil {
+		// Fatal here is acceptable since the handler cannot function without IDs.
+		logger.Fatal("failed to initialize ID generator", zap.Error(err))
+	}
@@
-		idGen: idGen,
+		idGen: idGen,
 	}
 }

Or, alternatively, change newMobileHandler to return (*mobileHandler, error) and propagate the error to the caller.

pkg/cache/redis.go (1)

74-87: Document minimum Redis version required for hash TTL commands

This implementation relies on Redis per-field hash TTL commands (HEXPIRE*, HTTL, etc.) and their FIELDS sub-arguments. Older Redis versions will either fail these commands or not support them at all, leading to runtime errors that are hard for integrators to diagnose.

It would help to make the minimum Redis version requirement explicit in the redisCache type documentation, for example:

-// redisCache implements the Cache interface using Redis as the backend.
-//
-// This implementation stores all data in a Redis hash, with each cache item
-// being a field in the hash. It uses Redis's built-in TTL functionality for
-// expiration and Lua scripts for atomic operations.
+// redisCache implements the Cache interface using Redis as the backend.
+//
+// This implementation stores all data in a Redis hash, with each cache item
+// being a field in the hash. It uses Redis's built-in TTL functionality for
+// expiration and Lua scripts for atomic operations.
+//
+// NOTE: This cache relies on Redis per-field hash TTL commands (e.g. HEXPIRE,
+// HEXPIREAT/HEXPIRETIME, HTTL/HPTTL). Ensure your Redis deployment is recent
+// enough to support these commands (Redis 7.4+), or use the in-memory cache
+// instead.

This mirrors the earlier review feedback and makes the requirement visible to operators configuring the service. Please verify the exact minimum Redis version and command names against official Redis documentation before finalizing the wording.

🧹 Nitpick comments (16)
internal/sms-gateway/modules/messages/service.go (2)

135-168: GetState: user-scoped cache/DB lookups are correct; consider passing context instead of Background

Using user.ID consistently for both cache keying and the repository filter keeps message state strictly scoped to the current user, and the negative-caching of ErrMessageNotFound avoids repeated DB hits. The behavior is sound.

Non-blocking: you might eventually want GetState to accept a context.Context and thread it through to cache.Get/cache.Set instead of using context.Background(), so callers can propagate deadlines and cancellations from HTTP handlers or workers.


229-239: Align Enqueue cache key with generated ExtID to avoid wasted cache entries

In Enqueue, when message.ID is empty you generate a new external ID:

if msg.ExtID == "" {
    msg.ExtID = s.idgen()
}
state.ID = msg.ExtID

but the cache entry is still written under message.ID:

if err := s.cache.Set(context.Background(), device.UserID, message.ID, anys.AsPointer(modelToMessageState(msg))); err != nil {
    ...
}

If message.ID == "", this writes a value under the ("", userID) key that GetState(user, state.ID) will never read, causing an avoidable DB hit on the first lookup and a “dummy” cache entry for the empty ID.

Consider switching the cache key to msg.ExtID (or state.ID) so it always matches what GetState will query:

- if err := s.cache.Set(context.Background(), device.UserID, message.ID, anys.AsPointer(modelToMessageState(msg))); err != nil {
+ if err := s.cache.Set(context.Background(), device.UserID, msg.ExtID, anys.AsPointer(modelToMessageState(msg))); err != nil {

This keeps cache keys consistent regardless of whether the client supplied an ID.

internal/sms-gateway/users/models.go (1)

10-27: User persistence model looks good; clarify cascade behavior for devices

userModel itself and Migrate look fine and consistent with Device.UserID (varchar(32)). The commented-out Devices []models.Device relation with OnDelete:CASCADE means there is currently no DB-level cascade from users to devices; any cleanup must be handled in application code.

If you intend DB-enforced cascade, consider either:

  • Re‑introducing the relation without gorm:"-", or
  • Moving the constraint:OnDelete:CASCADE tag onto Device.UserID.

Otherwise, consider deleting the commented field to avoid confusion.

Please confirm your intended deletion semantics and adjust the GORM tags accordingly if you want DB-level cascade.

internal/sms-gateway/modules/auth/service.go (2)

27-59: Service wiring is good; idgen now appears unused

Injecting usersSvc, devicesSvc, and onlineSvc via New looks clean and makes the dependencies explicit. However, idgen is still constructed and stored but not used anywhere in this file, which suggests leftover state from the previous design.

If token/ID generation has been fully delegated to devicesSvc, consider:

  • Removing the idgen field and the nanoid.Standard(21) call, and
  • Dropping the github.com/jaevor/go-nanoid dependency from this module if nothing else uses it.

This will simplify the service and avoid unnecessary RNG work at startup.

Please confirm whether idgen is used elsewhere in this package or can be safely removed.


90-97: Check semantics of user identifier passed through one-time codes

RegisterDevice now takes a users.User and delegates device creation to devicesSvc.Insert, which is a nice separation of concerns.

In AuthorizeUserByCode:

userID, err := s.codesCache.GetAndDelete(code)
...
user, err := s.usersSvc.GetByUsername(userID)

The variable is named userID, but it is passed to GetByUsername. This is fine if, in your domain, the value cached in GenerateUserCode is actually a username and not a database ID; otherwise it risks subtle mismatches.

Two concrete suggestions:

  • If the cached value is a username, rename variables/parameters (e.g. username string) for clarity across GenerateUserCode and AuthorizeUserByCode.
  • If the cached value is a primary key/ID, consider calling a GetByID‑style method on usersSvc instead of GetByUsername.

Clarifying this will help prevent future bugs and confusion.

Please double-check what value your handlers pass into GenerateUserCode and ensure it matches what GetByUsername expects.

Also applies to: 133-145

internal/sms-gateway/jwt/metrics.go (1)

57-79: Consider operation-specific histogram buckets.

All three duration histograms use identical buckets (5ms to 1s). Token validation is typically faster (cryptographic operations only), while issuance and revocation involve database operations. Consider tailoring buckets to each operation's expected latency profile for more granular observability.

internal/sms-gateway/jwt/module.go (1)

9-22: Consider conditional metrics and repository creation.

The module always provides NewMetrics and NewRepository (line 13), even when JWT is disabled (Config.Secret empty). This creates Prometheus metrics and potentially database queries that are never used. Consider providing these dependencies conditionally or document why they should always be created.

api/requests.http (1)

221-225: Remove unnecessary Content-Type header.

The DELETE request includes Content-Type: application/json but has no request body. This header is unnecessary for DELETE requests without a body.

internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

23-31: Consider distinguishing authentication vs system errors.

Both token parsing failures and user lookup errors return fiber.ErrUnauthorized. If GetByUsername can fail due to database errors (not just "user not found"), consider logging system errors or returning 503 Service Unavailable for transient failures, reserving 401 for genuine authentication issues.

internal/sms-gateway/users/service.go (1)

71-94: Ensure login cache never stores or logs plaintext passwords

Login/ChangePassword pass the raw password into loginCache.Get/Set/Delete. That’s fine at the service boundary, but it puts constraints on the cache implementation:

  • It must not use the raw password directly as a cache key or value, and
  • It must never log the password (even at debug) or expose it through metrics.

Please double‑check the loginCache implementation and, if needed, derive opaque keys (e.g., using a hash of username+password or some other irreversible token) so no plaintext credentials are persisted or logged by the caching layer.

Also applies to: 96-112

internal/sms-gateway/jwt/service.go (2)

53-112: GenerateToken validations and TTL handling are robust

The parameter validation (non-empty userID, non-empty scopes, non-negative TTL, zero TTL → default from config) plus capping expiry at min(ttl, s.config.TTL) gives a clear, bounded token lifetime model. The issuance path (claims construction, signing, persistence, and metrics) is consistent and free of obvious edge-case bugs.

You might consider using time.Now().UTC() for IssuedAt/ExpiresAt to make the intended timezone of JWT timestamps explicit, but it’s not required for correctness.


114-161: Consider normalizing ParseToken errors to ErrInvalidToken

The parsing path is generally correct: it enforces expiration, issued-at, issuer, and HS256-only, then checks revocation via the repository. One behavioral nuance is that parse failures return a wrapped error (fmt.Errorf("failed to parse token: %w", parseErr)) instead of the sentinel ErrInvalidToken, while structurally invalid tokens or bad claims use ErrInvalidToken.

If external callers or HTTP handlers are expected to branch on ErrInvalidToken, you may want to normalize all client-facing parse failures (including signature/format issues) to ErrInvalidToken and log the underlying parseErr internally, to avoid leaking parsing details and to keep the public API simpler.

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

7-56: User locals handling is now consistent; consider tightening WithUser panic semantics

The switch to SetUser + GetUser storing a users.User value in Locals fixes the previous pointer/value mismatch and makes HasUser and WithUser consistent across both Basic and Code auth flows. Granting permissions.ScopeAll on successful Basic auth also aligns with the new permissions middleware.

WithUser intentionally panics when no user is present (handler(*GetUser(c), c)), as documented. Given that your routes using WithUser already chain UserRequired() beforehand, this should be safe in practice. If you ever expect WithUser to be reused more freely, a small guard with a clearer panic message (or a 401) around GetUser(c) == nil would make failures easier to diagnose.

Also applies to: 82-109, 124-133

internal/sms-gateway/handlers/mobile.go (1)

121-153: Users service integration and auth composition look correct

  • In postDevice, reusing an authenticated user from userauth.GetUser and only generating username/password for new users matches the route docs (“Returns user credentials only for new users”). Passing *user into RegisterDevice and using username in the response is consistent with the new users.User model.
  • Updating getUserCode to accept users.User and wiring the route through userauth.NewBasic(h.usersSvc), UserRequired(), and WithUser(h.getUserCode) creates a clean, type-safe flow from Basic auth to code generation.
  • changePassword now correctly delegates to usersSvc.ChangePassword using device.UserID and logs on failure, returning a 401 to the client, which is a reasonable security posture.
  • Router changes that swap in userauth.NewBasic(h.usersSvc) for the relevant endpoints are aligned with the new users module.

Overall, the mobile handler’s integration with usersSvc and the updated authentication flow looks coherent and consistent with the rest of the refactor.

You already have a TODO noting that /user/password “should be under userauth.NewBasic protection instead of deviceauth”; aligning that route with the rest of the user-centric auth would be a good follow-up once you’re ready to change its behavior.

Also applies to: 186-207, 223-236, 241-263

pkg/cache/cache.go (1)

108-283: Cache interface and behavior look good; consider clarifying ErrKeyExpired semantics

The interface shape and method docs align with both memoryCache and redisCache implementations. One small suggestion: in the Get comment you state that an expired key yields ErrKeyExpired, but for backends like Redis that eagerly drop expired fields, callers will effectively see ErrKeyNotFound instead. You may want to add a short note that the exact error for expired entries is implementation-dependent (e.g., in-memory keeps expired items until Cleanup, Redis removes them immediately).

pkg/cache/options.go (1)

75-105: Clarify GetOption semantics for items without TTL and caches with zero default TTL

The getOptions structure and helpers (AndSetTTL, AndUpdateTTL, AndSetValidUntil, AndDefaultTTL, AndDelete) give a nice, expressive surface. The one missing piece is documentation for edge cases:

  • What should AndUpdateTTL do when an item has no existing TTL (e.g., validUntil is zero in the in-memory cache, or HTTL returns “no TTL” in Redis)?
  • How should AndDefaultTTL behave when the cache’s default TTL is zero (meaning “no expiration” in NewMemory, and effectively “no default TTL” in RedisConfig)?

Right now these behaviors are implicitly defined in memoryCache.Get and redisCache.Get, and they’re a bit inconsistent. Once you address the TTL handling bugs in those implementations, consider adding 1–2 sentences to the AndUpdateTTL and AndDefaultTTL comments to spell out the intended semantics in those edge cases so future backends can match them.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70e753c and d32510b.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (62)
  • api/mobile.http (1 hunks)
  • api/requests.http (5 hunks)
  • cmd/sms-gateway/main.go (1 hunks)
  • configs/config.example.yml (1 hunks)
  • deployments/grafana/dashboards/jwt.json (1 hunks)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • go.mod (1 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (10 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/metrics.go (1 hunks)
  • internal/sms-gateway/jwt/models.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/repository.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (1 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/openapi/docs.go (17 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
  • pkg/cache/cache.go (1 hunks)
  • pkg/cache/errors.go (1 hunks)
  • pkg/cache/memory.go (13 hunks)
  • pkg/cache/options.go (4 hunks)
  • pkg/cache/redis.go (10 hunks)
  • pkg/cache/typed.go (1 hunks)
💤 Files with no reviewable changes (3)
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/module.go
🚧 Files skipped from review as they are similar to previous changes (26)
  • configs/config.example.yml
  • internal/sms-gateway/handlers/logs/permissions.go
  • internal/config/module.go
  • internal/sms-gateway/users/errors.go
  • go.mod
  • internal/sms-gateway/handlers/settings/permissions.go
  • internal/sms-gateway/app.go
  • internal/sms-gateway/users/domain.go
  • internal/sms-gateway/jwt/disabled.go
  • internal/sms-gateway/users/cache.go
  • internal/sms-gateway/jwt/errors.go
  • deployments/prometheus/alerts/jwt-alerts.yml
  • internal/config/types.go
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
  • internal/sms-gateway/users/module.go
  • deployments/grafana/dashboards/jwt.json
  • internal/sms-gateway/handlers/messages/permissions.go
  • internal/sms-gateway/modules/webhooks/models.go
  • cmd/sms-gateway/main.go
  • internal/sms-gateway/handlers/thirdparty/module.go
  • pkg/cache/typed.go
  • internal/sms-gateway/handlers/logs/3rdparty.go
  • internal/sms-gateway/jwt/models.go
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/handlers/devices/permissions.go
  • internal/config/config.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • pkg/cache/redis.go
  • pkg/cache/cache.go
  • pkg/cache/memory.go
📚 Learning: 2025-09-10T23:54:16.828Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:118-121
Timestamp: 2025-09-10T23:54:16.828Z
Learning: Redis 7.4.0 introduced HEXPIRE, HEXPIREAT, and related commands that support per-field TTL on hash data structures. go-redis v9.6.0+ includes support for these commands with methods like HExpireAt.

Applied to files:

  • pkg/cache/redis.go
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.

Applied to files:

  • pkg/cache/redis.go
  • pkg/cache/cache.go
  • pkg/cache/memory.go
🧬 Code graph analysis (20)
internal/sms-gateway/users/models.go (1)
internal/sms-gateway/models/models.go (1)
  • SoftDeletableModel (12-15)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/jwt/module.go (6)
internal/sms-gateway/jwt/metrics.go (2)
  • NewMetrics (34-81)
  • Metrics (24-31)
internal/sms-gateway/jwt/repository.go (2)
  • NewRepository (14-18)
  • Repository (10-12)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/jwt.go (1)
  • Service (10-14)
internal/sms-gateway/jwt/service.go (1)
  • New (24-51)
internal/sms-gateway/jwt/models.go (1)
  • Migrate (32-37)
internal/sms-gateway/handlers/module.go (2)
internal/sms-gateway/app.go (1)
  • Module (35-60)
internal/sms-gateway/handlers/thirdparty/module.go (1)
  • Module (8-16)
internal/sms-gateway/handlers/thirdparty/auth.go (4)
internal/sms-gateway/handlers/thirdparty/permissions.go (1)
  • ScopeTokensManage (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInvalidParams (9-9)
  • ErrInitFailed (7-7)
  • ErrInvalidConfig (8-8)
  • ErrDisabled (6-6)
internal/sms-gateway/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
  • Device (17-28)
internal/sms-gateway/users/service.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/errors.go (1)
  • ErrExists (7-7)
pkg/crypto/passwords.go (2)
  • MakeBCryptHash (14-20)
  • CompareBCryptHash (22-27)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (17-17)
internal/sms-gateway/modules/messages/service.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/repository.go (1)
internal/sms-gateway/users/errors.go (1)
  • ErrNotFound (6-6)
internal/sms-gateway/handlers/webhooks/3rdparty.go (5)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/webhooks/permissions.go (3)
  • ScopeList (4-4)
  • ScopeWrite (5-5)
  • ScopeDelete (6-6)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/3rdparty.go (3)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (17-21)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • NewBasic (21-59)
  • UserRequired (114-122)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
  • NewJWT (13-38)
internal/sms-gateway/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
  • ErrInvalidConfig (8-8)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
  • SetScopes (15-17)
  • ScopeAll (10-10)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/jwt/service.go (5)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/repository.go (1)
  • Repository (10-12)
internal/sms-gateway/jwt/metrics.go (3)
  • Metrics (24-31)
  • StatusError (20-20)
  • StatusSuccess (19-19)
internal/sms-gateway/jwt/jwt.go (2)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInitFailed (7-7)
  • ErrInvalidParams (9-9)
  • ErrInvalidToken (10-10)
  • ErrTokenRevoked (11-11)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (4)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (24-36)
internal/sms-gateway/modules/messages/service.go (1)
  • Service (31-43)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • SetUser (88-90)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
internal/sms-gateway/handlers/mobile.go (3)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • GetUser (102-109)
internal/sms-gateway/handlers/settings/3rdparty.go (5)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/settings/permissions.go (2)
  • ScopeRead (4-4)
  • ScopeWrite (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/webhooks/permissions.go (1)
  • ScopeWrite (5-5)
internal/sms-gateway/modules/auth/service.go (2)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/models.go (1)
  • Device (17-28)
pkg/cache/cache.go (1)
pkg/cache/options.go (2)
  • Option (9-9)
  • GetOption (89-89)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (204-208)
🪛 Gitleaks (8.29.0)
api/mobile.http

[high] 71-71: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 72-72: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Benchmark
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (30)
internal/sms-gateway/modules/messages/service.go (2)

15-15: users package import matches new user type usage

Importing github.com/android-sms-gateway/server/internal/sms-gateway/users is consistent with the updated method signatures using users.User, and there’s no conflict with existing models usage. Given users.User exposes an ID string field, continuing to dereference user.ID is type-safe. (Based on relevant_code_snippets for internal/sms-gateway/users/domain.go.)


124-133: SelectStates: user scoping and error context look good

Overriding filter.UserID with user.ID correctly enforces per-user scoping regardless of caller-supplied filters, which is important for multi-tenant safety. The new wrapped error ("can't select messages: %w") adds useful context without changing behavior for callers that unwrap.

internal/sms-gateway/models/migration.go (1)

12-13: Device-only migration: ensure users/jwt migrations are invoked elsewhere

Migrate now only migrates Device, which is fine as long as internal/sms-gateway/users.Migrate (and any jwt/token migrations) are wired into app startup separately. Otherwise new deployments will miss the users schema.

Please double-check your app/bootstrap wiring to confirm that users.Migrate and jwt-related migrations are called alongside models.Migrate.

internal/sms-gateway/handlers/webhooks/permissions.go (1)

3-7: Webhooks scope constants are clear and reusable

The scope constants are well named (list / write / delete) and will keep handlers/OpenAPI definitions consistent.

internal/sms-gateway/jwt/metrics.go (4)

8-21: LGTM!

The metric name constants and status label definitions are well-structured and follow Prometheus naming conventions.


23-31: LGTM!

The Metrics struct is well-designed with appropriate field types for JWT operation tracking.


83-96: LGTM!

The increment methods provide a clean interface for updating JWT operation counters with appropriate status labels.


98-117: LGTM!

The observe methods correctly implement duration tracking using the standard Prometheus timer pattern with defer for reliable measurement.

api/mobile.http (1)

71-72: Verify test credentials are not production secrets.

The password values appear hardcoded in this test request file. Ensure these are dummy credentials for development/testing only and not production secrets.

Based on static analysis hints flagging potential API keys.

internal/sms-gateway/handlers/thirdparty/permissions.go (1)

1-5: LGTM!

The permission scope constant follows the established naming pattern and will be used for JWT-based access control on token management endpoints.

internal/sms-gateway/jwt/module.go (1)

24-26: LGTM!

The migration registration in init() ensures the JWT tokens table is created during database initialization.

internal/sms-gateway/handlers/module.go (1)

9-9: LGTM!

The thirdparty module is correctly wired into the handlers composition, following the established pattern for module integration.

Also applies to: 40-40

internal/sms-gateway/handlers/devices/3rdparty.go (2)

9-9: LGTM!

The migration from models.User to users.User and the addition of JWT authentication in OpenAPI specs are consistent with the broader JWT integration. The changes properly align handler signatures with the new user domain type.

Also applies to: 12-12, 36-36, 46-46, 60-60, 72-72


87-88: LGTM!

The permission scopes are correctly enforced using middleware composition. The middleware chain properly validates scopes before invoking handlers.

api/requests.http (2)

4-4: LGTM!

The JWT token environment variable follows the established pattern for request configuration.


38-39: LGTM!

The Bearer token authentication additions provide flexibility for testing JWT-based auth while preserving Basic auth as fallback options. This approach facilitates validation of both authentication methods.

Also applies to: 83-84, 88-89, 108-110

internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (2)

13-22: LGTM!

The Bearer token extraction logic correctly handles the Authorization header with case-insensitive matching and safely strips the prefix. The middleware appropriately delegates to the next handler when a Bearer token is not present, allowing other authentication methods to process the request.


33-37: LGTM!

The middleware correctly stores the authenticated user and token scopes in the request context, making them available to downstream handlers via the established userauth and permissions patterns.

internal/sms-gateway/handlers/3rdparty.go (2)

20-67: Explicit constructor & dependency wiring look solid

The move to an explicit constructor with usersSvc, jwtSvc, and the various controllers (including authHandler) is clean and makes the handler’s dependencies very clear. The base Handler initialization is consistent with the rest of the codebase, and keeping jwtService only as a constructor parameter while storing jwtSvc is fine.


72-81: Auth middleware chain for /3rdparty/v1 (including /auth) is coherent—verify it matches your intent

router.Use(userauth.NewBasic(h.usersSvc), jwtauth.NewJWT(h.jwtSvc, h.usersSvc), userauth.UserRequired()) ensures that all routes registered after this (including /auth, /messages, /webhooks, etc.) require either valid Basic or Bearer credentials and a resolved user. Health remains unauthenticated since it’s registered before the Use, which is typically desirable.

Please double‑check that /3rdparty/v1/auth is indeed meant to require an already authenticated user (Basic or JWT) and that you don’t need a more permissive path just for initial token issuance.

internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)

1-36: Scope storage and enforcement middleware are well‑structured

Using Fiber locals plus HasScope/RequireScope gives a clear, composable pattern for scope checks, and recognizing ScopeAll in HasScope centralizes the “super‑scope” behavior nicely. The comma‑ok type assertion protects against bad locals values, so there’s no panic risk here.

internal/sms-gateway/jwt/jwt.go (1)

10-27: JWT service interface and claims shape align with middleware usage

The Service interface and Claims/TokenInfo types are minimal and to the point, and they line up with how jwtauth.NewJWT uses ParseToken and the UserID/Scopes fields. Embedding jwt.RegisteredClaims keeps you compatible with the underlying library’s validation model.

internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

45-52: Route-level scope checks combined with WithUser are wired correctly

The updated handlers take user users.User and are only invoked via userauth.WithUser, while per-route authorization is enforced with permissions.RequireScope(ScopeList/ScopeWrite/ScopeDelete). Given the upstream /3rdparty/v1 middleware chain (Basic/JWT + UserRequired), this results in a clear, layered auth model: authenticate → require user → require scope → invoke handler with a strongly typed user.

Also applies to: 69-85, 100-108, 110-113

internal/sms-gateway/jwt/config.go (1)

18-31: Config validation for Secret and TTL is now robust

The Validate() method correctly enforces a non-empty secret, a minimum length of 32 bytes, and a strictly positive TTL, which should prevent most misconfiguration errors at startup.

internal/sms-gateway/users/service.go (1)

35-94: Overall users.Service flows look correct

User creation, lookup, login verification with BCrypt, and best-effort login caching are wired cleanly; error wrapping preserves root causes while keeping messages meaningful. No functional issues stand out in these methods.

internal/sms-gateway/handlers/settings/3rdparty.go (1)

36-40: Settings 3rd‑party handlers now correctly use JWT + scoped permissions

Using users.User in handler signatures, adding @Security JWTAuth to the docs, and wiring routes through permissions.RequireScope(ScopeRead/ScopeWrite) plus userauth.WithUser gives clear, consistent access control on settings without changing core behavior.

Also applies to: 48-90, 92-124, 126-129

internal/sms-gateway/jwt/service.go (2)

24-51: Constructor wiring and ID factory handling look solid

New validates Config, enforces non-nil Repository/Metrics, and correctly propagates nanoid.Standard errors instead of risking a nil idFactory. The resulting service struct wiring is cohesive and idiomatic for dependency injection; no changes needed here.


163-177: Revocation flow and metrics are straightforward

RevokeToken cleanly delegates to Repository.Revoke and records revocation metrics based on error presence. Treating “no matching token” as a successful no-op is a reasonable, idempotent behavior for this kind of endpoint.

internal/sms-gateway/jwt/repository.go (1)

14-47: Token repository implementation looks correct and idiomatic

The GORM usage for insert, revocation (single UPDATE with revoked_at is null guard), and revocation checks via COUNT is straightforward and safe. Error wrapping adds useful context without leaking SQL details. I don’t see any functional or concurrency issues here.

pkg/cache/errors.go (1)

5-50: Error documentation is consistent and helpful

The expanded comments for ErrKeyNotFound, ErrKeyExpired, and ErrKeyExists accurately describe how the in-memory and Redis caches use these errors, and the examples read well. No changes needed.

Copy link

@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: 1

♻️ Duplicate comments (1)
internal/sms-gateway/handlers/thirdparty/auth.go (1)

59-76: Validate ttl to avoid time.Duration overflow and clarify zero/huge TTL behavior

req.TTL is converted with:

time.Duration(req.TTL) * time.Second

Without bounds, very large ttl values can overflow time.Duration (int64) and yield incorrect or even negative lifetimes. The semantics for ttl == 0 are also unclear (immediately expired vs. “use default TTL”).

Consider:

  • Adding explicit validation for ttl (e.g., reject <= 0, and cap it at a sane upper bound before converting), and
  • Clearly defining how 0 should behave (either reject with 400 or treat it as “use configured default”).

You can enforce this either via validator tags on smsgateway.TokenRequest or an explicit check here before calling GenerateToken.

🧹 Nitpick comments (3)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

24-35: Verify identity semantics between JWT claims and users service

claims.UserID is passed into usersSvc.GetByUsername, and a similar pattern exists in auth.Service.AuthorizeUserByCode, where a userID string is also resolved via GetByUsername. If this value is actually a stable user ID (not a login/username), consider:

  • Switching to a dedicated GetByID-style lookup on usersSvc, or
  • Renaming the claim/variables and service method to consistently reflect that this is a username.

This will avoid subtle bugs if you later distinguish between immutable IDs and changeable usernames, and it keeps the token → user resolution unambiguous.

internal/sms-gateway/modules/auth/service.go (2)

60-74: for range 3 requires Go 1.22+; consider a more portable loop

The for range 3 construct is only supported in newer Go versions. If this project targets or may be built with Go < 1.22, this will not compile. A classic counted loop is more portable and equally clear:

-	for range 3 {
+	for i := 0; i < 3; i++ {

The rest of the logic (retrying on rand.Read or SetOrFail failures) can remain unchanged.


125-138: Align one-time-code user lookup with users service identity model

AuthorizeUserByCode pulls a userID string from codesCache and then calls:

user, err := s.usersSvc.GetByUsername(userID)

This mirrors the pattern in jwtauth.NewJWT, where claims.UserID is also resolved via GetByUsername. If the cached value and claim are truly immutable IDs (not usernames), it would be clearer and safer to:

  • Introduce an ID-based lookup on usersSvc (e.g., GetByID), and store IDs in the cache/claims, or
  • Rename these fields/methods to consistently reflect that they are usernames, not IDs.

That way, future schema changes (e.g., distinct user IDs vs. usernames) won’t silently break these auth paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d32510b and 185514c.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • go.mod (2 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (5 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/openapi/docs.go (31 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go
🧰 Additional context used
🧬 Code graph analysis (1)
internal/sms-gateway/modules/auth/service.go (2)
internal/sms-gateway/cache/factory.go (1)
  • Cache (14-14)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: E2E
  • GitHub Check: Lint
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
go.mod (2)

18-18: Dependency version is appropriate; no changes needed.

The version v5.3.0 is the latest stable release of golang-jwt/jwt/v5 and addresses the critical DoS vulnerability (CVE-2025-30204). No security advisories block this version.


60-60: No issues found—JWT version coherence verified.

The codebase uses only github.com/golang-jwt/jwt/v5 in all JWT-related imports (found in internal/sms-gateway/jwt/service.go and internal/sms-gateway/jwt/jwt.go). No v4 imports exist, confirming consistent behavior and no confusion risk despite v4 being present as an indirect dependency.

internal/sms-gateway/openapi/docs.go (1)

21-87: Generated OpenAPI changes align with new JWT/token flow

The new /3rdparty/v1/auth/token and /3rdparty/v1/auth/token/{jti} paths, JWTAuth security definition, and smsgateway.TokenRequest/TokenResponse schemas all look consistent with the handlers in thirdparty/auth.go. Since this file is generated, no manual tweaks are needed as long as swagger annotations stay in sync with behavior.

Also applies to: 1787-1827, 1903-1912, 1935-1946

internal/sms-gateway/handlers/messages/permissions.go (1)

4-13: Message permission scope constants look good

Scope names and values (messages:send/read/list/export) are clear and match how they’re used in the 3rd‑party messages router.

internal/sms-gateway/handlers/thirdparty/auth.go (1)

100-119: JWT error mapping middleware is well-scoped

Wrapping the auth routes with errorHandler and translating jwt.ErrInvalidParams, ErrInitFailed/ErrInvalidConfig, and ErrDisabled into precise HTTP statuses and user-friendly messages is a solid pattern; it keeps handlers lean and centralizes JWT error semantics.

internal/sms-gateway/handlers/messages/3rdparty.go (1)

46-66: JWT + scope wiring for 3rd‑party messages endpoints looks consistent

The added @Security JWTAuth + 403 responses, updated handler signatures to users.User, and router wiring:

  • ScopeSend for POST /3rdparty/v1/messages
  • ScopeList for GET /3rdparty/v1/messages
  • ScopeRead for GET /3rdparty/v1/messages/{id}
  • ScopeExport for POST /3rdparty/v1/messages/inbox/export

are all consistent with the new messages permission constants and the generated OpenAPI spec. Behaviorally this should enforce both authentication and fine-grained scopes on all message operations.

Also applies to: 178-199, 216-232, 246-263, 284-290

Copy link

@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: 1

♻️ Duplicate comments (1)
internal/sms-gateway/handlers/mobile.go (1)

56-56: Handle the error from nanoid.Standard.

The error from nanoid.Standard(21) is silently ignored. If initialization fails and returns a nil function, calling h.idGen() at line 146 will cause a nil pointer panic.

Apply this diff to handle the error:

-	idGen, _ := nanoid.Standard(21)
+	idGen, err := nanoid.Standard(21)
+	if err != nil {
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}
🧹 Nitpick comments (3)
internal/sms-gateway/modules/auth/service.go (2)

96-106: Consider validating PrivateToken configuration for non‑public mode

AuthorizeRegistration assumes config.PrivateToken is correctly set when Mode != ModePublic. If it is left empty, all non‑empty tokens will be rejected (and only an empty token would pass), which can be a subtle misconfiguration. Consider adding a startup‑time config validation (or at least a warning log) when mode is private but PrivateToken is blank.


126-135: Clarify naming for value passed into GetByUsername

userID from codesCache.GetAndDelete(code) is passed to s.usersSvc.GetByUsername(userID), but per internal/sms-gateway/users/service.go this parameter is conceptually a username/login, not an internal ID. Renaming the parameter and local variable to username (or similar) would better reflect intent and avoid confusion for future readers.

internal/sms-gateway/handlers/3rdparty.go (1)

69-81: Verify JWT middleware behavior for Basic‑only flows and /auth routes

All non‑health routes now share this chain:

  • userauth.NewBasic(h.usersSvc)
  • jwtauth.NewJWT(h.jwtSvc, h.usersSvc)
  • userauth.UserRequired()

For endpoints that should be reachable with either Basic or JWT (notably /3rdparty/v1/auth), jwtauth.NewJWT should behave as an additive/optional authenticator: it should only attach a user when a valid JWT is present and avoid hard‑failing on a missing Authorization: Bearer header, so that Basic can still succeed and satisfy UserRequired().

If any /auth endpoints are intended to be accessible without prior Basic/JWT auth (e.g., token issuance by some other credential), you may want to register them on a router group that does not include this middleware stack.

Please double‑check the jwtauth middleware semantics and adjust if it currently rejects requests that rely solely on Basic auth.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 185514c and 5502158.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (35)
  • cmd/sms-gateway/main.go (1 hunks)
  • configs/config.example.yml (1 hunks)
  • go.mod (2 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (5 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (1 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/openapi/docs.go (31 hunks)
💤 Files with no reviewable changes (3)
  • internal/sms-gateway/modules/auth/module.go
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/models/models.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • internal/sms-gateway/handlers/messages/permissions.go
  • internal/sms-gateway/handlers/module.go
  • internal/sms-gateway/handlers/thirdparty/auth.go
  • internal/sms-gateway/modules/webhooks/models.go
  • cmd/sms-gateway/main.go
  • internal/config/module.go
  • internal/sms-gateway/handlers/webhooks/permissions.go
  • internal/sms-gateway/handlers/logs/permissions.go
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/app.go
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • internal/sms-gateway/models/migration.go
  • go.mod
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T23:45:45.502Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: internal/sms-gateway/online/module.go:14-16
Timestamp: 2025-09-10T23:45:45.502Z
Learning: fx.Decorate in uber-go/fx creates module-scoped decorated dependencies, not global modifications. Using fx.Decorate(func(log *zap.Logger) *zap.Logger { return log.Named("module_name") }) is a correct pattern for creating module-specific loggers that are scoped only to that module's dependency graph.

Applied to files:

  • internal/sms-gateway/handlers/thirdparty/module.go
🧬 Code graph analysis (10)
internal/sms-gateway/modules/messages/service.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/logs/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/logs/permissions.go (1)
  • ScopeRead (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • SetUser (88-90)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/settings/permissions.go (2)
  • ScopeRead (4-4)
  • ScopeWrite (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/messages/permissions.go (4)
  • ScopeList (10-10)
  • ScopeSend (6-6)
  • ScopeRead (8-8)
  • ScopeExport (12-12)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/mobile.go (2)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • GetUser (102-109)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/config/config.go (1)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/handlers/thirdparty/module.go (4)
internal/sms-gateway/app.go (1)
  • Module (35-60)
internal/sms-gateway/handlers/module.go (1)
  • Module (16-41)
internal/sms-gateway/modules/auth/module.go (1)
  • Module (10-29)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • NewAuthHandler (24-35)
internal/sms-gateway/modules/auth/service.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: E2E
  • GitHub Check: Test
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (22)
internal/sms-gateway/handlers/thirdparty/module.go (1)

8-16: LGTM! Clean module structure.

The module follows the established fx.Module pattern and correctly uses logger.WithNamedLogger for module-scoped logging. The wiring is straightforward and aligns with other modules in the codebase.

internal/sms-gateway/handlers/settings/permissions.go (1)

3-6: LGTM! Clear permission scope definitions.

The scope constants follow a consistent resource:action naming pattern and integrate cleanly with the new permissions middleware framework.

internal/sms-gateway/handlers/devices/permissions.go (1)

3-6: LGTM! Consistent scope definitions.

The permission scopes follow the same resource:action pattern as other modules, ensuring consistency across the codebase.

configs/config.example.yml (2)

41-44: LGTM! Clear JWT configuration.

The JWT configuration block is well-documented with appropriate defaults. The comment clearly indicates that leaving the secret empty disables JWT functionality, which provides a safe default for existing deployments.


50-56: LGTM! Comment clarifications.

The updated comments remove the "in hours" phrase, making them more generic and consistent with the use of duration types.

internal/sms-gateway/handlers/webhooks/3rdparty.go (2)

46-53: LGTM! Correct user type migration.

The handler signature correctly migrates from models.User to users.User, aligning with the new users domain model.


114-116: LGTM! Proper middleware ordering.

The routes correctly apply permission scopes before the user authentication handler, ensuring proper authorization flow.

internal/config/config.go (2)

91-95: LGTM! Well-structured JWT configuration.

The JWT struct uses appropriate types (Duration for TTL, string for Secret/Issuer) and includes proper YAML/envconfig tags for configuration binding.


131-134: LGTM! Sensible defaults.

The default JWT configuration provides reasonable values:

  • 24-hour token TTL balances security with usability
  • Empty secret disables JWT by default (safe for existing deployments)
  • Default issuer "sms-gate.app" provides a reasonable identifier
internal/sms-gateway/handlers/devices/3rdparty.go (1)

46-55: LGTM! Correct user type migration.

The handler signature correctly migrates from models.User to users.User, consistent with the broader refactoring.

internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

14-42: The middleware code is correct—no fixes required.

The verification confirms that claims.UserID works properly with GetByUsername(). The system design stores the username as the User.ID field (set during user creation in line 36: ID: username). While the naming could be clearer—UserID semantically suggests an identifier separate from username—the middleware functions correctly because User.ID and username are equivalent in this codebase. The GetByUsername() method internally calls GetByID() with the same value.

internal/sms-gateway/handlers/logs/3rdparty.go (1)

5-7: LGTM! JWT authentication and permissions integration looks good.

The integration of JWT authentication and scope-based permissions is well-structured:

  • User type correctly migrated to users.User
  • JWTAuth security annotation properly added
  • Permission scope (ScopeRead) enforced before authentication
  • Changes align with the broader JWT subsystem introduction

Also applies to: 28-28, 40-40, 45-45

internal/config/types.go (1)

11-48: LGTM! Well-designed Duration wrapper type.

The implementation is clean and follows Go best practices:

  • Nil-safe accessor methods with reasonable defaults
  • Proper error wrapping in unmarshal methods
  • Compile-time interface assertions
  • Consistent delegation from UnmarshalYAML to UnmarshalText
internal/sms-gateway/handlers/mobile.go (1)

19-19: LGTM! User service integration and type migration executed correctly.

The changes successfully integrate the new users service and migrate from models.User to users.User:

  • Constructor properly accepts and wires usersSvc
  • User creation and authentication delegated to usersSvc
  • Password change flow updated to use usersSvc.ChangePassword
  • All handler signatures and routing consistently updated

Also applies to: 32-32, 43-74, 122-152, 197-197, 230-232, 242-242, 260-260

internal/sms-gateway/handlers/settings/3rdparty.go (1)

8-8: LGTM! JWT authentication and granular permission scopes properly applied.

The integration is well-structured with appropriate scope differentiation:

  • ScopeRead for GET operations
  • ScopeWrite for PUT and PATCH operations
  • User type consistently migrated to users.User
  • Security annotations properly added to all endpoints

Also applies to: 12-12, 39-39, 48-48, 60-60, 72-72, 95-95, 107-107, 127-129

internal/sms-gateway/modules/messages/service.go (1)

15-15: LGTM! Method signatures correctly updated for user type migration.

The changes are straightforward and consistent:

  • Import added for the new users package
  • Method signatures updated to accept users.User
  • No logic changes, maintaining existing behavior

Also applies to: 124-124, 135-135

internal/sms-gateway/openapi/docs.go (1)

1-1946: LGTM! Generated OpenAPI documentation reflects JWT integration correctly.

This file is auto-generated by swaggo/swag and correctly reflects:

  • New token management endpoints (/3rdparty/v1/auth/token)
  • JWTAuth security definition and integration across endpoints
  • New health probe endpoints (live, ready, startup)
  • TokenRequest/TokenResponse schema definitions
  • 403 Forbidden responses where appropriate

The past issues regarding security annotations and TTL type mismatches have been addressed. Any further changes should be made in the source Swagger annotations, not in this generated file.

internal/sms-gateway/handlers/messages/3rdparty.go (1)

12-12: LGTM! Comprehensive JWT authentication and granular permission scopes well-implemented.

The integration demonstrates excellent separation of concerns with distinct scopes:

  • ScopeList for GET /messages
  • ScopeSend for POST /messages
  • ScopeRead for GET /messages/:id
  • ScopeExport for POST /messages/inbox/export

All handler signatures consistently updated to users.User, and security annotations properly added to OpenAPI documentation.

Also applies to: 17-17, 49-49, 59-59, 66-66, 181-181, 193-193, 198-198, 219-219, 226-226, 231-231, 249-249, 257-257, 262-262, 285-289

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

7-7: LGTM! Authentication middleware successfully refactored for JWT integration.

The changes are well-structured and address previous issues:

  • NewBasic now uses users.Service and grants ScopeAll for basic auth
  • SetUser provides consistent user storage across middlewares
  • GetUser correctly returns *users.User with nil-safe type assertion
  • WithUser consistently dereferences the pointer for handlers
  • Type mismatch issue from previous review is resolved

The integration properly supports both basic auth (with full scopes) and code-based auth flows.

Also applies to: 9-9, 16-59, 82-82, 88-90, 96-96, 102-108, 130-130, 132-132

internal/sms-gateway/modules/auth/service.go (2)

23-53: Constructor wiring and new service dependencies look consistent

The added usersSvc, devicesSvc, and onlineSvc fields and the updated New constructor parameters are aligned and keep this service focused on orchestration rather than direct data access. No issues noticed here.


83-90: RegisterDevice delegation to devices service looks correct

Using users.User and delegating persistence to devicesSvc.Insert(user.ID, &device) cleanly separates concerns between auth and devices. The new signature aligns with the public users service usage.

internal/sms-gateway/handlers/3rdparty.go (1)

20-67: Explicit constructor and handler dependencies look good

The new thirdPartyHandler fields and newThirdPartyHandler signature (including usersSvc, jwtSvc, authHandler, logger, and validator) are consistent and make the wiring clearer than fx‑based params. No problems spotted here.

Copy link

@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: 1

♻️ Duplicate comments (3)
internal/sms-gateway/handlers/devices/3rdparty.go (1)

87-88: Fix inconsistent route parameter syntax.

Line 88 uses :id without a leading slash, which is inconsistent with Fiber routing conventions. Per the past review comment, this should be /:id to match other handlers like webhooks.

Apply this diff:

-	router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
+	router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

124-133: Guard WithUser against missing user for clearer failures

WithUser currently does:

return handler(*GetUser(c), c)

If a route is mistakenly wired without an auth middleware that calls SetUser, this will panic with a nil‑pointer dereference, which is hard to diagnose.

Consider adding an explicit nil check and panicking with a clearer message, as previously suggested, e.g.:

func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler {
	return func(c *fiber.Ctx) error {
		user := GetUser(c)
		if user == nil {
			panic("WithUser called without authenticated user in context")
		}
		return handler(*user, c)
	}
}

This keeps the contract (“must be used after auth middleware”) but gives a much more actionable failure if it’s violated.

internal/sms-gateway/handlers/mobile.go (1)

56-56: Handle the error from nanoid.Standard(21).

This issue was already flagged in a previous review. The error from nanoid.Standard(21) should not be silently ignored, as a nil function will cause a panic when h.idGen() is called.

🧹 Nitpick comments (7)
internal/sms-gateway/users/models.go (1)

13-13: Consider removing commented-out code or adding a TODO.

The commented-out Devices relation should either be removed if not needed, or documented with a TODO comment if it's planned for future implementation.

Apply this diff if removing is preferred:

 	ID           string `gorm:"primaryKey;type:varchar(32)"`
 	PasswordHash string `gorm:"not null;type:varchar(72)"`
-	// Devices      []models.Device `gorm:"-,foreignKey:UserID;constraint:OnDelete:CASCADE"`
 
 	models.SoftDeletableModel

Or add context if keeping:

-	// Devices      []models.Device `gorm:"-,foreignKey:UserID;constraint:OnDelete:CASCADE"`
+	// TODO: Add Devices relation once bidirectional relationship is needed
+	// Devices      []models.Device `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
internal/config/module.go (1)

128-134: Remove redundant type conversion.

On line 131, cfg.JWT.TTL is already of type Duration (which is time.Duration per internal/config/types.go), so the explicit time.Duration() conversion is redundant.

Apply this diff:

 	fx.Provide(func(cfg Config) jwt.Config {
 		return jwt.Config{
 			Secret: cfg.JWT.Secret,
-			TTL:    time.Duration(cfg.JWT.TTL),
+			TTL:    cfg.JWT.TTL,
 			Issuer: cfg.JWT.Issuer,
 		}
 	}),
internal/sms-gateway/users/module.go (1)

12-31: Users module DI and migration wiring look solid (minor nit on cache key)

The users FX module is well‑structured: a named logger, a private loginCache built via cache.Factory, a private repository, and a public NewService, with errors from cache creation correctly aborting startup. The init‑time db.RegisterMigration(Migrate) also follows the existing migration pattern.

If you expect to reuse "users:login" elsewhere, consider extracting it into a const to avoid typos and ease future changes; otherwise this inline literal is fine.

internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

113-117: Please verify middleware ordering between RequireScope and WithUser

Right now the routes are registered as:

router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.get))
router.Post("", permissions.RequireScope(ScopeWrite), userauth.WithUser(h.post))
router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.delete))

This makes permissions.RequireScope(...) run before userauth.WithUser. That’s fine if permissions.HasScope relies only on JWT‑attached claims (or other context) that are already set by upstream middleware, but it will fail if scope checks depend on userauth having populated the user first.

Please confirm that HasScope does not depend on userauth.GetUser(c) (or similar). If it does, you’ll want to swap the order:

router.Get("", userauth.WithUser(h.get), permissions.RequireScope(ScopeList))

(and similarly for POST/DELETE).

internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

14-22: Consider extracting the magic number for clarity.

The token extraction logic is correct and properly validates the Bearer scheme. However, the magic number 7 (length of "Bearer ") could be extracted as a constant for improved readability.

+const bearerPrefix = "Bearer "
+
 func NewJWT(jwtSvc jwt.Service, usersSvc *users.Service) fiber.Handler {
 	return func(c *fiber.Ctx) error {
 		token := c.Get("Authorization")
 
-		if len(token) <= 7 || !strings.EqualFold(token[:7], "Bearer ") {
+		if len(token) <= len(bearerPrefix) || !strings.EqualFold(token[:len(bearerPrefix)], bearerPrefix) {
 			return c.Next()
 		}
 
-		token = token[7:]
+		token = token[len(bearerPrefix):]
internal/sms-gateway/users/service.go (1)

35-60: Handle user creation races on unique constraint, not just Exists pre‑check

Create currently does a read (Exists) before Insert. Under concurrent creates for the same username, this can still race and let the DB surface a uniqueness error that is returned as a generic “failed to create user” instead of ErrExists.

Consider also mapping the repository’s unique‑constraint error from Insert to ErrExists (and/or relying primarily on the DB constraint) so behavior is deterministic under contention.

internal/sms-gateway/handlers/mobile.go (1)

131-133: Optional: Document the credential generation logic.

The username takes characters 0-5 and the password takes characters 7-20 from the 21-character ID, skipping the character at index 6. While this is likely intentional for visual separation, consider adding a brief comment explaining the rationale for maintainability.

Example:

 id := h.idGen()
+// Split the 21-char ID: first 6 for username, skip char 6 as separator, remaining 14 for password
 username = strings.ToUpper(id[:6])
 password = strings.ToLower(id[7:])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a6c1d8 and c56a4ab.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (42)
  • cmd/sms-gateway/main.go (1 hunks)
  • configs/config.example.yml (1 hunks)
  • go.mod (2 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (5 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (1 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/openapi/docs.go (31 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
💤 Files with no reviewable changes (3)
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/module.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • internal/sms-gateway/handlers/settings/permissions.go
  • internal/sms-gateway/handlers/logs/permissions.go
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • internal/sms-gateway/handlers/messages/permissions.go
  • internal/sms-gateway/users/errors.go
  • internal/sms-gateway/handlers/logs/3rdparty.go
  • internal/sms-gateway/modules/webhooks/models.go
  • internal/sms-gateway/users/cache.go
  • internal/sms-gateway/users/repository.go
  • internal/sms-gateway/models/migration.go
  • go.mod
  • cmd/sms-gateway/main.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T23:45:45.502Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: internal/sms-gateway/online/module.go:14-16
Timestamp: 2025-09-10T23:45:45.502Z
Learning: fx.Decorate in uber-go/fx creates module-scoped decorated dependencies, not global modifications. Using fx.Decorate(func(log *zap.Logger) *zap.Logger { return log.Named("module_name") }) is a correct pattern for creating module-specific loggers that are scoped only to that module's dependency graph.

Applied to files:

  • internal/sms-gateway/handlers/thirdparty/module.go
🧬 Code graph analysis (17)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (3)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • SetUser (88-90)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
internal/config/module.go (2)
internal/config/config.go (1)
  • JWT (91-95)
internal/config/types.go (1)
  • Duration (11-11)
internal/config/config.go (1)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/settings/permissions.go (2)
  • ScopeRead (4-4)
  • ScopeWrite (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/modules/settings/models.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/module.go (3)
internal/sms-gateway/cache/factory.go (1)
  • Factory (16-18)
internal/sms-gateway/users/service.go (1)
  • NewService (21-33)
internal/sms-gateway/users/models.go (1)
  • Migrate (22-27)
internal/sms-gateway/users/service.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/errors.go (1)
  • ErrExists (7-7)
pkg/crypto/passwords.go (2)
  • MakeBCryptHash (14-20)
  • CompareBCryptHash (22-27)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (17-17)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
  • SetScopes (15-17)
  • ScopeAll (10-10)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/module.go (2)
internal/sms-gateway/app.go (1)
  • Module (35-60)
internal/sms-gateway/handlers/thirdparty/module.go (1)
  • Module (8-16)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/modules/auth/service.go (3)
internal/sms-gateway/online/service.go (2)
  • Service (14-17)
  • New (28-37)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/models.go (1)
  • Device (17-28)
internal/sms-gateway/handlers/webhooks/3rdparty.go (7)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/messages/permissions.go (1)
  • ScopeList (10-10)
internal/sms-gateway/handlers/webhooks/permissions.go (3)
  • ScopeList (4-4)
  • ScopeWrite (5-5)
  • ScopeDelete (6-6)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/settings/permissions.go (1)
  • ScopeWrite (5-5)
internal/sms-gateway/modules/messages/service.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/models.go (1)
internal/sms-gateway/models/models.go (1)
  • SoftDeletableModel (12-15)
internal/sms-gateway/handlers/thirdparty/module.go (3)
internal/sms-gateway/app.go (1)
  • Module (35-60)
internal/sms-gateway/handlers/module.go (1)
  • Module (16-41)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • NewAuthHandler (24-35)
internal/sms-gateway/handlers/3rdparty.go (7)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/handlers/devices/3rdparty.go (1)
  • ThirdPartyController (27-31)
internal/sms-gateway/handlers/logs/3rdparty.go (1)
  • ThirdPartyController (21-23)
internal/sms-gateway/handlers/webhooks/3rdparty.go (1)
  • ThirdPartyController (27-31)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (18-22)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • NewBasic (21-59)
  • UserRequired (114-122)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
  • NewJWT (14-42)
internal/sms-gateway/handlers/mobile.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • GetUser (102-109)
  • NewBasic (21-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build / Docker image (linux/amd64)
  • GitHub Check: Build / Docker image (linux/arm64)
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (47)
internal/sms-gateway/users/models.go (1)

22-27: LGTM!

The migration function properly wraps errors and provides clear context.

internal/sms-gateway/users/domain.go (1)

1-19: LGTM!

Clean domain model with proper separation from the persistence layer. The newUser constructor correctly maps fields from the userModel.

internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)

19-26: LGTM!

The scope checking logic correctly handles both exact matches and the wildcard ScopeAll scope in a single pass.


28-36: LGTM!

The middleware correctly enforces scope requirements with a clear error message.

internal/sms-gateway/handlers/thirdparty/auth.go (2)

59-76: LGTM!

The token generation handler correctly validates the request, generates the token with specified scopes and TTL, and returns a properly formatted response.


100-120: LGTM!

The error handler properly maps JWT service errors to appropriate HTTP status codes with user-friendly messages.

internal/sms-gateway/modules/auth/service.go (3)

23-53: LGTM!

The refactoring properly delegates user management to the users.Service, improving separation of concerns. The explicit constructor signature is clearer than the previous FX-based approach.


83-90: LGTM!

The updated RegisterDevice correctly accepts the new users.User domain type and properly delegates device creation to the devices service.


126-138: LGTM!

The AuthorizeUserByCode method correctly retrieves and returns the user via users.Service, with proper error handling.

internal/sms-gateway/modules/settings/models.go (1)

7-7: LGTM!

The update to use users.User is consistent with the broader migration to the new users domain model and properly maintains the foreign key constraint.

Also applies to: 15-15

internal/sms-gateway/handlers/module.go (1)

9-9: LGTM!

The third-party module is properly wired into the handlers module composition, extending support for JWT authentication endpoints.

Also applies to: 40-40

internal/sms-gateway/app.go (1)

10-60: JWT and users FX modules are wired correctly into the app graph

Adding users.Module() and jwt.Module() to the top-level Module (and importing their packages) cleanly integrates user management and JWT into the existing FX graph; the order relative to other modules is fine given FX’s dependency-driven resolution, and it also guarantees the users migration init runs.

internal/sms-gateway/handlers/thirdparty/module.go (1)

1-16: Third‑party handler module wiring looks consistent

Using logger.WithNamedLogger("3rdparty") to scope logs for this module and exposing NewAuthHandler via fx.Provide integrates cleanly with the existing handlers.Module composition; this matches the established pattern of module-scoped named loggers. Based on learnings, this use of a logger decorator is appropriate.

internal/sms-gateway/handlers/devices/permissions.go (1)

1-6: Devices scope constants are clear and consistent

The ScopeList and ScopeDelete values follow the same pattern as other handlers’ scope constants and are ready to be consumed by the permissions middleware.

internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

33-111: Move to users.User and JWT/scopes on webhook endpoints is coherent

Switching the handler signatures to func (h *ThirdPartyController) get/post/delete(user users.User, c *fiber.Ctx) and using user.ID with webhooksSvc aligns with the new users domain and the userauth.WithUser middleware. The added @Security JWTAuth plus @Failure 403 annotations match the new scope checks and give accurate OpenAPI documentation for JWT‑protected, scope‑gated access.

internal/config/config.go (4)

3-3: LGTM!

The time import is necessary for the JWT TTL default initialization.


22-22: LGTM!

The JWT field is properly integrated into the Config struct with appropriate YAML tags and comments.


131-134: LGTM!

The JWT defaults are sensible: 24-hour TTL is reasonable for session tokens, and leaving Secret empty ensures users must explicitly configure it.


91-95: No action required—JWT secret validation already exists.

The JWT Secret field is properly validated in internal/sms-gateway/jwt/config.go. The Config.Validate() method checks that the secret is not empty and meets the minimum length requirement (minSecretLength). The concern in the review comment does not apply.

Likely an incorrect or invalid review comment.

internal/sms-gateway/handlers/devices/3rdparty.go (3)

9-12: LGTM!

The imports correctly reflect the shift to scope-based permissions and the new users domain type.


36-36: LGTM!

The JWTAuth security annotations correctly document the new JWT authentication option alongside the existing API authentication.

Also applies to: 60-60


46-46: LGTM!

The handler signatures correctly use the new users.User type, maintaining compatibility with the existing logic.

Also applies to: 72-72

configs/config.example.yml (2)

41-44: LGTM!

The JWT configuration block is well-structured with clear comments. Leaving secret empty in the example is appropriate, as it forces explicit configuration and the comment clearly indicates the behavior.


50-50: LGTM!

The comment updates improve clarity without changing functionality.

Also applies to: 52-53, 55-56

internal/sms-gateway/handlers/webhooks/permissions.go (1)

1-7: LGTM!

The permission scope constants follow the established pattern and naming conventions used across other handlers.

internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (2)

24-35: LGTM!

The token parsing and user fetching logic has proper error handling that correctly distinguishes between authentication failures (401) and server errors (500).


37-41: LGTM!

The middleware correctly sets the authenticated user and scopes in the request context, enabling downstream permission checks.

internal/sms-gateway/modules/messages/service.go (3)

15-15: LGTM!

The users package import is necessary for the type changes in this file.


124-133: LGTM!

The type change to users.User aligns with the broader refactor, and the added error wrapping improves error context for debugging.


135-135: LGTM!

The signature change to users.User is consistent with the broader refactor and maintains compatibility with the existing implementation.

internal/config/types.go (5)

11-11: LGTM!

The Duration type wrapper enables custom unmarshaling behavior for configuration.


14-19: LGTM!

The nil-safe accessor correctly returns a zero duration for nil values.


22-27: LGTM!

The String() method properly handles nil values and delegates to the standard library for non-nil cases.


29-36: LGTM!

The UnmarshalText implementation correctly parses duration strings and wraps errors for better diagnostics.


38-48: LGTM!

The UnmarshalYAML implementation correctly decodes YAML and delegates to UnmarshalText, with proper interface assertions for compile-time verification.

internal/sms-gateway/handlers/settings/3rdparty.go (4)

8-8: LGTM!

The imports correctly reflect the shift to scope-based permissions and the new users domain type.

Also applies to: 12-12


39-39: LGTM!

The JWTAuth security annotations correctly document JWT authentication support across all endpoints.

Also applies to: 60-60, 95-95


48-48: LGTM!

The handler signatures correctly use the new users.User type, maintaining compatibility with the existing logic.

Also applies to: 72-72, 107-107


127-129: LGTM!

The route registrations properly enforce scope-based permissions with appropriate read/write distinctions.

internal/sms-gateway/handlers/messages/3rdparty.go (1)

12-18: User type migration and scope‑based routing look consistent

The switch to users.User in handler signatures, plus wiring routes through permissions.RequireScope(...) and userauth.WithUser, is coherent with the new users/jwt modules:

  • All handlers only rely on user.ID, which users.User exposes.
  • Per‑route scopes (ScopeList, ScopeSend, ScopeRead, ScopeExport) match the new permissions middleware contract.
  • Swagger additions for JWTAuth and 403 Forbidden align with the new enforcement.

No issues from this file’s changes.

Also applies to: 65-176, 198-214, 231-244, 262-282, 284-290

internal/sms-gateway/openapi/docs.go (1)

21-87: JWT token endpoints and security definitions align with handler changes

The generated OpenAPI now describes:

  • /3rdparty/v1/auth/token POST and /3rdparty/v1/auth/token/{jti} DELETE with both ApiAuth and JWTAuth security and appropriate 401/403/500 responses.
  • JWT‑protected 3rd‑party messages/webhooks endpoints with explicit 403 responses.
  • smsgateway.TokenRequest/TokenResponse schemas and a JWTAuth securityDefinition.

This matches the new JWT + permissions wiring in the handlers; no additional OpenAPI changes needed here.

Also applies to: 88-128, 330-517, 519-645, 1787-1827, 1904-1912, 1935-1946

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

16-21: Basic/code auth now correctly populate users and scopes

The Basic and Code middlewares now:

  • Authenticate via users.Service.Login / auth.Service.AuthorizeUserByCode.
  • Store a users.User value in Locals via SetUser.
  • Expose retrieval through GetUser/HasUser and, for Basic, grant permissions.ScopeAll.

This fixes the previous pointer/value mismatch and cleanly integrates with the permissions/jwt middlewares.

Also applies to: 49-56, 82-90, 95-107

internal/sms-gateway/handlers/3rdparty.go (1)

8-15: 3rd‑party handler wiring cleanly integrates users, JWT, and auth routes

The updated thirdPartyHandler composition looks sound:

  • Injects usersSvc and jwtSvc and feeds them into userauth.NewBasic and jwtauth.NewJWT, then enforces userauth.UserRequired for all 3rd‑party v1 routes registered after the Use call.
  • Registers health endpoints before the auth middleware, keeping /3rdparty/v1/health as an unauthenticated readiness probe, which matches the OpenAPI.
  • Wires the new /auth sub‑router through authHandler, so token issue/revoke endpoints share the same auth stack.

No issues spotted in this wiring.

Also applies to: 20-67, 69-81

internal/sms-gateway/handlers/mobile.go (4)

43-55: LGTM: Clean explicit dependency constructor.

The new constructor design with explicit dependencies is clear and maintainable, replacing the fx-based approach effectively. All dependencies are properly initialized and assigned.

Also applies to: 57-74


141-152: LGTM: Device registration flow is correct.

The device registration logic properly handles both authenticated and new users. The nil-pointer safety for dereferencing user at line 141 is guaranteed by the preceding logic, and the response correctly provides credentials only for newly created users.


197-207: LGTM: Clean integration with users service.

The changes to getUserCode, changePassword, and routing properly integrate with the new users.Service:

  • Signature changes align with middleware expectations
  • Password change logic correctly delegates to the users service
  • Basic auth middleware updated to use the users service

These changes are consistent with the architectural shift to the users subsystem.

Also applies to: 230-236, 241-263


127-129: No action required — user.ID correctly represents the username.

Verification confirms the code is correct: in users.Service.Create, the username parameter is explicitly assigned to the ID field (line 51 of service.go: ID: username). The User domain model stores username as ID by design. The code at lines 127-129 properly extracts this for the Login response field and aligns with the service contract.

Copy link

@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

♻️ Duplicate comments (6)
internal/sms-gateway/users/cache.go (1)

42-45: Avoid credential‑derived cache keys; key on non‑secret data instead

loginCache.makeKey currently hashes username + "\x00" + password and all cache methods take username, password. Even though you hash, this still effectively uses credentials as the cache key and has the issues already called out in the earlier review:

  • Deterministic, unsalted credential hashes are now stored in/cache-accessible (Redis, metrics, debugging) and become another password‑derived artifact to protect.
  • The cache effectively turns into an authentication oracle keyed by credential tuple.
  • It forces the rest of the code (e.g., Service.Login/ChangePassword) to pass plaintext passwords into the cache layer.

Given this is just an optimization, it would be safer to redesign the cache API to never depend on the raw password — e.g. key on username (or user ID) and, if you still want to cache authentication, cache the password hash or a short‑lived opaque login token instead, invalidated explicitly on password change.

I’d strongly recommend changing makeKey and the cache methods to accept a non‑secret identifier and then adapting Service accordingly, rather than keying on username,password.

Also applies to: 47-76

internal/sms-gateway/users/service.go (1)

71-94: Don’t push plaintext passwords into the cache API; make cache invalidation best‑effort

Service.Login and Service.ChangePassword both pass the raw password/currentPassword into the login cache (Get/Set/Delete), and ChangePassword hard‑fails if cache.Delete returns an error. This repeats the earlier concerns:

  • The cache layer now sees plaintext passwords purely to compute a key, which widens the surface where secrets can accidentally leak (logs/metrics/instrumentation).
  • Password change becomes dependent on cache health; a transient cache issue can prevent users from changing their password, even though the source of truth is the DB.

Consider instead:

  • Refactoring the login cache API so it never needs the plaintext password (e.g. keying by username or a non‑secret token and/or caching the password hash or user data), and
  • Treating cache deletion in ChangePassword as best‑effort — log a warning on failure but still proceed to hash and update the password in the repository so users aren’t blocked by cache issues.

This keeps the cache as an optimization rather than part of the critical password‑change path.

Also applies to: 96-104

internal/sms-gateway/handlers/messages/3rdparty.go (1)

287-287: Fix route parameter syntax for :id

Register currently uses:

router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)

Per Fiber routing conventions and to stay consistent with the webhooks handler, this should include the leading slash so the final route matches /3rdparty/v1/messages/{id}:

-	router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
+	router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)

This mirrors the earlier review feedback on the same line.

internal/sms-gateway/handlers/devices/3rdparty.go (1)

88-88: Align Delete route parameter with Fiber conventions (/:id)

Register currently has:

router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))

To be consistent with other handlers (e.g., webhooks) and Fiber’s usual pattern for path parameters under a group, this should use "/:id":

-	router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
+	router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))

Same issue was already noted in the earlier review.

internal/sms-gateway/handlers/thirdparty/auth.go (1)

65-68: TTL conversion may overflow for large values.

The conversion time.Duration(req.TTL) * time.Second can overflow time.Duration (which is int64 nanoseconds) for large TTL values. Additionally, TTL == 0 semantics are unclear—it could mean "immediately expired" or "use default."

Consider adding validation in the handler or relying on the JWT service to enforce bounds:

+const maxTTLSeconds = 365 * 24 * 60 * 60 // 1 year max
+
 func (h *AuthHandler) postToken(user users.User, c *fiber.Ctx) error {
 	req := new(smsgateway.TokenRequest)
 	if err := h.BodyParserValidator(c, req); err != nil {
 		return err
 	}
+
+	if req.TTL > maxTTLSeconds {
+		return fiber.NewError(fiber.StatusBadRequest, "TTL exceeds maximum allowed value")
+	}

 	token, err := h.jwtSvc.GenerateToken(c.Context(), user.ID, req.Scopes, time.Duration(req.TTL)*time.Second)
internal/sms-gateway/handlers/mobile.go (1)

56-56: Handle the error from nanoid.Standard.

The error from nanoid.Standard(21) is silently ignored. If initialization fails, h.idGen will be nil and calling it at line 131 will cause a panic.

-	idGen, _ := nanoid.Standard(21)
+	idGen, err := nanoid.Standard(21)
+	if err != nil {
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}
🧹 Nitpick comments (3)
internal/sms-gateway/users/service.go (1)

35-59: Optional: rely on DB uniqueness rather than Exists+Insert TOCTOU

Create does an Exists check and then Insert. Between those two calls another concurrent insert can still succeed, leading to a duplicate‑key error that will be surfaced as a generic “failed to create user” instead of ErrExists.

If the repository/DB enforces a unique constraint on the username (which it probably should), consider:

  • Either dropping the pre‑check and mapping duplicate‑key errors from Insert to ErrExists, or
  • Keeping the pre‑check but still handling duplicate‑key errors explicitly.

Not critical, but it will make behavior under race conditions more predictable.

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

130-133: Potential nil dereference in WithUser if called without authentication.

GetUser(c) can return nil, and dereferencing it at line 132 would panic. The comment on line 127 acknowledges this ("If the user is not present, it will panic"), but consider adding a defensive check for clearer error messages in case of middleware misconfiguration.

 func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler {
 	return func(c *fiber.Ctx) error {
+		user := GetUser(c)
+		if user == nil {
+			panic("WithUser: no authenticated user in context; ensure auth middleware runs first")
+		}
-		return handler(*GetUser(c), c)
+		return handler(*user, c)
 	}
 }

This makes debugging easier if routes are misconfigured.

internal/sms-gateway/handlers/mobile.go (1)

230-233: Password change error handling could leak information.

The error message "failed to change password" is returned for all errors, but logging the actual error while returning a generic message is good practice. However, returning 401 Unauthorized for any failure (including internal errors) may be misleading. Consider distinguishing between authentication failures and internal errors:

 if err := h.usersSvc.ChangePassword(c.Context(), device.UserID, req.CurrentPassword, req.NewPassword); err != nil {
 	h.Logger.Error("failed to change password", zap.Error(err))
-	return fiber.NewError(fiber.StatusUnauthorized, "failed to change password")
+	if errors.Is(err, users.ErrInvalidCredentials) {
+		return fiber.NewError(fiber.StatusUnauthorized, "invalid current password")
+	}
+	return fiber.NewError(fiber.StatusInternalServerError, "failed to change password")
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38bd382 and e9cfbac.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (42)
  • cmd/sms-gateway/main.go (1 hunks)
  • configs/config.example.yml (1 hunks)
  • go.mod (2 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (5 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (1 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/openapi/docs.go (31 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
💤 Files with no reviewable changes (3)
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/modules/auth/module.go
🚧 Files skipped from review as they are similar to previous changes (17)
  • internal/sms-gateway/handlers/module.go
  • internal/sms-gateway/users/models.go
  • internal/sms-gateway/modules/webhooks/models.go
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • internal/sms-gateway/users/errors.go
  • internal/config/types.go
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go
  • internal/config/module.go
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go
  • internal/sms-gateway/handlers/messages/permissions.go
  • internal/sms-gateway/users/module.go
  • go.mod
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/handlers/webhooks/permissions.go
  • internal/sms-gateway/handlers/logs/3rdparty.go
  • internal/sms-gateway/users/repository.go
  • internal/sms-gateway/handlers/settings/permissions.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T23:45:45.502Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: internal/sms-gateway/online/module.go:14-16
Timestamp: 2025-09-10T23:45:45.502Z
Learning: fx.Decorate in uber-go/fx creates module-scoped decorated dependencies, not global modifications. Using fx.Decorate(func(log *zap.Logger) *zap.Logger { return log.Named("module_name") }) is a correct pattern for creating module-specific loggers that are scoped only to that module's dependency graph.

Applied to files:

  • internal/sms-gateway/handlers/thirdparty/module.go
🧬 Code graph analysis (12)
internal/sms-gateway/users/cache.go (4)
pkg/cache/typed.go (2)
  • Typed (89-91)
  • NewTyped (118-122)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/options.go (2)
  • AndSetTTL (122-126)
  • WithTTL (40-49)
pkg/cache/errors.go (2)
  • ErrKeyNotFound (17-17)
  • ErrKeyExpired (34-34)
internal/sms-gateway/handlers/webhooks/3rdparty.go (5)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/webhooks/permissions.go (3)
  • ScopeList (4-4)
  • ScopeWrite (5-5)
  • ScopeDelete (6-6)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/3rdparty.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (18-22)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • NewBasic (21-59)
  • UserRequired (114-122)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
  • NewJWT (14-42)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/messages/permissions.go (4)
  • ScopeList (10-10)
  • ScopeSend (6-6)
  • ScopeRead (8-8)
  • ScopeExport (12-12)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/config/config.go (1)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/modules/messages/service.go (2)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/modules/messages/repository_filter.go (1)
  • MessagesSelectFilter (16-23)
internal/sms-gateway/handlers/thirdparty/auth.go (3)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInvalidParams (9-9)
  • ErrInitFailed (7-7)
  • ErrInvalidConfig (8-8)
  • ErrDisabled (6-6)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
  • SetScopes (15-17)
  • ScopeAll (10-10)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/mobile.go (3)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • GetUser (102-109)
  • NewBasic (21-59)
internal/sms-gateway/modules/auth/service.go (3)
internal/sms-gateway/cache/factory.go (1)
  • Cache (14-14)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/models.go (1)
  • Device (17-28)
internal/sms-gateway/handlers/thirdparty/module.go (3)
internal/sms-gateway/app.go (1)
  • Module (35-60)
internal/sms-gateway/handlers/module.go (1)
  • Module (16-41)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • NewAuthHandler (24-35)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/settings/permissions.go (2)
  • ScopeRead (4-4)
  • ScopeWrite (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: Analyze (go)
🔇 Additional comments (22)
internal/sms-gateway/models/migration.go (1)

12-13: The review comment is incorrect—users and jwt migrations are properly wired.

Both internal/sms-gateway/users/module.go and internal/sms-gateway/jwt/module.go contain init() functions that call db.RegisterMigration(Migrate), registering their respective migrations for execution during application startup. This follows the same pattern used in models/module.go. No action is needed; the refactoring maintains correct migration orchestration.

Likely an incorrect or invalid review comment.

internal/sms-gateway/handlers/logs/permissions.go (1)

1-5: LGTM!

The permissions constant definition is clean and follows the established pattern for scope definitions across other handlers in this PR.

internal/sms-gateway/handlers/devices/permissions.go (1)

1-6: LGTM!

The permissions constants are well-defined and follow the established pattern for scope definitions across other handlers.

internal/sms-gateway/modules/messages/service.go (2)

124-133: LGTM! Good error wrapping added.

The signature change from models.User to users.User aligns with the new users subsystem introduced in this PR. The added error wrapping on line 129 improves error context for debugging.


135-168: LGTM!

The signature change is consistent with the broader refactoring to use the public users.User type. The logic for cache retrieval and fallback to repository remains sound.

internal/sms-gateway/modules/auth/service.go (3)

35-53: LGTM! Constructor refactored appropriately.

The constructor signature change from fx.In-based parameters to explicit parameters aligns with the broader architectural shift. The initialization is clean and all dependencies are properly wired.


83-90: LGTM!

The signature change to accept users.User is consistent with the new users subsystem. The device registration logic remains unchanged.


92-106: LGTM! Secure token comparison.

The use of subtle.ConstantTimeCompare on line 101 prevents timing attacks when validating the private token. The public/private mode logic is clear and correct.

cmd/sms-gateway/main.go (1)

13-16: LGTM!

The OpenAPI security definition for JWT authentication is correctly formatted and consistent with the existing security definitions. This is a documentation-only change that properly reflects the new JWT authentication capability.

internal/sms-gateway/app.go (1)

10-10: LGTM!

The module wiring correctly integrates the new JWT and users modules into the application. The users module is registered before the auth module (which depends on it), and the JWT module is registered appropriately.

Also applies to: 24-24, 44-44, 59-59

internal/sms-gateway/handlers/thirdparty/module.go (1)

1-16: LGTM!

The module definition follows the established fx pattern correctly. The use of logger.WithNamedLogger("3rdparty") creates a properly scoped logger for the thirdparty handler module, consistent with the project's dependency injection practices.

internal/config/config.go (1)

91-95: The review comment is incorrect. JWT Secret validation already exists and is enforced at startup.

The jwt.Config struct has a Validate() method that checks if the Secret is empty. This validation is called in the jwt.Service.New() constructor during initialization—when the fx dependency injection framework constructs the service, it will invoke config.Validate() and fail application startup if the Secret is missing. The validation is already in place and working as intended.

Likely an incorrect or invalid review comment.

configs/config.example.yml (1)

41-44: JWT config block and task interval comments look consistent

The new jwt block (secret, ttl, issuer) and the updated tasks.*.interval / max_age comments align with the rest of the configuration style and the duration literals already used in this file. No issues from my side.

Also applies to: 50-56

internal/sms-gateway/users/domain.go (1)

5-19: Domain User mapping is straightforward and correct

User encapsulates only the expected public fields and newUser cleanly maps from userModel. Looks good.

internal/sms-gateway/handlers/settings/3rdparty.go (1)

8-13: Settings handlers’ JWT + scope wiring looks correct

Switching handler signatures to users.User, adding JWTAuth to Swagger, and registering routes with permissions.RequireScope(ScopeRead/ScopeWrite) in combination with userauth.WithUser is consistent and enforces the expected per‑user, per‑scope access control.

Also applies to: 39-40, 48-55, 60-61, 72-90, 95-96, 107-124, 126-129

internal/sms-gateway/handlers/messages/3rdparty.go (1)

66-176: JWT + scope‑based access and users.User propagation look good

The handlers correctly accept users.User, pass user.ID into devices/messages services, and are wrapped with the appropriate permissions.RequireScope calls (list/send/read/export). Swagger updates (JWTAuth, 403 responses) match the new auth model. No functional issues spotted here.

Also applies to: 198-214, 231-244, 262-281, 285-290

internal/sms-gateway/handlers/devices/3rdparty.go (1)

33-55: Devices handlers’ user scoping and permissions look correct

Using users.User in get/remove, passing user.ID into devicesSvc, and wrapping the routes with permissions.RequireScope(ScopeList/ScopeDelete) plus userauth.WithUser gives the expected per‑user, per‑scope behavior. Swagger’s JWTAuth additions are consistent with other 3rd‑party handlers.

Also applies to: 57-84, 86-89

internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

8-16: Webhooks handlers’ JWT + scope integration looks solid

Using users.User, adding JWTAuth + 403 responses in Swagger, and wrapping routes with permissions.RequireScope(ScopeList/ScopeWrite/ScopeDelete) and userauth.WithUser is consistent with the rest of the 3rd‑party surface. The /:id route for delete is correctly formed.

Also applies to: 33-52, 55-87, 89-111, 113-116

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

66-86: NewCode does not set any permission scopes, unlike NewBasic.

NewBasic sets permissions.ScopeAll (line 55) after authentication, but NewCode only calls SetUser without setting any scopes. This appears intentional since code-based auth is used for limited operations (device registration), but consider documenting this distinction in the function comment for clarity.

internal/sms-gateway/handlers/3rdparty.go (1)

69-93: Well-structured middleware chain and route registration.

The authentication middleware ordering (Basic → JWT → UserRequired) correctly allows either authentication method while ensuring a user is present. The auth routes are properly registered within the protected group.

internal/sms-gateway/handlers/thirdparty/auth.go (1)

100-120: Error handler provides good user-facing error translation.

The error mapping correctly distinguishes between client errors (400 for invalid params), configuration issues (500/501), and passes through other errors. The user-friendly messages avoid leaking internal details.

internal/sms-gateway/handlers/mobile.go (1)

114-153: Device registration flow correctly integrates with new users service.

The postDevice handler properly handles both authenticated and unauthenticated flows:

  • Authenticated: uses existing user from context
  • Unauthenticated: creates new user via usersSvc.Create

The flow correctly dereferences the user when calling RegisterDevice and returns the generated credentials.

Copy link

@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: 1

♻️ Duplicate comments (3)
internal/sms-gateway/users/service.go (1)

93-95: Cache deletion failure blocks password change unnecessarily.

The Login method treats cache failures as non-fatal (logs and continues), but ChangePassword returns immediately on cache.Delete error. This inconsistency could block password changes due to transient cache issues.

As noted in a previous review, consider making cache deletion best-effort:

-	if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
-		return err
-	}
+	if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
+		s.logger.Warn("failed to delete login cache on password change", zap.String("username", username), zap.Error(err))
+	}
internal/sms-gateway/users/repository.go (1)

59-67: UpdatePassword silently succeeds when user doesn't exist.

GORM's Update returns no error when zero rows are affected. If the user ID doesn't exist, this succeeds without updating anything—inconsistent with GetByID returning ErrNotFound.

As noted in a previous review, check RowsAffected:

 func (r *repository) UpdatePassword(ID string, passwordHash string) error {
-	if err := r.db.Model((*userModel)(nil)).
+	result := r.db.Model((*userModel)(nil)).
 		Where("id = ?", ID).
-		Update("password_hash", passwordHash).Error; err != nil {
+		Update("password_hash", passwordHash)
+	if err := result.Error; err != nil {
 		return fmt.Errorf("can't update password: %w", err)
 	}
+	if result.RowsAffected == 0 {
+		return ErrNotFound
+	}
 
 	return nil
 }
internal/sms-gateway/handlers/thirdparty/auth.go (1)

59-76: Guard against TTL overflow and clarify zero/huge TTL semantics.

Line 65 converts req.TTL (uint64 seconds) to time.Duration (int64 nanoseconds):

time.Duration(req.TTL) * time.Second

This can silently overflow for very large values, producing incorrect or negative expiries. Additionally, TTL == 0 semantics are undefined (does it mean "immediately expired" or "use default TTL"?).

Consider:

  • Adding validation on TTL (e.g., validate:"omitempty,gt=0" plus an upper bound check like req.TTL <= math.MaxInt64/1e9)
  • Defining explicit behavior for TTL == 0 (reject with 400 or interpret as "use default TTL")
🧹 Nitpick comments (1)
internal/sms-gateway/modules/auth/service.go (1)

114-118: Consider adding error logging for async online status update.

The goroutine silently discards any error from SetOnline. While this may be intentional for fire-and-forget semantics, consider logging failures for observability:

 go func(id string) {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
-	s.onlineSvc.SetOnline(ctx, id)
+	if err := s.onlineSvc.SetOnline(ctx, id); err != nil {
+		s.logger.Debug("failed to set device online", zap.String("deviceId", id), zap.Error(err))
+	}
 }(device.ID)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9cfbac and 1202a7f.

📒 Files selected for processing (6)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/openapi/docs.go (31 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • SetUser (88-90)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
internal/sms-gateway/handlers/thirdparty/auth.go (5)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInvalidParams (9-9)
  • ErrInitFailed (7-7)
  • ErrInvalidConfig (8-8)
  • ErrDisabled (6-6)
internal/sms-gateway/users/service.go (4)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/crypto/passwords.go (2)
  • MakeBCryptHash (14-20)
  • CompareBCryptHash (22-27)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (17-17)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/errors.go (2)
  • ErrNotFound (6-6)
  • ErrExists (7-7)
pkg/mysql/errors.go (1)
  • IsDuplicateKeyViolation (9-15)
internal/sms-gateway/modules/auth/service.go (3)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/cache/factory.go (1)
  • Cache (14-14)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (12)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

14-41: LGTM! Well-structured JWT middleware with proper error handling.

The middleware correctly:

  • Falls through when no Bearer token is present (allowing other auth mechanisms)
  • Distinguishes between "user not found" (401) and internal errors (500)
  • Sets both user and scopes in the request context

One consideration: the silent pass-through on missing/non-Bearer tokens (lines 18-20) is intentional for supporting multiple auth mechanisms, but ensure upstream middleware enforces authentication where required.

internal/sms-gateway/users/service.go (2)

35-51: LGTM! User creation with proper password hashing.

Password is hashed before storage, errors are wrapped with context, and the method returns a public User representation.


53-60: LGTM!

Simple and clean delegation to repository.

internal/sms-gateway/openapi/docs.go (1)

1-6: Generated file - no manual changes needed.

This file is auto-generated by swaggo/swag. The JWT security definitions, token endpoints, and 403 responses appear correctly generated. Any adjustments should be made to the source handler annotations, not this file directly.

internal/sms-gateway/users/repository.go (3)

37-37: LGTM! Correct GORM pointer usage.

The Take(user) call now correctly passes *userModel instead of **userModel. The error handling properly maps gorm.ErrRecordNotFound to the domain ErrNotFound.


22-31: LGTM!

Clean existence check using COUNT query.


47-57: LGTM! Proper duplicate key handling.

The Insert method correctly uses mysql.IsDuplicateKeyViolation to detect conflicts and wraps both the domain error and original error for debugging.

internal/sms-gateway/modules/auth/service.go (3)

35-53: LGTM! Clean constructor with explicit dependencies.

The constructor now takes explicit parameters instead of fx.Params, improving testability and clarity. The users service is properly injected.


125-138: LGTM! Correct method name usage.

The code now correctly uses s.usersSvc.GetByID(userID) instead of the previously misnamed GetByUsername. Error handling is appropriate.


83-90: LGTM!

Device registration correctly uses the users.User type and delegates to devicesSvc.

internal/sms-gateway/handlers/thirdparty/auth.go (2)

101-121: LGTM!

The error handler properly maps JWT service errors to appropriate HTTP status codes with user-friendly messages for administrative issues.


37-41: No action required — ScopeTokensManage is correctly accessible.

The constant is defined in the same package (internal/sms-gateway/handlers/thirdparty/permissions.go) and used in the same package (auth.go). In Go, package-level identifiers are automatically accessible within the same package without imports or qualifiers. The code is correct as written.

@capcom6 capcom6 added the ready label Nov 25, 2025
@capcom6 capcom6 marked this pull request as ready for review November 25, 2025 10:53
Copy link

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/cache/redis.go (2)

295-315: AndDelete() without TTL options is silently ignored.

If only AndDelete() is passed without any TTL options, the code falls through to lines 306-314 which performs a simple HGet, ignoring the delete flag entirely. The o.delete check is only used when TTL options trigger the Lua script path.

Consider restructuring to handle delete-only case:

+	if o.delete && !o.hasTTLOptions() {
+		// Delete-only case: use the Lua script with no TTL changes
+		delArg := "1"
+		result, err := r.client.Eval(ctx, getAndUpdateTTLScript, []string{r.key}, key, delArg, 0, 0).Result()
+		if err != nil {
+			return nil, fmt.Errorf("can't get cache item: %w", err)
+		}
+		if value, ok := result.(string); ok {
+			return []byte(value), nil
+		}
+		return nil, ErrKeyNotFound
+	}
+
 	// Handle TTL options atomically using Lua script
 	var ttlTimestamp, ttlDelta int64

Alternatively, you could consolidate by always using the Lua script when any option is set, removing the fallback at lines 305-315.


423-446: Non-atomic HSetNX + HExpireAt creates race window.

Unlike Set() which uses pipelining, here HSetNX and HExpireAt are separate operations. If HSetNX succeeds but HExpireAt fails (line 440-442), the value persists in cache without TTL, potentially becoming stale indefinitely.

Consider using a Lua script for atomicity:

+const setOrFailScript = `
+local success = redis.call('HSETNX', KEYS[1], ARGV[1], ARGV[2])
+if success == 1 and ARGV[3] ~= "0" then
+  redis.call('HExpireAt', KEYS[1], tonumber(ARGV[3]), 'FIELDS', '1', ARGV[1])
+end
+return success
+`

 func (r *redisCache) SetOrFail(ctx context.Context, key string, value []byte, opts ...Option) error {
-	val, err := r.client.HSetNX(ctx, r.key, key, value).Result()
-	if err != nil {
-		return fmt.Errorf("can't set cache item: %w", err)
-	}
-
-	if !val {
-		return ErrKeyExists
-	}
-
 	options := new(options)
 	if r.ttl > 0 {
 		options.validUntil = time.Now().Add(r.ttl)
 	}
 	options.apply(opts...)

-	if !options.validUntil.IsZero() {
-		if err := r.client.HExpireAt(ctx, r.key, options.validUntil, key).Err(); err != nil {
-			return fmt.Errorf("can't set cache item ttl: %w", err)
-		}
+	ttlArg := "0"
+	if !options.validUntil.IsZero() {
+		ttlArg = fmt.Sprintf("%d", options.validUntil.Unix())
+	}
+
+	result, err := r.client.Eval(ctx, setOrFailScript, []string{r.key}, key, value, ttlArg).Result()
+	if err != nil {
+		return fmt.Errorf("can't set cache item: %w", err)
+	}
+	if result.(int64) == 0 {
+		return ErrKeyExists
 	}

 	return nil
 }
♻️ Duplicate comments (9)
pkg/cache/redis.go (1)

74-87: Document Redis version requirement.

The implementation uses Redis 7.4.0+ commands (HEXPIREAT, HTTL, HEXPIRE with FIELDS parameter) for per-field hash TTL support. This minimum version requirement should be documented to prevent runtime errors with older Redis instances. Based on learnings, Redis 7.4.0 introduced these commands.

 // redisCache implements the Cache interface using Redis as the backend.
 //
+// Requires Redis 7.4.0 or later for per-field hash TTL support (HEXPIREAT, HTTL, HEXPIRE with FIELDS).
+//
 // This implementation stores all data in a Redis hash, with each cache item
api/requests.http (1)

206-219: "devices:write" is an invalid scope—must be replaced with "devices:delete".

The verification confirms "devices:write" does not exist in the permission definitions. The devices handler only defines "devices:list" and "devices:delete". Using an undefined scope will cause authorization failures.

     "scopes": [
         "messages:send",
         "messages:read",
         "devices:list",
-        "devices:write",
+        "devices:delete",
         "webhooks:list",
         "webhooks:write",
         "settings:read",
         "settings:write",
         "logs:read"
     ]
internal/config/config.go (1)

22-23: Empty default JWT secret is unsafe; enforce non-empty secret or explicit disablement

Config.JWT and defaultConfig now initialize TTL and Issuer but leave JWT.Secret empty. Unless another layer explicitly:

  • treats Secret == "" as “JWT completely disabled” and refuses to issue/verify tokens, or
  • fails fast on startup when Secret is empty,

there’s a risk of accidentally running with an empty signing secret.

Consider:

  • Adding validation in the config/jwt module to error out (or hard‑disable JWT) when Secret is empty.
  • Optionally adding a comment near JWT.Secret or in defaultConfig clarifying that a non‑empty secret must be provided via env/config for JWT to function.

This mirrors the earlier bot review note and still appears unresolved in this file.

Also applies to: 91-95, 131-134

internal/sms-gateway/users/cache.go (1)

15-22: Reconsider credential-derived cache keys and treat cache misses as non-errors

Two points here:

  1. Hashing username/password into the cache key is still risky

makeKey derives the cache key from username + "\x00" + password hashed with SHA‑256. As noted in the earlier review, this effectively turns the cache into a credential oracle and has several security drawbacks (no salt, deterministic mapping, invalidation issues on password change, timing leakage).

Prefer alternatives such as:

  • Keying by user ID or by a server‑generated opaque login/session identifier, or
  • If you must derive from credentials, using an HMAC with a server‑side secret and per‑user salt, and ensuring explicit invalidation on password changes.

This mirrors the prior bot comment and still appears unresolved.

  1. Cache “not found/expired” should probably be treated as a miss, not an error

In Get, any error from c.storage.Get (including cache.ErrKeyNotFound / cache.ErrKeyExpired) is wrapped and returned as:

return nil, fmt.Errorf("failed to get user from cache: %w", err)

That forces callers to special‑case “cache miss via error”. It’s typically cleaner to do:

user, err := c.storage.Get(...)
if errors.Is(err, cache.ErrKeyNotFound) || errors.Is(err, cache.ErrKeyExpired) {
    return nil, nil // cache miss
}
if err != nil {
    return nil, fmt.Errorf("failed to get user from cache: %w", err)
}

so cache misses behave as a normal “no cached login” path and only real cache/storage failures propagate as errors.

Also applies to: 32-45, 47-81

internal/sms-gateway/jwt/config.go (1)

12-16: Consider validating the Issuer field.

The Issuer field is used in token creation but is not validated. While an empty issuer may be technically valid for JWT, validating it ensures consistent token configuration and prevents potential issues with token verification downstream.

This was raised in a previous review. If an empty issuer is intentionally acceptable in your use case, consider adding a comment to document this decision.

internal/sms-gateway/users/service.go (1)

87-103: Cache deletion should be best-effort in ChangePassword.

The cache.Delete error at lines 93-95 causes the password change to fail entirely. This blocks users from changing passwords due to transient cache issues, even though the Login method treats cache failures as non-fatal.

This was raised in a previous review. Consider making cache deletion best-effort:

 	if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
-		return err
+		s.logger.Warn("failed to delete login cache on password change", zap.String("username", username), zap.Error(err))
 	}
internal/sms-gateway/handlers/devices/3rdparty.go (1)

87-88: Fix inconsistent route parameter syntax.

Line 88 uses :id without a leading slash, while the webhooks handler uses /:id. This was flagged in a previous review.

Apply this diff:

-	router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
+	router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
internal/sms-gateway/users/repository.go (1)

22-67: UpdatePassword silently “succeeds” for non-existent users; align with GetByID semantics

GetByID correctly maps ErrRecordNotFound to ErrNotFound, but UpdatePassword will return nil even when no row matches the given ID. If callers expect a missing user to be reported (e.g., to be consistent with ErrNotFound), capture the GORM result and check RowsAffected:

result := r.db.Model((*userModel)(nil)).
	Where("id = ?", ID).
	Update("password_hash", passwordHash)

if err := result.Error; err != nil {
	return fmt.Errorf("can't update password: %w", err)
}
if result.RowsAffected == 0 {
	return ErrNotFound
}
return nil

This avoids silent no-ops when the user doesn’t exist.

#!/bin/bash
# See how UpdatePassword errors are surfaced through the users service.
rg -n 'UpdatePassword\(' internal/sms-gateway/users --type=go -C3
internal/sms-gateway/handlers/mobile.go (1)

43-74: Handle nanoid.Standard error instead of risking a nil id generator

newMobileHandler currently ignores the error from nanoid.Standard(21):

idGen, _ := nanoid.Standard(21)

If initialization ever fails, idGen will be nil and any call to h.idGen() (e.g., in postDevice) will panic. It’s safer to fail fast during handler construction:

 func newMobileHandler(
@@
-	validator *validator.Validate,
-) *mobileHandler {
-	idGen, _ := nanoid.Standard(21)
+	validator *validator.Validate,
+) *mobileHandler {
+	idGen, err := nanoid.Standard(21)
+	if err != nil {
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}
@@
-		idGen: idGen,
+		idGen: idGen,
 	}
 }

Alternatively, you could return an error from the constructor instead of panicking, depending on how this is wired into DI.

#!/bin/bash
# Check all usages of idGen to confirm it's only initialized via newMobileHandler.
rg -n 'idGen' internal/sms-gateway/handlers/mobile.go -n -C3
🧹 Nitpick comments (8)
internal/sms-gateway/users/domain.go (1)

12-19: Consider adding a nil check for defensive coding.

The newUser constructor will panic if model is nil. While this is an internal function and callers likely ensure non-nil input, a nil check could prevent unexpected panics during future refactoring.

 func newUser(model *userModel) *User {
+	if model == nil {
+		return nil
+	}
 	return &User{
 		ID: model.ID,
 
 		CreatedAt: model.CreatedAt,
 		UpdatedAt: model.UpdatedAt,
 	}
 }
pkg/cache/options.go (1)

122-126: Consider handling non-positive TTL consistently with WithTTL.

WithTTL (line 40-49) explicitly handles non-positive TTL by setting validUntil to zero time (no expiration). However, AndSetTTL passes the value through directly. If a negative duration is passed, time.Now().Add(*o.setTTL) in memory.go will set expiration in the past, causing immediate expiration.

This inconsistency may be intentional (to allow forced expiration), but if not, consider aligning the behavior:

 func AndSetTTL(ttl time.Duration) GetOption {
 	return func(o *getOptions) {
+		if ttl <= 0 {
+			return // No-op for non-positive TTL, or set to nil
+		}
 		o.setTTL = &ttl
 	}
 }
internal/config/types.go (1)

29-36: Nil receiver check missing in UnmarshalText.

If UnmarshalText is called on a nil pointer, the assignment *d = Duration(t) at line 34 will panic. While typically unmarshaling is done on non-nil pointers, adding a nil check would make this more robust.

 func (d *Duration) UnmarshalText(text []byte) error {
+	if d == nil {
+		return fmt.Errorf("can't unmarshal into nil Duration")
+	}
 	t, err := time.ParseDuration(string(text))
 	if err != nil {
 		return fmt.Errorf("can't parse duration: %w", err)
 	}
 	*d = Duration(t)
 	return nil
 }
internal/sms-gateway/handlers/settings/3rdparty.go (1)

36-45: Consider adding 403 Forbidden response to Swagger documentation.

With the new permissions.RequireScope middleware, the endpoint can return a 403 status when the required scope is missing. The Swagger annotations don't document this response.

 //	@Success		200	{object}	smsgateway.DeviceSettings	"Settings"
 //	@Failure		401	{object}	smsgateway.ErrorResponse	"Unauthorized"
+//	@Failure		403	{object}	smsgateway.ErrorResponse	"Forbidden - missing required scope"
 //	@Failure		500	{object}	smsgateway.ErrorResponse	"Internal server error"

Similar additions should be made for the PUT and PATCH endpoints (lines 57-69 and 92-104).

deployments/grafana/dashboards/jwt.json (1)

36-39: Hardcoded datasource UID reduces portability.

The datasource UID edqp0a73uh2bka is hardcoded throughout the dashboard. This will cause the dashboard to fail when imported into other Grafana instances with different datasource configurations.

Consider using a template variable for the datasource or the default Prometheus datasource reference:

"datasource": {
    "type": "prometheus",
    "uid": "${DS_PROMETHEUS}"
}

Then add a corresponding datasource variable to the templating section.

internal/sms-gateway/jwt/repository.go (1)

20-46: JWT token repository implementation looks correct; consider exposing revocation “no-op” if you ever need it

Insert, Revoke, and IsRevoked are all using WithContext, target the correct model, and handle errors consistently. The count-based IsRevoked is fine for a unique jti. If, in future, callers need to know whether Revoke actually touched a row (e.g., to distinguish “already revoked / unknown” from success), you could capture the GORM result and inspect RowsAffected, but that’s not required for the current usage.

pkg/cache/typed.go (1)

89-387: Typed cache correctly handles pointer T; reflection could be cached per instance

Verification confirms all usages pass pointer types (*User, *loginCacheWrapper, *MyData), and the newItem[T] reflection check correctly enforces this constraint. The design is functionally solid. If performance becomes a concern on hot paths, consider caching reflect.Type inside the Typed[T] instance during NewTyped rather than recomputing it on every Get or Drain call.

internal/sms-gateway/handlers/mobile.go (1)

223-236: Fix error handling in changePassword to distinguish auth failures from internal errors.

The current implementation maps all errors from ChangePassword to 401 Unauthorized, but the method can fail in multiple ways:

  • Auth failures: Login fails on invalid current password or user not found
  • Internal errors: cache deletion, password hashing, or DB update failures

These internal errors should return 500 Internal Server Error, not 401.

To fix this, the handler needs to differentiate error types. Consider:

  1. Adding custom error types in the users service (e.g., ErrInvalidPassword, ErrUserNotFound)
  2. Wrapping internal errors distinctly so the handler can inspect them with errors.As

Example:

if err := h.usersSvc.ChangePassword(c.Context(), device.UserID, req.CurrentPassword, req.NewPassword); err != nil {
	if errors.Is(err, users.ErrInvalidPassword) || errors.Is(err, users.ErrUserNotFound) {
		return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials")
	}
	h.Logger.Error("failed to change password", zap.Error(err))
	return fiber.NewError(fiber.StatusInternalServerError, "failed to change password")
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1202a7f and 4e7b849.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (62)
  • api/mobile.http (1 hunks)
  • api/requests.http (5 hunks)
  • cmd/sms-gateway/main.go (1 hunks)
  • configs/config.example.yml (1 hunks)
  • deployments/grafana/dashboards/jwt.json (1 hunks)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • go.mod (2 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (5 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/metrics.go (1 hunks)
  • internal/sms-gateway/jwt/models.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/repository.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (1 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/openapi/docs.go (31 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
  • pkg/cache/cache.go (1 hunks)
  • pkg/cache/errors.go (1 hunks)
  • pkg/cache/memory.go (13 hunks)
  • pkg/cache/options.go (4 hunks)
  • pkg/cache/redis.go (10 hunks)
  • pkg/cache/typed.go (1 hunks)
💤 Files with no reviewable changes (3)
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/modules/auth/module.go
🚧 Files skipped from review as they are similar to previous changes (19)
  • internal/sms-gateway/handlers/thirdparty/module.go
  • api/mobile.http
  • deployments/prometheus/alerts/jwt-alerts.yml
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/users/module.go
  • cmd/sms-gateway/main.go
  • internal/config/module.go
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go
  • internal/sms-gateway/jwt/models.go
  • internal/sms-gateway/modules/webhooks/models.go
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go
  • internal/sms-gateway/users/errors.go
  • internal/sms-gateway/handlers/thirdparty/auth.go
  • internal/sms-gateway/jwt/module.go
  • internal/sms-gateway/handlers/webhooks/permissions.go
  • internal/sms-gateway/handlers/settings/permissions.go
  • go.mod
  • internal/sms-gateway/jwt/disabled.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • pkg/cache/memory.go
  • pkg/cache/cache.go
  • pkg/cache/redis.go
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.

Applied to files:

  • pkg/cache/memory.go
  • pkg/cache/cache.go
  • pkg/cache/redis.go
📚 Learning: 2025-09-10T23:54:16.828Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:118-121
Timestamp: 2025-09-10T23:54:16.828Z
Learning: Redis 7.4.0 introduced HEXPIRE, HEXPIREAT, and related commands that support per-field TTL on hash data structures. go-redis v9.6.0+ includes support for these commands with methods like HExpireAt.

Applied to files:

  • pkg/cache/redis.go
🧬 Code graph analysis (21)
internal/sms-gateway/handlers/logs/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/logs/permissions.go (1)
  • ScopeRead (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/users/models.go (1)
internal/sms-gateway/models/models.go (1)
  • SoftDeletableModel (12-15)
internal/config/config.go (1)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/users/service.go (4)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/crypto/passwords.go (2)
  • MakeBCryptHash (14-20)
  • CompareBCryptHash (22-27)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (17-17)
internal/sms-gateway/modules/messages/service.go (2)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/module.go (2)
internal/sms-gateway/app.go (1)
  • Module (35-60)
internal/sms-gateway/handlers/thirdparty/module.go (1)
  • Module (8-16)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/errors.go (2)
  • ErrNotFound (6-6)
  • ErrExists (7-7)
pkg/mysql/errors.go (1)
  • IsDuplicateKeyViolation (9-15)
internal/sms-gateway/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
  • Device (17-28)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/settings/permissions.go (2)
  • ScopeRead (4-4)
  • ScopeWrite (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
  • ErrInvalidConfig (8-8)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (204-208)
internal/sms-gateway/users/cache.go (4)
pkg/cache/typed.go (2)
  • Typed (89-91)
  • NewTyped (118-122)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/options.go (1)
  • AndSetTTL (122-126)
pkg/cache/errors.go (2)
  • ErrKeyNotFound (17-17)
  • ErrKeyExpired (34-34)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
  • SetScopes (15-17)
  • ScopeAll (10-10)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/modules/auth/service.go (3)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/cache/factory.go (1)
  • Cache (14-14)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/messages/permissions.go (4)
  • ScopeList (10-10)
  • ScopeSend (6-6)
  • ScopeRead (8-8)
  • ScopeExport (12-12)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-134)
internal/sms-gateway/handlers/3rdparty.go (4)
internal/sms-gateway/handlers/logs/3rdparty.go (1)
  • ThirdPartyController (21-23)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (18-22)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • NewBasic (21-59)
  • UserRequired (114-122)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
  • NewJWT (14-42)
internal/sms-gateway/jwt/errors.go (1)
internal/sms-gateway/jwt/service.go (1)
  • New (24-51)
pkg/cache/cache.go (1)
pkg/cache/options.go (2)
  • Option (9-9)
  • GetOption (89-89)
internal/sms-gateway/handlers/mobile.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • GetUser (102-109)
  • NewBasic (21-59)
pkg/cache/redis.go (1)
pkg/cache/options.go (1)
  • AndDelete (204-208)
🔇 Additional comments (53)
pkg/cache/errors.go (2)

6-17: Documentation enhancement verified and accurate.

The API methods referenced in the examples (Get and GetAndDelete) exist in the cache package with compatible signatures, and ErrKeyNotFound is correctly returned when keys don't exist. The documentation and examples are technically sound.


36-49: SetOrFail method verified and documentation is accurate.

Verification confirms that SetOrFail is properly implemented across multiple cache backends (memory, Redis, and typed cache) and is part of the public Cache interface. The ErrKeyExists documentation at lines 36-49 accurately describes when this error is returned by SetOrFail operations.

pkg/cache/redis.go (9)

51-72: LGTM!

The configuration struct is well-documented with clear field descriptions.


132-159: LGTM!

The constructor has good validation, proper handling of client ownership, and automatic prefix normalization.


177-179: LGTM!

Correct implementation - Redis handles TTL-based expiration natively. Based on learnings.


197-203: LGTM!

Simple and correct use of HDEL with proper error wrapping.


225-244: LGTM!

Atomic drain via Lua script is correct. The fallback from UNLINK to DEL handles older Redis gracefully.


353-355: Note: depends on Get bug fix.

This method will not work correctly until the AndDelete() handling bug in Get is fixed. Once fixed, this delegation pattern is appropriate.


379-398: LGTM!

Good use of pipelining to ensure HSet and HExpireAt execute atomically.


461-467: LGTM!

Correct ownership semantics - only closes client if cache created it.


469-470: LGTM!

Good practice to have compile-time interface verification.

internal/sms-gateway/handlers/logs/permissions.go (1)

1-5: LGTM!

The permission constant follows the established <resource>:<action> naming convention used across other handlers in this PR.

internal/sms-gateway/users/models.go (2)

10-19: LGTM!

The model structure is well-designed:

  • varchar(72) appropriately accommodates bcrypt hashes (60 chars) with headroom
  • Soft delete via SoftDeletableModel embedding is consistent with other models
  • The unexported userModel correctly limits visibility to the package

21-26: Migration function looks good.

Error wrapping provides clear context for debugging migration failures.

pkg/cache/cache.go (2)

1-104: Excellent documentation coverage.

The package-level documentation provides comprehensive examples for all cache backends and typed cache usage. This will significantly help developers understand the caching API.


108-283: Verification complete: all error sentinels are properly exported.

All three error sentinels referenced in the Cache interface documentation are correctly defined and exported in pkg/cache/errors.go:

  • ErrKeyNotFound
  • ErrKeyExpired
  • ErrKeyExists

The interface contract is complete and accurate.

internal/sms-gateway/handlers/devices/permissions.go (1)

1-6: LGTM!

The permission constants follow the established naming convention. Note that only list and delete operations are defined, which correctly reflects the available device operations (no create/update operations for devices via this API).

api/requests.http (3)

4-4: LGTM!

Good addition of the JWT token environment variable for testing Bearer authentication.


36-48: LGTM!

The commented-out Basic auth with active Bearer auth provides a clear testing pattern for JWT authentication.


221-225: LGTM!

The DELETE endpoint for token revocation follows RESTful conventions with the token ID in the path.

internal/sms-gateway/handlers/messages/permissions.go (1)

1-13: LGTM!

Clean permission scope constants with consistent naming pattern (messages:<action>) and appropriate documentation. The constants align well with the broader permission/JWT framework introduced in this PR.

pkg/cache/memory.go (3)

207-241: LGTM! Well-structured Get method with proper locking strategy.

The method correctly:

  • Uses RLock for read-only access when no options are provided
  • Upgrades to Lock when options require modifications
  • Checks expiration before applying TTL modifications (line 226)
  • Handles delete-on-read atomically

The code is well-documented and follows Go concurrency best practices.


161-175: Drain implementation correctly handles atomic snapshot.

The cleanup callback pattern ensures expired items are removed before the snapshot is taken, and the atomic swap of m.items guarantees consistency. Based on learnings, this aligns with the established pattern where cleanup removes expired items first, then executes the callback.


409-421: Clean cleanup implementation with callback pattern.

The callback is executed while holding the write lock, ensuring atomicity of cleanup operations combined with any additional actions (like the snapshot in Drain). This matches the documented behavior from learnings.

pkg/cache/options.go (2)

1-208: LGTM overall! Well-designed options pattern.

The functional options pattern is cleanly implemented with:

  • Clear separation between Option (for Set) and GetOption (for Get)
  • Comprehensive documentation with usage examples
  • Consistent naming conventions (With* for Set, And* for Get)

98-105: The review comment is incorrect. The isEmpty() method is actively used in the codebase—it's called at line 281 in pkg/cache/redis.go. The method should be kept as it serves a legitimate purpose.

Likely an incorrect or invalid review comment.

internal/sms-gateway/handlers/module.go (1)

40-40: LGTM!

The thirdparty.Module() is correctly wired into the handlers module composition. This aligns with the JWT authentication infrastructure introduced in this PR, where the thirdparty module provides NewAuthHandler for 3rd-party authentication flows.

internal/sms-gateway/handlers/logs/3rdparty.go (1)

3-8: JWT/logs handler wiring looks good; verify middleware stack for scopes & user loading

The switch to users.User, added JWTAuth security annotation, and routing via permissions.RequireScope(ScopeRead) + userauth.WithUser(h.get) look consistent with the new auth model. Just make sure the router/group that mounts this controller also attaches the JWT middleware (that populates scopes and user) before these handlers so RequireScope and WithUser always see a populated context.

Also applies to: 25-29, 40-46

internal/sms-gateway/app.go (1)

10-11: Fx module wiring for users/jwt modules is straightforward

Registering users.Module() and jwt.Module() in the main Module is clean and keeps composition centralized. No issues from this file alone; dependency injection should work as long as each submodule correctly provides its own config, migrations, and services.

Also applies to: 24-25, 35-60

internal/sms-gateway/models/migration.go (1)

12-14: Device-only AutoMigrate fits modular schema split; ensure other domain migrations are registered

Limiting Migrate to &Device{} aligns with moving users/JWT schema into their own modules. Just double‑check that:

  • internal/sms-gateway/users/models.go and internal/sms-gateway/jwt/models.go are wired into the migration registration (via the DB module) so user/token tables are still created during bootstrap.
internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1)

1-19: Tokens table migration looks consistent with JWT token storage requirements

The tokens schema (ID, user_id, expires_at, timestamps, revoked_at, indexes, FK to users(id) with ON DELETE CASCADE) is well‑structured and matches a typical JWT token persistence model. No issues from the migration itself.

configs/config.example.yml (1)

41-44: Ensure jwt.secret == empty truly disables JWT and doesn’t allow tokens with an empty secret

The example config documents:

jwt:
  secret: # jwt secret (leave empty to disable JWT functionality)

This is good UX, but it must match the implementation: the JWT module should not issue or validate tokens at all when secret is empty (and ideally log clearly that JWT is disabled), rather than treating "" as a valid signing key.

Please verify that:

  • Startup either fails or cleanly disables JWT when jwt.secret is empty.
  • No code path signs/verifies tokens with an empty secret.

The updated task interval comments below are clear and consistent.

Also applies to: 50-56

internal/sms-gateway/modules/messages/service.go (1)

15-16: Switch to users.User in messages service is consistent and preserves behaviour

Updating SelectStates and GetState to accept users.User while continuing to use user.ID for filters and cache keys keeps semantics intact and aligns the messages domain with the new users model. The added fmt.Errorf("can't select messages: %w", err) in SelectStates also improves error context. No issues from this file.

Also applies to: 124-168

internal/sms-gateway/jwt/errors.go (1)

1-12: LGTM!

Well-defined sentinel errors following Go conventions. Good organization with all JWT-related errors centralized in one file, enabling consistent error wrapping with fmt.Errorf("%w: ...") throughout the package.

internal/config/types.go (1)

47-48: Good use of compile-time interface assertions.

These assertions ensure Duration correctly implements yaml.Unmarshaler and encoding.TextUnmarshaler at compile time rather than failing at runtime.

internal/sms-gateway/jwt/jwt.go (1)

10-27: LGTM!

Clean interface and type definitions. The Service interface provides a well-defined contract for JWT operations. The Claims struct properly embeds jwt.RegisteredClaims while adding custom fields with appropriate JSON tags. The TokenInfo struct exposes the necessary fields for token consumers.

internal/sms-gateway/handlers/settings/3rdparty.go (1)

126-130: LGTM!

The route registration correctly applies scope-based access control with appropriate permission levels: read access for GET and write access for PUT/PATCH. The userauth.WithUser wrapper properly extracts the authenticated user for handler use.

internal/sms-gateway/jwt/config.go (1)

18-32: LGTM!

The validation logic is thorough and correctly handles edge cases:

  • Empty secret check prevents misconfiguration
  • Minimum length requirement (32 bytes) enforces adequate key strength for HMAC
  • Non-positive TTL check prevents both zero and negative durations

Error messages are clear and consistently wrapped with ErrInvalidConfig.

internal/sms-gateway/users/service.go (2)

62-85: LGTM on login flow structure.

The login flow correctly:

  • Checks cache first for performance
  • Falls back to repository lookup on cache miss
  • Treats non-ErrKeyNotFound cache errors as non-fatal (logs warning, continues)
  • Caches successful authentications with error logged but not propagated

35-60: LGTM!

The Create method correctly hashes the password before storage. The GetByID method is a clean pass-through to the repository with proper domain type conversion via newUser.

internal/sms-gateway/handlers/messages/3rdparty.go (1)

66-176: LGTM!

The migration to users.User type and integration of permission scopes are correctly implemented. The handler logic remains intact and properly uses user.ID for service calls.

deployments/grafana/dashboards/jwt.json (1)

146-156: Metric queries are now correct.

The validation success rate query correctly uses sms_auth_jwt_tokens_validated_total which matches the metric defined in internal/sms-gateway/jwt/metrics.go. The previous issues with incorrect metric names have been addressed.

internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

113-117: LGTM!

The permission scope integration and migration to users.User are correctly implemented. Route parameter syntax /:id is correct and consistent with Fiber conventions.

internal/sms-gateway/handlers/devices/3rdparty.go (1)

46-55: LGTM!

The migration to users.User type is correctly implemented. The handler properly uses user.ID for device service calls.

internal/sms-gateway/handlers/3rdparty.go (2)

74-78: LGTM!

The authentication middleware chain is correctly ordered: Basic auth and JWT auth both attempt to authenticate and set the user context, then UserRequired() ensures at least one method succeeded. This allows clients to use either authentication method.


80-81: LGTM!

The auth handler is correctly registered under /auth after the authentication middleware, allowing token generation/revocation endpoints to require authentication.

internal/sms-gateway/openapi/docs.go (2)

1-6: Generated file correctly reflects handler annotations.

This file is auto-generated by swaggo/swag. The JWT authentication security definitions, token management endpoints, and 403 responses are correctly generated from the handler annotations. Previous issues regarding missing jti path parameter and security annotations have been addressed.


1916-1921: JWTAuth security definition is correctly configured.

The JWT authentication is defined as an apiKey type in the Authorization header, which aligns with the Bearer token format expected by the jwtauth.NewJWT middleware.

internal/sms-gateway/modules/auth/service.go (1)

26-137: Auth service integration with usersSvc and code-based auth looks coherent

Service now cleanly depends on *users.Service, AuthorizeUserByCode resolves userID from codesCache and then GetByID, and the public RegisterDevice signature matches users.User. This removes the earlier GetByUsername/ID mismatch and keeps the code-based auth flow straightforward. If you ever need more context on cache failures, you could wrap the GetAndDelete error similar to the user lookup, but functionally this is fine as-is.

internal/sms-gateway/handlers/mobile.go (1)

121-153: Device registration flow with existing/new users looks consistent

postDevice correctly prefers an authenticated user from userauth.GetUser(c) and falls back to generating a new username/password pair when unauthenticated, creating the user via usersSvc.Create and then registering the device via authSvc.RegisterDevice(*user, ...). Returning Login: username and Password: password only when a new user is created matches the documented “returns user credentials only for new users” behavior (password will naturally be empty for existing users). This flow looks good.

internal/sms-gateway/jwt/metrics.go (1)

8-117: JWT metrics align correctly with alert rules and are production-ready

Verification confirms all Prometheus alert expressions reference the correct metric names (sms_auth_jwt_*), use the proper status label, and apply histogram bucket syntax correctly. Alert thresholds and queries are properly wired to the metrics defined in the code.

internal/sms-gateway/jwt/service.go (1)

24-177: JWT service behavior is correct; defensive scopes copy is optional

The code is functionally sound and correctly generates, validates, and revokes tokens. The verification found no call sites within the internal/sms-gateway directory, but this does not prove all callers treat the scopes slice as immutable—they may exist in tests, other packages, or external code.

The defensive copy suggestion remains optional but sensible as a best practice to prevent accidental mutation by any caller:

Scopes: append([]string(nil), scopes...),

No changes are required; the code is correct as-is. If you adopt the defensive copy, consider it a minor robustness improvement rather than a fix for a current bug.

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

49-133: WithUser usages are correctly guarded by UserRequired middleware throughout codebase

Verification confirms the original analysis: all WithUser call sites are protected by either global or per-route auth middleware chains. Thirdparty routes apply router.Use(userauth.NewBasic, jwtauth.NewJWT, userauth.UserRequired) before handler registration, while mobile routes apply middleware per-route. This ensures a user is always present before WithUser dereferencies GetUser(c), preventing nil panics. The documented panic-on-misuse behavior is correctly mitigated by middleware ordering.

Comment on lines +285 to +331
router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.list))
router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUser(h.post))
router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)

router.Post("inbox/export", userauth.WithUser(h.postInboxExport))
router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix inconsistent route parameter syntax.

Line 287 uses :id without a leading slash, while the webhooks handler (line 116) uses /:id. This inconsistency could cause routing issues in Fiber.

Apply this diff:

-	router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
+	router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.list))
router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUser(h.post))
router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
router.Post("inbox/export", userauth.WithUser(h.postInboxExport))
router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport))
router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.list))
router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUser(h.post))
router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport))
🤖 Prompt for AI Agents
internal/sms-gateway/handlers/messages/3rdparty.go around lines 285-289: the GET
route uses an inconsistent parameter syntax `:id` (no leading slash) which can
break Fiber routing; change the route path to include the leading slash so it
reads `"/:id"` (keep the same permissions, userauth wrapper and route name),
then run tests or start the server to confirm routing behaves like the other
handlers.

Copy link

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
pkg/cache/memory.go (1)

225-236: AndUpdateTTL may produce incorrect results when item has no expiration.

When o.updateTTL != nil and the item's validUntil is the zero time (no expiration), adding a duration to time.Time{} produces an unexpected timestamp (year 0001 + duration).

case o.updateTTL != nil:
    item.validUntil = item.validUntil.Add(*o.updateTTL) // If validUntil is zero, this is problematic

Consider handling the zero-time case:

 case o.updateTTL != nil:
+  if item.validUntil.IsZero() {
+    item.validUntil = time.Now().Add(*o.updateTTL)
+  } else {
     item.validUntil = item.validUntil.Add(*o.updateTTL)
+  }
pkg/cache/redis.go (1)

420-443: Non-atomic TTL setting in SetOrFail may leave orphaned entries.

If HSetNX succeeds but HExpireAt fails (e.g., network error), the cache entry exists without a TTL and may persist indefinitely. Unlike Set which uses pipelining, SetOrFail cannot easily pipeline since it needs the HSetNX result first.

Consider using a Lua script for atomicity, or document the risk:

local inserted = redis.call('HSETNX', KEYS[1], ARGV[1], ARGV[2])
if inserted == 1 and tonumber(ARGV[3]) > 0 then
    redis.call('HExpireAt', KEYS[1], tonumber(ARGV[3]), 'FIELDS', '1', ARGV[1])
end
return inserted

Alternatively, add a rollback on TTL failure:

 if !options.validUntil.IsZero() {
     if expErr := r.client.HExpireAt(ctx, r.key, options.validUntil, key).Err(); expErr != nil {
+        // Rollback: delete the field since we couldn't set TTL
+        _ = r.client.HDel(ctx, r.key, key)
         return fmt.Errorf("failed to set cache item ttl: %w", expErr)
     }
 }
internal/sms-gateway/handlers/base/handler_test.go (1)

272-335: ValidateStruct tests no longer assert failures for non‑OK cases

In TestHandler_ValidateStruct, you changed invalid cases to expectedStatus: fiber.StatusInternalServerError, but the assertions still only treat fiber.StatusBadRequest as the “error expected” sentinel:

if test.expectedStatus == fiber.StatusOK && err != nil { ... }
if test.expectedStatus == fiber.StatusBadRequest && err == nil { ... }

With expectedStatus now set to StatusInternalServerError, the second condition never triggers, so invalid cases will pass even if ValidateStruct stops returning an error.

To restore the intended check with minimal change, treat any non‑OK status as “error expected”:

-            if test.expectedStatus == fiber.StatusBadRequest && err == nil {
-                t.Errorf("Expected error, got nil")
-            }
+            if test.expectedStatus != fiber.StatusOK && err == nil {
+                t.Errorf("Expected error, got nil")
+            }

This keeps the table the same while ensuring all invalid scenarios still assert that an error is returned.

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

124-139: Update WithUser comment to match current behavior (no longer panics)

The comment says “If the user is not present, it will panic.” but the implementation now returns 401 Unauthorized instead:

user := GetUser(c)
if user == nil {
    return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
}

Please update the comment so it doesn’t mislead readers.

♻️ Duplicate comments (10)
pkg/cache/redis.go (2)

78-91: Document Redis 7.4.0+ version requirement.

The implementation uses Redis 7.4.0+ commands (HEXPIREAT, HTTL, HEXPIRE with FIELDS parameter) for per-field hash TTL. This requirement should be documented to prevent runtime failures with older Redis versions.

 // RedisCache implements the Cache interface using Redis as the backend.
 //
+// IMPORTANT: Requires Redis 7.4.0 or later for per-field hash TTL support
+// (HEXPIREAT, HTTL, HEXPIRE commands with FIELDS parameter).
+//
 // This implementation stores all data in a Redis hash, with each cache item

43-48: HTTL returns an array, causing Lua script failure.

The HTTL command returns an array of TTL values (one per field). The current code treats ttl as a scalar:

local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
if ttl < 0 then  -- comparing array to number
    ttl = 0
end
local newTtl = ttl + ttlDelta  -- adding array to number

This will cause a Lua runtime error when updateTTL option is used.

Apply this fix to extract the first element from the array:

-  local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
-  if ttl < 0 then
+  local ttlArr = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
+  local ttl = ttlArr[1] or -1
+  if ttl == nil or ttl < 0 then
     ttl = 0
   end
   local newTtl = ttl + ttlDelta

Note: This was flagged in a past review but appears unresolved in the current code.

internal/sms-gateway/handlers/devices/3rdparty.go (1)

92-95: Consider aligning Delete route path with other handlers (/:id).

This route still uses ":id" without a leading slash. Previous review already suggested switching to "/:id" for consistency with other handlers (e.g., webhooks) and typical Fiber routing patterns:

-	router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
+	router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))

Please double-check the group prefix and confirm the desired final path.

internal/sms-gateway/users/repository.go (1)

59-67: Check RowsAffected to detect non-existent users.

GORM's Update does not return an error when zero rows are affected. If the user ID doesn't exist, this method silently succeeds without updating anything, which is inconsistent with GetByID returning ErrNotFound.

Apply this diff:

 func (r *repository) UpdatePassword(id string, passwordHash string) error {
-	if err := r.db.Model((*userModel)(nil)).
+	result := r.db.Model((*userModel)(nil)).
 		Where("id = ?", id).
-		Update("password_hash", passwordHash).Error; err != nil {
+		Update("password_hash", passwordHash)
+	if err := result.Error; err != nil {
 		return fmt.Errorf("can't update password: %w", err)
 	}
+	if result.RowsAffected == 0 {
+		return ErrNotFound
+	}
 
 	return nil
 }
internal/sms-gateway/jwt/config.go (1)

18-32: Consider validating the Issuer field.

The past review comment about missing Issuer validation remains unaddressed. While the Issuer claim is optional per RFC 7519, it is used in token creation (service.go:86) and validation (service.go:132). An empty issuer could lead to tokens with an empty "iss" claim, which may not align with security best practices.

internal/sms-gateway/handlers/thirdparty/auth.go (2)

60-82: TTL overflow validation issue remains unresolved.

The past review comment about potential TTL overflow when converting uint64 to time.Duration (int64) was not addressed. Line 70 includes a nolint:gosec comment claiming validation happens in the service, but the service only validates ttl >= 0 (service.go:73-75) and does not check for overflow.


97-105: jti parameter validation is still missing.

The past review comment about validating the jti parameter was marked as addressed, but the current code (lines 98-100) still does not validate that jti is non-empty before calling jwtSvc.RevokeToken. While Fiber's routing ensures the parameter exists, an empty string could still be passed.

Apply this diff:

 func (h *AuthHandler) deleteToken(user users.User, c *fiber.Ctx) error {
 	jti := c.Params("jti")
+	if jti == "" {
+		return fiber.NewError(fiber.StatusBadRequest, "jti parameter is required")
+	}
 
 	if err := h.jwtSvc.RevokeToken(c.Context(), user.ID, jti); err != nil {
internal/sms-gateway/handlers/messages/3rdparty.go (1)

329-329: Fix inconsistent route parameter syntax.

Line 329 uses :id without a leading slash, while other handlers (e.g., webhooks at line 126) use /:id. This inconsistency could cause routing issues in Fiber.

Apply this diff:

-	router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
+	router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

61-86: Code-based auth now stores user consistently with Basic path

NewCode now calls SetUser(c, *user), matching the storage shape expected by GetUser. This resolves the earlier mismatch between storing *users.User and asserting users.User.

internal/sms-gateway/handlers/mobile.go (1)

43-75: Handle nanoid.Standard error to avoid nil idGen panic

newMobileHandler still ignores the error from nanoid.Standard:

const idLength = 21
idGen, _ := nanoid.Standard(idLength)

If initialization fails, idGen will be nil and h.idGen() will panic when registering a new device.

Handle the error explicitly, e.g.:

- const idLength = 21
- idGen, _ := nanoid.Standard(idLength)
+ const idLength = 21
+ idGen, err := nanoid.Standard(idLength)
+ if err != nil {
+     panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+ }

(or, alternatively, propagate the error out of the constructor instead of panicking, if you’re willing to adjust call sites).

🧹 Nitpick comments (6)
pkg/cache/typed.go (1)

338-358: Consider documenting partial failure behavior in Drain.

If storage.Drain() succeeds but unmarshalling fails for any item, the cache is already cleared but the caller receives a nil map. This is acceptable for corrupted data scenarios, but the documentation could clarify this behavior to set caller expectations.

Consider adding to the method documentation:

 // Returns:
 //   - map[string]T: A map containing all non-expired key-typed value pairs
-//   - error: nil on success, or an error if the cache operation fails
+//   - error: nil on success, or an error if the cache operation or unmarshalling fails.
+//            Note: if unmarshalling fails, the cache will already be cleared.
api/mobile.http (1)

70-73: Use distinct example values for current vs new password.

Having both currentPassword and newPassword set to the same literal makes the example harder to reason about and could hide issues when manually testing. Consider using clearly different sample values (or placeholders) to illustrate a real password change.

api/requests.http (1)

220-224: Consider removing unnecessary Content-Type header for DELETE request.

The DELETE request at line 221 has no request body, so the Content-Type: application/json header on line 223 is unnecessary. This is a minor inconsistency.

 ###
 DELETE {{baseUrl}}/3rdparty/v1/auth/token/w8pxz0a4Fwa4xgzyCvSeC HTTP/1.1
 Authorization: Basic {{credentials}}
-Content-Type: application/json
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

24-27: Consider logging token parse failures for debugging.

While returning 401 is correct, silently discarding parse errors can make debugging difficult. Consider adding debug-level logging for operational visibility.

This would require passing a logger to the middleware:

claims, err := jwtSvc.ParseToken(c.Context(), token)
if err != nil {
    // log.Debug("JWT parse failed", zap.Error(err))
    return fiber.ErrUnauthorized
}
internal/sms-gateway/jwt/repository.go (1)

20-47: JWT token repository behavior looks sound; consider whether silent “no-op” revoke is desired

Insert, Revoke, and IsRevoked are correctly using WithContext and GORM, and the revocation check via COUNT(revoked_at IS NOT NULL) is fine.

One thing to double‑check is whether you intentionally treat “token not found / already revoked” as a successful Revoke (no RowsAffected check). That’s nice and idempotent, but if the API contract expects 404/409‑style behavior, you may want to inspect db.RowsAffected and surface that to callers instead.

internal/sms-gateway/handlers/mobile.go (1)

228-241: Consider distinguishing auth failures from internal errors in changePassword

changePassword now delegates to h.usersSvc.ChangePassword, but any error results in:

h.Logger.Error("failed to change password", zap.Error(err))
return fiber.NewError(fiber.StatusUnauthorized, "failed to change password")

This maps all failures (including DB or service outages) to 401 Unauthorized, which can mask server-side issues and doesn’t fully match the swagger docs that mention a 500 path.

If usersSvc.ChangePassword exposes a well-known “invalid credentials” error, you might:

  • return 401 for that specific error, and
  • return 500 for all other unexpected errors.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e7b849 and fce4a07.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (65)
  • .golangci.yml (2 hunks)
  • api/mobile.http (1 hunks)
  • api/requests.http (5 hunks)
  • cmd/sms-gateway/main.go (1 hunks)
  • configs/config.example.yml (1 hunks)
  • deployments/grafana/dashboards/jwt.json (1 hunks)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • go.mod (2 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/base/handler.go (2 hunks)
  • internal/sms-gateway/handlers/base/handler_test.go (8 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (5 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/metrics.go (1 hunks)
  • internal/sms-gateway/jwt/models.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/repository.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (2 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/openapi/docs.go (25 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
  • pkg/cache/cache.go (1 hunks)
  • pkg/cache/errors.go (1 hunks)
  • pkg/cache/memory.go (13 hunks)
  • pkg/cache/options.go (4 hunks)
  • pkg/cache/redis.go (10 hunks)
  • pkg/cache/typed.go (1 hunks)
💤 Files with no reviewable changes (3)
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/module.go
  • internal/sms-gateway/modules/auth/repository.go
✅ Files skipped from review due to trivial changes (1)
  • internal/sms-gateway/handlers/settings/permissions.go
🚧 Files skipped from review as they are similar to previous changes (21)
  • cmd/sms-gateway/main.go
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go
  • go.mod
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • internal/sms-gateway/modules/messages/service.go
  • pkg/cache/errors.go
  • internal/sms-gateway/users/domain.go
  • internal/sms-gateway/app.go
  • internal/sms-gateway/jwt/models.go
  • internal/sms-gateway/handlers/thirdparty/module.go
  • deployments/grafana/dashboards/jwt.json
  • internal/sms-gateway/jwt/jwt.go
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
  • deployments/prometheus/alerts/jwt-alerts.yml
  • internal/sms-gateway/handlers/module.go
  • internal/sms-gateway/users/cache.go
  • internal/sms-gateway/users/service.go
  • internal/sms-gateway/users/errors.go
  • internal/config/config.go
  • configs/config.example.yml
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • pkg/cache/cache.go
  • pkg/cache/redis.go
  • pkg/cache/memory.go
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.

Applied to files:

  • pkg/cache/cache.go
  • pkg/cache/redis.go
  • pkg/cache/memory.go
📚 Learning: 2025-09-10T23:54:16.828Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:118-121
Timestamp: 2025-09-10T23:54:16.828Z
Learning: Redis 7.4.0 introduced HEXPIRE, HEXPIREAT, and related commands that support per-field TTL on hash data structures. go-redis v9.6.0+ includes support for these commands with methods like HExpireAt.

Applied to files:

  • pkg/cache/redis.go
🧬 Code graph analysis (21)
internal/sms-gateway/users/models.go (5)
internal/sms-gateway/models/models.go (1)
  • SoftDeletableModel (12-16)
internal/sms-gateway/jwt/models.go (1)
  • Migrate (33-38)
internal/sms-gateway/models/migration.go (1)
  • Migrate (13-18)
internal/sms-gateway/modules/settings/models.go (1)
  • Migrate (28-33)
internal/sms-gateway/modules/webhooks/models.go (1)
  • Migrate (39-44)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • SetUser (88-90)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
internal/sms-gateway/handlers/logs/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/logs/permissions.go (1)
  • ScopeRead (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-139)
internal/sms-gateway/handlers/webhooks/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/webhooks/permissions.go (3)
  • ScopeList (4-4)
  • ScopeWrite (5-5)
  • ScopeDelete (6-6)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-139)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/settings/permissions.go (2)
  • ScopeRead (4-4)
  • ScopeWrite (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-139)
internal/sms-gateway/handlers/thirdparty/auth.go (5)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/handlers/thirdparty/permissions.go (1)
  • ScopeTokensManage (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-139)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/jwt/errors.go (3)
  • ErrInvalidParams (9-9)
  • ErrInitFailed (7-7)
  • ErrDisabled (6-6)
internal/sms-gateway/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
  • ErrInvalidConfig (8-8)
internal/sms-gateway/modules/auth/service.go (5)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/modules/devices/service.go (1)
  • Service (15-24)
internal/sms-gateway/online/service.go (2)
  • Service (14-17)
  • New (28-37)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/models.go (2)
  • Device (18-29)
  • NewDevice (31-37)
internal/sms-gateway/jwt/errors.go (1)
internal/sms-gateway/jwt/service.go (1)
  • New (24-51)
internal/sms-gateway/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
  • Device (18-29)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/messages/permissions.go (4)
  • ScopeList (10-10)
  • ScopeSend (6-6)
  • ScopeRead (8-8)
  • ScopeExport (12-12)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-139)
internal/sms-gateway/handlers/3rdparty.go (6)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/handlers/logs/3rdparty.go (1)
  • ThirdPartyController (21-23)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (19-23)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • NewBasic (21-59)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
  • NewJWT (14-42)
internal/sms-gateway/users/module.go (3)
internal/sms-gateway/cache/factory.go (1)
  • Factory (16-18)
internal/sms-gateway/users/service.go (1)
  • NewService (21-33)
internal/sms-gateway/users/models.go (1)
  • Migrate (29-34)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (130-139)
pkg/cache/typed.go (3)
pkg/cache/cache.go (1)
  • Cache (112-283)
pkg/cache/options.go (3)
  • Option (9-9)
  • GetOption (89-89)
  • AndDelete (204-208)
pkg/cache/errors.go (1)
  • ErrFailedToCreateZeroValue (53-53)
internal/sms-gateway/modules/webhooks/models.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/errors.go (2)
  • ErrNotFound (6-6)
  • ErrExists (7-7)
pkg/mysql/errors.go (1)
  • IsDuplicateKeyViolation (13-19)
internal/sms-gateway/jwt/service.go (5)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/repository.go (1)
  • Repository (10-12)
internal/sms-gateway/jwt/metrics.go (3)
  • Metrics (24-31)
  • StatusError (20-20)
  • StatusSuccess (19-19)
internal/sms-gateway/jwt/jwt.go (3)
  • Service (10-14)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInitFailed (7-7)
  • ErrInvalidParams (9-9)
  • ErrInvalidToken (10-10)
  • ErrTokenRevoked (11-11)
pkg/cache/redis.go (1)
pkg/cache/options.go (1)
  • AndDelete (204-208)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
  • SetScopes (15-17)
  • ScopeAll (10-10)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (204-208)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: Lint
  • GitHub Check: Analyze (go)
🔇 Additional comments (44)
.golangci.yml (1)

255-260: Configuration updates align well with JWT integration.

The addition of the JWT Claims exclusion pattern (line 260) is appropriate—external JWT claim types from github.com/golang-jwt/jwt/v5 should not be subject to exhaustruct's strict field initialization requirement, as the library design intentionally permits optional fields. The pattern correctly targets Claims types while being properly scoped to the v5 package.

pkg/cache/cache.go (1)

1-283: Comprehensive interface documentation looks good.

The expanded documentation for the Cache interface provides clear usage examples for all methods, consistent parameter/return documentation, and helpful behavioral notes (e.g., Redis handling expiration automatically in Cleanup). The examples correctly demonstrate error handling patterns with ErrKeyNotFound and ErrKeyExpired.

pkg/cache/typed.go (2)

184-195: Error wrapping in SetOrFail may hide ErrKeyExists from callers.

When the underlying cache returns ErrKeyExists, wrapping it with fmt.Errorf("failed to set value in cache: %w", setErr) preserves the error chain for errors.Is(). However, the documentation at line 181-182 suggests callers should check for cache.ErrKeyExists, so the wrapping is acceptable. The %w verb ensures errors.Is(err, cache.ErrKeyExists) still works.


392-407: Reflection-based instantiation correctly addresses pointer type requirement.

The newItem[T] helper properly:

  1. Verifies T is a pointer type via t.Kind() != reflect.Ptr
  2. Creates a new instance using reflect.New(t.Elem()) which allocates the underlying struct
  3. Returns a meaningful error wrapping ErrFailedToCreateZeroValue

This correctly fixes the previous issue where var value T for pointer types resulted in nil receivers during Unmarshal.

pkg/cache/options.go (2)

38-49: Negative TTL semantics may be surprising.

When ttl < 0, the code sets validUntil = time.Now(), which means the item expires immediately upon insertion. This might be counterintuitive - a negative TTL could be expected to behave like zero (no expiration) or trigger an error.

Is this intentional behavior? If so, consider documenting it explicitly:

 // WithTTL sets the TTL (time to live) for an item.
 //
 // The item will expire after the given duration from the time of insertion.
-// A TTL of zero or negative means the item will not expire.
+// A TTL of zero means the item will not expire.
+// A negative TTL means the item expires immediately (useful for invalidation).

Or if immediate expiration isn't the intent:

 case ttl < 0:
-  o.validUntil = time.Now()
+  o.validUntil = time.Time{}

76-105: Clean option pattern implementation.

The use of pointer fields (validUntil *time.Time, setTTL *time.Duration, etc.) correctly distinguishes between "not set" and "set to zero value". The isEmpty() helper provides an efficient check for the common case of no options.

pkg/cache/memory.go (2)

161-175: Drain correctly leverages cleanup pattern.

Based on learnings, the cleanup method removes expired items before executing the callback. This ensures cpy contains only non-expired items without additional filtering. The atomic swap of maps under the write lock is clean.


286-329: Set and SetOrFail implementations are correct.

  • Set properly applies default TTL before custom options
  • SetOrFail correctly checks expiration status before returning ErrKeyExists, allowing expired keys to be overwritten as documented
pkg/cache/redis.go (1)

282-330: Get implementation correctly optimizes for common case.

The isEmpty() check enables a fast path using direct HGet when no options are provided, avoiding Lua script overhead. The error mapping from redis.Nil to ErrKeyNotFound is correct.

Note: Unlike the memory cache, Redis never returns ErrKeyExpired since Redis automatically removes expired fields. The documentation at lines 255-256 mentions this error but it won't occur in practice for Redis. Consider clarifying this in the method docs.

internal/sms-gateway/handlers/base/handler.go (2)

20-42: Idiomatic wrapping of parse errors looks good

Using fmt.Errorf("failed to parse …: %w", err) for body/query/params parsing is idiomatic and preserves the underlying error for higher-level handling without changing control flow. No issues spotted here.


44-55: Consistent validation error wrapping; behavior now depends on upstream mapping

Wrapping both validator and Validatable errors with fmt.Errorf("failed to validate: %w", err) is consistent and keeps the root cause accessible. Given the tests now expect generic 500s for these cases, this centralizes responsibility for mapping validation vs. server errors into your global error handler, which seems intentional.

internal/sms-gateway/handlers/base/handler_test.go (1)

77-239: Status expectations updated to match new error semantics

The updated expectedStatus values for body, query, and params tests (500 for validation failures, 422 for the no‑body parse error, 404 for missing path segments) are consistent with the new behavior of the handler helpers and Fiber’s default error handling. The table‑driven tests remain clear and still exercise the main happy/invalid paths.

internal/sms-gateway/handlers/logs/3rdparty.go (1)

5-8: JWT + scope wiring for logs looks consistent.

Using permissions.RequireScope(ScopeRead) together with userauth.WithUser and documenting @Security JWTAuth matches the new auth/permissions model, and the 501 Not Implemented response matches the swagger docs.

Also applies to: 37-38, 49-54, 56-58

internal/sms-gateway/models/migration.go (1)

13-17: Confirm global migration wiring includes users (and other module) migrations.

Migrate here now only migrates Device, relying on per-module Migrate functions (e.g., users.Migrate, webhooks, jwt, settings). Please double-check that the application’s migration entrypoint invokes all of these module-level Migrate functions so user tables and related schemas are actually created.

internal/sms-gateway/handlers/logs/permissions.go (1)

1-5: Log scope constant is consistent with the permissions model.

ScopeRead = "logs:read" follows the same pattern as other handlers and cleanly centralizes the log-read scope.

internal/sms-gateway/handlers/devices/permissions.go (1)

1-6: Device scope constants match intended usage.

ScopeList and ScopeDelete are clearly named and align with the permissions middleware and route usage in the devices handler.

internal/sms-gateway/handlers/devices/3rdparty.go (1)

9-16: Devices 3rd‑party handler now correctly uses users + scopes.

Switching to users.User with userauth.WithUser, adding @Security JWTAuth, and guarding list/delete with ScopeList / ScopeDelete are all consistent with the new JWT + permissions framework; the device queries remain properly scoped by user.ID.

Also applies to: 39-61, 63-90

internal/sms-gateway/users/models.go (1)

10-34: User ORM model and migration look sound and consistent.

userModel (ID varchar(32), PasswordHash varchar(72)) together with TableName() string { return "users" } and a module-level Migrate mirrors the pattern used by other modules and matches Device.UserID’s type, so the schema looks coherent.

internal/sms-gateway/handlers/webhooks/permissions.go (1)

1-7: Webhook scope constants align with the shared permission scheme.

ScopeList, ScopeWrite, and ScopeDelete are clearly named and consistent with how other handler packages expose their scopes.

internal/sms-gateway/handlers/messages/permissions.go (1)

1-13: LGTM!

The permission scope constants are well-documented and follow a consistent naming pattern (messages:<action>) that aligns with other permission scopes in the codebase (e.g., settings:read, settings:write).

internal/sms-gateway/handlers/settings/3rdparty.go (1)

137-141: LGTM!

The permission middleware integration is well-structured:

  • ScopeRead correctly protects the GET endpoint
  • ScopeWrite correctly protects both PUT and PATCH endpoints
  • Middleware ordering is correct (permission check before user extraction)
internal/sms-gateway/users/repository.go (2)

22-45: LGTM!

The Exists and GetByID methods are correctly implemented:

  • Exists uses an efficient COUNT query
  • GetByID properly maps gorm.ErrRecordNotFound to domain ErrNotFound
  • The previous issue with Take(&user) has been fixed to Take(user)

47-57: LGTM!

The Insert method correctly handles duplicate key violations by wrapping ErrExists with the underlying error, providing good error context for debugging while maintaining a clean domain error interface.

internal/sms-gateway/users/module.go (2)

12-27: LGTM!

The module wiring is well-structured:

  • Private provisions for internal dependencies (loginCache, repository)
  • Public provision for Service to be consumed by other modules
  • Error wrapping in the cache factory provides good context

The previous review issue about the missing newRepository provision has been addressed.


29-32: Migration registration follows framework pattern.

The init() function with the nolint:gochecknoinits directive is appropriate here as it follows the framework's migration registration pattern.

internal/sms-gateway/jwt/disabled.go (1)

1-28: LGTM!

This is a clean implementation of the Null Object pattern for the JWT service. When JWT is disabled (empty secret), all operations consistently return ErrDisabled, providing clear feedback to callers. The interface compliance is correct and the design allows graceful degradation.

internal/sms-gateway/modules/webhooks/models.go (1)

8-8: LGTM! Type refactor to users.User is consistent with the PR's user domain changes.

The import and type change align with the broader refactoring to consolidate user-related types into the dedicated users package. The GORM foreign key relationship and constraints remain unchanged.

Also applies to: 24-24

internal/config/module.go (1)

140-146: LGTM! JWT config provider follows established patterns.

The provider correctly maps application configuration to the jwt.Config type, consistent with other configuration providers in this module. The secret validation is appropriately handled downstream in the JWT module (returns disabled service when empty).

internal/sms-gateway/jwt/module.go (1)

9-22: LGTM! Well-structured fx module with appropriate conditional service initialization.

The module correctly:

  • Marks internal dependencies (Metrics, Repository) as private
  • Provides a disabled service when no secret is configured, enabling graceful degradation
  • Returns the full service with proper error propagation when configured
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

14-41: LGTM! Well-implemented JWT middleware with proper fallthrough for multi-auth support.

The middleware correctly:

  • Uses case-insensitive Bearer prefix check per RFC 7235
  • Falls through to next handler when no Bearer token is present, allowing other auth methods (Basic auth) to coexist
  • Distinguishes between user-not-found (401) and internal errors (500)
  • Safely dereferences the user pointer after error check
internal/config/types.go (1)

11-48: LGTM!

The Duration wrapper type is well-implemented with proper nil-safety, clean YAML/text unmarshaling delegation, and compile-time interface assertions.

internal/sms-gateway/jwt/errors.go (1)

5-12: LGTM!

The error definitions follow standard Go patterns and provide clear, specific error types for the JWT subsystem.

internal/sms-gateway/openapi/docs.go (1)

1-1955: LGTM!

The OpenAPI documentation has been regenerated with the JWT authentication security definitions and token management endpoints properly documented. Past review concerns about security annotations and parameter definitions have been addressed.

internal/sms-gateway/handlers/webhooks/3rdparty.go (1)

56-126: LGTM!

The handler has been properly updated to use the new users.User type and integrated with the permission-based authorization system. The routes are correctly wrapped with scope checks and the OpenAPI documentation reflects the JWT authentication requirements.

internal/sms-gateway/jwt/service.go (1)

24-182: LGTM!

The JWT service implementation is well-structured with comprehensive validation, proper error handling, and observability through metrics. Past review concerns about userID validation and parameter checks have been appropriately addressed.

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)

16-57: Basic auth integration with users.Service looks correct

The Basic middleware cleanly decodes credentials, delegates to usersSvc.Login, and, on success, stores a users.User via SetUser and grants ScopeAll. This is consistent with the new users domain and should work well with downstream middlewares.


88-109: Storing users.User by value is fine; pointer returned by GetUser is safe

SetUser stores a users.User value in Locals, and GetUser asserts that value and returns its address. Go will correctly heap-allocate the value when its address escapes, so the returned pointer remains valid. This is a reasonable pattern for this small struct.

internal/sms-gateway/handlers/3rdparty.go (1)

20-80: Third‑party handler wiring and auth middleware composition look correct

The new constructor neatly injects usersSvc, jwtSvc, and authHandler, and Register applies:

  • userauth.NewBasic(h.usersSvc)
  • jwtauth.NewJWT(h.jwtSvc, h.usersSvc)
  • userauth.UserRequired()

after registering the health endpoint, so /3rdparty/v1/health stays open while /auth, /messages, etc. require either Basic or JWT. This is consistent with the new JWT and users modules.

internal/sms-gateway/modules/auth/service.go (2)

85-145: User/device auth flows via usersSvc and devicesSvc look consistent

  • RegisterDevice now explicitly takes a users.User and passes user.ID into devicesSvc.Insert, matching the Device.UserID ownership model.
  • AuthorizeUserByCode correctly retrieves the user ID from codesCache and then resolves the user via usersSvc.GetByID, returning *users.User as expected by the updated middlewares.

Both flows align with the new users domain and the code-based auth middleware.


55-83: Based on my verification, the review comment is incorrect. Here's what I found:

  1. Fact check: The for range int syntax (iterating over an integer directly) was indeed introduced in Go 1.22. ✓
  2. Project verification: The main module's go.mod specifies go 1.24.3, which is well above Go 1.22.
  3. Conclusion: The syntax for range 3 is fully supported and compatible with the project's Go version (1.24.3).

The review comment incorrectly frames this as a potential issue by suggesting "If your module still targets an earlier Go version, this won't compile." However, the module explicitly targets Go 1.24.3, so there is no compatibility issue.


No issue with for range 3 — project targets Go 1.24.3, which fully supports integer-based range loops introduced in Go 1.22.

The syntax is valid and compatible with your project's Go version. No action is required.

internal/sms-gateway/jwt/metrics.go (1)

8-117: JWT metrics are well-structured and idiomatic

Metric names, labels (status), and histogram buckets are reasonable, and the Observe* helpers around prometheus.NewTimer give clean timing instrumentation. Wiring via NewMetrics() should integrate smoothly with the jwt service.

internal/sms-gateway/handlers/mobile.go (3)

123-155: Device registration flow correctly switches to usersSvc while preserving behavior

For /mobile/v1/device:

  • Existing users: userauth.GetUser(c) populates user *users.User, which you pass to RegisterDevice(*user, ...).
  • New users: you generate an ID, derive username/password, create the user via h.usersSvc.Create, and reuse the returned *users.User for device registration.

The response uses Login: username and Password: password, so credentials remain only non-empty for newly created users, matching the route description.


201-212: getUserCode signature change to users.User fits WithUser middleware

getUserCode now takes users.User, and in Register you use userauth.WithUser(h.getUserCode), which dereferences *users.User from Locals. This keeps types aligned across userauth, auth.Service, and the mobile handler.


243-272: Mobile routes now correctly use userauth.NewBasic(h.usersSvc) for user-based flows

In Register, the /mobile/v1/device and /mobile/v1/user/code endpoints are now protected with userauth.NewBasic(h.usersSvc) (and for /device also userauth.NewCode plus server key logic). This lines up with the new users service API and the updated userauth middleware.

@github-actions
Copy link

github-actions bot commented Nov 28, 2025

🤖 Pull request artifacts

Platform File
🐳 Docker GitHub Container Registry
🍎 Darwin arm64 server_Darwin_arm64.tar.gz
🍎 Darwin x86_64 server_Darwin_x86_64.tar.gz
🐧 Linux arm64 server_Linux_arm64.tar.gz
🐧 Linux i386 server_Linux_i386.tar.gz
🐧 Linux x86_64 server_Linux_x86_64.tar.gz
🪟 Windows arm64 server_Windows_arm64.zip
🪟 Windows i386 server_Windows_i386.zip
🪟 Windows x86_64 server_Windows_x86_64.zip

Copy link

@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: 0

🧹 Nitpick comments (4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4)

16-21: Update NewBasic comment to match actual Basic auth behavior

The comment says that when the header “is not in the correct format, the request is passed through”, but malformed/undecodable credentials or missing username:password currently result in 401 Unauthorized, not Next(). The basic <username>:<password> wording is also slightly misleading given the base64 encoding.

I suggest tightening the doc comment to reflect the real flow:

-// NewBasic returns a middleware that checks if the Authorization header is set to basic <username>:<password>>.
-// If the header is not set, or is not in the correct format, the request is passed through.
-// If the header is set, the middleware will decode the header contents, check if the credentials are in the correct format,
-// and then authenticate the user using the given users service.
-// If the user is authenticated, the middleware will set the user in the locals map.
+// NewBasic returns a middleware that optionally performs HTTP Basic authentication.
+// If the "Authorization" header is missing or does not start with "Basic ", the request is passed through unchanged.
+// If the header is present, the middleware expects a base64-encoded "username:password" payload, decodes it,
+// validates the credentials format, and authenticates the user using the given users service.
+// On invalid or failed authentication it returns 401 Unauthorized; on success it stores the user in Locals.

---

`77-83`: **Consider aligning scopes between Code auth and Basic auth**

The Basic-auth path grants `permissions.ScopeAll`, but the Code-auth path only sets the user and does not assign any scopes. If code-based authorization is expected to confer the same permissions as a Basic login, the current behavior may leave code-authenticated users under‑scoped.

If they should have full access, mirror the Basic path:

```diff
-		user, err := authSvc.AuthorizeUserByCode(code)
+		user, err := authSvc.AuthorizeUserByCode(code)
 		if err != nil {
 			return fiber.ErrUnauthorized
 		}
 
-		SetUser(c, *user)
+		SetUser(c, *user)
+		permissions.SetScopes(c, []string{permissions.ScopeAll})

If the asymmetry is intentional, adding a short comment here explaining why Code auth does not set scopes would help future readers.


99-108: Tighten GetUser documentation and clarify storage expectations

The implementation now consistently asserts users.User and returns a pointer to that value, which pairs correctly with SetUser and fixes the earlier pointer/value mismatch.

The comment still refers to LocalsUser and doesn’t say that we expect a value, not a pointer. You could clarify it as:

-// GetUser returns the user stored in the Locals of the given context.
-// It returns nil if the Locals do not contain a user under the key LocalsUser.
-// The user is stored in the Locals by the NewBasic and NewCode middlewares.
+// GetUser returns the user stored in the Locals of the given context.
+// It returns nil if the Locals do not contain a user under the key localsUser.
+// The user is stored in Locals by the NewBasic and NewCode middlewares via SetUser,
+// and is retrieved as a users.User value (exposed here as *users.User for convenience).

No changes needed to the function body itself.


125-139: Sync WithUser docs with behavior and reuse fiber.ErrUnauthorized

The comment still says “If the user is not present, it will panic”, but the implementation now (correctly) returns an unauthorized error instead, so the doc is stale. Also, you can simplify the unauthorized branch by reusing fiber.ErrUnauthorized for consistency with the rest of this file.

Suggested changes:

-// WithUser is a decorator that provides the current user to the handler.
-// It assumes that the user is stored in the Locals under the key LocalsUser.
-// If the user is not present, it will panic.
-//
-// It is a convenience function that wraps the call to GetUser and calls the
-// handler with the user as the first argument.
+// WithUser is a decorator that provides the current user to the handler.
+// It assumes that the user is stored in Locals under the key localsUser.
+// If the user is not present, it returns 401 Unauthorized.
+//
+// It is a convenience function that wraps the call to GetUser and calls the
+// handler with the user as the first argument.
 func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler {
 	return func(c *fiber.Ctx) error {
 		user := GetUser(c)
 		if user == nil {
-			return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
+			return fiber.ErrUnauthorized
 		}
 
 		return handler(*user, c)
 	}
 }

This keeps behavior the same but makes docs accurate and unauthorized handling consistent with the rest of the middleware.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fce4a07 and 544d933.

📒 Files selected for processing (3)
  • internal/sms-gateway/handlers/base/handler_test.go (9 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • pkg/cache/memory.go (14 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/sms-gateway/handlers/base/handler_test.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.

Applied to files:

  • pkg/cache/memory.go
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • pkg/cache/memory.go
🧬 Code graph analysis (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
  • SetScopes (15-17)
  • ScopeAll (10-10)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (204-208)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
pkg/cache/memory.go (2)

9-56: LGTM! Well-documented cache implementation.

The type definition and constructor are clean and well-documented. The use of sync.RWMutex for concurrent access is appropriate, and the zero-TTL semantic (no expiration) is clearly explained.


232-236: Based on my verification, I have confirmed that the review comment is accurate and raises a valid concern. Here are my findings:

  1. Code Behavior Confirmed: Lines 232-236 (13-17 in the snippet) show exactly what the review describes - when validUntil.IsZero() (non-expiring item), AndUpdateTTL converts it to an expiring item by setting validUntil = time.Now().Add(*o.updateTTL).

  2. Documentation Gap: The AndUpdateTTL documentation states it "adds the specified duration to the item's existing TTL" but does not explicitly address the edge case of items with no expiration.

  3. No Test Coverage: I searched all test files (memory_test.go, memory_edge_test.go, memory_concurrency_test.go, etc.) and found no tests covering AndUpdateTTL behavior, particularly for non-expiring items.

  4. Semantic Ambiguity: The behavior could be surprising to users who might expect:

    • AndUpdateTTL to be a no-op on items with no expiration, or
    • The update to extend from an existing default TTL rather than starting fresh from now

Rewritten Review Comment:

Undocumented AndUpdateTTL behavior for non-expiring items may surprise users.

When AndUpdateTTL is called on an item with no expiration (validUntil.IsZero()), this code converts it from never-expiring to expiring by setting validUntil = time.Now().Add(*o.updateTTL). The documentation does not explicitly describe this behavior, and there are no test cases covering it.

Recommend:

  1. Update AndUpdateTTL documentation to explicitly state the behavior for non-expiring items
  2. Add test case demonstrating this behavior for clarity and regression prevention
  3. Verify this semantic is intentional (consider if AndUpdateTTL should be a no-op for non-expiring items instead)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4)

7-9: Imports correctly reflect new permissions/users responsibilities

The added imports for permissions and users are appropriate for the refactor toward user- and scope‑aware middlewares; no changes needed.


88-90: SetUser / GetUser value semantics look good

Storing users.User by value in Locals and exposing it via GetUser keeps the middleware API simple and avoids sharing mutable pointers in context. Given the current small shape of users.User, this is a good trade‑off.


111-114: UserRequired behavior matches its documentation

UserRequired simply wraps HasUser and returns 401 Unauthorized when no user is present, which is exactly what the comment describes. This is a clean, reusable guard to combine with WithUser.


49-56: Based on my verification of the codebase, I can now provide the rewritten review comment:


The Login method contract guarantees non-nil user when error is nil—no defensive check needed.

The usersSvc.Login method (internal/sms-gateway/users/service.go:59) explicitly returns (nil, error) on all error paths and (*User, nil) on success. Every branch—cache hit, GetByID success, and password validation—follows this pattern with no path returning (nil, nil). The existing error check at line 50 is sufficient; dereferencing user at line 55 is safe.

@github-actions github-actions bot removed the ready label Nov 29, 2025
Copy link

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/cache/options.go (1)

38-49: Clarify: negative TTL causes immediate expiry, not "no expiry".

The docstring states "A TTL of zero or negative means the item will not expire," but the implementation differs:

  • Zero TTL → time.Time{} (zero value, likely interpreted as no expiry)
  • Negative TTL → time.Now() (immediate expiry)

If this is intentional, update the documentation to clarify:

-// A TTL of zero or negative means the item will not expire.
+// A TTL of zero means the item will not expire.
+// A negative TTL means the item expires immediately.
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

66-86: NewCode middleware is missing the permissions.SetScopes() call that NewBasic implements.

Both NewBasic (line 55) and NewCode authenticate users and store them via SetUser, but only NewBasic calls permissions.SetScopes(c, []string{permissions.ScopeAll}). The NewCode middleware should set scopes identically since both represent full user authentication. Add permissions.SetScopes(c, []string{permissions.ScopeAll}) after line 82 in NewCode to match the pattern used by NewBasic and ensure code-based auth users receive proper permissions.

♻️ Duplicate comments (11)
pkg/cache/redis.go (2)

43-48: Critical: HTTL returns an array, causing arithmetic failure.

The HTTL command with FIELDS parameter returns an array of TTL values (e.g., {3600}), not a scalar. Line 47 attempts arithmetic on the array directly, which will fail at runtime.

Apply this diff to extract the first element:

-  local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
+  local ttlArr = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
+  local ttl = ttlArr[1]
   if ttl < 0 then
     ttl = 0
   end

78-91: Document Redis 7.4.0+ requirement.

The implementation uses HExpireAt, HTTL, and HExpire with the FIELDS parameter, which require Redis 7.4.0 or later. This should be explicitly documented to prevent runtime errors with older Redis instances.

Add the version requirement to the type documentation:

 // RedisCache implements the Cache interface using Redis as the backend.
 //
+// Requires Redis 7.4.0 or later for per-field hash TTL support.
+//
 // This implementation stores all data in a Redis hash, with each cache item
api/requests.http (1)

200-218: Verify scope name: "devices:write" may not exist.

Based on past review comments, the scope "devices:write" on line 211 does not match the defined permission constants in devices/permissions.go. The codebase reportedly defines only "devices:list" and "devices:delete", not "devices:write". If this is still accurate, using this scope will cause authorization failures.

Run the following script to verify the currently defined device and webhook permission scopes:

#!/bin/bash
# Description: Verify all defined permission scope constants

echo "=== All Permission Scope Constants ==="
rg -n 'Scope.*=.*".*:.*"' --type go

echo ""
echo "=== Devices Permissions ==="
rg -n 'Scope' internal/sms-gateway/handlers/devices/permissions.go --type go 2>/dev/null || echo "File not found"

echo ""
echo "=== Webhooks Permissions ==="
rg -n 'Scope' internal/sms-gateway/handlers/webhooks/permissions.go --type go 2>/dev/null || echo "File not found"

echo ""
echo "=== Settings Permissions ==="
rg -n 'Scope' internal/sms-gateway/handlers/settings/permissions.go --type go 2>/dev/null || echo "File not found"

If "devices:write" is not defined, update line 211 to use the correct scope constant (likely "devices:delete" or whichever scope is appropriate for write operations).

internal/sms-gateway/handlers/devices/3rdparty.go (1)

92-94: Fix route parameter syntax to include leading slash.

The route parameter on line 94 uses :id without a leading slash, which is inconsistent with Fiber routing conventions and other handlers in the codebase (e.g., webhooks handler uses /:id).

Apply this diff:

-	router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
+	router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
internal/sms-gateway/users/service.go (2)

59-82: Security risk: plaintext passwords used as cache keys.

The Login method passes plaintext passwords to the cache (lines 60 and 77). Depending on the cache implementation, this could expose passwords in logs, metrics, or external systems.

Consider refactoring the cache to key only on username:

 func (s *Service) Login(ctx context.Context, username, password string) (*User, error) {
-	cachedUser, err := s.cache.Get(ctx, username, password)
+	cachedUser, err := s.cache.Get(ctx, username)
 	if err == nil {
 		return cachedUser, nil
 	} else if !errors.Is(err, cache.ErrKeyNotFound) {
 		s.logger.Warn("failed to get user from cache", zap.String("username", username), zap.Error(err))
 	}
 
 	user, err := s.users.GetByID(username)
 	if err != nil {
 		return nil, err
 	}
 
 	if compErr := crypto.CompareBCryptHash(user.PasswordHash, password); compErr != nil {
 		return nil, fmt.Errorf("login failed: %w", compErr)
 	}
 
 	loggedInUser := newUser(user)
-	if setErr := s.cache.Set(ctx, username, password, *loggedInUser); setErr != nil {
+	if setErr := s.cache.Set(ctx, username, *loggedInUser); setErr != nil {
 		s.logger.Error("failed to cache user", zap.String("username", username), zap.Error(setErr))
 	}
 
 	return loggedInUser, nil
 }

Note: This requires updating the cache interface to remove the password parameter.


84-100: Reliability issue: password change fails when cache is unavailable.

ChangePassword returns immediately if cache.Delete fails (lines 90-92), preventing users from changing passwords when the cache is down. This is inconsistent with Login, which treats cache failures as non-fatal.

Make cache deletion best-effort:

 func (s *Service) ChangePassword(ctx context.Context, username, currentPassword, newPassword string) error {
 	_, err := s.Login(ctx, username, currentPassword)
 	if err != nil {
 		return err
 	}
 
-	if delErr := s.cache.Delete(ctx, username, currentPassword); delErr != nil {
-		return delErr
+	if delErr := s.cache.Delete(ctx, username); delErr != nil {
+		s.logger.Warn("failed to delete login cache on password change", zap.String("username", username), zap.Error(delErr))
 	}
 
 	passwordHash, err := crypto.MakeBCryptHash(newPassword)
 	if err != nil {
 		return fmt.Errorf("failed to hash password: %w", err)
 	}
 
 	return s.users.UpdatePassword(username, passwordHash)
 }
internal/sms-gateway/handlers/messages/3rdparty.go (1)

327-331: Fix inconsistent route parameter syntax.

Line 329 uses :id without a leading slash, which is inconsistent with other handlers (e.g., webhooks uses /:id). Line 331 also lacks a leading slash for inbox/export.

Apply this diff for consistency:

 	router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.list))
 	router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUser(h.post))
-	router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
+	router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
 
-	router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport))
+	router.Post("/inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport))
internal/sms-gateway/handlers/thirdparty/auth.go (2)

97-105: Validate jti parameter is non-empty.

Line 98 extracts jti from the path but does not validate it. An empty jti should be rejected with a 400 Bad Request before calling RevokeToken.

Apply this diff:

 func (h *AuthHandler) deleteToken(user users.User, c *fiber.Ctx) error {
 	jti := c.Params("jti")
+	if jti == "" {
+		return fiber.NewError(fiber.StatusBadRequest, "jti parameter is required")
+	}
 
 	if err := h.jwtSvc.RevokeToken(c.Context(), user.ID, jti); err != nil {
 		return fmt.Errorf("failed to revoke token: %w", err)
 	}

60-82: Validate TTL bounds before conversion to prevent overflow.

Line 70 converts req.TTL (a uint64 in seconds) to time.Duration via time.Duration(req.TTL) * time.Second. Since time.Duration is an int64, values above math.MaxInt64 will silently overflow, producing negative or incorrect durations. The //nolint:gosec comment says "validated in the service," but the overflow occurs before the service receives the value.

Add validation in the handler to ensure req.TTL is within safe bounds before the conversion.

Apply this diff:

 func (h *AuthHandler) postToken(user users.User, c *fiber.Ctx) error {
 	req := new(smsgateway.TokenRequest)
 	if err := h.BodyParserValidator(c, req); err != nil {
 		return fiber.NewError(fiber.StatusBadRequest, err.Error())
 	}
 
+	// Prevent overflow: max int64 seconds is ~292 years
+	const maxTTLSeconds = uint64(1<<63 - 1) / uint64(time.Second)
+	if req.TTL > maxTTLSeconds {
+		return fiber.NewError(fiber.StatusBadRequest, "ttl exceeds maximum allowed value")
+	}
+
 	token, err := h.jwtSvc.GenerateToken(
 		c.Context(),
 		user.ID,
 		req.Scopes,
-		time.Duration(req.TTL)*time.Second, //nolint:gosec // validated in the service
+		time.Duration(req.TTL)*time.Second,
 	)
internal/sms-gateway/users/repository.go (1)

59-67: Check RowsAffected to detect non-existent users.

GORM's Update does not return an error when zero rows are affected. If the user ID doesn't exist, this method silently succeeds without updating anything, which is inconsistent with GetByID returning ErrNotFound.

Apply this diff:

 func (r *repository) UpdatePassword(id string, passwordHash string) error {
-	if err := r.db.Model((*userModel)(nil)).
+	result := r.db.Model((*userModel)(nil)).
 		Where("id = ?", id).
-		Update("password_hash", passwordHash).Error; err != nil {
+		Update("password_hash", passwordHash)
+	if err := result.Error; err != nil {
 		return fmt.Errorf("can't update password: %w", err)
 	}
+	if result.RowsAffected == 0 {
+		return ErrNotFound
+	}
 
 	return nil
 }
internal/sms-gateway/handlers/mobile.go (1)

43-75: Handle the error from nanoid.Standard.

Line 57 ignores the error from nanoid.Standard(21). If initialization fails and returns a nil function, calling h.idGen() at line 134 will cause a nil pointer panic.

Apply this diff:

 func newMobileHandler(
 	authSvc *auth.Service,
 	usersSvc *users.Service,
 	devicesSvc *devices.Service,
 
 	messagesCtrl *messages.MobileController,
 	webhooksCtrl *webhooks.MobileController,
 	settingsCtrl *settings.MobileController,
 	eventsCtrl *events.MobileController,
 
 	logger *zap.Logger,
 	validator *validator.Validate,
 ) *mobileHandler {
 	const idLength = 21
-	idGen, _ := nanoid.Standard(idLength)
+	idGen, err := nanoid.Standard(idLength)
+	if err != nil {
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}
 
 	return &mobileHandler{
🧹 Nitpick comments (6)
deployments/prometheus/alerts/jwt-alerts.yml (2)

24-32: Consider raising the threshold for revocation failures.

The alert condition > 0 will fire on any non-zero error rate, which may be too sensitive over a 5m evaluation window. Occasional transient errors during token revocation operations could trigger frequent critical alerts and cause alert fatigue. Consider using a small positive threshold (e.g., > 0.01 errors/s) to distinguish between occasional glitches and sustained failures.


5-5: Inconsistent division-by-zero protection approach.

Line 5 uses clamp_min(rate(...), 1e-9) while line 35 uses max(rate(...), 0.00001) for division-by-zero protection. Both are functionally valid (though with slightly different epsilon values: 1e-9 vs 1e-5), but unifying the approach would improve consistency and maintainability.

Apply this diff to unify the approach:

-        expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / max(rate(sms_auth_jwt_tokens_validated_total[5m]), 0.00001) > 0.1
+        expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / clamp_min(rate(sms_auth_jwt_tokens_validated_total[5m]), 1e-9) > 0.1

Also applies to: 35-35

internal/sms-gateway/handlers/base/handler.go (2)

20-42: Error wrapping in parser helpers may change HTTP status behavior

Switching BodyParserValidator, QueryParserValidator, and ParamsParserValidator to return fmt.Errorf("failed to parse ...: %w", err) instead of (presumably) returning *fiber.Error directly changes the error type seen by Fiber and any central error handler. If callers still just return h.BodyParserValidator(...), parse failures might now be treated as generic 500s unless your error mapping uses errors.As to unwrap and inspect underlying *fiber.Error or other typed causes.

If your intent is to (a) keep using 4xx for client parse errors and (b) gain better cause preservation, consider either:

  • translating these wrapped errors back into explicit HTTP-aware errors at the handler boundary, or
  • updating your global error handler to use errors.As on err to recover *fiber.Error / validation error types and map them to the desired status codes.

Please double‑check the end‑to‑end HTTP status behavior for bad JSON / query / param inputs after this change.


44-57: Validation error wrapping is good; consider a dedicated ValidationError type

Wrapping both h.Validator and Validatable.Validate() errors with fmt.Errorf("failed to validate: %w", err) is a nice improvement for preserving root causes while adding context. However, downstream code now only has the generic "failed to validate" surface unless it explicitly uses errors.Is/errors.As against the wrapped cause.

If you expect many callers or a central error handler to special‑case validation failures (e.g., always map them to 422 with structured details), it might be cleaner to introduce a small typed error such as:

type ValidationError struct {
    Err error
}

func (e *ValidationError) Error() string { return "validation failed: " + e.Err.Error() }
func (e *ValidationError) Unwrap() error { return e.Err }

and return &ValidationError{Err: err} from here. That keeps the error chain intact and gives downstream code a stable type to match on, instead of relying on prefix strings.

internal/sms-gateway/jwt/repository.go (2)

38-47: Consider distinguishing between non-existent and non-revoked tokens.

IsRevoked returns false for both non-existent tokens and tokens that exist but aren't revoked. Depending on security requirements, you may want to treat unknown tokens as invalid/revoked.

If stricter validation is desired:

 func (r *Repository) IsRevoked(ctx context.Context, jti string) (bool, error) {
-	var count int64
-	if err := r.db.WithContext(ctx).Model((*tokenModel)(nil)).
-		Where("id = ? and revoked_at is not null", jti).
-		Count(&count).Error; err != nil {
-		return false, fmt.Errorf("can't check if token is revoked: %w", err)
+	var token tokenModel
+	if err := r.db.WithContext(ctx).Where("id = ?", jti).First(&token).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return true, nil // Treat unknown tokens as revoked
+		}
+		return false, fmt.Errorf("can't check if token is revoked: %w", err)
 	}
-
-	return count > 0, nil
+	return token.RevokedAt != nil, nil
 }

28-36: Consider checking affected rows to detect no-op revocations.

The Revoke method silently succeeds when the token doesn't exist or is already revoked. This may be intentional for idempotency, but if you need to distinguish these cases for logging or metrics, check RowsAffected.

 func (r *Repository) Revoke(ctx context.Context, jti, userID string) error {
-	if err := r.db.WithContext(ctx).Model((*tokenModel)(nil)).
+	result := r.db.WithContext(ctx).Model((*tokenModel)(nil)).
 		Where("id = ? and user_id = ? and revoked_at is null", jti, userID).
-		Update("revoked_at", gorm.Expr("NOW()")).Error; err != nil {
+		Update("revoked_at", gorm.Expr("NOW()"))
+	if result.Error != nil {
-		return fmt.Errorf("can't revoke token: %w", err)
+		return fmt.Errorf("can't revoke token: %w", result.Error)
 	}
+	// Optionally log or return error if result.RowsAffected == 0
 
 	return nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2643ec and 10c5aa1.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (66)
  • .golangci.yml (2 hunks)
  • api/mobile.http (1 hunks)
  • api/requests.http (5 hunks)
  • cmd/sms-gateway/main.go (1 hunks)
  • configs/config.example.yml (1 hunks)
  • deployments/grafana/dashboards/jwt.json (1 hunks)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • go.mod (2 hunks)
  • internal/config/config.go (4 hunks)
  • internal/config/module.go (2 hunks)
  • internal/config/types.go (1 hunks)
  • internal/sms-gateway/app.go (4 hunks)
  • internal/sms-gateway/handlers/3rdparty.go (2 hunks)
  • internal/sms-gateway/handlers/base/handler.go (2 hunks)
  • internal/sms-gateway/handlers/base/handler_test.go (9 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (6 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (3 hunks)
  • internal/sms-gateway/handlers/logs/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/messages/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/messages/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4 hunks)
  • internal/sms-gateway/handlers/mobile.go (8 hunks)
  • internal/sms-gateway/handlers/module.go (2 hunks)
  • internal/sms-gateway/handlers/settings/3rdparty.go (8 hunks)
  • internal/sms-gateway/handlers/settings/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/module.go (1 hunks)
  • internal/sms-gateway/handlers/thirdparty/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/webhooks/3rdparty.go (5 hunks)
  • internal/sms-gateway/handlers/webhooks/permissions.go (1 hunks)
  • internal/sms-gateway/jwt/config.go (1 hunks)
  • internal/sms-gateway/jwt/disabled.go (1 hunks)
  • internal/sms-gateway/jwt/errors.go (1 hunks)
  • internal/sms-gateway/jwt/jwt.go (1 hunks)
  • internal/sms-gateway/jwt/metrics.go (1 hunks)
  • internal/sms-gateway/jwt/models.go (1 hunks)
  • internal/sms-gateway/jwt/module.go (1 hunks)
  • internal/sms-gateway/jwt/repository.go (1 hunks)
  • internal/sms-gateway/jwt/service.go (1 hunks)
  • internal/sms-gateway/models/migration.go (1 hunks)
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1 hunks)
  • internal/sms-gateway/models/models.go (0 hunks)
  • internal/sms-gateway/modules/auth/cache.go (0 hunks)
  • internal/sms-gateway/modules/auth/module.go (0 hunks)
  • internal/sms-gateway/modules/auth/repository.go (0 hunks)
  • internal/sms-gateway/modules/auth/service.go (4 hunks)
  • internal/sms-gateway/modules/messages/service.go (3 hunks)
  • internal/sms-gateway/modules/settings/models.go (2 hunks)
  • internal/sms-gateway/modules/webhooks/models.go (2 hunks)
  • internal/sms-gateway/openapi/docs.go (25 hunks)
  • internal/sms-gateway/users/cache.go (1 hunks)
  • internal/sms-gateway/users/domain.go (1 hunks)
  • internal/sms-gateway/users/errors.go (1 hunks)
  • internal/sms-gateway/users/models.go (1 hunks)
  • internal/sms-gateway/users/module.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
  • pkg/cache/cache.go (1 hunks)
  • pkg/cache/errors.go (1 hunks)
  • pkg/cache/memory.go (14 hunks)
  • pkg/cache/options.go (4 hunks)
  • pkg/cache/redis.go (10 hunks)
  • pkg/cache/typed.go (1 hunks)
💤 Files with no reviewable changes (4)
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/modules/auth/cache.go
  • internal/sms-gateway/modules/auth/module.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/cache/cache.go
🚧 Files skipped from review as they are similar to previous changes (21)
  • internal/sms-gateway/jwt/module.go
  • cmd/sms-gateway/main.go
  • internal/sms-gateway/handlers/settings/permissions.go
  • internal/sms-gateway/handlers/thirdparty/module.go
  • go.mod
  • pkg/cache/errors.go
  • internal/sms-gateway/users/module.go
  • internal/config/types.go
  • internal/sms-gateway/handlers/devices/permissions.go
  • internal/sms-gateway/handlers/webhooks/permissions.go
  • .golangci.yml
  • internal/config/module.go
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/jwt/models.go
  • internal/sms-gateway/modules/messages/service.go
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
  • internal/sms-gateway/jwt/errors.go
  • internal/sms-gateway/models/migration.go
  • internal/sms-gateway/handlers/base/handler_test.go
  • internal/sms-gateway/users/models.go
  • deployments/grafana/dashboards/jwt.json
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • pkg/cache/redis.go
  • pkg/cache/memory.go
📚 Learning: 2025-09-10T23:54:16.828Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:118-121
Timestamp: 2025-09-10T23:54:16.828Z
Learning: Redis 7.4.0 introduced HEXPIRE, HEXPIREAT, and related commands that support per-field TTL on hash data structures. go-redis v9.6.0+ includes support for these commands with methods like HExpireAt.

Applied to files:

  • pkg/cache/redis.go
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.

Applied to files:

  • pkg/cache/redis.go
  • pkg/cache/memory.go
🧬 Code graph analysis (21)
internal/sms-gateway/handlers/module.go (1)
internal/sms-gateway/handlers/thirdparty/module.go (1)
  • Module (8-16)
internal/sms-gateway/jwt/disabled.go (2)
internal/sms-gateway/jwt/jwt.go (3)
  • Service (10-14)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/sms-gateway/jwt/errors.go (1)
  • ErrDisabled (6-6)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/settings/permissions.go (2)
  • ScopeRead (4-4)
  • ScopeWrite (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
internal/sms-gateway/jwt/service.go (5)
internal/sms-gateway/jwt/config.go (1)
  • Config (12-16)
internal/sms-gateway/jwt/repository.go (1)
  • Repository (10-12)
internal/sms-gateway/jwt/metrics.go (3)
  • Metrics (24-31)
  • StatusError (20-20)
  • StatusSuccess (19-19)
internal/sms-gateway/jwt/jwt.go (3)
  • Service (10-14)
  • TokenInfo (23-27)
  • Claims (16-21)
internal/sms-gateway/jwt/errors.go (4)
  • ErrInitFailed (7-7)
  • ErrInvalidParams (9-9)
  • ErrInvalidToken (10-10)
  • ErrTokenRevoked (11-11)
internal/sms-gateway/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
  • ErrInvalidConfig (8-8)
internal/config/config.go (2)
internal/config/types.go (1)
  • Duration (11-11)
internal/worker/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/handlers/logs/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/logs/permissions.go (1)
  • ScopeRead (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
internal/sms-gateway/handlers/3rdparty.go (4)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/handlers/thirdparty/auth.go (1)
  • AuthHandler (19-23)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • NewBasic (21-59)
  • UserRequired (116-124)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
  • NewJWT (14-42)
internal/sms-gateway/handlers/thirdparty/auth.go (8)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/thirdparty/permissions.go (1)
  • ScopeTokensManage (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/jwt/errors.go (3)
  • ErrInvalidParams (9-9)
  • ErrInitFailed (7-7)
  • ErrDisabled (6-6)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/devices/permissions.go (2)
  • ScopeList (4-4)
  • ScopeDelete (5-5)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
  • SetScopes (15-17)
  • ScopeAll (10-10)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/cache.go (5)
pkg/cache/typed.go (2)
  • Typed (89-91)
  • NewTyped (118-122)
internal/config/config.go (1)
  • Cache (83-85)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/options.go (2)
  • AndSetTTL (122-126)
  • WithTTL (38-49)
pkg/cache/errors.go (2)
  • ErrKeyNotFound (19-19)
  • ErrKeyExpired (36-36)
internal/sms-gateway/modules/auth/service.go (2)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/modules/webhooks/models.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (6)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/modules/messages/service.go (1)
  • Service (27-39)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • SetUser (88-90)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
internal/sms-gateway/users/service.go (5)
internal/sms-gateway/modules/messages/service.go (2)
  • NewService (41-64)
  • Service (27-39)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/crypto/passwords.go (2)
  • MakeBCryptHash (14-20)
  • CompareBCryptHash (22-27)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (19-19)
pkg/cache/redis.go (1)
pkg/cache/options.go (1)
  • AndDelete (204-208)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (204-208)
internal/sms-gateway/handlers/mobile.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
  • GetUser (103-110)
  • NewBasic (21-59)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/errors.go (2)
  • ErrNotFound (6-6)
  • ErrExists (7-7)
pkg/mysql/errors.go (1)
  • IsDuplicateKeyViolation (13-19)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
internal/sms-gateway/handlers/messages/permissions.go (4)
  • ScopeList (10-10)
  • ScopeSend (6-6)
  • ScopeRead (8-8)
  • ScopeExport (12-12)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
🪛 Gitleaks (8.29.1)
api/mobile.http

[high] 71-71: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 72-72: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: E2E
  • GitHub Check: Test
  • GitHub Check: Benchmark
  • GitHub Check: Analyze (go)
🔇 Additional comments (53)
deployments/prometheus/alerts/jwt-alerts.yml (2)

4-12: Good division-by-zero protection and label usage.

The alert correctly uses the status="error" label and protects the division with clamp_min(...) to prevent NaN results. The 5% error rate threshold is reasonable for a warning-level alert.


14-22: Latency alert well-configured.

Using p99 quantile to capture tail latency is a good observability practice. The 0.5s threshold for JWT issuance seems reasonable, and the 10m evaluation window allows for transient latency spikes to stabilize before triggering.

configs/config.example.yml (1)

41-44: Good security practice: empty Secret disables JWT.

The configuration correctly documents that leaving the secret empty disables JWT functionality, which prevents accidental deployment with insecure tokens. This aligns with the disabled service implementation in the codebase.

internal/sms-gateway/handlers/logs/3rdparty.go (3)

5-7: LGTM! Import updates align with JWT integration.

The new imports for permissions middleware and users domain are correctly integrated to support scope-based authorization.


37-49: LGTM! Handler correctly updated for JWT authentication.

The JWTAuth security annotation and users.User parameter type properly reflect the new authentication mechanism.


57-57: LGTM! Permission enforcement properly integrated.

The middleware chain correctly enforces the logs:read scope before passing the authenticated user to the handler.

internal/sms-gateway/users/errors.go (1)

1-8: LGTM! Clean sentinel error definitions.

The error constants follow Go idioms and provide clear error semantics for user operations.

internal/sms-gateway/modules/webhooks/models.go (1)

8-24: LGTM! Consistent migration to users domain.

The User field type update maintains existing foreign key constraints while adopting the new users package.

internal/sms-gateway/app.go (1)

10-60: LGTM! JWT and users modules properly wired.

The fx module initialization correctly integrates the new users and JWT subsystems into the application dependency graph.

internal/sms-gateway/handlers/messages/permissions.go (1)

1-13: LGTM! Well-defined permission scopes.

The scope constants are clearly documented and follow a consistent naming convention for message-related operations.

internal/sms-gateway/handlers/module.go (1)

9-41: LGTM! Thirdparty module correctly wired.

The module registration follows the established fx pattern and properly integrates the thirdparty handlers.

internal/sms-gateway/handlers/thirdparty/permissions.go (1)

1-5: LGTM! Token management scope defined.

The scope constant follows the established pattern and will be used to protect token-related endpoints.

api/requests.http (2)

4-4: LGTM! JWT Bearer authentication examples added.

The jwtToken environment variable and Bearer auth examples properly demonstrate the new JWT authentication mechanism alongside existing Basic authentication.

Also applies to: 38-39, 83-84, 88-89, 108-109


220-224: LGTM! Token revocation endpoint example.

The DELETE endpoint demonstrates the token revocation flow with proper authentication.

internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

14-42: LGTM! Well-structured JWT authentication middleware.

The middleware correctly:

  • Validates the Authorization header format with proper length checks
  • Uses case-insensitive comparison for the Bearer prefix
  • Handles parse errors and missing users with appropriate HTTP status codes
  • Integrates cleanly with the userauth and permissions middleware

The optional nature of the middleware (delegating to next handler when no Bearer token is present) allows handlers to explicitly enforce authentication via other middleware.

internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)

9-36: LGTM! Clean scope-based permissions implementation.

The middleware provides a straightforward and effective way to enforce scope-based access control:

  • Clear separation between setting and checking scopes
  • The ScopeAll constant provides a convenient wildcard mechanism
  • Proper use of slices.ContainsFunc for flexible scope matching
  • Appropriate 403 Forbidden response with descriptive error message
internal/sms-gateway/jwt/jwt.go (1)

10-27: LGTM! Clean JWT service interface design.

The interface and types are well-structured:

  • Clear method signatures for token lifecycle operations
  • Claims struct properly extends standard JWT claims with custom fields
  • TokenInfo provides a complete representation of generated tokens
  • Good separation of concerns between service operations
internal/sms-gateway/handlers/settings/3rdparty.go (1)

138-140: LGTM! Proper integration of permissions middleware.

The route registration correctly:

  • Uses empty string paths for group-based routing
  • Applies appropriate scope requirements (ScopeRead for GET, ScopeWrite for PUT/PATCH)
  • Wraps handlers with userauth.WithUser for user context
internal/sms-gateway/jwt/config.go (1)

18-31: LGTM! Comprehensive configuration validation.

The Validate() method properly checks:

  • Secret presence and minimum length (32 bytes)
  • TTL is positive (rejects zero and negative values)
  • Clear error messages wrapping ErrInvalidConfig
internal/sms-gateway/openapi/docs.go (3)

21-137: LGTM! JWT authentication endpoints properly documented.

The generated OpenAPI spec correctly includes:

  • Security requirements (ApiAuth and JWTAuth) for both token endpoints
  • Complete parameter definition for the jti path parameter in the DELETE endpoint
  • Proper request/response schemas for token operations
  • Appropriate error responses (400, 401, 403, 500)

1796-1836: LGTM! Token request/response schemas properly defined.

The TokenRequest and TokenResponse definitions are well-structured:

  • TokenRequest requires scopes array and has optional ttl (in seconds)
  • TokenResponse includes all necessary fields (id, access_token, expires_at, token_type)
  • Proper field descriptions and validation constraints

1916-1921: LGTM! JWTAuth security definition correctly added.

The security definition properly configures JWT authentication as an API key in the Authorization header, consistent with standard Bearer token authentication patterns.

internal/sms-gateway/handlers/webhooks/3rdparty.go (2)

43-63: LGTM! JWT security and permission scope integration looks correct.

The OpenAPI annotations properly document both ApiAuth and JWTAuth security methods, and the 403 Forbidden response is correctly added. The handler signature update to users.User aligns with the new users domain model.


123-127: LGTM! Route registration with permission scopes is correctly implemented.

The permission middleware is properly chained before userauth.WithUser, ensuring scope validation occurs after authentication but before the handler receives the user. The scope constants (ScopeList, ScopeWrite, ScopeDelete) are appropriately mapped to their respective endpoints.

internal/sms-gateway/jwt/repository.go (1)

20-26: LGTM! Insert implementation is correct.

Error wrapping is appropriate, and context is properly passed to GORM.

internal/sms-gateway/handlers/messages/3rdparty.go (1)

56-173: LGTM! Handler signature updates and JWT annotations are correct.

The migration from models.User to users.User is consistent, and OpenAPI documentation properly includes both ApiAuth and JWTAuth security with 403 Forbidden responses for scope-protected endpoints.

pkg/cache/options.go (2)

75-105: LGTM! GetOption infrastructure is well-designed.

The functional options pattern is correctly applied with clear separation between Set and Get behaviors. The isEmpty() helper enables efficient short-circuit checks.


191-208: LGTM! AndDelete provides clean atomic get-and-delete semantics.

Good documentation noting the equivalence to GetAndDelete for discoverability.

internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)

88-110: LGTM! SetUser/GetUser pattern is correctly implemented.

The consistent value storage and pointer retrieval pattern ensures type assertions work correctly across all middleware paths. This addresses the previous review concern about pointer vs value mismatches.


126-141: LGTM! WithUser now properly handles missing user with 401 response.

The nil check with fiber.ErrUnauthorized provides a clear error instead of the previous potential panic, addressing the feedback from the prior review.


49-58: LGTM! NewBasic authentication flow is correct.

The middleware properly authenticates via usersSvc.Login, stores the user via SetUser, and grants full permissions with ScopeAll. The early return on missing/invalid Authorization header enables middleware chaining.

internal/sms-gateway/handlers/3rdparty.go (2)

35-67: LGTM!

The constructor properly initializes all dependencies, and the new fields for usersSvc, jwtSvc, and authHandler are correctly wired.


69-93: LGTM!

Authentication middleware is correctly configured with both Basic auth (line 75) and JWT auth (line 76), and the auth handler routes are properly registered at line 80.

pkg/cache/typed.go (1)

1-407: LGTM!

The typed cache implementation properly handles pointer types via the newItem helper (lines 392-407), which creates non-nil instances before unmarshalling. This addresses the previous pointer unmarshalling issue. The comprehensive documentation and consistent error handling throughout are excellent.

internal/sms-gateway/modules/auth/service.go (3)

35-53: LGTM!

The constructor properly injects the usersSvc dependency and initializes all fields correctly.


85-96: LGTM!

The method signature correctly uses the users.User domain type, aligning with the new users module integration.


133-145: LGTM!

The method correctly returns *users.User and delegates to usersSvc.GetByID, properly integrating with the new users service.

internal/sms-gateway/users/repository.go (3)

22-31: LGTM!

The Exists method correctly uses a COUNT query and proper error handling.


33-45: LGTM!

The GetByID method correctly passes user (not &user) to Take and properly maps gorm.ErrRecordNotFound to ErrNotFound.


47-57: LGTM!

The Insert method properly detects duplicate key violations and maps them to ErrExists.

internal/sms-gateway/jwt/service.go (4)

24-51: LGTM!

The constructor properly validates all inputs (config, tokens repository, metrics) and initializes the nanoid factory with appropriate error handling.


53-117: LGTM!

Token generation correctly validates inputs (userID non-empty at line 63, scopes non-empty at line 68, TTL non-negative at line 73), enforces maximum TTL via min() at line 89, and properly integrates with metrics.


119-166: LGTM!

Token parsing correctly validates JWT claims with required options (expiration, issued-at, issuer, signing method) and checks revocation status before returning claims.


168-182: LGTM!

Token revocation properly delegates to the repository and records metrics.

internal/sms-gateway/jwt/metrics.go (1)

1-117: LGTM!

The metrics implementation properly defines counters with status labels and histograms with appropriate latency buckets. The observe methods correctly use timers to record durations.

internal/sms-gateway/handlers/mobile.go (3)

116-156: LGTM!

The device registration flow correctly integrates with the new users service, creating users via usersSvc.Create (line 138) for unauthenticated requests and using authenticated users when available.


201-212: LGTM!

The method signature correctly uses the users.User domain type.


228-241: LGTM!

Password change correctly delegates to usersSvc.ChangePassword with proper error handling.

pkg/cache/memory.go (5)

207-245: LGTM! Well-designed Get method with proper concurrency control.

The implementation correctly:

  • Uses RLock for the fast path (no options) to allow concurrent reads
  • Upgrades to Lock when modifications are needed (delete or TTL updates)
  • Handles the AndDelete option by removing items even if expired, then returning ErrKeyExpired through the getItem check - this provides auto-cleanup
  • Implements updateTTL semantics correctly: for items without expiration, it sets an expiration from now; for items with expiration, it extends the expiration time by the specified duration

161-175: LGTM! Drain correctly leverages cleanup for atomic operation.

The implementation correctly uses the cleanup helper to atomically remove expired items and execute the callback under a write lock. Based on learnings, expired items are removed before the callback executes, so the returned map contains only non-expired items without needing additional filtering.


321-333: LGTM! SetOrFail provides proper atomic check-and-set.

The implementation correctly:

  • Holds the write lock for the entire operation to prevent race conditions
  • Treats expired items as non-existent (allows overwrite)
  • Returns ErrKeyExists only for non-expired items
  • Provides the expected semantics for conditional writes

413-425: LGTM! Cleanup helper correctly implements atomic cleanup with callback.

The implementation correctly captures the timestamp once and uses it consistently for all expiration checks, ensuring a consistent view during cleanup. The callback is executed under the write lock after expired items are removed, which matches the documented behavior in the learnings and enables atomic compound operations.


132-138: LGTM! All remaining methods are correctly implemented.

The helper methods and remaining public methods all follow proper patterns:

  • Appropriate locking for thread safety
  • Correct delegation and composition (e.g., GetAndDelete delegates to Get)
  • Proper TTL precedence in newItem (default first, then options override)
  • Consistent error handling in getItem/getValue
  • Appropriate no-op Close for memory-based implementation

Also applies to: 265-267, 290-296, 346-356, 370-403, 439-441

Copy link

@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: 3

🧹 Nitpick comments (1)
deployments/prometheus/alerts/jwt-alerts.yml (1)

24-32: Reconsider alert sensitivity: triggering on any revocation error may cause alert fatigue.

The alert on line 25 triggers whenever rate(sms_auth_jwt_tokens_revoked_total{status="error"}[5m]) > 0, meaning any single revocation error in the 5-minute window fires a critical alert. For operational stability, consider setting a threshold that allows minor transient errors (e.g., > 0.001 or > 0.01 per second) before triggering, since occasional failures are typically tolerable and expected in distributed systems.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10c5aa1 and a512179.

📒 Files selected for processing (3)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • pkg/cache/memory.go (14 hunks)
  • pkg/cache/options.go (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.

Applied to files:

  • pkg/cache/memory.go
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.

Applied to files:

  • pkg/cache/memory.go
🧬 Code graph analysis (1)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (205-209)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (5)
deployments/prometheus/alerts/jwt-alerts.yml (1)

1-42: Approve: Division-by-zero protection applied and alerts align with metrics.

The alert expressions on lines 5 and 35 both correctly use division-by-zero protection (clamp_min and max respectively), preventing NaN results when rates drop to zero during low-traffic periods. The metric selectors (status="error") match the available labels in the metrics infrastructure.

pkg/cache/options.go (1)

1-209: Well-structured options API with clear semantics.

The use of pointer types in getOptions for validUntil, setTTL, and updateTTL correctly distinguishes between "not set" and "set to zero value". The isEmpty() helper enables efficient short-circuiting in cache implementations. Documentation is thorough with useful examples.

pkg/cache/memory.go (3)

161-175: LGTM!

The Drain implementation correctly leverages the cleanup callback pattern. Based on learnings, the cleanup method removes expired items first before executing the callback, so no additional expiration filtering is needed when copying items to the result map.


207-245: Good optimization: read lock for simple gets, write lock only when options require mutation.

The bifurcation between RLock for option-free reads and Lock when options may modify state is a sensible optimization for read-heavy workloads.


232-236: Reasonable fallback for updateTTL on never-expiring items.

When an item has no expiration (validUntil.IsZero()), treating updateTTL as "set TTL from now" is a sensible design choice rather than silently doing nothing or erroring.

- name: jwt-alerts
rules:
- alert: JWT_Validation_ErrorRate_High
expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / clamp_min(rate(sms_auth_jwt_tokens_validated_total[5m]), 1e-9) > 0.1
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix annotation threshold mismatch: expression uses 10%, annotation claims 5%.

Line 5 sets the threshold to > 0.1 (10%), but the annotation on line 12 states "threshold: 5%". This discrepancy could confuse operators and make runbooks inaccurate.

Decide which threshold is intended and update accordingly:

  • If 10% is correct, update the annotation to "description: "JWT validation error rate is {{ $value | humanizePercentage }} (threshold: 10%)"
  • If 5% is correct, update the expression to > 0.05

Also applies to: 12-12

🤖 Prompt for AI Agents
In deployments/prometheus/alerts/jwt-alerts.yml around lines 5 and 12, the
Prometheus expression uses a 10% threshold ("> 0.1") while the annotation text
claims a 5% threshold, causing a mismatch; choose the intended threshold and
make them consistent — either change the annotation on line 12 to read
"description: \"JWT validation error rate is {{ $value | humanizePercentage }}
(threshold: 10%)\"" if 10% is correct, or change the expression on line 5 to use
"> 0.05" if 5% is correct, ensuring both expression and annotation match
exactly.

service: jwt
annotations:
summary: "JWT revocation failures detected"
description: "JWT revocation errors occurring at rate {{ $value }}/s"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clarify alert description: does the metric expose reason labels?

The description states "database errors," but the query filters only on status="error" without a reason label. If the underlying metric does not distinguish database errors from other failure types, the description is misleading. Either:

  1. Update the description to accurately reflect "JWT revocation failures" (all error types), or
  2. If database-specific errors should be monitored separately, add a reason label to the metric in internal/sms-gateway/jwt/metrics.go and update the alert query to filter by reason="database_error".

Comment on lines 237 to 238
case o.defaultTTL:
item.validUntil = time.Now().Add(m.ttl)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: AndDefaultTTL() causes immediate expiration when cache has no default TTL.

When m.ttl is 0 (no expiration), this sets validUntil = time.Now(), causing the item to expire immediately. This contradicts the documented behavior that "A TTL of zero means items do not expire by default." The newItem method (lines 350-352) handles this correctly by checking m.ttl > 0 first.

Apply this diff to align with newItem behavior:

 			case o.defaultTTL:
-				item.validUntil = time.Now().Add(m.ttl)
+				if m.ttl > 0 {
+					item.validUntil = time.Now().Add(m.ttl)
+				} else {
+					item.validUntil = time.Time{}
+				}
🤖 Prompt for AI Agents
In pkg/cache/memory.go around lines 237 to 238, the AndDefaultTTL() branch sets
item.validUntil = time.Now().Add(m.ttl) unconditionally, which makes items
expire immediately when m.ttl == 0; change this to mirror newItem by only
setting validUntil when m.ttl > 0 (i.e., if m.ttl > 0 then set validUntil =
time.Now().Add(m.ttl), otherwise leave item.validUntil zero/nil so items do not
expire).

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.

2 participants