Skip to content

Conversation

@capcom6
Copy link
Member

@capcom6 capcom6 commented Nov 18, 2025

Summary by CodeRabbit

  • New Features

    • JWT auth: issue, validate and revoke tokens via new 3rd‑party auth endpoints; JWT can be used alongside Basic auth.
    • Centralized users service and login flows integrated across mobile, auth and handlers.
  • Configuration

    • New JWT config block with sensible defaults; example config and API request environments updated.
  • Security

    • Scope-based permission checks enforced across 3rd‑party and API endpoints.
  • Observability

    • JWT metrics, Grafana dashboard and Prometheus alerts added.
  • Documentation

    • OpenAPI, examples and request collections 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 users package, replaces legacy auth repo/cache with a users service and login cache, expands cache primitives/backends, updates handlers and middleware for JWT and scope checks, adds token persistence/migration, metrics, FX wiring, OpenAPI updates and observability artifacts.

Changes

Cohort / File(s) Summary
Build deps & config
go.mod, internal/config/config.go, internal/config/types.go, internal/config/module.go
Bump client dependency, add github.com/golang-jwt/jwt/v5, introduce config.Duration type and jwt config block; provide fx mapping from config.JWTjwt.Config.
JWT subsystem
internal/sms-gateway/jwt/*.go, internal/sms-gateway/jwt/models.go, internal/sms-gateway/jwt/repository.go, internal/sms-gateway/jwt/module.go, internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
New JWT package: config validation, errors, Service interface, Claims/TokenInfo types, token GORM model + migration, repository (Insert/Revoke/IsRevoked), metrics, disabled & production service implementations, and fx wiring.
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, api/requests.http
Add AuthHandler with POST/DELETE token endpoints, error mapping, fx provider, tokens:manage scope, and HTTP examples using jwtToken.
Users domain, repo, service & module
internal/sms-gateway/users/* (domain.go, models.go, repository.go, service.go, cache.go, errors.go, module.go)
New users package: public User domain, GORM userModel + migration, repository (Exists/GetByID/Insert/UpdatePassword), login cache, Service (Create/GetByUsername/Login/ChangePassword), error vars, and fx wiring.
Auth module refactor
internal/sms-gateway/modules/auth/* (service.go, removed repository.go, removed cache.go, module.go)
Remove legacy auth repository/cache; auth service now depends on users.Service, signatures updated to use users.User, and DI wiring adjusted.
Handlers & middleware (JWT, permissions, userauth)
internal/sms-gateway/handlers/**, internal/sms-gateway/handlers/middlewares/*, internal/sms-gateway/handlers/module.go, internal/sms-gateway/app.go
Add jwtauth middleware, permissions middleware, adapt userauth to use users.Service and store users.User; update handlers to accept users.User, apply scope checks, register auth routes; FX modules include users/jwt.
Specific handler updates
internal/sms-gateway/handlers/*/3rdparty.go, internal/sms-gateway/handlers/mobile.go, internal/sms-gateway/handlers/3rdparty.go
Replace models.User with users.User, wrap endpoints with permissions.RequireScope, register auth routes, and update mobile/device registration to create/use users via 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 entity and its migration, migrate devices-only, update DeviceSettings/Webhook to reference users.User, add tokens migration and token model.
Cache core & implementations
pkg/cache/cache.go, pkg/cache/typed.go, pkg/cache/memory.go, pkg/cache/redis.go, pkg/cache/options.go, pkg/cache/errors.go
Add public Cache interface, generic Typed[T] wrapper, expand GetOption API (AndSetTTL/AndUpdateTTL/AndDelete/AndSetValidUntil/AndDefaultTTL), implement TTL-aware memory and Redis backends, add docs and a new cache error.
Permission constants
internal/sms-gateway/handlers/*/permissions.go
Add scope constants (e.g., messages:send/read/list/export, devices:list/delete, logs:read, settings:read/write, webhooks:list/write/delete, tokens:manage).
Handler helpers & tests
internal/sms-gateway/handlers/base/handler.go, internal/sms-gateway/handlers/base/handler_test.go
Replace Fiber-specific error construction with wrapped fmt.Errorf in validators and adjust tests expecting updated status codes.
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/mobile.http
Add OpenAPI JWTAuth/security and token schemas, JWT metrics, Grafana dashboard, Prometheus alerts, update example config and HTTP request examples.
Misc: lint/config
.golangci.yml
Adjust exhaustruct exclusion patterns.

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: GetByUsername(userID)
    UsersSvc-->>JWTMw: *User
    JWTMw->>JWTMw: SetUser + SetScopes
    JWTMw->>Handler: Next()
    Handler-->>Client: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Focus review areas:
    • internal/sms-gateway/jwt/* — token signing, TTL/issuer validation, revocation persistence, metrics and error mapping.
    • internal/sms-gateway/users/* — bcrypt usage, repository error handling, cache key derivation/TTL, migrations.
    • pkg/cache/* — generic Typed[T] reflection/instantiation, memory concurrency and TTL semantics, Redis Lua scripts and H* field TTL handling.
    • Handlers/middlewares & FX wiring — many changed signatures, permission enforcement, OpenAPI consistency, and removal of legacy auth wiring.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title '[jwt] add module and config' is overly broad and incomplete. While JWT module and config additions are present, the changeset includes far more: JWT service implementation, authentication middleware, permission scoping, user service refactoring, database migrations, metrics, API endpoints, and significant architectural changes across multiple handlers. Consider revising the title to better reflect the scope, such as '[jwt] implement JWT authentication with user service refactoring' or listing the main components being added.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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: 0

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

4-12: Investigate potential alert duplication.

JWT_Validation_ErrorRate_High (lines 4–12) and JWT_Validation_Failures_High (lines 34–42) appear to monitor nearly identical conditions: both trigger when the JWT validation error rate exceeds 10% over a 5-minute window. The naming suggests different purposes, but the expressions are functionally equivalent. Clarify whether this duplication is intentional (e.g., for redundancy or future differentiation) or if one should be removed or renamed with a distinct purpose.

Also applies to: 34-42


5-5: Standardize division-by-zero guards for consistency.

Lines 5 and 35 both protect against division by zero but use different approaches: clamp_min(..., 1e-9) vs. max(..., 0.00001). For consistency and maintainability, standardize on a single approach. The max() idiom is more commonly used in Prometheus expressions.

Apply this diff to standardize:

-        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
+        expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / max(rate(sms_auth_jwt_tokens_validated_total[5m]), 0.00001) > 0.1

Also applies to: 35-35


24-32: Verify critical severity for revocation alert.

Line 28 assigns severity: critical to an alert that fires whenever the revocation error rate is > 0 (line 25). This means even a single revocation failure within a 5-minute window triggers a critical-severity alert. Consider whether this is appropriate for your operational posture, or if a warning severity with a higher threshold (e.g., sustained error rate > 0.01/s) would be more suitable. Critical alerts should typically indicate active service degradation or data loss risk.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a512179 and f9de36b.

📒 Files selected for processing (2)
  • deployments/prometheus/alerts/jwt-alerts.yml (1 hunks)
  • pkg/cache/memory.go (13 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: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: Analyze (go)
🔇 Additional comments (7)
pkg/cache/memory.go (7)

9-56: LGTM! Well-documented cache structure and constructor.

The type definition and constructor are clear and comprehensive. The documentation properly explains the use cases, TTL behavior, and thread-safety guarantees.


58-91: LGTM! Clean item structure with proper expiration logic.

The memoryItem structure and isExpired method are well-designed. The nil safety check and zero-time handling ensure robust expiration semantics.


93-175: LGTM! Cleanup and drain operations are correctly implemented.

The methods properly handle expired items and maintain thread safety. The Drain implementation cleverly uses the cleanup callback to atomically copy and clear the cache.

Based on learnings, the cleanup method correctly removes expired items before executing the callback, ensuring Drain returns only non-expired items.


207-254: LGTM! Get method with proper option handling and fixed TTL logic.

The implementation correctly handles:

  • Fast path with read lock for simple gets
  • Write lock for option-based modifications
  • Early return for expired/missing keys
  • Various TTL update strategies

The defaultTTL case (lines 244-249) now properly checks m.ttl > 0 before setting expiration, addressing the bug from the previous review where zero TTL would cause immediate expiration.


256-342: LGTM! Set operations are thread-safe and semantically correct.

All three methods properly implement their semantics:

  • GetAndDelete correctly delegates to Get with the AndDelete option
  • Set unconditionally overwrites with proper locking
  • SetOrFail atomically checks for non-expired keys and returns ErrKeyExists appropriately

344-434: LGTM! Helper methods are well-structured and consistent.

The internal helpers maintain consistent behavior:

  • newItem correctly applies default TTL only when m.ttl > 0 (lines 359-361)
  • getItem and getValue properly distinguish between missing and expired keys
  • cleanup efficiently removes expired entries while maintaining atomicity for the callback

436-453: LGTM! Resource cleanup and interface compliance.

The Close method appropriately returns nil for the memory implementation, and the compile-time interface check ensures MemoryCache correctly implements the Cache interface.

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

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/middlewares/userauth/userauth.go (1)

66-86: NewCode path does not set permission scopes.

NewBasic sets permissions.SetScopes(c, []string{permissions.ScopeAll}) on successful auth, but NewCode only stores the user without setting any scopes. This inconsistency means code-based authentication will fail all RequireScope checks.

If code-based auth should grant full access like Basic auth:

 		SetUser(c, *user)
+		permissions.SetScopes(c, []string{permissions.ScopeAll})
 
 		return c.Next()

If code-based auth has limited scopes, explicitly set the appropriate ones.

♻️ Duplicate comments (9)
internal/config/config.go (1)

125-128: Critical: JWT Secret still has no default value or validation.

As noted in previous reviews, the JWT initialization leaves Secret empty, making all tokens insecure. The application should either fail at startup when Secret is not provided or include explicit documentation and validation.

Based on past review comments.

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

51-54: Critical: Credential-based cache keys still pose security risks.

As noted in previous reviews, using hashed credentials as cache keys creates security vulnerabilities:

  • Deterministic hashing without salt enables correlation attacks
  • Cache becomes an authentication oracle (cache hit proves valid credentials)
  • Password changes won't invalidate old cache entries
  • Timing differences could leak credential validity information

Consider using session tokens, opaque identifiers, or adding a server-side salt to the hash function instead.

Based on past review comments.

api/requests.http (1)

200-218: Critical: Verify scope name devices:write against defined constants.

Based on previous review comments, the scope "devices:write" on line 211 may not match the permission constants defined in the codebase. The devices permissions file defines "devices:list" and "devices:delete". Please verify this scope is valid or update it to match the actual permission constants.

Run the following script to verify all defined device permission scopes:

#!/bin/bash
# Description: Find device permission scope constants

echo "=== Device Permission Scopes ==="
rg -n 'Scope.*=.*"devices:' --type go -C 2

Based on past review comments.

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

8-32: JWT config validation is solid; optionally validate Issuer as well

The Secret and TTL checks look good (including rejecting non‑positive TTL). If your tokens rely on a non‑empty iss claim, you may also want Validate to reject empty Issuer values, similar to how Secret is handled; otherwise, consider explicitly documenting that Issuer is optional.

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

43-58: Handle nanoid.Standard error and tighten ID slicing for credentials

Two related issues in the ID/credential generation path:

  1. nanoid.Standard error handling
    At line 57 the error from nanoid.Standard(idLength) is ignored. If it fails and returns a nil generator, h.idGen() will panic at runtime when postDevice runs.

  2. Credential slicing looks off‑by‑one
    In postDevice, username is id[:6] and password is id[7:], so the character at index 6 is unused. With idLength = 21 this still works, but it’s surprising and easy to break if idLength changes.

Consider something along these lines:

 func newMobileHandler(
@@
-) *mobileHandler {
-	const idLength = 21
-	idGen, _ := nanoid.Standard(idLength)
+) *mobileHandler {
+	const idLength = 21
+	idGen, err := nanoid.Standard(idLength)
+	if err != nil {
+		// Fail fast during startup rather than leaving a nil generator.
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}
@@
 func (h *mobileHandler) postDevice(c *fiber.Ctx) error {
@@
-	if authUser := userauth.GetUser(c); authUser != nil {
+	if authUser := userauth.GetUser(c); authUser != nil {
 		user = authUser
 		username = user.ID
 	} else {
 		id := h.idGen()
-		username = strings.ToUpper(id[:6])
-		password = strings.ToLower(id[7:])
+		username = strings.ToUpper(id[:6])
+		password = strings.ToLower(id[6:])
#!/bin/bash
# Grep for other nanoid.Standard usages that may also ignore errors
rg -n 'nanoid\.Standard\('

Also applies to: 123-139

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

59-99: Avoid using plaintext passwords as cache keys and make cache delete best‑effort

Two security/robustness concerns here (previously noted in an earlier review):

  1. Plaintext passwords as cache keys
    Login and ChangePassword pass the raw password / currentPassword into the cache (Get/Set/Delete). Depending on cache backend and observability, this risks leaking secrets (e.g., via keys in logs, metrics, Redis key inspection). It’s safer for the cache API to never see the raw password at all—e.g., key only by username or by a short‑lived opaque login token.

  2. Password change fails on cache.Delete error
    ChangePassword returns immediately on s.cache.Delete failure, even though Login treats cache errors as non‑fatal. This can block password changes purely due to cache issues.

Suggested direction:

  • Refactor loginCache so its public API doesn’t require the plaintext password (e.g., Get(ctx, username), Set(ctx, username, user), Delete(ctx, username)), and update the callsites accordingly.
  • Make cache deletion best‑effort in ChangePassword so storage is updated even if cache eviction fails, e.g.:
 func (s *Service) ChangePassword(ctx context.Context, username, currentPassword, newPassword string) error {
@@
-	if delErr := s.cache.Delete(ctx, username, currentPassword); delErr != nil {
-		return delErr
-	}
+	if delErr := s.cache.Delete(ctx, username, currentPassword); delErr != nil {
+		s.logger.Warn("failed to delete login cache on password change",
+			zap.String("username", username),
+			zap.Error(delErr),
+		)
+	}
#!/bin/bash
# Locate loginCache definition to plan a safe API change
rg -n 'type loginCache' internal/sms-gateway/users pkg/cache
internal/sms-gateway/users/repository.go (1)

59-67: Check RowsAffected to detect non-existent users.

GORM's Update doesn't return an error when zero rows are affected. If the user ID doesn't exist, this method silently succeeds, 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/messages/3rdparty.go (1)

327-332: 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 depending on Fiber's path resolution.

Apply this diff for consistency:

-	router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
+	router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
pkg/cache/redis.go (1)

25-52: Fix HTTL return handling in Lua TTL script (HTTL with FIELDS returns an array)

When using HTTL key FIELDS 1 field, Redis returns an array of TTLs; here ttl is treated as a scalar (if ttl < 0 / ttl + ttlDelta), which will cause a Lua runtime error once this path is exercised, breaking Get calls that use TTL‑update options.

Update the script to read the first element of the returned array before doing arithmetic:

 elseif ttlDelta > 0 then
-  local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
-  if ttl < 0 then
-    ttl = 0
-  end
-  local newTtl = ttl + ttlDelta
-  redis.call('HExpire', KEYS[1], newTtl, 'FIELDS', '1', field)
+  local ttlArr = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
+  local ttl = ttlArr[1] or 0
+  if ttl < 0 then
+    ttl = 0
+  end
+  local newTtl = ttl + ttlDelta
+  redis.call('HExpire', KEYS[1], newTtl, 'FIELDS', '1', field)
 end

This matches the expected HTTL response shape and keeps the existing negative‑TTL handling.

Please double‑check against your target Redis version’s HTTL documentation to confirm the array return shape when using FIELDS.

🧹 Nitpick comments (9)
internal/sms-gateway/handlers/devices/permissions.go (1)

1-6: Permission scope definitions look good; consider adding GoDoc comments.

The scope constants and their values are clear and consistent, and centralizing them in this package is a good approach. Since ScopeList and ScopeDelete are exported, you may optionally add brief GoDoc comments (e.g., // ScopeList is the permission required to list devices.) to improve discoverability in IDEs and godoc.

internal/sms-gateway/handlers/base/handler.go (1)

44-58: Consider more specific error messages for validation paths.

The error wrapping correctly preserves the error chain. However, both validation paths (Line 47 for Validator.Var and Line 53 for custom Validate()) use the identical message "failed to validate". More specific messages would improve debugging.

Apply this diff for clearer error messages:

 	if h.Validator != nil {
 		if err := h.Validator.Var(out, "required,dive"); err != nil {
-			return fmt.Errorf("failed to validate: %w", err)
+			return fmt.Errorf("failed to validate struct: %w", err)
 		}
 	}

 	if req, ok := out.(Validatable); ok {
 		if err := req.Validate(); err != nil {
-			return fmt.Errorf("failed to validate: %w", err)
+			return fmt.Errorf("failed to validate business rules: %w", err)
 		}
 	}
internal/sms-gateway/handlers/logs/permissions.go (1)

3-5: Consider adding documentation for the scope constant.

The constant definition is correct and follows a clear naming pattern. However, adding a godoc comment would help developers understand its purpose and usage within the permissions middleware.

Apply this diff to add documentation:

+// ScopeRead defines the permission scope required to read logs.
 const (
 	ScopeRead = "logs:read"
 )
internal/config/types.go (1)

11-48: Duration wrapper and unmarshalling look correct; consider minor API polish

The nil-safe accessors and YAML/Text unmarshalling are implemented cleanly and should behave as expected. Optionally, you could:

  • Add a doc comment on the exported Duration type itself for lint friendliness.
  • Add MarshalText / MarshalYAML if you later need symmetric serialization.
internal/sms-gateway/handlers/settings/3rdparty.go (1)

8-13: Handlers correctly migrated to users.User and scope‑guarded; minor redundancy only

The migration from models.User to users.User and the addition of permissions.RequireScope + JWTAuth annotations look coherent and match the new auth model. One small thing you might revisit later: both put and patch call BodyParserValidator and then c.BodyParser(&settings) on the same body, which is safe but slightly redundant—reusing the validated DTO to build the map, or validating the map directly, could simplify this path.

Also applies to: 47-67, 82-101, 117-140

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

21-33: Consider validating dependencies in NewService

NewService assumes users, cache, and logger are non‑nil. If any of these are ever mis‑wired (tests, future module changes), you’ll get a panic on first use rather than a clear initialization error. It’s worth either:

  • Adding nil checks and panicking/logging with a clear message, or
  • Returning (*Service, error) and failing early at wiring time.
pkg/cache/options.go (1)

11-21: Cache option plumbing and docs look consistent; ensure backends honor new GetOptions

The expanded documentation for WithTTL/WithValidUntil and the new GetOption/getOptions helpers (AndSetTTL, AndUpdateTTL, AndSetValidUntil, AndDefaultTTL, AndDelete) are internally consistent and keep the behavior clear. Just make sure all cache backends (memory, Redis, typed wrappers) interpret the new getOptions fields (setTTL, updateTTL, defaultTTL, delete) in the same way so callers get uniform semantics across implementations.

Also applies to: 23-50, 52-74, 76-107, 108-209

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

235-238: Differentiate auth vs internal errors in ChangePassword (optional)

Currently any error from usersSvc.ChangePassword (including DB/cache issues) is mapped to 401 Unauthorized. For better client semantics, distinguish bad credentials (401) from internal failures (500). This requires custom error types or checking error types in the handler, since the service layer currently returns untyped errors.

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

28-36: Consider checking RowsAffected when revoking tokens.

Revoke silently succeeds if the token doesn't exist or is already revoked (due to the revoked_at is null condition). Depending on the use case, callers may want to know if revocation actually occurred.

If explicit feedback is needed:

 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 err := result.Error; err != nil {
 		return fmt.Errorf("can't revoke token: %w", err)
 	}
+	if result.RowsAffected == 0 {
+		return ErrTokenNotFound // or a specific "already revoked" error
+	}
 
 	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 f9de36b and 53baff7.

⛔ 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 (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 (4)
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/modules/auth/module.go
  • internal/sms-gateway/modules/auth/cache.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/sms-gateway/main.go
🚧 Files skipped from review as they are similar to previous changes (22)
  • internal/sms-gateway/handlers/settings/permissions.go
  • internal/sms-gateway/handlers/devices/3rdparty.go
  • pkg/cache/errors.go
  • internal/sms-gateway/handlers/base/handler_test.go
  • .golangci.yml
  • internal/sms-gateway/jwt/module.go
  • internal/sms-gateway/handlers/webhooks/permissions.go
  • internal/sms-gateway/handlers/messages/permissions.go
  • go.mod
  • internal/sms-gateway/handlers/thirdparty/module.go
  • internal/sms-gateway/users/errors.go
  • internal/sms-gateway/app.go
  • internal/sms-gateway/handlers/logs/3rdparty.go
  • internal/config/module.go
  • internal/sms-gateway/handlers/module.go
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
  • internal/sms-gateway/modules/settings/models.go
  • internal/sms-gateway/handlers/thirdparty/auth.go
  • internal/sms-gateway/jwt/metrics.go
  • pkg/cache/cache.go
  • internal/sms-gateway/jwt/jwt.go
  • internal/sms-gateway/jwt/models.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/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/config/config.go (2)
internal/config/types.go (1)
  • Duration (11-11)
internal/worker/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/modules/webhooks/models.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/jwt/errors.go (1)
internal/sms-gateway/jwt/service.go (1)
  • New (24-51)
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/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
  • ErrInvalidConfig (8-8)
internal/sms-gateway/users/cache.go (5)
pkg/cache/typed.go (2)
  • Typed (89-91)
  • NewTyped (118-122)
internal/config/config.go (1)
  • Cache (78-80)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/options.go (1)
  • AndSetTTL (123-127)
pkg/cache/errors.go (2)
  • ErrKeyNotFound (19-19)
  • ErrKeyExpired (36-36)
internal/sms-gateway/modules/messages/service.go (1)
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 (132-141)
internal/sms-gateway/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
  • Device (18-29)
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/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)
pkg/cache/redis.go (1)
pkg/cache/options.go (1)
  • AndDelete (205-209)
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 (132-141)
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/devices/3rdparty.go (1)
  • ThirdPartyController (19-23)
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/jwtauth/jwtauth.go (1)
  • NewJWT (14-42)
internal/sms-gateway/users/service.go (3)
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)
internal/sms-gateway/jwt/service.go (6)
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/config/types.go (1)
  • Duration (11-11)
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/modules/auth/service.go (4)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/modules/devices/service.go (1)
  • Service (15-24)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/models.go (2)
  • Device (18-29)
  • NewDevice (31-37)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (205-209)
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)
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 (1)
  • NewBasic (21-59)
🪛 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: Benchmark
  • GitHub Check: Test
  • GitHub Check: Analyze (go)
🔇 Additional comments (44)
deployments/grafana/dashboards/jwt.json (1)

1-1192: ✅ All previously flagged metric issues have been resolved.

The dashboard now correctly references the actual metrics defined in the JWT service:

  • Lines 88, 152, 314, 389, 609, 674, 896, 996: All metric names are now correct and consistent with the service implementation (sms_auth_jwt_tokens_issued_total, sms_auth_jwt_tokens_validated_total, sms_auth_jwt_tokens_revoked_total)
  • Error metrics: Lines 389 and 996 correctly use the existing counters with status="error" filters instead of querying non-existent error metrics
  • Histogram metrics: Latency queries (lines 489, 499, 509, 776, 786, 796, 1096, 1106, 1116) correctly reference histogram buckets for percentile calculations
  • Templating variables: Lines 1146 and 1169 properly reference the base metric for dynamic instance/job discovery

The dashboard is well-structured with comprehensive JWT observability coverage: token issuance/validation/revocation rates, success rates, error tracking, and latency distributions across p50/p95/p99 percentiles.

internal/sms-gateway/handlers/base/handler.go (3)

20-26: LGTM! Standard error wrapping improves portability.

The shift from fiber.NewError to standard Go error wrapping with fmt.Errorf follows best practices and reduces coupling to the Fiber framework.


28-34: LGTM! Consistent error handling.

The error wrapping is consistent with the pattern established in BodyParserValidator.


36-42: LGTM! Consistent error handling.

The error wrapping is consistent with the pattern established in the other parser methods.

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

1-5: Scope naming pattern is consistent across handlers.

The "logs:read" scope follows the established naming convention used throughout handler packages. All scopes follow the "resource:action" format in lowercase: messages:read, messages:send, settings:read, webhooks:list, devices:delete, etc. No inconsistencies found.

deployments/prometheus/alerts/jwt-alerts.yml (1)

1-32: LGTM! Previous critical issues have been resolved.

All previously identified issues have been addressed:

  • Division-by-zero protection is now in place using max(rate(...), 0.00001) (line 5)
  • Metric label selectors correctly use status="error" instead of non-existent reason labels
  • Threshold consistency between expressions and annotations is maintained (10% throughout)

The alert definitions are now correct and will function as intended.

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

35-53: LGTM! Clean refactor to explicit dependency injection.

The constructor signature has been properly refactored from an fx Params bundle to explicit arguments, making dependencies clear and testable. The migration from internal models to the public users.Service is architecturally sound.


133-145: LGTM! Previous method naming issue resolved.

The method now correctly calls GetByID (line 139), resolving the previous critical issue where GetByUsername was being called with a user ID parameter. The return type has been properly updated to *users.User.

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

8-28: LGTM! Clean no-op implementation for disabled JWT.

The disabled service correctly implements the Service interface by returning ErrDisabled for all operations. This provides a safe fallback when JWT is not configured, following the Null Object pattern.

api/mobile.http (1)

66-73: LGTM! Static analysis warning is a false positive.

The static analysis tool flagged these as potential API keys, but these are test credentials in an HTTP request file used for development and testing purposes. No security concerns here.

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

8-26: LGTM! Consistent type migration to public users.User.

The change from models.User to users.User aligns with the broader architectural refactor. GORM constraints and relationships are correctly preserved.

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

13-18: LGTM! Migration responsibility correctly split.

The removal of User from this migration function aligns with the architectural decision to manage user migrations in the dedicated users module. The Device model correctly remains here.

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

12-32: LGTM! Well-structured fx module following best practices.

The module correctly encapsulates:

  • Private dependencies (login cache, repository) to prevent external coupling
  • Public service API for consumers
  • Proper error handling in the cache factory (line 19)
  • Migration registration in init for database schema management

The dependency wiring is clean and follows fx conventions.

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

1-5: LGTM! Permission constant follows established conventions.

The new ScopeTokensManage constant follows the naming pattern of other scope constants in the codebase and provides appropriate permission control for token management operations.

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

10-34: LGTM!

The user model follows established patterns in the codebase, with appropriate field types and a consistent migration implementation.

configs/config.example.yml (1)

41-44: LGTM! JWT config includes sensible defaults.

The configuration properly documents that leaving secret empty disables JWT functionality, providing a clear opt-in mechanism for JWT authentication.

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

5-19: LGTM!

The domain model is clean and follows standard patterns. The constructor correctly maps fields from the database model to the domain type.

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

127-175: LGTM!

The migration from models.User to users.User is correctly applied. The new type has the required ID field, and all usages are consistent with the new domain model.

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

14-42: LGTM!

The JWT authentication middleware is well-implemented with proper error handling:

  • Correctly validates Bearer token format (case-insensitive per RFC 6750)
  • Distinguishes between authentication failures (401) and system errors (500)
  • Properly sets user and permission scopes in the request context
internal/sms-gateway/jwt/errors.go (1)

5-12: Error set for JWT is clear and consistent

The exported error values cover the main failure modes and are suitable for wrapping and checks; no changes needed.

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

3-36: Scope middleware logic is solid; Go version confirms compatibility

The locals keying and HasScope/RequireScope behavior (including ScopeAll as wildcard) look good and integrate cleanly with the auth layer. The slices.ContainsFunc usage is fully supported by your Go 1.24.3 and test toolchain 1.23.2, so no compatibility concerns.

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

8-11: LGTM!

Import changes correctly introduce the permissions middleware and users package, aligning with the broader migration from models.User to users.User.


56-63: LGTM!

Handler signature correctly updated to use users.User. The implementation properly accesses user.ID which is available on the new users.User type.


123-127: LGTM!

Route registration correctly applies scope-based authorization before user extraction. The middleware chain order (RequireScopeWithUser) ensures authorization is checked before handlers execute.

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

22-31: LGTM!

The Exists method correctly uses a COUNT query with proper error wrapping.


33-45: LGTM!

The GetByID method now correctly passes user (not &user) to GORM's Take, and properly maps gorm.ErrRecordNotFound to the domain-specific ErrNotFound.


47-57: LGTM!

The Insert method correctly detects duplicate key violations using the MySQL helper and wraps them with the domain-specific ErrExists error.

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

21-58: LGTM!

NewBasic correctly authenticates via usersSvc.Login, stores the user via SetUser, and grants full permissions with ScopeAll. The flow is consistent with the JWT-based authorization model.


88-110: LGTM!

SetUser and GetUser are well-implemented. The type assertion correctly expects a value type and returns a pointer for nil-safety.


132-141: LGTM!

WithUser properly checks for nil user before dereferencing, returning fiber.ErrUnauthorized instead of panicking. This addresses the past review feedback.

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

10-18: LGTM!

Repository struct and constructor follow the established pattern.


20-26: LGTM!

Insert correctly uses context and wraps errors appropriately.


38-47: LGTM!

IsRevoked correctly returns false for non-existent tokens (count=0), which is appropriate—non-existent tokens should be rejected by signature verification, not revocation checks.

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

12-16: LGTM!

Import changes correctly introduce permissions middleware and users package.


76-173: LGTM!

Handler signature updated to users.User. The implementation correctly uses user.ID for device selection and logging.


195-211: LGTM!

Handler signature updated correctly. The user parameter is properly passed to SelectStates.


228-242: LGTM!

Handler signature updated correctly. The user parameter is properly passed to GetState.


260-282: LGTM!

Handler signature updated correctly. The user.ID is properly used for device lookup and logging.

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

20-67: Constructor & dependency wiring look correct

The new fields (usersSvc, jwtSvc, authHandler) and newThirdPartyHandler wiring correctly initialize base.Handler and all controllers; no obvious nil or mis‑wiring risks here.


69-81: Auth middleware order correctly supports both Basic and JWT

Applying userauth.NewBasic before jwtauth.NewJWT, followed by userauth.UserRequired(), lets clients authenticate with either Basic (for initial token issuance) or Bearer JWT. Since jwtauth.NewJWT is a no‑op when the header is not Bearer ..., it won't interfere with Basic‑only requests to /auth/token.

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

24-181: JWT service implementation is consistent and defensively validated

New/GenerateToken/ParseToken/RevokeToken handle input validation, token issuance, revocation checks, and metrics in a coherent way; capping TTL via min(ttl, s.config.TTL) and rejecting empty userID/scopes avoid malformed tokens. No correctness issues stand out.

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

21-191: Swagger output now matches JWT auth flow and token API

The generated definitions for JWTAuth, /3rdparty/v1/auth/token (with TokenRequest/TokenResponse and seconds‑based ttl), /3rdparty/v1/auth/token/{jti}, and the added JWTAuth security entries on 3rd‑party routes align with the new handlers and middleware. Changes look correct for clients.

pkg/cache/typed.go (1)

29-407: Typed cache generics & pointer instantiation are handled correctly

The Typed[T Item] wrapper now instantiates T via newItem[T]() before unmarshalling and enforces that T is a pointer type, which fixes the previous nil‑pointer unmarshalling issue while keeping the API simple. Delegation to the underlying Cache is clean and consistent across methods.

pkg/cache/memory.go (1)

9-450: In‑memory cache TTL semantics and concurrency look solid

MemoryCache correctly distinguishes not‑found vs expired entries, applies default TTL only when ttl > 0, and uses cleanup plus map swapping to ensure Drain returns only non‑expired items while remaining data‑race‑free. The Get/Set/SetOrFail paths are consistent with the documented behavior.

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 (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)

66-86: Code-authenticated requests will fail scope checks—NewCode must set appropriate scopes.

Unlike NewBasic, which sets permissions.ScopeAll (line 55), NewCode only stores the user without calling permissions.SetScopes(). Since all protected endpoints use permissions.RequireScope() middleware, code-authenticated users will have empty scopes and cannot pass these checks. Either set permissions.ScopeAll here as well, or set specific scopes based on the authorization code type.

pkg/cache/memory.go (1)

177-412: Potential data race on memoryItem.validUntil in Get/getItem

MemoryCache.Get obtains a *memoryItem under the mutex, but getItem later calls item.isExpired(time.Now()) after the getter returns, i.e., after the lock has been released. In the no‑options path you do:

  • RLock → read itemRUnlock inside the getter, then
  • getItem runs item.isExpired(time.Now()) with no lock held.

In the options path, the getter holds a write lock while mutating item.validUntil, then defers Unlock before returning; getItem again calls item.isExpired after the lock is released. If other goroutines concurrently call Set, SetOrFail, or Get with TTL‑updating options on the same key, you can end up with unsynchronized reads/writes of memoryItem.validUntil, which is a concurrency bug under the Go memory model.

To make the implementation truly “safe for concurrent use” as documented, consider restructuring so that:

  • The expiration check and value read happen while holding either the read or write lock (e.g., move isExpired and value extraction into the getter closures), and
  • getItem/getValue no longer call isExpired on item after the lock has been released, or you treat memoryItem as immutable and replace map entries instead of mutating them in place.

That would eliminate the potential data race while preserving the existing TTL and option semantics.

♻️ Duplicate comments (11)
pkg/cache/errors.go (1)

21-36: Documentation still doesn't clarify backend-specific behavior.

The documentation for ErrKeyExpired remains generic and doesn't specify that this error is only returned by the memory cache implementation. Based on the retrieved learnings, the Redis implementation uses native field-level TTL and expired fields are automatically removed, meaning Redis returns ErrKeyNotFound for both expired and non-existent keys.

This is the same issue raised in the previous review, which was marked as addressed but doesn't appear to be reflected in the current code.

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

7-8: DeviceSettings.User now uses users.User – ensure association remains correct

Aligning DeviceSettings.User with the new users.User type is consistent with the broader refactor. As with Webhook, please verify that GORM treats users.User as the correct associated model (table name, primary key) so foreignKey:UserID;constraint:OnDelete:CASCADE still works as intended.

Same concern as in modules/webhooks/models.go.

Also applies to: 17-18

internal/config/config.go (1)

21-22: Empty default JWT secret is still possible – enforce non‑empty secret at startup

Config.JWT.Secret has no default and Default() only sets TTL and Issuer, so if JWT__SECRET/config is missing the app can run with an empty secret, producing trivially forgeable tokens. Either:

  • Fail fast on startup when cfg.JWT.Secret == "" (e.g., in jwt.Config validation or module wiring), or
  • At minimum, document here that Secret must be explicitly configured and is never allowed to be empty.

This was already raised on a previous revision and still appears unresolved.

Also applies to: 87-91, 125-128

go.mod (1)

7-7: Avoid merging client-go pseudo‑version to master; jwt/v5 addition is fine

github.com/android-sms-gateway/client-go is still pinned to a pseudo‑version (v1.9.6-0.20251123133512-f7816d96f90a), which is suitable for dev but brittle for master (reproducibility and traceability). Please switch to a tagged release (e.g., v1.9.6 or another stable tag) before merging and regenerate go.sum via go mod tidy.

The direct github.com/golang-jwt/jwt/v5 v5.3.0 requirement looks appropriate; coexisting with an indirect v4 is expected given dependencies.

Also applies to: 18-18

api/requests.http (1)

200-218: Critical: Invalid scope devices:write on line 211.

Based on past review comments and the permission constants defined in internal/sms-gateway/handlers/devices/permissions.go, the scope "devices:write" does not exist. The devices package only defines:

  • ScopeList = "devices:list"
  • ScopeDelete = "devices:delete"

Using an undefined scope will cause authorization failures when this token is used against device endpoints.

Apply this diff to fix the scope name:

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

Based on learnings from past review comments.

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

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

The error from nanoid.Standard(idLength) is silently ignored. If initialization fails and returns a nil function, calling h.idGen() will cause a nil pointer panic.

This was flagged in a previous review. Apply the suggested fix to handle the error properly.

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

97-102: Validate jti parameter is non-empty.

The handler extracts jti from path parameters but doesn't validate it before passing to RevokeToken. An empty jti should be rejected with a 400 Bad Request.

This was flagged in a previous review. Apply the suggested fix:

 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/users/service.go (2)

59-65: Cache keying with raw password is a security concern.

The loginCache.Get and Set methods receive the plaintext password, which risks leaking secrets through logs, metrics, or external cache systems. Consider keying the cache only on username (or a derived non-secret value).

This was flagged in a previous review. The suggested approach is to change the cache API to never include plaintext passwords in keys.


90-92: Make cache deletion best-effort in ChangePassword.

Returning immediately on cache.Delete error blocks password changes due to cache problems, even though Login treats cache failures as non-fatal. This inconsistency could lock users out of password changes during cache outages.

This was flagged in a previous review. Apply the suggested fix:

-	if delErr := s.cache.Delete(ctx, username, currentPassword); delErr != nil {
-		return delErr
-	}
+	if delErr := s.cache.Delete(ctx, username, currentPassword); delErr != nil {
+		s.logger.Warn("failed to delete login cache on password change", zap.String("username", username), zap.Error(delErr))
+	}
internal/sms-gateway/handlers/messages/3rdparty.go (1)

327-331: Fix inconsistent route parameter syntax.

Line 329 uses :id without a leading slash, while the webhooks handler 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)
internal/sms-gateway/handlers/devices/3rdparty.go (1)

92-95: Fix inconsistent route parameter syntax.

Line 94 uses :id without a leading slash, while the webhooks handler uses /:id. This inconsistency could cause routing issues in Fiber.

Apply this diff:

-	router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
+	router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
🧹 Nitpick comments (13)
internal/sms-gateway/handlers/logs/permissions.go (1)

3-5: Scope constant is clear and idiomatic

ScopeRead = "logs:read" is well‑named, exported appropriately for cross‑package use, and follows a conventional domain:action scope pattern. No changes needed here.

If more log‑related scopes are added later, consider grouping them in this file or a shared scopes module for discoverability.

deployments/prometheus/alerts/jwt-alerts.yml (1)

24-32: Consider relaxing the > 0 threshold or add context to runbooks.

The JWT_Revocation_Failures alert fires with critical severity whenever the revocation error rate is non-zero—even a single error in a 5-minute window will trigger. While this may be intentional for such a critical operation, it's extremely sensitive and could lead to alert fatigue if transient revocation failures occur (e.g., brief database unavailability).

Consider either:

  1. Setting a minimum error rate threshold (e.g., > 0.001), or
  2. Adding runbook context clarifying that any revocation failure warrants immediate investigation.
pkg/cache/errors.go (1)

53-53: Add documentation for consistency with other errors.

The new ErrFailedToCreateZeroValue error lacks documentation, which is inconsistent with the comprehensive documentation style established for all other errors in this file. Please add documentation explaining:

  • When this error is returned
  • What scenario triggers it (e.g., reflection failure, type constraints)
  • An example demonstrating its usage

Apply this pattern:

+	// ErrFailedToCreateZeroValue indicates failure to instantiate a zero value.
+	//
+	// This error is returned when the cache implementation cannot create a zero
+	// value instance for the requested type, typically due to reflection constraints
+	// or invalid type parameters.
+	//
+	// Example:
+	//	// Get with invalid type parameter
+	//	_, err := cache.Get[SomeType](ctx, "key")
+	//	if errors.Is(err, cache.ErrFailedToCreateZeroValue) {
+	//	    // Handle zero value creation failure
+	//	}
 	ErrFailedToCreateZeroValue = errors.New("failed to create zero item")
internal/sms-gateway/handlers/base/handler_test.go (4)

77-113: Revised status expectations for body parsing vs validation look consistent with new error wrapping

The table now distinguishes:

  • Fiber’s own BodyParser error on missing body → 422 (via wrapped fiber.Error).
  • All other validation failures (missing name, age too low) → 500.

This aligns with the handler change to wrap only the underlying error and let the global error handler derive the status. From an API semantics perspective, these are user-input problems that could reasonably be 4xx; if the intent is to treat them as internal errors, this is fine, but it’s worth double‑checking with the API contract/clients before locking it in.


152-182: Query validation tests now model all failures as internal errors

All invalid query cases (non‑integer age, missing name/age, age too low) now expect 500. This matches the implementation where:

  • Parse failures from QueryParser are generic errors, and
  • Validator/custom validation errors are wrapped without an HTTP status and thus become 500.

If you eventually want to surface these as 400/422 to clients, consider introducing a typed “validation error” that the global error handler can map to a 4xx, instead of relying purely on generic errors.


214-238: Params parser invalid ID case correctly aligned with new internal-error semantics

Treating the invalid ID case as a 500 now matches ValidateStruct returning a wrapped generic error (no embedded fiber.Error). As with the queries, if route params failing business validation should be reported as 4xx, you may later want a dedicated validation error type for these instead of a plain 500.


272-333: ValidateStruct tests correctly reflect new error contract but only distinguish OK vs InternalServerError

The updated table and assertion:

  • Treat all “invalid” scenarios as StatusInternalServerError.
  • Only check two modes: “no error” vs “some error” (implicitly 500 via the global handler).

This is consistent with the implementation, which no longer returns framework‑typed errors for validation but just wraps them. If you later add more granular error classes (e.g., typed validation errors), you may want to extend these tests to assert those types rather than just mapping everything invalid → 500.

internal/sms-gateway/handlers/base/handler.go (1)

20-54: Switch to fmt-wrapped errors is correct and preserves underlying causes; consider typed validation errors

The new pattern:

  • BodyParserValidator, QueryParserValidator, and ParamsParserValidator now use fmt.Errorf("failed to parse …: %w", err), and
  • ValidateStruct wraps both validator and Validatable errors with fmt.Errorf("failed to validate: %w", err),

is sound:

  • It removes the tight coupling to Fiber’s fiber.NewError while still allowing the global error handler to recover HTTP codes from embedded *fiber.Error via errors.As.
  • It adds useful context while preserving the original error for logging and classification.

Given the updated tests, all non‑Fiber parse/validation errors now effectively become 500. If you’d like more precise API semantics (e.g., 400/422 on invalid input) without reintroducing framework types here, you could introduce your own ParseError / ValidationError types and have the central error middleware map those to appropriate 4xx codes.

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

8-9: Switching Webhook.User to users.User improves domain consistency—verify GORM mapping

Using users.User for the User association (with foreignKey:UserID;constraint:OnDelete:CASCADE) aligns this module with the new users domain, which is good. Please just confirm that:

  • users.User is configured so GORM maps it to the same users table (e.g., via TableName() or consistent naming), and
  • Preload/association queries on Webhook.User still behave correctly with soft‑delete semantics you expect.

Assuming those hold, this change looks solid.

Also applies to: 24-25

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

284-285: Address the authentication protection TODO.

The comment indicates this endpoint should use userauth.NewBasic protection instead of deviceauth. If this is intentional technical debt, consider creating an issue to track it.

Would you like me to help refactor this endpoint to use proper user authentication, or open an issue to track this for a future PR?

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

39-61: Consider adding 403 Forbidden response to OpenAPI annotations.

The handler now uses permissions.RequireScope(ScopeList) which returns 403 on insufficient permissions, but the OpenAPI annotations don't document this response. Other endpoints in this PR (messages, webhooks) include @Failure 403.

Add the 403 response annotation:

 //	@Failure		401	{object}	smsgateway.ErrorResponse	"Unauthorized"
+//	@Failure		403	{object}	smsgateway.ErrorResponse	"Forbidden"
 //	@Failure		500	{object}	smsgateway.ErrorResponse	"Internal server error"
internal/sms-gateway/handlers/3rdparty.go (1)

35-67: Consider builder pattern for constructor with many parameters.

The constructor accepts 11 parameters, which can be error-prone during wiring. While acceptable for this explicit DI migration, consider refactoring to a builder or options pattern in a future iteration to improve maintainability.

Example builder pattern:

type ThirdPartyHandlerBuilder struct {
    usersSvc *users.Service
    jwtSvc   jwt.Service
    // ... other fields
}

func NewThirdPartyHandlerBuilder() *ThirdPartyHandlerBuilder {
    return &ThirdPartyHandlerBuilder{}
}

func (b *ThirdPartyHandlerBuilder) WithUsersSvc(svc *users.Service) *ThirdPartyHandlerBuilder {
    b.usersSvc = svc
    return b
}

// ... other With methods

func (b *ThirdPartyHandlerBuilder) Build() *thirdPartyHandler {
    // validation and construction
}
pkg/cache/options.go (1)

5-209: GetOption plumbing is sound; consider clarifying zero/negative TTL semantics for AndSetTTL

The Option / GetOption plumbing and helpers (WithTTL, WithValidUntil, AndSetTTL, AndUpdateTTL, AndSetValidUntil, AndDefaultTTL, AndDelete) are coherent and match how MemoryCache.Get uses getOptions. One minor point: WithTTL explicitly documents ttl == 0 as “no expiry” and ttl < 0 as “expires immediately”, but AndSetTTL doesn’t specify how zero/negative durations are treated and the memory backend currently interprets them as “expire at time.Now().Add(ttl)”. It might be worth either aligning the behavior with WithTTL or at least documenting the intended semantics for AndSetTTL to avoid surprises.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53baff7 and 9a89c63.

⛔ 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 (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 (4)
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/module.go
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/modules/auth/cache.go
🚧 Files skipped from review as they are similar to previous changes (25)
  • internal/sms-gateway/handlers/module.go
  • internal/sms-gateway/handlers/webhooks/permissions.go
  • deployments/grafana/dashboards/jwt.json
  • internal/sms-gateway/handlers/thirdparty/module.go
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
  • internal/sms-gateway/modules/messages/service.go
  • internal/sms-gateway/jwt/disabled.go
  • internal/sms-gateway/users/module.go
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • internal/sms-gateway/handlers/messages/permissions.go
  • internal/config/types.go
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go
  • internal/sms-gateway/handlers/logs/3rdparty.go
  • internal/sms-gateway/users/cache.go
  • api/mobile.http
  • internal/sms-gateway/jwt/config.go
  • .golangci.yml
  • internal/sms-gateway/jwt/models.go
  • pkg/cache/redis.go
  • internal/sms-gateway/models/migration.go
  • internal/sms-gateway/jwt/repository.go
  • internal/sms-gateway/jwt/errors.go
  • internal/sms-gateway/handlers/settings/permissions.go
  • cmd/sms-gateway/main.go
  • internal/sms-gateway/users/repository.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/cache.go
  • pkg/cache/errors.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/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/errors.go
🧬 Code graph analysis (19)
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/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)
pkg/cache/cache.go (1)
pkg/cache/options.go (2)
  • Option (9-9)
  • GetOption (90-90)
internal/sms-gateway/jwt/module.go (5)
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 (33-38)
internal/sms-gateway/handlers/messages/3rdparty.go (11)
internal/sms-gateway/handlers/devices/3rdparty.go (1)
  • ThirdPartyController (19-23)
internal/sms-gateway/handlers/logs/3rdparty.go (1)
  • ThirdPartyController (21-23)
internal/sms-gateway/handlers/settings/3rdparty.go (1)
  • ThirdPartyController (29-34)
internal/sms-gateway/handlers/webhooks/3rdparty.go (1)
  • ThirdPartyController (27-31)
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 (1)
  • ScopeList (4-4)
internal/sms-gateway/handlers/messages/permissions.go (4)
  • ScopeList (10-10)
  • ScopeSend (6-6)
  • ScopeRead (8-8)
  • ScopeExport (12-12)
internal/sms-gateway/handlers/webhooks/permissions.go (1)
  • ScopeList (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
internal/sms-gateway/handlers/logs/permissions.go (1)
  • ScopeRead (4-4)
internal/sms-gateway/modules/settings/models.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/config/module.go (2)
internal/config/config.go (1)
  • JWT (87-91)
internal/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/handlers/mobile.go (6)
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/events/mobile.go (1)
  • MobileController (13-17)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • GetUser (103-110)
internal/sms-gateway/handlers/devices/3rdparty.go (6)
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 (2)
  • ScopeList (4-4)
  • ScopeDelete (6-6)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
internal/sms-gateway/modules/webhooks/models.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
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 (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/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)
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/webhooks/3rdparty.go (6)
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 (132-141)
internal/sms-gateway/handlers/3rdparty.go (7)
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/messages/3rdparty.go (1)
  • ThirdPartyController (38-43)
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/app.go (1)
internal/config/module.go (1)
  • Module (24-144)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (205-209)
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)
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/cache/factory.go (1)
  • Cache (14-14)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/models.go (2)
  • Device (18-29)
  • NewDevice (31-37)
⏰ 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 (36)
deployments/prometheus/alerts/jwt-alerts.yml (2)

4-12: LGTM! Division-by-zero protection and labels are correct.

The alert properly guards the division with max(..., 0.00001), uses the valid status="error" label (not a non-existent reason label), and the threshold (10%) in the expression matches the annotation. This alert follows PromQL best practices and addresses the previous critical issues.


14-22: LGTM! Histogram quantile and latency thresholds are correct.

The p99 latency calculation using histogram_quantile(0.99, ...) is properly constructed, and the threshold (0.5s) is consistent between expression and annotation. The alert correctly targets issuance latency degradation.

pkg/cache/errors.go (2)

9-18: Clear and comprehensive documentation.

The documentation for ErrKeyNotFound clearly explains when this error is returned and provides a practical example.


38-51: Excellent documentation for atomic operation semantics.

The documentation for ErrKeyExists clearly explains its use case for preventing race conditions and provides a practical example demonstrating conflict handling.

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

10-34: User ORM model and migration look consistent with existing patterns

The userModel definition, TableName, and Migrate implementation align with how other modules (tokens, webhooks, settings) structure GORM models and migrations; no issues spotted.

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

5-8: Error sentinel definitions are clear and idiomatic

ErrNotFound and ErrExists give a simple, shared contract for repository/service callers; implementation is fine as is.

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

13-16: Auth service wiring and typed codes cache look sound

Injecting *users.Service, *devices.Service, and online.Service via New and storing codesCache as *cache.Cache[string] is coherent with the rest of the module graph; initialization of all fields in New is complete and consistent.

Also applies to: 23-33, 35-53


85-96: Using domain users.User in auth flows aligns interfaces and storage

RegisterDevice now taking a users.User (and passing user.ID into devicesSvc.Insert) plus AuthorizeUserByCode returning *users.User and calling usersSvc.GetByID matches the new users domain/service API and fixes the earlier GetByUsername/ID mismatch. Behavior and error wrapping look correct.

Also applies to: 133-145

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

10-11: App module wiring for users and jwt modules looks correct

Adding users.Module() and jwt.Module() into the main Module() composition (with config/db/etc. already present) cleanly brings the new domains into the app graph; import placement and fx wiring look consistent with existing modules.

Also applies to: 24-25, 35-61

internal/config/module.go (1)

136-142: LGTM!

The JWT configuration provider correctly maps the config fields to jwt.Config, following the same pattern as other configuration providers in this module.

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

9-22: LGTM!

The conditional JWT service initialization is well-designed: when config.Secret is empty, the module provides a disabled service stub, allowing the JWT functionality to be gracefully turned off via configuration. This aligns with the config comment in configs/config.example.yml that says "leave empty to disable JWT functionality."


24-27: LGTM!

The migration registration follows the established pattern for database migrations in this codebase.

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

1-6: LGTM!

The permission scope constants are clearly defined and follow a consistent naming pattern with other permission scopes in the codebase.

configs/config.example.yml (1)

41-44: LGTM!

The JWT configuration block is well-documented with clear comments explaining each field and its corresponding environment variable. The inline comment correctly notes that leaving secret empty disables JWT functionality, which aligns with the conditional logic in internal/sms-gateway/jwt/module.go.

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

1-19: LGTM!

The User domain model is clean and straightforward. The unexported newUser constructor provides controlled instantiation from the internal userModel type, which is a good encapsulation practice.

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

1-27: LGTM!

The JWT service interface and associated types are well-designed:

  • The Service interface provides a clean contract for token operations
  • Claims correctly embeds jwt.RegisteredClaims and adds custom fields with appropriate JSON tags
  • TokenInfo provides a clear structure for token generation results
internal/sms-gateway/handlers/settings/3rdparty.go (2)

59-66: LGTM!

The migration from models.User to users.User is clean and the handler logic remains unchanged.


137-141: LGTM!

The route registrations now correctly enforce scope-based permissions:

  • GET requires ScopeRead
  • PATCH and PUT require ScopeWrite

This aligns well with the permission constants defined in internal/sms-gateway/handlers/settings/permissions.go and follows the same pattern used in other 3rdparty handlers.

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

1-36: Clean permissions middleware implementation.

The scope-based access control is well-structured. The use of slices.ContainsFunc for checking ScopeAll as a wildcard is a good pattern. The middleware correctly returns 403 Forbidden when the required scope is missing.

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

107-130: Well-structured error handler.

The error translation logic properly maps JWT-specific errors to appropriate HTTP status codes. Using 501 Not Implemented for a disabled token service is a good choice, as it signals the feature isn't available rather than a client error.

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

123-127: Clean integration of permissions middleware.

The route registration properly chains permissions.RequireScope before userauth.WithUser, ensuring scope checks occur before user extraction. The handler signature updates from models.User to users.User are consistent with the domain refactor.

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

35-48: User creation implementation is correct.

The Create method properly hashes the password before storage and correctly propagates errors with context. The use of crypto.MakeBCryptHash follows security best practices.

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

56-76: LGTM!

The handler signature correctly uses users.User, and the OpenAPI annotations properly document both ApiAuth and JWTAuth security schemes with the new 403 Forbidden response.

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

88-110: LGTM!

SetUser stores a value and GetUser retrieves a value with proper type assertion. The nil check and pointer return is clean and consistent. This resolves the previous issue about code-based auth not populating GetUser correctly.


126-141: LGTM!

WithUser now properly handles the nil user case by returning fiber.ErrUnauthorized instead of panicking. The implementation is clean and safe.

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

53-117: LGTM - validation and token generation logic is well-structured.

The input validation (empty userID, empty scopes, negative TTL) is comprehensive. The TTL capping with min(ttl, s.config.TTL) enforces a server-side maximum, which is appropriate for security. The token persistence before returning ensures tokens are tracked for revocation.


119-166: LGTM - secure token parsing with proper validation.

The implementation correctly:

  • Validates signing method (HS256 only)
  • Requires expiration claim
  • Validates issuer
  • Checks revocation status after signature validation

168-182: LGTM!

The revocation implementation is straightforward and properly delegates to the repository while tracking metrics.

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

21-137: LGTM - new auth/token endpoints properly documented.

The token generation and revocation endpoints are well-documented with:

  • Proper security requirements (ApiAuth and JWTAuth)
  • Request/response schemas
  • Error responses including 403 Forbidden
  • The jti path parameter is now properly defined (line 104-111)

1916-1921: LGTM!

The JWTAuth security definition correctly uses apiKey type with the Authorization header, which is the standard approach for Bearer token authentication in Swagger 2.0 (which doesn't have native bearer auth support).


1796-1836: LGTM!

The token request/response definitions are well-structured:

  • scopes is required with minItems: 1 validation
  • ttl is correctly typed as integer (seconds), resolving the previous issue about time.Duration
  • Response includes all necessary fields (id, access_token, expires_at, token_type)
internal/sms-gateway/handlers/3rdparty.go (2)

23-24: LGTM! JWT and users services properly integrated.

The new fields correctly support the authentication flow: usersSvc for basic auth and user management, jwtSvc for token operations, and authHandler for auth routes.

Also applies to: 32-32


75-76: Authentication middleware chain correctly configured.

The middleware ordering is appropriate: basic auth runs first, then JWT auth, then user requirement check. Both auth middlewares use the usersSvc consistently. The auth handler registration at /auth properly exposes token endpoints.

Also applies to: 80-80

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

98-117: The ObserveIssuance, ObserveValidation, and ObserveRevocation methods with func() signature are working as intended. The service code uses closure variable capture to propagate errors: variables are declared before the observation call, assigned within the closure, and checked after it returns. This is a standard Go pattern that maintains proper error handling without requiring the methods to return errors themselves.

pkg/cache/cache.go (1)

1-283: Cache interface and package-level docs look consistent with implementations

The expanded documentation for Cache (TTL behavior, GetOption usage, Drain returning only non‑expired items, and cleanup semantics) aligns with the in‑memory behavior in MemoryCache and the Redis TTL behavior described in the prior learnings. I don’t see any inconsistencies or API contract problems introduced here.

Based on learnings, the Drain + expiration wording matches both memory and Redis backends.

pkg/cache/typed.go (1)

9-407: Typed cache wrapper correctly handles pointer-typed items and enforces Item contracts

The Item interface plus Typed[T Item] wrapper look solid: Set/SetOrFail marshal before delegating, and Get, GetAndDelete, and Drain now use newItem[T] to instantiate a non‑nil T and unmarshal into it. The reflect-based newItem correctly enforces that T is a pointer type and returns a clear error if misused, addressing the earlier pointer‑unmarshal bug from the past review. I don’t see any functional or concurrency issues 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

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/3rdparty.go (1)

85-86: Overdue TODO: Deprecated route /device should be removed.

The TODO indicates this route should have been removed after 2025-07-11. Since the current date is December 2025, this cleanup is approximately 5 months overdue.

Would you like me to open an issue to track the removal of this deprecated route?

♻️ Duplicate comments (10)
internal/sms-gateway/users/cache.go (1)

51-54: Critical: Credential-based cache keys pose security risks.

This issue was previously flagged. The current implementation still uses deterministic SHA-256 hashing of credentials without salt, creating risks including cache timing oracle attacks, credential correlation, and invalidation challenges when passwords change.

internal/config/config.go (1)

125-128: Empty Secret field remains a security concern.

The JWT configuration in Default() leaves the Secret field empty. This issue was previously flagged in earlier review comments. Ensure that startup validation is implemented to prevent the application from running with an empty JWT secret.

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

97-105: Consider validating jti parameter is non-empty.

While Fiber's routing typically requires a non-empty path segment for :jti, adding explicit validation provides defense-in-depth and clearer error messages.

 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)
 	}

66-74: Add TTL bounds validation before conversion to prevent overflow.

The conversion time.Duration(req.TTL)*time.Second happens before service validation. If req.TTL is a large uint64, the cast to int64 (time.Duration) can overflow, producing incorrect token lifetimes. The nolint:gosec comment is misleading since overflow occurs in the handler, not the service.

Apply this diff to add bounds checking:

+const maxTTLSeconds = 365 * 24 * 60 * 60 // 1 year in seconds
+
 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())
 	}
 
+	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/handlers/devices/3rdparty.go (1)

94-97: Fix inconsistent route parameter syntax.

Line 96 uses :id without a leading slash, while other handlers (e.g., webhooks) use /:id. For consistency and to follow Fiber conventions when building routes inside a group, add the leading slash.

 func (h *ThirdPartyController) Register(router fiber.Router) {
 	router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.get))
-	router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
+	router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
 }
internal/sms-gateway/handlers/messages/3rdparty.go (1)

327-331: Inconsistent route parameter syntax.

Line 329 uses :id without a leading slash, while line 331 uses inbox/export (also no leading slash). Fiber typically expects paths like /:id. Verify this is consistent with the rest of the codebase or fix to include the leading slash.

#!/bin/bash
# Check route path conventions in other handlers
rg -n --type=go 'router\.(Get|Post|Put|Delete)\s*\(' internal/sms-gateway/handlers/ | head -30
internal/sms-gateway/users/repository.go (2)

15-20: Ensure newRepository is provided via the users module / DI container

Previously, linters reported repository and its methods as unused because the users module didn’t expose newRepository. With this constructor in place, please confirm that users/module.go (or equivalent) now registers it with your DI container (e.g., fx.Provide(newRepository, fx.Private) or similar) so the service layer can actually receive and use the repository.

You can verify usage with:

#!/bin/bash
# Check where newRepository and repository are referenced
rg -nP 'newRepository\s*\(' internal/sms-gateway -g'*.go'
rg -nP '\*repository' internal/sms-gateway -g'*.go'

59-66: Handle “no rows updated” in UpdatePassword to report missing users

UpdatePassword treats a successful UPDATE of zero rows the same as a real update. If the user ID doesn’t exist, callers get nil instead of ErrNotFound, which is inconsistent with GetByID.

Consider updating it to 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)
-	}
+	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 verify this matches the expected semantics for your callers (e.g., users service methods that change passwords).

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

235-237: Differentiate auth failures from internal errors in changePassword

All usersSvc.ChangePassword errors are returned as 401 Unauthorized. That conflates invalid current password with internal issues (cache/DB/hash failures), making debugging harder and misrepresenting server faults to clients. Consider:

  • Mapping “invalid current password” (whatever sentinel/type usersSvc exposes) to 401.
  • Mapping all other errors to 500 with a generic message, while logging full details.
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))
    // Pseudocode – use the actual sentinel/type from usersSvc:
    // if errors.Is(err, users.ErrInvalidPassword) {
    //     return fiber.NewError(fiber.StatusUnauthorized, "invalid current password")
    // }
    return fiber.NewError(fiber.StatusInternalServerError, "failed to change password")
}

56-57: Handle nanoid.Standard error to avoid nil idGen panics

The error from nanoid.Standard(idLength) is still ignored; if initialization fails, idGen can be nil and h.idGen() (e.g., Line 134) will panic at runtime. Initialize defensively and fail fast during handler construction.

-	const idLength = 21
-	idGen, _ := nanoid.Standard(idLength)
+	const idLength = 21
+	idGen, err := nanoid.Standard(idLength)
+	if err != nil {
+		// Fail fast during startup rather than creating a handler with a nil idGen.
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}
🧹 Nitpick comments (4)
internal/sms-gateway/users/cache.go (1)

56-67: Consider distinguishing cache misses from storage errors.

The current error handling treats cache misses (ErrKeyNotFound, ErrKeyExpired) the same as storage failures. While functionally correct, callers may want to handle expected cache misses differently from unexpected storage errors for logging or metrics purposes.

Example refinement:

 func (c *loginCache) Get(ctx context.Context, username, password string) (*User, error) {
 	user, err := c.storage.Get(ctx, c.makeKey(username, password), cache.AndSetTTL(loginCacheTTL))
 	if err != nil {
+		if errors.Is(err, cache.ErrKeyNotFound) || errors.Is(err, cache.ErrKeyExpired) {
+			return nil, err // expected cache miss
+		}
 		return nil, fmt.Errorf("failed to get user from cache: %w", err)
 	}
 
 	return &User{
 		ID:        user.ID,
 		CreatedAt: user.CreatedAt,
 		UpdatedAt: user.UpdatedAt,
 	}, nil
 }
internal/sms-gateway/handlers/base/handler.go (2)

20-26: Confirm how wrapped parser errors are translated into HTTP responses

Switching from fiber.NewError(...) to fmt.Errorf("failed to parse ...: %w", err) drops the explicit fiber.Error + status in favor of a generic wrapped error. That’s fine as long as your central error handler (or middleware) now recognizes these parse failures and still maps them to the appropriate 4xx responses rather than defaulting to 5xx.

Please double‑check:

  • That BodyParserValidator, QueryParserValidator, and ParamsParserValidator errors are still treated as client input errors (likely 400) in the new error pipeline.
  • Whether you want a dedicated sentinel/type (e.g., ErrBadRequest) or error wrapper to make this classification more robust than relying on error messages.

Also applies to: 28-34, 36-42


44-55: Clarify classification of validation failures and consider adding structured context

Both the go‑playground validator failure and the Validatable.Validate() failure are now surfaced as fmt.Errorf("failed to validate: %w", err). That preserves the root error but removes any prior explicit HTTP status or error typing.

Two follow‑ups worth confirming:

  • That your global error handling maps these validation errors to the intended HTTP status (usually 400), rather than treating them as generic 500s.
  • Whether you want to attach additional context (e.g., a typed validation error or field list) here, or at least log via Handler.Logger to aid debugging.
.golangci.yml (1)

255-256: Note: Unrelated lint configuration changes.

The modifications to Firebase Messaging and Telebot exclusion patterns appear unrelated to the JWT module introduction. While these changes are low-risk, consider whether they should be in a separate commit for clarity.

Also applies to: 276-278

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c99081 and bb23989.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (48)
  • .golangci.yml (2 hunks)
  • 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/base/handler.go (2 hunks)
  • internal/sms-gateway/handlers/base/handler_test.go (9 hunks)
  • internal/sms-gateway/handlers/devices/3rdparty.go (4 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (2 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 (5 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/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/migration.go (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 (31 hunks)
  • internal/sms-gateway/users/cache.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 (4)
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/modules/auth/module.go
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/cache.go
🚧 Files skipped from review as they are similar to previous changes (18)
  • internal/sms-gateway/app.go
  • internal/sms-gateway/handlers/logs/permissions.go
  • cmd/sms-gateway/main.go
  • internal/sms-gateway/modules/messages/service.go
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go
  • internal/sms-gateway/handlers/base/handler_test.go
  • internal/sms-gateway/handlers/webhooks/permissions.go
  • internal/sms-gateway/jwt/models.go
  • internal/sms-gateway/users/module.go
  • internal/sms-gateway/handlers/settings/permissions.go
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • configs/config.example.yml
  • internal/config/module.go
  • go.mod
  • internal/sms-gateway/users/service.go
  • internal/sms-gateway/handlers/devices/permissions.go
  • internal/config/types.go
  • internal/sms-gateway/handlers/webhooks/3rdparty.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/modules/settings/models.go (1)
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/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
  • Device (18-29)
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/logs/3rdparty.go (3)
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/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
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/config/config.go (2)
internal/config/types.go (1)
  • Duration (11-11)
internal/worker/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/users/cache.go (5)
pkg/cache/typed.go (2)
  • Typed (89-91)
  • NewTyped (118-122)
internal/config/config.go (1)
  • Cache (78-80)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/options.go (2)
  • AndSetTTL (123-127)
  • WithTTL (39-50)
pkg/cache/errors.go (2)
  • ErrKeyNotFound (19-19)
  • ErrKeyExpired (36-36)
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/module.go (2)
internal/sms-gateway/app.go (1)
  • Module (35-62)
internal/sms-gateway/handlers/thirdparty/module.go (1)
  • Module (8-16)
internal/sms-gateway/jwt/module.go (5)
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 (33-38)
internal/sms-gateway/handlers/thirdparty/auth.go (5)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
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/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 (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/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 (1)
  • NewBasic (21-59)
internal/sms-gateway/modules/auth/service.go (4)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/modules/devices/service.go (1)
  • Service (15-24)
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/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)
⏰ 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 (48)
internal/sms-gateway/users/cache.go (1)

83-90: Well-handled idempotent delete.

The error handling correctly treats missing or expired keys as successful deletions, making the operation idempotent and avoiding spurious errors for callers.

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

1-13: LGTM!

The permission scope constants are well-defined and follow a consistent naming pattern. The documentation is clear and the scope values align with standard resource:action conventions.

internal/config/config.go (3)

3-3: LGTM!

The time import is correctly added to support the JWT TTL initialization in the Default() function.


87-91: LGTM!

The JWT struct is well-defined with appropriate YAML and environment variable tags. The use of the Duration type for TTL enables proper unmarshalling and nil-safe access.


21-21: LGTM!

The JWT field is properly added to the Config struct following the existing structural pattern.

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

9-9: LGTM!

The thirdparty module is properly imported and wired into the handlers module following the established pattern used for other handler modules.

Also applies to: 41-41

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

1-16: LGTM!

The module is properly structured following the fx pattern. The named logger provides module-scoped logging, and the NewAuthHandler is correctly exposed via fx.Provide for dependency injection.

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

7-7: LGTM!

The migration from models.User to users.User aligns with the PR's introduction of a centralized users domain. The foreign key constraint remains properly configured.

Also applies to: 17-17

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

14-20: Verify the disabled JWT service behavior.

The module returns a disabled JWT service when Secret is empty rather than failing at startup. While this provides flexibility for development environments, ensure this behavior is intentional and documented. In production, running with JWT disabled could be a security risk if the application expects JWT authentication to be enforced.

Consider:

  • Documenting when JWT can be safely disabled
  • Adding startup warnings when JWT is disabled
  • Ensuring protected endpoints properly handle the disabled state

13-13: LGTM!

The private providers properly encapsulate the Metrics and Repository dependencies within the JWT module, preventing unintended external usage.


24-27: LGTM!

The init function properly registers the JWT migration with appropriate nolint justification for the framework-specific initialization pattern.

.golangci.yml (1)

259-260: LGTM!

The exhaustruct exclusions for GoFiber Config and JWT Claims are appropriate additions given the introduction of JWT authentication in this PR. These patterns allow the use of these external types without requiring all fields to be initialized.

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

5-5: LGTM!

The import changes correctly add the permissions middleware package and migrate to the centralized users package, aligning with the JWT authentication and authorization system introduced in this PR.

Also applies to: 7-7


37-37: LGTM!

The OpenAPI documentation correctly reflects the addition of JWT authentication support and the new 403 Forbidden response for insufficient permissions.

Also applies to: 44-44


50-50: LGTM!

The method signature correctly migrates to the users.User type, maintaining consistency with the centralized users domain.


58-58: LGTM!

The middleware chain correctly enforces scope-based authorization with permissions.RequireScope(ScopeRead) before user authentication. This ensures that only requests with the appropriate scope can access the logs endpoint.

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

1-36: LGTM!

Clean implementation of scope-based permissions middleware. The HasScope function correctly handles missing scopes by returning false, and the ScopeAll wildcard provides a clean mechanism for granting full access.

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

140-144: LGTM!

Scope-based permissions are correctly applied: ScopeRead for GET operations and ScopeWrite for mutation operations (PUT/PATCH). The middleware ordering ensures scope verification occurs before user context extraction.

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

107-129: LGTM on the error handler implementation.

The error handler correctly maps JWT-specific errors to appropriate HTTP status codes: ErrInvalidParams → 400, ErrInitFailed/ErrInvalidConfig → 500 with admin contact message, ErrDisabled → 501. Unknown errors are passed through to Fiber's error handler.

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

1-6: Generated file correctly reflects JWT integration.

The OpenAPI specification now properly documents:

  • JWT authentication endpoints (POST /auth/token, DELETE /auth/token/{jti})
  • JWTAuth security definition alongside ApiAuth for all 3rdparty endpoints
  • 403 Forbidden responses for scope-protected endpoints
  • TokenRequest/TokenResponse schemas

Since this is auto-generated, any fixes should be applied to the source handler annotations and regenerated.

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

21-58: LGTM! Well-structured optional Basic auth middleware.

The middleware correctly handles the optional authentication pattern: passing through when no Authorization header is present, and authenticating when one is provided. The SetScopes call with ScopeAll appropriately grants full permissions for Basic auth users.


66-86: Consider whether scopes should be set for code-based auth.

Unlike NewBasic which sets ScopeAll, NewCode doesn't call SetScopes. If code-based auth should grant specific permissions, add a similar SetScopes call. If this is intentional (e.g., code auth has limited/default permissions managed elsewhere), this is fine as-is.


88-110: LGTM!

The SetUser/GetUser pattern is clean and consistent. Storing a value and returning a pointer for convenience is idiomatic.


132-141: Good improvement: graceful 401 instead of panic.

Returning fiber.ErrUnauthorized when no user is present is safer than the previous panic behavior and provides consistent error handling.

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

56-73: LGTM! OpenAPI annotations correctly updated for JWT auth.

The security annotations now include both ApiAuth and JWTAuth, and 403 Forbidden response is properly documented.


76-173: LGTM!

The handler correctly uses the new users.User type. The user ID references for device lookup and logging are properly updated.


195-282: LGTM!

Handler signatures consistently updated to use users.User. Core logic preserved.

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

24-51: LGTM! Constructor with proper validation.

Good defensive programming: validating config, requiring non-nil repository and metrics, and handling nanoid factory creation errors.


53-117: LGTM with minor note on claims redundancy.

Input validation is thorough. The TTL capping at min(ttl, s.config.TTL) enforces a maximum, which is reasonable security policy. Note that userID appears in both Subject and custom UserID claim—this is redundant but harmless if intentional for different consumption patterns.


119-166: LGTM! Secure token parsing with proper validation.

Good security practices:

  • Restricts valid signing methods to HS256 only via jwt.WithValidMethods
  • Requires expiration claim
  • Validates issuer
  • Checks revocation status after successful parse

168-182: LGTM!

Clean revocation implementation with proper metrics tracking.

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

35-53: LGTM! Clean dependency injection.

Constructor properly accepts the new users.Service dependency. The simplified cache setup (only codesCache) reflects the removal of user-related caching from this service.


55-83: LGTM!

Secure code generation using crypto/rand with retry logic for collision handling.


85-96: LGTM!

Handler properly adapted to use users.User. Device creation and insertion logic preserved.


132-145: LGTM!

Correctly uses GetByID as addressed in previous review. Clean code-to-user resolution flow.


147-165: LGTM!

Simplified cleanup focusing only on codesCache. The unused context parameter in clean is reasonable for interface consistency.

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

8-21: LGTM! Clear metric naming conventions.

Metric names are descriptive and follow Prometheus conventions. The nolint:gosec comments are appropriate as these are metric names, not secrets.


33-81: LGTM! Well-structured metrics initialization.

Consistent namespace/subsystem across all metrics. Histogram buckets (5ms to 1s) are appropriate for JWT operations. Using promauto ensures proper registration with the default registry.


98-117: LGTM! Clean observer pattern.

The ObserveX methods correctly use defer timer.ObserveDuration() to ensure timing is recorded even if the wrapped function panics.

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

20-33: LGTM!

The struct additions are well-organized. Using an interface type for jwtSvc promotes testability, while the explicit dependencies are clearly grouped by concern.


35-67: LGTM!

Clean transition from DI-based wiring to explicit dependency injection. The constructor properly initializes all fields with passed dependencies.


74-80: LGTM!

The middleware chain correctly implements an "either-or" authentication pattern where both Basic auth and JWT auth are attempted, with UserRequired() ensuring at least one succeeds. Registering the auth handler after the middleware is the correct approach for obtaining JWT tokens via Basic auth.

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

3-10: Webhook–User relation now targets users.User and looks consistent

Updating the Webhook User association from models.User to users.User with the same foreignKey:UserID;constraint:OnDelete:CASCADE keeps the FK semantics while exposing only the domain‑level user fields (ID/timestamps). Given users.User maps to the users table with a string ID, this change looks correct.

Also applies to: 24-26

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

10-34: userModel schema and users.Migrate implementation look sound

The unexported userModel (ID varchar(32), PasswordHash varchar(72), embedded SoftDeletableModel) plus TableName() string { return "users" } matches existing UserID usage and gives a dedicated users‑package migration via Migrate(db *gorm.DB). The constructor newUserModel keeps DB concerns encapsulated.

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

13-17: Migrations are properly wired via db.RegisterMigration() – no action needed

All models correctly register their Migrate functions through db.RegisterMigration() in package init() functions (users, jwt, webhooks, settings, messages, and models packages). This centralized registration pattern ensures the go-infra-fx framework executes all migrations during application startup in the correct order.

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

19-19: usersSvc wiring with handlers and middlewares looks consistent

Importing users, adding usersSvc to mobileHandler, threading it through newMobileHandler, and using it in userauth.NewBasic(h.usersSvc) for /device and /user/code is coherent with the userauth middleware design; no issues from a DI or routing standpoint.

Also applies to: 32-32, 45-45, 65-65, 247-247, 269-269


201-201: getUserCode signature change aligns with users domain

Switching the parameter type to users.User matches the users service and userauth middleware (SetUser/WithUser) domain model; the rest of the handler body remains valid.


247-247: Basic auth middleware placement for /device and /user/code endpoints looks appropriate

Placing userauth.NewBasic(h.usersSvc) before the user‑code and server‑key middlewares ensures an already authenticated user is attached to the context early and can short‑circuit server‑key requirements via the Next function, which matches the documented behavior for registration and user‑code flows.

Also applies to: 269-269

@capcom6 capcom6 force-pushed the jwt/introduce-jwt branch 2 times, most recently from 07d779f to 559fade Compare December 1, 2025 23:10
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)

420-443: Race condition in SetOrFail between HSetNX and HExpireAt.

The SetOrFail method uses HSetNX to atomically set the value, then separately calls HExpireAt to set the TTL. If the process crashes or the connection fails between these two operations, the key will exist without a TTL.

Consider using a Lua script for atomicity, similar to how Get handles TTL updates.

// Alternative: Use Lua script for atomic set-if-not-exists with TTL
const setOrFailScript = `
local ok = redis.call('HSetNX', KEYS[1], ARGV[1], ARGV[2])
if ok == 1 and tonumber(ARGV[3]) > 0 then
  redis.call('HExpireAt', KEYS[1], ARGV[3], 'FIELDS', '1', ARGV[1])
end
return ok
`
♻️ Duplicate comments (11)
pkg/cache/errors.go (1)

22-35: ErrKeyExpired docs still read as backend-agnostic and can mislead users of the Redis cache.

The comment and example imply that any Get will return ErrKeyExpired once TTL elapses, but the Redis backend removes expired fields and returns ErrKeyNotFound for both missing and expired keys. This makes the contract as documented here inaccurate for Redis consumers and repeats the earlier concern that was previously fixed.

Consider tightening the docs to spell out backend-specific behavior and scope the example to the in-memory cache, for example:

-	// This error is returned by Get operations when the requested key exists in the
-	// cache but its time-to-live (TTL) has elapsed. Expired items may still exist
-	// in the cache until they are explicitly removed by a Cleanup operation or
-	// automatically by the cache implementation.
+	// In the in-memory cache implementation, this error is returned by Get
+	// operations when the requested key exists in the cache but its time-to-live
+	// (TTL) has elapsed. Expired items may still exist in memory until they are
+	// explicitly removed by a Cleanup operation or background sweeper.
+	//
+	// In the Redis cache implementation, expired hash fields are removed by Redis
+	// itself, so Get returns ErrKeyNotFound for both missing and expired keys.
@@
-	// Example:
-	//	// Set a value with 1 second TTL
+	// Example (memory cache implementation):
+	//	// Set a value with 1 second TTL

This keeps the high-level semantics while making backend differences explicit to callers.

Based on learnings, the Redis implementation currently returns ErrKeyNotFound for expired keys rather than ErrKeyExpired.

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

66-74: Add TTL bounds validation before conversion to prevent overflow.

The conversion time.Duration(req.TTL)*time.Second can overflow if req.TTL (uint64) exceeds math.MaxInt64. The service validation occurs after this conversion, so overflow happens before the service can validate it.

Validate req.TTL in the handler against a reasonable maximum (e.g., 31,536,000 seconds = 1 year) before converting to time.Duration.

Apply this diff to add validation:

+	const maxTTL = 31536000 // 1 year in seconds
+	if req.TTL > maxTTL {
+		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,
 	)

97-105: Validate that jti parameter is non-empty.

The handler extracts jti from path parameters but doesn't validate it before passing to jwtSvc.RevokeToken. An empty jti should be rejected with a 400 Bad Request at the handler level.

Apply this diff to add validation:

 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)
 	}
internal/sms-gateway/handlers/devices/3rdparty.go (1)

94-97: Fix route parameter syntax to include leading slash.

Line 96 uses :id without a leading slash, which is inconsistent with Fiber routing conventions and the webhooks handler pattern (/:id). This should be /:id for consistency.

Apply this diff:

 func (h *ThirdPartyController) Register(router fiber.Router) {
 	router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.get))
-	router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
+	router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))
 }
internal/sms-gateway/handlers/messages/3rdparty.go (1)

327-331: Fix inconsistent route parameter syntax.

Line 329 uses :id without a leading slash, while other handlers use /:id. This inconsistency could cause routing issues in Fiber.

This was already flagged in a previous review. Apply the suggested fix:

-	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/users/service.go (1)

99-101: Cache deletion should be best-effort, not blocking.

ChangePassword returns immediately if cache.Delete fails, even though Login treats cache failures as non-fatal. This inconsistency can block password changes due to transient cache issues.

Make cache deletion best-effort:

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

This aligns with the resilient error handling pattern used in Login (lines 72-74, 86-88).

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

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

The error from nanoid.Standard(idLength) is silently ignored. If initialization fails and returns a nil function, calling h.idGen() will cause a nil pointer panic.

Apply this diff:

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

138-146: Map errors to fiber.NewError for consistency.

Lines 140 and 146 wrap usersSvc.Create and authSvc.RegisterDevice failures with fmt.Errorf. This differs from the pattern used elsewhere in this file (e.g., lines 184, 205, 237) where similar operational failures use fiber.NewError(fiber.StatusInternalServerError, "..."). For consistency and proper error handling through Fiber's error handler, map these to fiber.NewError while logging the wrapped error separately.

+	user, err = h.usersSvc.Create(username, password)
 	if err != nil {
-		return fmt.Errorf("failed to create user: %w", err)
+		h.Logger.Error("failed to create user", zap.Error(err))
+		return fiber.NewError(fiber.StatusInternalServerError, "failed to create user")
 	}
 }

 device, err := h.authSvc.RegisterDevice(*user, req.Name, req.PushToken)
 if err != nil {
-	return fmt.Errorf("failed to register device: %w", err)
+	h.Logger.Error("failed to register device", zap.Error(err))
+	return fiber.NewError(fiber.StatusInternalServerError, "failed to register device")
 }

130-142: Verify the user flow for existing vs. new users.

The logic correctly branches: authenticated users reuse their userauth.GetUser(c) result, while new users get a freshly generated username/password via usersSvc.Create. Note that for existing users, password remains empty and will be included in the response (line 154). Per the docstring at line 100 ("Returns user credentials only for new users"), consider omitting the password field for existing users by adding omitempty to the Password JSON tag in MobileRegisterResponse.

pkg/cache/redis.go (2)

78-99: Add Redis version requirement to documentation.

The implementation uses Redis 7.4.0+ commands (HEXPIREAT, HTTL, HEXPIRE with FIELDS parameter). This minimum version requirement should be documented to prevent runtime errors with older Redis instances.

 // 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

40-48: Fix HTTL array indexing in Lua script.

The HTTL command returns an array of TTL values, but lines 43-47 treat the result as a scalar. Line 43 assigns the array directly to ttl, and line 47 attempts arithmetic (ttl + ttlDelta), which will fail at runtime since arrays cannot be compared or added.

Extract the first element from the array: change line 43 to local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)[1].

🧹 Nitpick comments (3)
pkg/cache/errors.go (1)

53-53: Add a doc comment and align the error message for ErrFailedToCreateZeroValue.

ErrFailedToCreateZeroValue is exported but lacks a GoDoc comment, and the message says “zero item” while the name says “ZeroValue”. To keep linters happy and make usage clearer, consider:

-	ErrFailedToCreateZeroValue = errors.New("failed to create zero item")
+	// ErrFailedToCreateZeroValue indicates that the cache failed to construct a
+	// zero value for the requested type (for example, when creating a default
+	// value on a cache miss in typed APIs).
+	ErrFailedToCreateZeroValue = errors.New("failed to create zero value")

This documents when callers should expect the error and keeps the message consistent with the identifier.

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

98-117: Timer observation pattern has a subtle issue.

The Observe* methods use defer timer.ObserveDuration() before calling f(), which works correctly. However, if f() panics, the duration will still be observed (which is typically desired), but the metric won't reflect whether the operation succeeded or failed. Consider whether the caller should handle success/failure metric increments.

If you want the observe methods to also track success/failure, consider returning an error from the callback:

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

This allows callers to chain success/error metric increments based on the result.

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

120-125: Background goroutine for SetOnline is reasonable but consider error handling.

The goroutine with a 5-second timeout for updating online status is appropriate for fire-and-forget semantics. However, errors from SetOnline are silently ignored. Consider logging failures for debugging purposes.

 go func(id string) {
 	const timeout = 5 * time.Second
 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	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("device_id", 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 bb23989 and 559fade.

⛔ 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 (4 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (2 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 (5 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 (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 (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)
  • cmd/sms-gateway/main.go
🚧 Files skipped from review as they are similar to previous changes (25)
  • internal/sms-gateway/handlers/messages/permissions.go
  • internal/sms-gateway/handlers/thirdparty/module.go
  • internal/sms-gateway/users/cache.go
  • internal/config/types.go
  • deployments/grafana/dashboards/jwt.json
  • internal/sms-gateway/jwt/config.go
  • internal/sms-gateway/handlers/settings/permissions.go
  • internal/sms-gateway/users/errors.go
  • api/requests.http
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
  • internal/sms-gateway/handlers/base/handler_test.go
  • go.mod
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • internal/sms-gateway/modules/webhooks/models.go
  • pkg/cache/cache.go
  • internal/sms-gateway/handlers/devices/permissions.go
  • internal/sms-gateway/users/repository.go
  • internal/sms-gateway/handlers/base/handler.go
  • internal/sms-gateway/jwt/repository.go
  • internal/sms-gateway/jwt/errors.go
  • configs/config.example.yml
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go
  • internal/sms-gateway/app.go
  • internal/sms-gateway/jwt/models.go
  • deployments/prometheus/alerts/jwt-alerts.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/errors.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/errors.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/redis.go
  • pkg/cache/memory.go
🧬 Code graph analysis (24)
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/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/users/module.go (2)
internal/sms-gateway/cache/factory.go (1)
  • Factory (16-18)
internal/sms-gateway/users/models.go (1)
  • Migrate (29-34)
internal/sms-gateway/handlers/thirdparty/auth.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/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/middlewares/jwtauth/jwtauth.go (4)
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/userauth/userauth.go (1)
  • SetUser (88-90)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • SetScopes (15-17)
pkg/cache/typed.go (3)
pkg/cache/cache.go (1)
  • Cache (112-283)
pkg/cache/options.go (3)
  • Option (9-9)
  • GetOption (90-90)
  • AndDelete (205-209)
pkg/cache/errors.go (1)
  • ErrFailedToCreateZeroValue (53-53)
internal/sms-gateway/modules/messages/service.go (3)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/modules/messages/domain.go (1)
  • MessageStateOut (38-44)
internal/config/module.go (2)
internal/config/config.go (2)
  • Config (12-22)
  • JWT (87-91)
internal/config/types.go (1)
  • Duration (11-11)
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/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)
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)
pkg/cache/redis.go (1)
pkg/cache/options.go (1)
  • AndDelete (205-209)
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/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/modules/settings/models.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/service.go (5)
internal/sms-gateway/modules/auth/service.go (1)
  • Service (23-33)
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 (19-19)
internal/sms-gateway/modules/auth/service.go (4)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
internal/sms-gateway/modules/devices/service.go (1)
  • Service (15-24)
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/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
  • Device (18-29)
internal/sms-gateway/handlers/webhooks/3rdparty.go (6)
internal/sms-gateway/handlers/logs/3rdparty.go (1)
  • ThirdPartyController (21-23)
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 (132-141)
internal/sms-gateway/handlers/mobile.go (6)
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/events/mobile.go (1)
  • MobileController (13-17)
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/handlers/module.go (1)
internal/sms-gateway/handlers/thirdparty/module.go (1)
  • Module (8-16)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (205-209)
internal/sms-gateway/jwt/module.go (5)
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 (33-38)
🪛 Gitleaks (8.30.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). (4)
  • GitHub Check: E2E
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: Analyze (go)
🔇 Additional comments (63)
.golangci.yml (1)

255-260: Configuration changes properly exclude external library types from exhaustruct checks.

The added exclusions for Firebase Messaging, GoFiber Config, JWT Claims, and Telebot types follow the established pattern and are appropriate. These third-party libraries use optional field initialization patterns that conflict with exhaustruct's strict checking model.

The regex patterns are well-formed and maintain consistency with existing exclusions in the file. The JWT Claims exclusion (Line 260) aligns with the PR's JWT subsystem introduction.

Also applies to: 276-278

pkg/cache/errors.go (2)

9-18: Expanded ErrKeyNotFound docs look good and align with cache semantics.

The description and example for ErrKeyNotFound are clear and match typical behavior for both memory and Redis backends (missing or explicitly deleted keys, and GetAndDelete on non-existent keys). Nothing to change here.


39-50: ErrKeyExists documentation is clear and helpful.

The description and example for ErrKeyExists do a good job explaining atomic “create if not exists” usage and concurrent scenarios; no changes needed.

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

9-9: LGTM!

The thirdparty module integration into the handlers DI graph is straightforward and correct.

Also applies to: 41-41

api/mobile.http (1)

71-72: Static analysis false positive.

The flagged values are test credentials in a development HTTP request file, not actual secrets. No action needed.

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

8-28: LGTM!

The disabled service implementation correctly provides a fail-safe JWT service when JWT is not configured. All methods consistently return ErrDisabled, and the implementation properly satisfies the Service interface.

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

10-34: LGTM!

The user model follows the established pattern in the codebase, with appropriate field types and migration function. The varchar(72) for PasswordHash is suitable for bcrypt hashes.


13-13: The ID field uses the username as the identifier, not a UUID or ULID. The varchar(32) constraint is appropriate for username storage.

Likely an incorrect or invalid review comment.

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

10-27: LGTM!

The JWT service interface is well-designed with clean separation of concerns. The Claims struct appropriately includes user identification and authorization scopes, and the TokenInfo provides essential token metadata for clients.

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

21-58: LGTM!

The NewBasic middleware has been correctly updated to use the new users.Service. The authentication flow properly stores the authenticated user with full permissions.


88-110: LGTM!

The SetUser and GetUser helpers provide consistent type handling. Both NewBasic and NewCode correctly dereference user pointers before storing, and GetUser properly retrieves the value and returns a pointer.


132-141: LGTM!

The WithUser decorator now correctly handles the nil case by returning 401 Unauthorized, which is the appropriate response when authentication is missing.

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

35-67: LGTM!

The refactored constructor with explicit dependencies is cleaner and more maintainable than the previous fx-based parameter struct approach.


74-80: LGTM!

The middleware chain is correctly ordered:

  1. Basic authentication (optional)
  2. JWT authentication (optional)
  3. User required (ensures at least one auth succeeded)

The auth handler registration under /auth is properly scoped.

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

14-14: User migration is properly wired via users/module.go.

The User model migration has been successfully moved to the dedicated users package. It is correctly registered during application initialization through the users/module.go init() function, which calls db.RegisterMigration(Migrate) at package startup—following the same pattern used by other packages in the codebase (jwt, models, messages).

internal/config/module.go (1)

136-142: LGTM!

The JWT config binding correctly maps all fields from Config.JWT to jwt.Config, with proper conversion of Duration to time.Duration.

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

1-5: LGTM!

The permission scope constant follows the established pattern and will be used for access control on log endpoints.

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

7-7: LGTM!

The refactoring to use users.User aligns with the new users domain structure while preserving the foreign key relationship and cascade behavior.

Also applies to: 17-17

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

14-20: LGTM! Good fallback pattern.

The conditional Service provider elegantly handles the case where JWT is not configured by returning a disabled service when Secret is empty, allowing the application to start without JWT in certain deployment modes.


25-27: LGTM!

Migration registration follows the established pattern in the codebase.

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

16-23: LGTM!

The login cache initialization properly handles errors from the factory and wraps them with context.


30-32: LGTM!

Migration registration follows the established pattern.

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

5-7: LGTM!

The updates properly integrate JWT authentication with the logs endpoint, including:

  • Type refactoring to use users.User
  • API documentation updated with JWTAuth security and 403 response
  • Handler signature updated consistently

Also applies to: 37-37, 44-44, 50-50


58-58: The middleware execution order is correct.

Both RequireScope and WithUser depend on context set by the parent router's middleware chain (userauth.NewBasic() and jwtauth.NewJWT()), not on each other. By the time the route handlers execute, both c.Locals("user:scopes") and c.Locals("localsUser") are already populated. RequireScope can safely execute first to validate scopes before WithUser extracts the user.

Likely an incorrect or invalid review comment.

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

1-7: LGTM!

The permission scope constants follow the established naming pattern and align with standard CRUD operations for webhooks.

internal/config/config.go (1)

125-128: Acknowledge past review: disabled service pattern mitigates the concern.

The past review flagged the empty Secret default as a critical security issue. This concern is now partially mitigated by the disabled service pattern in internal/sms-gateway/jwt/module.go (line 15), which returns a disabled JWT service when Secret is empty, preventing insecure token generation.

However, consider adding a startup validation or clear documentation (as suggested in the past review) to make it explicit that JWT functionality will be disabled unless Secret is configured via environment variable or config file.

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

5-10: LGTM!

Clean domain model with appropriate field types and visibility.


12-19: LGTM!

Clean constructor pattern with appropriate visibility and straightforward field mapping.

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

127-140: LGTM!

Clean type migration from models.User to users.User. The usage of user.ID remains valid with the new type.


142-175: LGTM!

Consistent type migration in GetState method. Field access patterns remain valid.

pkg/cache/options.go (1)

76-209: LGTM!

Well-structured functional options for Get operations with comprehensive documentation. The getOptions helpers (apply and isEmpty) provide clean option accumulation and optimization.

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

107-130: LGTM!

Clean error mapping with appropriate HTTP status codes for each JWT error type.

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

60-138: LGTM!

Clean type migration to users.User with consistent usage of user.ID across all handler methods.


140-144: LGTM!

Proper scope-based authorization applied to all routes. Read operations require ScopeRead, write operations require ScopeWrite.

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

56-121: LGTM!

Consistent type migration to users.User with proper field usage across all webhook handlers.


123-127: LGTM!

Well-structured authorization with appropriate scopes: ScopeList for GET, ScopeWrite for POST, and ScopeDelete for DELETE operations.

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

53-92: LGTM!

Consistent type migration to users.User with proper field usage in both device handlers.

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

14-42: The JWT middleware correctly allows pass-through when the Authorization header is missing or malformed. This is intentional design: the route middleware chain includes userauth.UserRequired() immediately after jwtauth.NewJWT(), which enforces that a user must be authenticated (via JWT or Basic auth) before reaching handlers. No change needed.

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

56-76: LGTM on handler method signature and OpenAPI updates.

The method signature update to users.User and the addition of JWTAuth security with 403 Forbidden response documentation properly align with the new JWT authentication subsystem.

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

1-6: This is a generated file - changes look consistent.

The OpenAPI specification has been regenerated with JWT authentication support. The changes include:

  • New /3rdparty/v1/auth/token endpoints for token generation and revocation
  • JWTAuth security definition and requirements across all protected endpoints
  • 403 Forbidden response documentation for all secured endpoints
  • TokenRequest and TokenResponse schema definitions

Since this file is auto-generated by swaggo/swag, ensure the source handler annotations are kept in sync for future regeneration.

pkg/cache/typed.go (2)

228-244: LGTM on the Get method implementation.

The implementation correctly:

  1. Retrieves data from storage first
  2. Creates a new instance via newItem[T]() to handle pointer types
  3. Unmarshals into the properly instantiated instance
  4. Returns appropriate errors with context

This addresses the previous concern about pointer-typed cache items failing to unmarshal.


392-407: LGTM on the newItem helper.

The reflection-based instantiation correctly enforces that T must be a pointer type and creates a properly initialized instance for unmarshaling. The error handling uses the appropriate sentinel error.

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

53-117: LGTM on GenerateToken implementation.

The implementation includes proper validation for:

  • Non-empty userID (lines 63-66)
  • Non-empty scopes (lines 68-71)
  • Non-negative ttl (lines 73-76)

The TTL capping at line 89 using min(ttl, s.config.TTL) enforces a maximum token lifetime from configuration, which is appropriate security hygiene.


119-166: LGTM on ParseToken implementation.

The token validation is thorough:

  • Signature verification with secret
  • Expiration and issuedAt validation
  • Issuer verification
  • Allowed signing methods restricted to HS256
  • Revocation check after cryptographic validation

168-182: LGTM on RevokeToken implementation.

Simple delegation to the repository with proper metrics integration. The revocation operation correctly passes both jti and userID for authorization verification.

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

43-75: LGTM on the constructor refactoring.

The transition from an fx-based params struct to explicit constructor arguments improves clarity and makes dependencies explicit. The initialization of embedded base.Handler and wiring of services/controllers is correct.


201-212: Signature update and authentication flow look correct.

The parameter type change from models.User to users.User aligns with the new users package. The user code generation logic remains unchanged and properly delegates to authSvc.GenerateUserCode.


243-272: Middleware wiring correctly uses usersSvc.

The switch from authSvc to usersSvc for userauth.NewBasic at lines 247 and 269 aligns with the refactoring. The middleware chain and route registration look correct.

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

1-21: Well-structured metric constants.

The metric naming follows Prometheus conventions with clear namespace (sms) and subsystem (auth). The //nolint:gosec comments are appropriate for false positives on metric names containing "token".


33-81: Metrics initialization looks correct.

The use of promauto ensures metrics are automatically registered with the default registry. Histogram buckets (0.005s to 1s) are reasonable for authentication operations. The counter vectors correctly use the status label.

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

35-53: Clean constructor refactoring.

The explicit dependency injection pattern is clearer than the previous fx-based approach. The codesCache initialization with codeTTL is retained correctly.


85-96: Signature change from pointer to value is appropriate.

Taking users.User by value instead of pointer is reasonable since the struct is small (ID + timestamps). The device creation flow correctly uses user.ID for the foreign key.


132-145: User retrieval flow is correct.

The AuthorizeUserByCode method correctly retrieves and deletes the code atomically, then fetches the user via usersSvc.GetByUsername. The method naming (GetByUsername taking a userID) was addressed in previous commits per past review comments.


147-165: Cleanup logic correctly simplified.

The Run and clean methods now only operate on codesCache, which is appropriate since user cache management has moved to the users service.

pkg/cache/redis.go (3)

55-76: Good documentation for RedisConfig.

The struct documentation clearly explains the configuration options and the fallback behavior for client creation.


136-163: Constructor validation and client ownership tracking are correct.

The logic properly validates that either a client or URL is provided, handles URL parsing errors, and tracks ownedClient for proper cleanup in Close().


458-466: Close method correctly respects client ownership.

The ownedClient flag ensures that externally provided clients are not closed, which is the expected behavior.

pkg/cache/memory.go (6)

9-28: Comprehensive type documentation.

The documentation clearly explains the use cases, concurrency guarantees, and trade-offs compared to the Redis implementation.


74-91: Well-documented expiration check.

The isExpired method correctly handles nil safety and zero-value validUntil semantics (no expiration).


207-254: Get method correctly handles TTL options.

The implementation properly upgrades to a write lock when options are provided and handles all TTL modification cases. The AndDefaultTTL fix (lines 244-249) now correctly checks m.ttl > 0 before setting expiration, avoiding immediate expiration when no default TTL is configured.


330-342: SetOrFail correctly allows overwriting expired keys.

The check at line 335 properly treats expired keys as non-existent, allowing the set operation to proceed. This is consistent with the documented behavior.


422-434: Cleanup callback pattern is correct.

Based on learnings, the cleanup method removes expired items first, then executes the callback while still holding the lock. This ensures methods like Drain get a consistent view of non-expired items.


161-175: Drain method correctly uses cleanup callback.

The implementation atomically drains all non-expired items by leveraging the cleanup callback pattern. The old map is captured and replaced with a new empty map while holding the lock.

@capcom6 capcom6 added the ready label Dec 2, 2025
@github-actions github-actions bot removed the ready label Dec 3, 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: 4

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/mobile.go (1)

123-156: Align error handling and verify password field behavior in postDevice.

Two issues to address:

  1. Error handling pattern inconsistency
    Lines 140 and 146 use fmt.Errorf(...) directly, but other handlers in this file (patchDevice at lines 183–184, getUserCode at lines 204–205, changePassword at lines 236–237) follow a consistent pattern: log with h.Logger.Error() and return fiber.NewError(fiber.StatusInternalServerError, ...). The suggested diff aligns postDevice with the established pattern and prevents internal details from leaking to clients.

  2. Password field for existing users
    The swagger comment (line 100) correctly states "Returns user credentials only for new users". However, when authUser != nil, the password variable remains empty but is still included in MobileRegisterResponse. Verify the JSON tag on smsgateway.MobileRegisterResponse.Password—if it lacks omitempty, clients will receive "password": "" for existing users, contradicting the swagger documentation. If needed, either update the client-go type definition or conditionally populate the Password field only for new users.

♻️ Duplicate comments (16)
pkg/cache/errors.go (1)

21-36: Re‑verify ErrKeyExpired semantics across backends and align docs

The comment states that Get returns ErrKeyExpired when the key’s TTL has elapsed, but previous implementations had:

  • the in‑memory cache returning ErrKeyExpired for expired entries, and
  • the Redis cache using field‑level TTL and returning ErrKeyNotFound once Redis removes expired fields.

If Redis still behaves this way, the current doc and example would be misleading for Redis users. Please confirm current backend behavior and either:

  • ensure all backends surface ErrKeyExpired for TTL expiry, or
  • explicitly document that ErrKeyExpired is only produced by the in‑memory cache and that Redis maps expirations to ErrKeyNotFound so callers know how to interpret errors per backend.

Based on learnings, this was a previously noted discrepancy; worth double‑checking it’s truly resolved.

internal/config/config.go (1)

125-128: Verify JWT Secret validation at startup.

The default config initializes JWT.TTL and JWT.Issuer but leaves Secret empty. A past review flagged this as critical since an empty secret would make JWT tokens insecure.

Verify that Secret validation or disabled-service fallback is implemented in the JWT module initialization:

#!/bin/bash
# Description: Check if JWT module validates empty Secret or handles disabled state

# Search for Secret validation in JWT module
rg -nP --type=go -B2 -A5 'Secret.*==.*""' internal/sms-gateway/jwt/

# Check module initialization for Secret handling
rg -nP --type=go -A10 'func.*Module\(\)' internal/sms-gateway/jwt/module.go
go.mod (1)

7-7: Pseudo-version dependency flagged in previous review.

This concern has already been raised. The dependency should be updated to a stable release tag before merging.

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

51-81: Avoid using hashed credentials as cache keys in loginCache.

makeKey currently derives the cache key from username and password via plain SHA‑256, and Get/Set use this directly. This recreates several security issues previously raised:

  • Deterministic hash enables correlation of identical credential pairs and offline guessing for common passwords.
  • Cache effectively becomes an authentication oracle (cache hit implies valid creds).
  • Password changes don’t naturally invalidate old entries keyed by the previous password.
  • Keys derived directly from credentials are long‑lived sensitive artifacts (especially in Redis).

Prefer an opaque, non‑credential key, e.g.:

  • Key by stable user identifier only (after successful authentication), or
  • Derive a key using an HMAC with a server‑side secret and per‑user salt, without embedding the raw password.

Example direction (conceptual):

-func (c *loginCache) makeKey(username, password string) string {
-    hash := sha256.Sum256([]byte(username + "\x00" + password))
-    return hex.EncodeToString(hash[:])
-}
+func (c *loginCache) makeKey(userID string) string {
+    // e.g. prefix with namespace; you can also HMAC with a secret if needed
+    return "login:" + userID
+}
+
-func (c *loginCache) Get(ctx context.Context, username, password string) (*User, error) {
-    user, err := c.storage.Get(ctx, c.makeKey(username, password), cache.AndSetTTL(loginCacheTTL))
+func (c *loginCache) Get(ctx context.Context, userID string) (*User, error) {
+    user, err := c.storage.Get(ctx, c.makeKey(userID), cache.AndSetTTL(loginCacheTTL))

…with corresponding changes to Set/Delete and callers, and explicit invalidation on password change.

api/requests.http (1)

200-218: Fix non-existent devices:write scope in token request example.

The scopes example for POST /3rdparty/v1/auth/token still includes "devices:write" (line 211), but earlier analysis showed only "devices:list" and "devices:delete" are defined in the devices permissions. This mismatch will produce tokens that can’t satisfy any authorization checks.

Replace "devices:write" with the correct device permission constant (most likely "devices:delete"), so all scopes in this example exactly match the defined permission strings.

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

97-102: Validate jti parameter is non-empty.

The handler extracts jti from path parameters but doesn't validate it before passing to RevokeToken. An empty jti should be rejected with a 400 Bad Request.

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 {

66-74: Add validation for TTL bounds before conversion.

The nolint:gosec comment claims TTL is "validated in the service," but the conversion time.Duration(req.TTL)*time.Second happens in the handler. If req.TTL is a large uint64, the cast to int64 (time.Duration) can overflow before the service ever sees it. Validate req.TTL against a reasonable maximum (e.g., 31,536,000 seconds = 1 year) in the handler before conversion.

+const maxTTLSeconds = 31536000 // 1 year
+
 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())
 	}
 
+	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/handlers/devices/3rdparty.go (1)

94-97: Fix inconsistent route parameter syntax.

Line 96 uses :id without a leading slash, while the webhooks handler uses /:id. Per Fiber routing conventions, route parameters should include the leading slash for consistency.

 func (h *ThirdPartyController) Register(router fiber.Router) {
 	router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.get))
-	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)

68-91: Security issue: Raw passwords still used in cache operations.

Despite past review feedback, the Login method continues to pass plaintext passwords to the cache layer (lines 69, 86). Depending on the cache implementation (in-memory, Redis, metrics, logging), this risks propagating raw secrets into logs, metrics, or external systems.

The recommended fix is to key the cache only on username, retrieve the cached user, and then verify the password against the cached PasswordHash using crypto.CompareBCryptHash:

cachedUser, err := s.cache.Get(ctx, username)
if err == nil {
    if crypto.CompareBCryptHash(cachedUser.PasswordHash, password) == nil {
        return cachedUser, nil
    }
}

And for setting:

s.cache.Set(ctx, username, *loggedInUser)

93-109: Critical issues in ChangePassword remain unaddressed.

Two problems flagged in past reviews persist:

  1. Raw password in cache (line 99): The currentPassword is still passed to cache.Delete, exposing plaintext credentials to the cache layer.

  2. Hard-fail on cache deletion (lines 99-101): The method returns immediately if cache deletion fails, potentially blocking password changes due to cache unavailability. Cache operations should be best-effort.

Apply these fixes:

 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
+	// Best-effort cache invalidation - key only on username
+	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 standard Fiber routing conventions and could cause routing issues.

Apply this fix:

-	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/users/repository.go (1)

54-62: Check RowsAffected to detect non-existent users.

The UpdatePassword method does not check if any rows were updated. GORM's Update does not return an error when zero rows are affected, so if the user ID doesn't exist, this method silently succeeds without updating anything. This is inconsistent with GetByID returning ErrNotFound for non-existent users.

Apply this fix:

 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 (2)

43-75: Handle nanoid.Standard error instead of discarding it.

The constructor ignores the error from nanoid.Standard(idLength). If the implementation ever changes or is misused, idGen could be nil and later calls to h.idGen() would panic. Better to handle this explicitly (e.g., by panicking with a clear message or returning an error from the constructor) rather than discarding the error.

 func newMobileHandler(
@@
 ) *mobileHandler {
 	const idLength = 21
-	idGen, _ := nanoid.Standard(idLength)
+	idGen, err := nanoid.Standard(idLength)
+	if err != nil {
+		// This should never happen with a constant, valid idLength,
+		// but fail fast rather than constructing a broken handler.
+		panic(fmt.Sprintf("failed to initialize ID generator: %v", err))
+	}
@@
-		idGen: idGen,
+		idGen: idGen,
 	}
 }

235-238: Distinguish auth failures from internal errors in changePassword.

usersSvc.ChangePassword can plausibly fail for both auth reasons (wrong current password, unknown user) and internal ones (cache/DB errors, hashing issues). Mapping all failures to 401 both misleads clients and hides server problems; the swagger above also documents a 500.

Consider something along these lines (using your actual sentinel error(s)):

-	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 err := h.usersSvc.ChangePassword(c.Context(), device.UserID, req.CurrentPassword, req.NewPassword); err != nil {
+		h.Logger.Error("failed to change password", zap.Error(err))
+
+		// Replace `users.ErrInvalidPassword` (and any others) with the real auth error(s)
+		if errors.Is(err, users.ErrInvalidPassword) {
+			return fiber.NewError(fiber.StatusUnauthorized, "invalid current password")
+		}
+
+		return fiber.NewError(fiber.StatusInternalServerError, "failed to change password")
+	}

This keeps client-visible behavior clear while still logging the full underlying error.

pkg/cache/redis.go (2)

25-52: Fix HTTL result handling in Lua script (it returns an array).

HTTL returns an array of integers (one per field), not a single integer scalar. The current script treats the result as a number (if ttl < 0, ttl + ttlDelta), which will raise a Lua runtime error when this branch executes. (redis.io)

Because this script backs Get with AndUpdateTTL/AndDefaultTTL behavior, the bug can break cache operations that rely on TTL extension.

Suggested fix:

 elseif ttlDelta > 0 then
-  local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
-  if ttl < 0 then
-    ttl = 0
-  end
-  local newTtl = ttl + ttlDelta
-  redis.call('HExpire', KEYS[1], newTtl, 'FIELDS', '1', field)
+  local ttlArr = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field)
+  local ttl = ttlArr[1]
+  if ttl < 0 then
+    ttl = 0
+  end
+  local newTtl = ttl + ttlDelta
+  redis.call('HExpire', KEYS[1], newTtl, 'FIELDS', '1', field)
 end

This matches the documented HTTL response shape and preserves the intended “extend TTL by delta, treating missing/no‑TTL as zero” behavior. (upstash.com)


55-99: Clarify Redis version requirement in RedisCache docs.

The implementation relies on Redis 7.4.0+ hash field TTL commands (HEXPIREAT, HTTL, HEXPIRE with FIELDS). Older Redis versions will fail at runtime on these commands. (redis.io)

Consider adding an explicit note to the RedisCache type comment:

-// RedisCache implements the Cache interface using Redis as the backend.
-//
-// This implementation stores all data in a Redis hash, with each cache item
+// 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).
+//
+// 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.

Based on learnings, this will make the minimum Redis version expectation explicit for operators.

🧹 Nitpick comments (8)
pkg/cache/errors.go (1)

53-53: Consider documenting ErrFailedToCreateZeroValue and aligning its message

ErrFailedToCreateZeroValue is exported but currently undocumented, and its message says “zero item” while the name says “zero value”. Suggest adding a brief doc comment and tightening the message for consistency, e.g.:

-   ErrFailedToCreateZeroValue = errors.New("failed to create zero item")
+   // ErrFailedToCreateZeroValue indicates a failure to construct the zero value for an item.
+   ErrFailedToCreateZeroValue = errors.New("failed to create zero value")

This keeps the public API self‑documenting and avoids naming/message drift.

api/mobile.http (1)

71-72: Test payload sets identical current and new passwords.

Both currentPassword and newPassword are set to the same value. This won't effectively test the password change functionality. Consider using different values to validate the endpoint behavior.

Note: The Gitleaks warnings about "Generic API Key" are false positives—these are test credentials in a development .http file.

 {
-  "currentPassword": "8f8ijpnuvemq7y",
-  "newPassword": "8f8ijpnuvemq7y"
+  "currentPassword": "oldPassword123",
+  "newPassword": "newPassword456"
 }
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)

14-41: Clarify claim semantics vs. user lookup and consider Fiber context choice.

Two small points to tighten here:

  1. The claim field is named UserID but you call usersSvc.GetByUsername(claims.UserID). If the claim really stores a username, consider renaming the field (and the jwt.Service parameter) to avoid confusion; if it stores the user’s ID, this middleware should likely call a GetByID variant instead. The current naming/usage mismatch is easy to misread later.
  2. You're passing c.Context() into jwtSvc.ParseToken. In many Fiber versions the request context.Context is exposed via c.UserContext(), while c.Context() refers to the underlying fasthttp context. Please double‑check this against the Fiber version in use and switch to the appropriate method if needed.
pkg/cache/cache.go (1)

1-283: Double-check option helper names in Cache examples to match actual API.

The Cache interface and overall semantics look good and align with the memory/Redis behaviors (only non-expired items in Drain, Cleanup as explicit expiry sweep), but a couple of examples may be out of sync with the actual helpers:

  • Get example uses cache.AndUpdateTTL(30*time.Minute), while the provided snippets show AndSetTTL as the TTL-related GetOption.
  • Set examples reference WithValidUntil in comments, but only WithTTL is shown in options.go.

Please confirm the real helper names in options.go and update the examples if necessary so that the docs compile mentally for readers and match the concrete API.

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

38-47: Consider optimizing the existence check.

The current implementation uses Count to check if a token is revoked. For existence checks, a LIMIT 1 query is typically more efficient:

var exists bool
err := r.db.WithContext(ctx).
    Model((*tokenModel)(nil)).
    Select("1").
    Where("id = ? and revoked_at is not null", jti).
    Limit(1).
    Find(&exists).Error

However, the current implementation is correct and readable, so this optimization is optional.

pkg/cache/redis.go (2)

101-113: Fix return type name in NewRedis documentation.

The doc comment says the function returns *redisCache, but the actual exported type is *RedisCache. Small but worth correcting for clarity and tooling.

-// Returns:
-//
-//   - *redisCache: A new Redis cache instance
+// Returns:
+//
+//   - *RedisCache: A new Redis cache instance

468-469: Align compile-time check comment with exported type name.

The comment still refers to redisCache while the type is RedisCache. Not a functional issue, but aligning the name avoids confusion.

-// Compile-time check to ensure redisCache implements the Cache interface.
+// Compile-time check to ensure RedisCache implements the Cache interface.
 var _ Cache = (*RedisCache)(nil)
pkg/cache/memory.go (1)

436-453: Minor doc nit: type name in compile-time check comment.

Tiny documentation mismatch: the comment says memoryCache but the actual type is MemoryCache. Not harmful, but easy to tidy up.

-// Compile-time check to ensure memoryCache implements the Cache interface.
+// Compile-time check to ensure MemoryCache implements the Cache interface.
 var _ Cache = (*MemoryCache)(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 559fade and 206ce94.

⛔ 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 (4 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (2 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 (5 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 (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 (4)
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/modules/auth/module.go
  • internal/sms-gateway/modules/auth/cache.go
✅ Files skipped from review due to trivial changes (2)
  • internal/sms-gateway/handlers/settings/permissions.go
  • deployments/grafana/dashboards/jwt.json
🚧 Files skipped from review as they are similar to previous changes (18)
  • internal/sms-gateway/handlers/devices/permissions.go
  • internal/sms-gateway/users/models.go
  • internal/sms-gateway/app.go
  • internal/sms-gateway/modules/webhooks/models.go
  • internal/sms-gateway/handlers/thirdparty/module.go
  • internal/sms-gateway/handlers/webhooks/permissions.go
  • internal/sms-gateway/jwt/config.go
  • internal/config/types.go
  • internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
  • internal/sms-gateway/jwt/errors.go
  • internal/sms-gateway/handlers/base/handler.go
  • internal/sms-gateway/handlers/logs/permissions.go
  • deployments/prometheus/alerts/jwt-alerts.yml
  • internal/sms-gateway/modules/messages/service.go
  • internal/sms-gateway/users/module.go
  • internal/sms-gateway/handlers/thirdparty/permissions.go
  • cmd/sms-gateway/main.go
  • internal/config/module.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/errors.go
  • pkg/cache/redis.go
  • pkg/cache/options.go
  • pkg/cache/memory.go
  • pkg/cache/cache.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/errors.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/redis.go
  • pkg/cache/memory.go
  • pkg/cache/cache.go
🧬 Code graph analysis (25)
internal/sms-gateway/handlers/module.go (1)
internal/sms-gateway/handlers/thirdparty/module.go (1)
  • Module (8-16)
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)
pkg/cache/typed.go (3)
pkg/cache/cache.go (1)
  • Cache (112-283)
pkg/cache/options.go (3)
  • Option (9-9)
  • GetOption (90-90)
  • AndDelete (205-209)
pkg/cache/errors.go (1)
  • ErrFailedToCreateZeroValue (53-53)
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 (1)
  • ScopeList (4-4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
  • WithUser (132-141)
internal/sms-gateway/jwt/module.go (4)
internal/sms-gateway/jwt/repository.go (2)
  • NewRepository (14-18)
  • Repository (10-12)
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 (33-38)
internal/sms-gateway/users/cache.go (5)
pkg/cache/typed.go (2)
  • Typed (89-91)
  • NewTyped (118-122)
internal/config/config.go (1)
  • Cache (78-80)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
pkg/cache/options.go (2)
  • AndSetTTL (123-127)
  • WithTTL (39-50)
pkg/cache/errors.go (2)
  • ErrKeyNotFound (19-19)
  • ErrKeyExpired (36-36)
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/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)
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 (19-19)
internal/sms-gateway/users/repository.go (1)
internal/sms-gateway/users/errors.go (1)
  • ErrNotFound (6-6)
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 (205-209)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (3)
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/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/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 (2)
  • NewBasic (21-59)
  • UserRequired (116-124)
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 (132-141)
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/config/config.go (2)
internal/config/types.go (1)
  • Duration (11-11)
internal/worker/config/types.go (1)
  • Duration (11-11)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
  • AndDelete (205-209)
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/modules/auth/service.go (3)
internal/sms-gateway/users/service.go (1)
  • Service (13-19)
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/models.go (5)
internal/sms-gateway/models/models.go (1)
  • TimedModel (7-10)
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/users/models.go (1)
  • Migrate (29-34)
internal/sms-gateway/modules/settings/models.go (1)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
  • Device (18-29)
pkg/cache/cache.go (1)
pkg/cache/options.go (2)
  • Option (9-9)
  • GetOption (90-90)
🪛 Gitleaks (8.30.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). (5)
  • GitHub Check: Lint
  • GitHub Check: Benchmark
  • GitHub Check: Test
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (37)
.golangci.yml (1)

255-260: Verify that linter exclusion patterns align with actual project dependencies.

The changes add exclusion patterns for third-party types from the exhaustruct linter check:

  • Firebase Messaging (AndroidConfig, Message)
  • GoFiber Config (using regex .+Config)
  • JWT Claims from golang-jwt/jwt/v5 (using regex .+Claims)
  • Telebot types (LongPoller, ReplyMarkup, Settings)

The patterns appear syntactically correct and are appropriate for external library types that commonly have optional fields. However, the regex patterns (.+Config, .+Claims) are relatively broad and will match any type name matching those suffixes/patterns.

These changes align well with the PR's objective to add JWT authentication and integrate GoFiber. The exclusions are reasonable for configuration and claims structures where exhaustive initialization is impractical.

Please confirm that:

  1. The regex patterns (e.g., github.com/gofiber/.+Config, github.com/golang-jwt/jwt/v5.+Claims) match the intended types without unintended side effects
  2. All listed dependencies are actually used in the codebase (verify against go.mod)

You can verify this by checking if the linter runs cleanly and that no unintended exclusions are occurring.

Also applies to: 276-278

pkg/cache/errors.go (2)

6-19: ErrKeyNotFound documentation is clear and consistent

The expanded comment and example around ErrKeyNotFound read well and accurately describe missing-key semantics for Get/GetAndDelete. No issues from my side here.


38-51: ErrKeyExists docs and example look good

The description of ErrKeyExists in terms of SetOrFail and create‑if‑absent semantics is precise and the example is straightforward. Nothing to change here.

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

9-9: LGTM!

The thirdparty module import and wiring follow the existing fx module composition pattern. The integration correctly includes thirdparty.Module() as an option within the handlers module.

Also applies to: 41-41

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

1-36: LGTM! Clean permissions middleware implementation.

The implementation follows good practices:

  • Type-safe scope storage with proper type assertion handling
  • ScopeAll wildcard support for admin-level access
  • Clear error response for missing scopes

Minor consideration: the 403 response includes the required scope name ("scope required: "+scope), which is helpful for debugging but could be considered information disclosure. This is typically acceptable for internal APIs.

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

1-13: LGTM! Well-documented permission scope constants.

Clear naming convention (messages:<action>) and appropriate documentation for each scope. The separation of ScopeList from ScopeRead allows fine-grained access control.

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

85-96: LGTM! Clean device registration with user service integration.

The RegisterDevice method properly uses the new users.User type and delegates to the devices service. The by-value parameter for users.User is acceptable given the struct contains only simple fields.


35-53: LGTM! Explicit dependency injection improves clarity.

The constructor now accepts explicit dependencies rather than an fx.Params bundle, making the service's requirements immediately visible and easier to test.

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

1-28: LGTM! Clean null-object pattern for disabled JWT service.

This implementation provides a graceful fallback when JWT is not configured:

  • Consistently returns ErrDisabled for all operations
  • Unexported type with exported interface is idiomatic
  • Enables module wiring to provide a safe default without nil checks

This complements the config's empty Secret handling by providing a safe disabled state rather than failing at startup.

go.mod (1)

18-18: LGTM on jwt/v5 v5.3.0.

This version is not affected by the known vulnerabilities (CVE-2025-30204 was fixed in v5.2.2, and CVE-2024-51744 affected only v4.x).

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

7-7: LGTM on User type migration.

The change from models.User to users.User aligns with the new users domain package. The foreign key constraint is preserved correctly.

Also applies to: 17-17

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

14-20: LGTM on conditional service initialization.

The pattern of returning a disabled service when config.Secret is empty provides a clean opt-out mechanism for JWT authentication.


24-27: LGTM on migration registration.

The init() pattern for migration registration is consistent with the framework's conventions, and the nolint directive appropriately documents the intentional use.

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

37-37: LGTM on OpenAPI documentation updates.

The addition of JWTAuth security and 403 Forbidden response correctly documents the new authentication and authorization behavior.

Also applies to: 44-44


50-50: LGTM on permission-gated handler.

The middleware chain correctly applies scope checking before user extraction, and the handler signature aligns with the userauth.WithUser wrapper pattern.

Also applies to: 58-58

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

5-8: LGTM on sentinel error definitions.

Clean, idiomatic Go error values for user repository operations.

configs/config.example.yml (1)

41-44: JWT config block looks consistent with the new module.

jwt.secret, jwt.ttl (24h), and jwt.issuer match the new JWT config surface and follow the same duration style as other intervals. No issues from the sample config side.

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

11-38: Token persistence model & migration look sound.

tokenModel shape, table name, indexes, and the Migrate helper are consistent with existing model patterns and should work well for token lookup and cleanup.

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

5-19: Domain User type and mapper are straightforward and correct.

The exported User and newUser mapper cleanly separate domain from persistence without extra coupling. Looks good.

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

10-27: JWT service interface and claim types look well-structured.

The Service interface and Claims/TokenInfo types provide a clear, minimal surface for token generation, parsing, and revocation, and should be easy to integrate across handlers and middleware.

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

140-144: LGTM! Consistent permission enforcement across endpoints.

The route registrations correctly apply scope-based permissions with ScopeRead for GET and ScopeWrite for PUT/PATCH, matching the OpenAPI annotations and the permission middleware pattern.

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

107-130: Error handler provides clear error mapping.

The error handler correctly translates domain-specific JWT errors to appropriate HTTP status codes with user-friendly messages. The fallthrough for ErrInitFailed and ErrInvalidConfig is a reasonable choice for similar server-side issues.

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

123-127: LGTM! Well-structured permission enforcement.

Route registrations correctly apply granular scopes (ScopeList, ScopeWrite, ScopeDelete) and the delete route uses proper Fiber syntax with /:id.

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

1-6: Generated OpenAPI spec correctly reflects handler annotations.

The generated documentation properly includes:

  • New token endpoints with both ApiAuth and JWTAuth security
  • 403 Forbidden responses across all protected endpoints
  • jti path parameter for token revocation
  • TokenRequest and TokenResponse schema definitions

Since this is auto-generated, any remaining issues should be addressed in the source handler files.

pkg/cache/options.go (1)

76-209: LGTM!

The new GetOption API is well-designed and thoroughly documented. The option pattern is implemented correctly, and the examples clearly demonstrate the intended usage. The ability to perform atomic operations like get-and-delete or update TTL during retrieval provides useful flexibility.

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

28-36: Verify idempotent revocation behavior.

The Revoke method does not check RowsAffected, so it silently succeeds when revoking a non-existent or already-revoked token. If idempotent revocation is the intended behavior, this is fine. However, if callers need to know whether a token was actually revoked, consider checking RowsAffected and returning an error or specific sentinel value when zero rows are affected.

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

21-141: LGTM!

The authentication middleware has been correctly updated to use the new users.User type and integrate with the permissions system. The user storage and retrieval logic is now consistent across all authentication paths (NewBasic, NewCode), and the nil checks in WithUser prevent potential panics.

pkg/cache/typed.go (1)

1-407: LGTM!

The typed cache implementation is well-designed and correctly addresses the pointer instantiation issue flagged in previous reviews. The newItem[T]() helper ensures that pointer types are properly instantiated before unmarshaling, preventing nil pointer dereferences. The API is clean, well-documented, and provides a type-safe wrapper around the cache interface.

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

53-117: LGTM!

The GenerateToken method correctly validates all input parameters, including the userID validation that addresses past review feedback. The TTL capping at line 89 (min(ttl, s.config.TTL)) enforces a maximum TTL from configuration, which is a reasonable security policy even if it may reduce a caller's requested TTL.


119-182: LGTM!

The ParseToken and RevokeToken methods implement proper token validation and revocation flows with appropriate error handling and metrics tracking.

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

56-282: LGTM!

The handlers have been correctly updated to use the users.User type and integrate with the permissions system. The scope-based authorization is properly applied to each endpoint, and the documentation reflects the JWT authentication support.

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

21-44: LGTM!

The Exists and GetByID methods are correctly implemented. The past review issue regarding GetByID passing the wrong pointer type has been properly addressed.

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

201-212: getUserCode signature and usage look consistent.

Switching the parameter to users.User and using user.ID with GenerateUserCode matches the new users domain model and the expected userauth.WithUser callback shape. No issues here.


243-272: Auth wiring for mobile routes is coherent with the new users service.

Injecting usersSvc into the handler and reusing userauth.NewBasic(h.usersSvc) for /mobile/v1/device and /mobile/v1/user/code, plus userauth.WithUser(h.getUserCode), lines up with the new user-based auth model and the userauth/jwtauth middlewares. No functional issues spotted in this wiring.

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

8-117: JWT metrics implementation looks solid and idiomatic.

Metric names, labels, and the Metrics API (increment + observe wrappers) are clean, thread‑safe, and consistent with Prometheus best practices. The shared bucket configuration across issuance/validation/revocation also makes sense for latency SLOs.

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

20-80: JWT and user service wiring for 3rd‑party handler looks correct.

Injecting usersSvc and jwtSvc into thirdPartyHandler, then composing userauth.NewBasic(h.usersSvc), jwtauth.NewJWT(h.jwtSvc, h.usersSvc), and userauth.UserRequired() in Register matches the new auth model and the middleware contracts from jwtauth and userauth. The explicit constructor also makes DI clearer than the old fx params.

pkg/cache/memory.go (1)

30-365: In-memory cache TTL and concurrency behavior look correct.

The exported MemoryCache now has a clear API, and the internal mechanics line up with the documented semantics:

  • ttl == 0 truly means “no default expiration”, including in the defaultTTL path in Get.
  • Get with options performs delete/TTL updates under a write lock and then funnels through getItem/getValue, preserving the ErrKeyNotFound vs ErrKeyExpired distinction.
  • Drain uses cleanup to strip expired items before snapshotting and clearing the map, so callers only see non‑expired entries (matching the prior design).

No functional issues spotted here.

@capcom6 capcom6 added ready deployed The PR is deployed on staging labels Dec 3, 2025
@github-actions github-actions bot removed deployed The PR is deployed on staging ready labels Dec 5, 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: 2

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

207-217: Scope devices:write does not exist - use devices:delete instead.

Per previous review findings, devices/permissions.go only defines ScopeList = "devices:list" and ScopeDelete = "devices:delete". The scope "devices:write" on line 211 is invalid and should be replaced with "devices:delete" to match the defined permission constants.

internal/config/config.go (1)

125-128: Reminder: Validate JWT Secret at startup.

This is a duplicate of a previously flagged issue. The default configuration leaves Secret empty, which must be addressed through startup validation as discussed in the earlier review.

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

67-90: Security concern already flagged: Raw passwords in cache operations.

This is a duplicate of previously flagged issues. The Login method passes plaintext passwords to cache operations (lines 68, 85), which poses a security risk as discussed in earlier reviews.


92-108: Hard-fail on cache deletion already flagged.

This is a duplicate of a previously flagged issue. The ChangePassword method returns immediately on cache.Delete error (lines 98-100), potentially blocking password changes due to cache issues, as discussed in earlier reviews.

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

57-57: Error handling issue already flagged.

This is a duplicate of a previously flagged critical issue. The error from nanoid.Standard(21) is ignored, which could lead to a nil pointer panic when h.idGen() is called.


138-147: Error handling inconsistency already flagged.

This is a duplicate of a previously flagged issue. The use of fmt.Errorf instead of fiber.NewError for operational failures is inconsistent with the pattern used elsewhere in this file, as discussed in earlier reviews.


235-237: Error type distinction issue already flagged.

This is a duplicate of a previously flagged issue. All ChangePassword errors return 401, but internal errors should return 500 as discussed in earlier reviews.

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

95-96: Route parameter syntax issue already flagged.

This is a duplicate of a previously flagged issue. Line 96 uses :id without a leading slash, which is inconsistent with Fiber conventions as discussed in earlier reviews.

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

66-74: TTL validation still missing - potential integer overflow.

The conversion time.Duration(req.TTL)*time.Second on line 70 can overflow for large req.TTL values since time.Duration is int64 and the request field may accept very large values. Validate TTL bounds before conversion to prevent unexpected token lifetimes.

 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())
 	}

+	const maxTTLSeconds = 365 * 24 * 60 * 60 // 1 year
+	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,
 	)

97-105: Validate jti parameter is non-empty.

The handler extracts jti from path parameters but doesn't validate it before calling RevokeToken. An empty jti should be rejected at the handler level with a 400 Bad Request to fail fast with a clear error.

 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)
 	}

 	return c.SendStatus(fiber.StatusNoContent)
 }
internal/sms-gateway/handlers/messages/3rdparty.go (1)

327-331: Route parameter syntax is inconsistent with other handlers.

Line 329 uses :id without a leading slash, while the webhooks handler (line 126 in webhooks/3rdparty.go) uses /:id. This inconsistency was flagged in a previous review but hasn't been addressed.

-	router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
+	router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
🧹 Nitpick comments (6)
internal/sms-gateway/users/models.go (1)

10-15: userModel layout looks good; consider future‑proofing password hash length

Struct setup with an unexported GORM model and embedded SoftDeletableModel is clean and keeps persistence details internal. The varchar(72) for PasswordHash is fine for bcrypt‑style hashes but could be tight if you ever switch to longer encodings (e.g., some Argon2 variants). Consider bumping this to something more generous (e.g., 128–255) to avoid a schema change later.

-	PasswordHash string `gorm:"not null;type:varchar(72)"`
+	PasswordHash string `gorm:"not null;type:varchar(255)"`
deployments/grafana/dashboards/jwt.json (1)

36-39: Hardcoded Prometheus datasource UID reduces portability.

The datasource UID "edqp0a73uh2bka" is hardcoded throughout the dashboard. This will break when imported into a different Grafana instance. Consider using a variable like "${DS_PROMETHEUS}" or the default "Prometheus" name reference for better portability across environments.

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

23-27: Consider adding JSON tags to TokenInfo if serialized in API responses.

If TokenInfo is returned directly in HTTP responses, the field names will use Go's default capitalization (e.g., ID, AccessToken). Adding JSON tags would ensure consistent API naming conventions (e.g., "id", "access_token").

#!/bin/bash
# Check if TokenInfo is used in API response serialization
echo "=== TokenInfo usage in handlers ==="
rg -n 'TokenInfo' internal/sms-gateway/handlers/
internal/sms-gateway/handlers/devices/permissions.go (1)

3-6: Consider adding documentation comments for consistency.

The messages/permissions.go file includes documentation comments for each scope constant, but this file does not. For consistency across the codebase, consider adding brief doc comments.

 const (
+	// ScopeList is the permission scope required for listing devices.
 	ScopeList   = "devices:list"
+	// ScopeDelete is the permission scope required for deleting devices.
 	ScopeDelete = "devices:delete"
 )
internal/sms-gateway/jwt/repository.go (1)

28-36: Consider returning whether token was actually revoked.

The Revoke method succeeds silently if the token doesn't exist or is already revoked. While this may be intentional for idempotency, consider checking RowsAffected if callers need to know whether revocation actually occurred.

 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 err := result.Error; err != nil {
 		return fmt.Errorf("can't revoke token: %w", err)
 	}
+	// Optionally: if result.RowsAffected == 0 { return ErrTokenNotFound }

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

35-67: Consider using an options struct for the growing parameter list.

The constructor now accepts 10+ parameters. While functional, this can become unwieldy and order-dependent. An options/params struct would improve readability and make future additions easier.

+type ThirdPartyHandlerDeps struct {
+    UsersSvc        *users.Service
+    JWTService      jwt.Service
+    HealthHandler   *HealthHandler
+    MessagesHandler *messages.ThirdPartyController
+    WebhooksHandler *webhooks.ThirdPartyController
+    DevicesHandler  *devices.ThirdPartyController
+    SettingsHandler *settings.ThirdPartyController
+    LogsHandler     *logs.ThirdPartyController
+    AuthHandler     *thirdparty.AuthHandler
+    Logger          *zap.Logger
+    Validator       *validator.Validate
+}
+
+func newThirdPartyHandler(deps ThirdPartyHandlerDeps) *thirdPartyHandler {
     // use deps.UsersSvc, deps.JWTService, etc.
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f96266f and f5a90ef.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (62)
  • .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 (4 hunks)
  • internal/sms-gateway/handlers/devices/permissions.go (1 hunks)
  • internal/sms-gateway/handlers/logs/3rdparty.go (2 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 (5 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 (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/passwords.go (1 hunks)
  • internal/sms-gateway/users/repository.go (1 hunks)
  • internal/sms-gateway/users/service.go (1 hunks)
  • pkg/health/health.go (1 hunks)
💤 Files with no reviewable changes (4)
  • internal/sms-gateway/modules/auth/repository.go
  • internal/sms-gateway/modules/auth/cache.go
  • internal/sms-gateway/models/models.go
  • internal/sms-gateway/modules/auth/module.go
✅ Files skipped from review due to trivial changes (1)
  • internal/sms-gateway/handlers/base/handler.go
🚧 Files skipped from review as they are similar to previous changes (24)
  • go.mod
  • internal/sms-gateway/handlers/base/handler_test.go
  • internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go
  • cmd/sms-gateway/main.go
  • internal/config/types.go
  • internal/sms-gateway/handlers/settings/permissions.go
  • internal/sms-gateway/users/module.go
  • internal/sms-gateway/users/errors.go
  • internal/sms-gateway/jwt/models.go
  • internal/sms-gateway/modules/webhooks/models.go
  • internal/sms-gateway/users/domain.go
  • internal/sms-gateway/modules/messages/service.go
  • internal/sms-gateway/users/cache.go
  • internal/sms-gateway/jwt/module.go
  • internal/sms-gateway/handlers/middlewares/permissions/permissions.go
  • internal/sms-gateway/jwt/service.go
  • internal/config/module.go
  • internal/sms-gateway/handlers/thirdparty/module.go
  • internal/sms-gateway/handlers/module.go
  • internal/sms-gateway/jwt/errors.go
  • internal/sms-gateway/users/repository.go
  • deployments/prometheus/alerts/jwt-alerts.yml
  • .golangci.yml
  • internal/sms-gateway/models/migration.go
🧰 Additional context used
🧬 Code graph analysis (10)
internal/sms-gateway/handlers/thirdparty/auth.go (5)
internal/sms-gateway/handlers/base/handler.go (1)
  • Handler (15-18)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
  • RequireScope (28-36)
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/users/service.go (4)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/users/errors.go (1)
  • ErrExists (7-7)
internal/sms-gateway/users/passwords.go (2)
  • MakeBCryptHash (14-20)
  • CompareBCryptHash (22-27)
pkg/cache/errors.go (1)
  • ErrKeyNotFound (19-19)
internal/sms-gateway/handlers/devices/3rdparty.go (1)
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 (132-141)
internal/sms-gateway/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
  • 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/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/users/service.go (1)
  • Service (12-18)
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 (2)
internal/config/types.go (1)
  • Duration (11-11)
internal/worker/config/types.go (1)
  • Duration (11-11)
internal/sms-gateway/modules/settings/models.go (1)
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 (12-18)
internal/sms-gateway/users/domain.go (1)
  • User (5-10)
internal/sms-gateway/models/models.go (2)
  • Device (18-29)
  • NewDevice (31-37)
🪛 Gitleaks (8.30.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). (4)
  • GitHub Check: Benchmark
  • GitHub Check: Lint
  • GitHub Check: E2E
  • GitHub Check: Analyze (go)
🔇 Additional comments (50)
pkg/health/health.go (1)

37-42: Memory usage conversion to MiB is correct and safe

Using m.Alloc / oneMiB before casting to int keeps units consistent with "MiB" and avoids any realistic overflow concerns on supported architectures; the change looks good and the removal of the nolint is justified.

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

1-27: Package rename and bcrypt helpers look correct

package users matches the new module location and the bcrypt helpers are straightforward, with appropriate error wrapping via ErrPasswordInvalid. I don’t see any functional or security regressions introduced by this move.

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

17-23: Constructor is minimal and appropriate for an internal ORM model

newUserModel cleanly initializes only the explicit fields while relying on the embedded SoftDeletableModel defaults, and the //nolint:exhaustruct is an appropriate suppression here. No issues from a correctness or clarity standpoint.


25-34: TableName override and Migrate wiring look consistent with GORM usage

Overriding TableName() to "users" aligns the ORM model with the expected table name, and Migrate using db.AutoMigrate(new(userModel)) is idiomatic and keeps the migration surface narrow. Just make sure this function is wired into your global migrations alongside other modules so the users table is guaranteed to exist before use.

internal/sms-gateway/modules/settings/models.go (2)

7-7: LGTM! Import aligns with type migration.

The users package import is necessary for the users.User type reference on line 17.


17-17: LGTM! Foreign key relationship preserved.

The type migration from models.User to users.User maintains the foreign key constraint and cascade behavior. The relationship should function correctly as long as users.User follows GORM conventions with ID as the primary key.

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

8-8: LGTM! Imports align with JWT authentication integration.

The permissions and users package imports support the scope-based access control and type migration introduced in this file.

Also applies to: 12-12


50-50: LGTM! OpenAPI annotations reflect JWT authentication support.

The addition of JWTAuth security and 403 Forbidden response aligns with the scope-based permission checks introduced in the route registration.

Also applies to: 55-55


60-60: LGTM! Handler signature updated for type migration.

The signature change from models.User to users.User is consistent with the broader user domain refactoring.


72-72: LGTM! Consistent JWT authentication integration across all handlers.

The OpenAPI annotations and signature updates for put and patch handlers mirror the get handler changes, maintaining consistency across the API.

Also applies to: 80-80, 85-85, 108-108, 116-116, 121-121


141-143: Scope constants are properly defined and correctly used.

The ScopeRead and ScopeWrite constants are defined in permissions.go with appropriate values ("settings:read" and "settings:write"), and the routes correctly apply them via permissions.RequireScope(). The middleware order (permission check before user extraction) is correct.

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

1-5: LGTM!

Clean permission constant definition following the established naming pattern used by other handler modules (e.g., devices:list, messages:send).

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

8-28: LGTM!

Well-implemented Null Object pattern for the disabled JWT service. All three interface methods correctly return ErrDisabled, providing a clean fallback when JWT is not configured.

api/requests.http (1)

200-223: Token endpoints structure looks good.

The new /auth/token POST and DELETE endpoints are properly structured with correct authentication headers. The token generation request body with ttl and scopes array aligns with the JWT service interface.

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

1-1217: Dashboard metrics and structure look correct.

The dashboard provides comprehensive JWT observability with proper metric names matching those defined in internal/sms-gateway/jwt/metrics.go. The panels cover issuance, validation, and revocation rates/latencies appropriately.

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

8-10: LGTM on secret length requirement.

The 32-byte minimum for HMAC-SHA256 secrets aligns with security best practices (256-bit key strength).


18-31: Add validation for the Issuer field.

The Issuer field is used directly in token creation (line 86) and token validation (line 132) of service.go, but the Validate() method does not check that it is set. Add a validation check to ensure Issuer is not empty.

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

10-14: LGTM!

Clean service interface with appropriate method signatures. The context parameter enables cancellation and deadline propagation for token operations.


16-21: LGTM!

Standard JWT claims extension pattern with proper JSON tags for the custom fields.

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

57-58: Middleware ordering is correct.

The permissions.RequireScope(ScopeRead) middleware can access scopes because the global middleware chain in handlers/3rdparty.go registers jwtauth.NewJWT() before any route handlers run. The JWT middleware extracts scopes from token claims and populates them in the context via permissions.SetScopes() at the global level, making them available to all downstream route-level middleware. The route-level ordering aligns with the codebase pattern used consistently across all third-party handlers.

internal/config/config.go (1)

87-91: LGTM: JWT configuration structure is well-defined.

The JWT type is properly structured with appropriate YAML and environment variable tags. The use of the Duration type for TTL aligns with the existing codebase patterns (as seen in internal/config/types.go).

configs/config.example.yml (1)

41-44: LGTM: Clear example configuration with helpful guidance.

The JWT configuration block is well-documented with environment variable references and a clear comment explaining that an empty secret disables JWT functionality.

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

8-21: LGTM: Metric constants and labels are well-defined.

The metric naming follows Prometheus conventions, and the status label provides adequate granularity for success/error tracking. The bucket configuration for histograms (line 35) covers a good range for JWT operation latencies.


34-82: LGTM: Metrics initialization and registration.

The metrics are properly registered with promauto, ensuring they're available immediately. The use of CounterVec for token counts and Histogram for duration tracking is appropriate for JWT operations.


84-118: LGTM: Clean metric recording methods.

The increment and observe methods provide a clean interface for recording metrics. The timer pattern in the Observe methods correctly captures operation duration.

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

10-10: LGTM: JWT and users modules properly integrated.

The module registration follows the established fx pattern and is correctly positioned in the dependency graph.

Also applies to: 24-24, 45-45, 60-60

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

1-5: LGTM: Scope constant follows naming convention.

The ScopeTokensManage constant follows the established pattern for permission scopes seen in other handlers (e.g., ScopeList, ScopeDelete in devices/permissions.go).

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

42-42: LGTM: OpenAPI annotations updated for JWT authentication.

The JWTAuth security scheme and 403 Forbidden responses are properly added to reflect the new permission-based authorization.

Also applies to: 48-48, 67-67, 74-74


53-53: LGTM: Parameter types updated for new users domain.

The migration from models.User to users.User is consistent with the broader refactoring to introduce a dedicated users package.

Also applies to: 80-80

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

1-13: LGTM!

Well-documented permission scope constants with clear naming conventions. This serves as a good reference for other permission files in the codebase.

internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1)

3-13: Schema design looks appropriate for JWT token persistence.

The table structure correctly supports the JWT revocation and expiry use cases with appropriate indexes and foreign key constraints. The ON DELETE CASCADE ensures orphaned tokens are cleaned up when users are deleted.

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

1-7: LGTM with minor suggestion.

Scope constants follow consistent naming conventions. Consider adding documentation comments for consistency with messages/permissions.go, similar to the suggestion for devices/permissions.go.

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

107-130: Error handler implementation looks correct.

The error handler appropriately maps JWT-specific errors to meaningful HTTP responses. The ErrInvalidConfig and ErrInitFailed cases correctly return 500 with a user-friendly message, and ErrDisabled returns 501 Not Implemented which is appropriate for a feature toggle.


19-36: Handler structure and constructor look good.

Clean composition with embedded base.Handler and injected jwt.Service. Constructor properly initializes both.

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

123-127: LGTM! Routes properly wrapped with scope-based permissions.

The route registration correctly applies permissions.RequireScope before userauth.WithUser, ensuring scope validation occurs before handler execution. The pattern is consistent with the broader JWT authentication flow.

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

1-4: Generated file correctly reflects JWT authentication additions.

The OpenAPI spec properly documents the new /3rdparty/v1/auth/token endpoints, JWTAuth security definition, and 403 Forbidden responses across all protected endpoints. Previous review concerns (security annotations, TTL type, jti parameter) have been addressed.

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

38-47: Verify behavior for non-existent tokens.

IsRevoked returns false for tokens that don't exist in the database (count would be 0). Ensure the JWT validation flow elsewhere verifies token existence or that this behavior is intentional (e.g., tokens not persisted until revoked).

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

54-55: LGTM! Basic auth correctly grants full scope access.

The pattern of granting permissions.ScopeAll after successful Basic authentication is appropriate, as Basic auth represents full user credentials. The user storage via SetUser is consistent with the NewCode path.


132-140: LGTM! WithUser now returns proper 401 instead of panicking.

The previous concern about potential panics when GetUser returns nil has been addressed. The middleware now correctly returns fiber.ErrUnauthorized for unauthenticated requests.

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

76-172: LGTM! Handler properly updated for JWT auth integration.

The post handler correctly uses users.User and the Swagger annotations properly document JWTAuth security and 403 Forbidden response. The permission scope ScopeSend is appropriately applied at the route level.

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

20-33: LGTM on struct updates for JWT integration.

The new fields (usersSvc, jwtSvc, authHandler) align with the JWT authentication architecture. The organization keeps auth-related dependencies together.


74-80: Auth middleware stack and routing look correct.

The middleware chain correctly supports dual authentication (Basic + JWT) with UserRequired() enforcing that one must succeed. Auth routes being behind authentication is appropriate for token refresh/revocation operations where an authenticated session is required.

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

23-33: Service struct refactoring is clean.

The replacement of inline repository with *users.Service improves separation of concerns and aligns with the broader refactoring to use the public users service.


35-53: Constructor signature is well-structured.

Dependencies are clearly defined and the initialization is straightforward. The codesCache initialization with codeTTL maintains the one-time code functionality.


56-83: Code generation logic is sound with appropriate retry handling.

The 3-retry loop with cryptographic random generation and cache collision handling is well-designed. The 6-digit code format is reasonable for one-time codes.


85-96: RegisterDevice correctly uses value type for User.

Since only user.ID is needed, passing users.User by value is appropriate and avoids nil pointer concerns.


102-112: Good use of constant-time comparison for token validation.

Using subtle.ConstantTimeCompare for the private token check prevents timing attacks. This is a security best practice.


133-145: AuthorizeUserByCode refactored to use users.Service.

The function correctly retrieves the user ID from the codes cache and delegates to the users service. Error handling is appropriate with wrapped errors.


147-165: Cleanup logic is straightforward.

The hourly cleanup ticker for codesCache maintains proper cache hygiene. The context cancellation handling is correct.


120-125: Fire-and-forget goroutine is appropriate for this use case.

The detached goroutine with its own timeout context is well-suited for non-critical online status updates. The SetOnline method returns void, so there are no errors to log. No changes needed.

@capcom6 capcom6 added the deployed The PR is deployed on staging label Dec 5, 2025
@github-actions github-actions bot removed the deployed The PR is deployed on staging label Dec 6, 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: 0

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

66-74: Validate TTL bounds before conversion to prevent overflow.

Although a previous review comment indicated this was addressed, the handler still converts req.TTL directly to time.Duration without validation. If req.TTL exceeds math.MaxInt64 / int64(time.Second) (~292 years), the multiplication will overflow, potentially producing negative or incorrect durations.

Add validation before the conversion to ensure req.TTL is within acceptable bounds (e.g., 0 to 31,536,000 seconds for one year).

Apply this diff to add validation:

+	const maxTTLSeconds = 31536000 // 1 year
+	if req.TTL > maxTTLSeconds {
+		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ttl exceeds maximum allowed value of %d seconds", maxTTLSeconds))
+	}
+
 	token, err := h.jwtSvc.GenerateToken(
 		c.Context(),
 		user.ID,
 		req.Scopes,
 		time.Duration(req.TTL)*time.Second,
 	)
🧹 Nitpick comments (1)
internal/sms-gateway/handlers/thirdparty/auth.go (1)

98-106: Add validation for the jti parameter.

The jti parameter is extracted from the path but not validated before being passed to RevokeToken. Although the router should ensure the parameter exists, adding a defensive check improves error handling and provides a clearer error message if the parameter is somehow empty.

Apply this diff to add validation:

 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)
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5a90ef and ea2d117.

📒 Files selected for processing (3)
  • cmd/sms-gateway/main.go (2 hunks)
  • internal/sms-gateway/handlers/thirdparty/auth.go (1 hunks)
  • internal/sms-gateway/openapi/docs.go (31 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/sms-gateway/main.go
⏰ 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 (3)
internal/sms-gateway/handlers/thirdparty/auth.go (2)

25-42: LGTM!

The constructor and route registration are well-structured. The middleware ordering is correct, with errorHandler registered first to catch errors from downstream handlers, and both endpoints properly protected with scope and authentication requirements.


108-131: LGTM!

The error handler properly maps JWT service errors to appropriate HTTP responses with clear user-facing messages. The nolint:wrapcheck directive is correctly justified since Fiber's error handling mechanism requires unwrapped errors.

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

1-1999: Generated documentation looks correct.

This file is auto-generated by swaggo/swag (as noted in the header comment). The generated OpenAPI specification correctly reflects:

  • JWT authentication endpoints with proper security requirements (lines 26-145)
  • Complete parameter definitions including the jti path parameter (lines 112-119)
  • Appropriate schema definitions for TokenRequest and TokenResponse (lines 1840-1879)
  • JWTAuth security definition (lines 1960-1965)
  • Updated security requirements across existing endpoints

All previously flagged documentation issues appear to be resolved in the generated output.

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