-
Notifications
You must be signed in to change notification settings - Fork 27
[jwt] add module and config #191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
WalkthroughAdds a JWT authentication subsystem and a users service, plus cache and cache backends, many handler/middleware updates to use JWT/users, OpenAPI and observability artifacts, DB migrations for tokens, and fx module wiring/config types. Changes
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
sequenceDiagram
autonumber
actor Client
participant JWTMw as "JWT Middleware"
participant JWTSvc as "JWT Service"
participant Repo as "Token Repo"
participant UsersSvc as "Users Service"
participant Handler as "Protected Handler"
Client->>JWTMw: Request (Authorization: Bearer <token>)
JWTMw->>JWTSvc: ParseToken(ctx, token)
JWTSvc->>Repo: IsRevoked(ctx, jti)
Repo-->>JWTSvc: false
JWTSvc-->>JWTMw: Claims(userID, scopes)
JWTMw->>UsersSvc: GetByID(ctx, userID)
UsersSvc-->>JWTMw: *User
JWTMw->>JWTMw: SetUser + SetScopes
JWTMw->>Handler: Next()
Handler-->>Client: Response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes
Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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 inConfig.ValidateRight now only
TTL == 0is 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 validatingrevokeddependency inNew
Newassumesrevokedis non‑nil; if wiring ever passesnil,IsRevoked/Revokewill 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 configurableTwo small points in
GenerateToken:
- A negative
ttlargument will currently produce an already‑expired token vianow.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 inParseToken
ParseTokenis generally solid, but you might want to tighten and standardize behavior:
- You currently accept any
SigningMethodHMAC. To strictly mirrorGenerateToken(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.Validcase you return a plain"invalid token"error. If callers need to distinguish invalid/expired tokens from other failures (similar to howErrTokenRevokedis used), consider introducing anErrInvalidTokensentinel inerrors.goand returning that here.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis 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 appropriateThe 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: DisabledServiceimplementation is clear and consistentThe
disabledimplementation cleanly satisfiesServiceand consistently returnsErrDisabledfor all operations, which should make wiring and feature‑flagging straightforward for callers.
4a8df1c to
9a89acb
Compare
There was a problem hiding this 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 ofJWT.Secretand potential enable/disable behaviorRight now
JWT.Secrethas no default and an empty string is a valid in-memory value. Depending on howjwt.Configand 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
JWTand validate thatSecretis non-empty when enabled, or- Fail fast during config loading if
Secretis 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 requiringIssuerfor self‑hosted setupsThe default
TTLof 24 hours and a non-emptyIssuerhelp produce usable tokens out of the box. For self-hosted deployments, though, hard-coding"sms-gate.app"could lead toissclaims that don’t match the actual public host.You might consider either:
- deriving the default
Issuerfrom existing HTTP/API config if present, or- leaving
JWT.Issuerempty 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 nitThe wrapper around
time.Durationwith nil‑safeDuration()/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 likeAsDuration()orValue()for slightly clearer usage. Not a blocker.
29-45: Add nil‑guards in unmarshallers to avoid panics on misuse
UnmarshalTextandUnmarshalYAMLassumedis non‑nil. In normal struct-field usage that’s fine, but a manually used*Duration(or a future optional*Durationconfig 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
⛔ Files ignored due to path filters (1)
go.sumis 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.
There was a problem hiding this 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:
Nil pointer dereference at Line 60: The
authHandlerfield is never initialized in the constructor (see lines 75-86), so callingh.authHandler.Register()will panic.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
/tokenendpoint requires authentication to obtain an authentication token, which is logically impossible.The
/authroutes should be registered before theuserauthmiddleware 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.AuthHandlerbut never assigns it to theauthHandlerfield. This will cause a nil pointer dereference panic whenRegister()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
📒 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
fmtandtimeare 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.
There was a problem hiding this 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
Newfunction accepts an unexported*revokedStorageparameter, making it unusable by external packages. IfNewis intended only for internal module wiring, consider making it unexported (new). Alternatively, if external packages should construct the service, export therevokedStoragetype or accept an interface.
98-101: Consider wrapping error for better context.Line 100 returns a generic
"invalid token"error. Consider usingfmt.Errorfto 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
📒 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
AuthHandlerare 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.NewBasicanduserauth.UserRequiredmiddleware (lines 55-58), which means Basic authentication is required to access/auth/tokenand/auth/token/revoke.This appears to implement a token exchange pattern where clients trade Basic credentials for JWT tokens. However, please verify:
- Token generation: Is Basic auth → JWT exchange the intended flow?
- 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.Loggerdirectly without a name qualifier. Sub-handlers appropriately useNamed()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.gocurrently returnfiber.ErrNotImplemented(lines 28-30 and 32-34). The test requests inapi/requests.http(lines 195-208) are placeholders and cannot be meaningfully validated against non-existent logic.Once the
token()andtokenRevoke()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.
There was a problem hiding this 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/Revokecorrectly treat presence in the cache as revocation and now apply a TTL viacache.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
tokenparameter (and related local naming) tojtiorkeyto 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 builtinminfor broader Go version support
ExpiresAtusesnow.Add(min(ttl, s.config.TTL)). If the project targets Go versions without the builtinminfortime.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 / ErrUnexpectedSigningMethodRight now:
- Any
jwt.ParseWithClaimserror is wrapped asfmt.Errorf("failed to parse token: %w", err).ErrInvalidTokenis only returned when the claims type assertion fails orparsedToken.Validis false.ErrUnexpectedSigningMethodis 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 usingErrTokenRevokedfor the revocation path.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 goodError 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 coherentThe module cleanly encapsulates its own cache and
RevokedStorageas private providers and exposes onlyService, with a straightforward disabled path whenConfig.Secretis 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
NewvalidatesConfig, rejects a nilRevokedStorage, and wires anidFactoryfrom nanoid, so misconfigurations or missing dependencies fail fast at startup.
There was a problem hiding this 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
RevokedStoragetype andNewRevokedStorageconstructor 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
IsRevokedmethod 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
ttlparameter andcache.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
📒 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)
9673e3c to
9d5cf91
Compare
There was a problem hiding this 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.secretfield is empty by default, which will cause the JWT service to operate in disabled mode (returningErrDisabledfor 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
deleteTokenhandler returnsErrNotImplemented. According to the JWT service interface, this should calljwtSvc.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
GetByIDandInsert, 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
⛔ Files ignored due to path filters (1)
go.sumis 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 outsidemodels.MigrateRestricting
AutoMigratehere toDeviceonly matches the move ofUserinto theuserspackage, so this function’s scope is now clear and focused. Just make sure:
- There is a corresponding
users.Migrate(or similar) that migrates theusersschema.- 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
ErrDisabledfor all JWT operations when the service is not configured. This provides graceful degradation whenconfig.Secretis 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
/authroute 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.Loggerinternal/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.Usertousers.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.Secretis 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.Usertousers.Useris 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.Userconsistently, 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.Userthroughout.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
Takefor 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 useusers.User.Also applies to: 43-43, 66-66, 96-96
internal/config/config.go (1)
91-95: The codebase properly validates empty JWT secrets. TheConfig.Validate()method ininternal/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 ininternal/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
userspackage import supports the migration of the User type frommodelstousers, while themodelsimport remains for Device and other types.
45-45: LGTM! Cache type updated consistently.The
usersCachefield type and initialization correctly reflect the migration tousers.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.Userhave been consistently updated. Verification confirms:
RegisterUserreturn values assigned tousers.Uservariables (mobile.go:104)RegisterDevicereceivesusers.Userparameters correctly (mobile.go:123)AuthorizeUserreturn values assigned tousers.Uservariables (userauth.go:48)AuthorizeUserByCodereturn values assigned tousers.Uservariables (userauth.go:75)- No remaining
models.Userreferences outside the auth moduleThe type migration is complete and consistent throughout the codebase.
There was a problem hiding this 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
📒 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.goat line 13 viafx.Provide(newRepository, fx.Private), which addresses the previous review concern. Thefx.Privatescope appropriately restricts visibility to the module while making it available for injection to internal consumers.
There was a problem hiding this 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.localfile that developers can customize locally without risk of accidental exposure.internal/sms-gateway/handlers/3rdparty.go (1)
36-83:authSvcis unused; confirm intended auth coverage for/3rdparty/v1/auth
- The
authSvc *auth.Servicefield (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 beforeh.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 changesThe 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
Loginalready treats the cache as best‑effort (miss or failure just falls back to DB) andSetonly 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’sExists→Insertsequence is still racy under concurrent creation attempts; the DB primary key will save you, but the resulting error won’t beErrExists.internal/sms-gateway/modules/auth/service.go (1)
24-36: Auth service integration withusers.Serviceand code-based auth looks sane
- Injecting
usersSvc *users.Serviceand delegatingRegisterDeviceandAuthorizeUserByCodeto it is a clean separation of concerns.AuthorizeUserByCode’s flow (codesCache.GetAndDelete→usersSvc.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
userIDinGenerateUserCode/codesCachetousernamefor clarity, since it’s passed toGetByUsername.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 afterGetUser.While
HasUserchecksGetUser(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
📒 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 consistentSchema and
Migrateimplementation 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 strategyUsing 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 graphAdding
users.Module()andjwt.Module()to the top‑levelModuleis consistent with how other modules are registered, and should give both services access to config/logger/cache as intended. No issues from this diff.
There was a problem hiding this 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 fieldsyntax. 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: GuardWithUseragainst missing user for a clearer failure mode
WithUsercurrently 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: Handlenanoid.Standarderror to avoid nilidGenpanicsAt Line 56 you ignore the error from
nanoid.Standard(21):idGen, _ := nanoid.Standard(21)If initialization fails,
idGenwill be nil and any laterh.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
idGenis 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
ChangePasswordcurrently treats any error froms.cache.Deleteas fatal. If the login entry has already expired or was never cached,Deletewill likely returncache.ErrKeyNotFound, which would cause a valid password change to fail purely due to cache state, even though you’ve already authenticated viaLogin.You already treat
cache.ErrKeyNotFoundas benign inLogin; 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, thenUpdatePassword) looks good.internal/sms-gateway/handlers/mobile.go (2)
114-152: Clarify response semantics for existing vs new users inpostDeviceThe flow for creating/registering a device with the new
users.Servicelooks 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
LoginandPassword.- For already authenticated users, you set
Login = usernamebut leavePasswordempty.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‑emptyLoginas “new credentials”, this could be confusing.If the intent is truly “credentials only for new users”, consider leaving
Loginempty (or documenting that onlyPasswordis conditional) in the authenticated‑user branch.
223-236: Differentiate invalid password from internal failures inchangePassword
changePasswordcurrently treats any error fromh.usersSvc.ChangePasswordas “Invalid current password” and returns401: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
ChangePasswordcan fail for reasons other than a bad current password (e.g. DB error, cache backend failure, hashing error). Mapping all of those to401hides server‑side issues and misleads clients.Consider distinguishing:
- Invalid current password →
401 Unauthorizedwith 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 fromCompareBCryptHash) versus other error types.internal/sms-gateway/handlers/3rdparty.go (1)
64-87: Confirm that protecting/3rdparty/v1/authwith Basic auth is intentionalIn
Registeryou add:router.Use( userauth.NewBasic(h.usersSvc), userauth.UserRequired(), ) h.authHandler.Register(router.Group("/auth"))Given this order, the
/3rdparty/v1/authendpoints registered byauthHandlerwill also go throughNewBasic+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
/authendpoints 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
/authbefore callingrouter.Use.Please verify that the current middleware scope matches the desired API behavior.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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
newItemtonewMemoryItemimproves 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 authThe switch to
users.ServiceinNewBasicplus the newsetUser/GetUserpairing makes the user storage consistent:
- Both
NewBasicandNewCodenow store ausers.Uservalue viasetUser.GetUsertype‑asserts tousers.Userand returns a pointer, andHasUserjust checks for non‑nil.This removes the earlier pointer/value mismatch between Basic and Code flows and should make
GetUser/HasUserreliable 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 consistentThe new
thirdPartyHandlershape andnewThirdPartyHandlerconstructor wire in:
usersSvc *users.Servicefor basic auth viauserauth.NewBasic.authHandler *thirdparty.AuthHandlerfor JWT/3rd‑party auth endpoints.base.Handlerwithloggerandvalidator.The explicit constructor keeps dependencies clear and avoids fx‑specific parameter structs here. No issues from a correctness or maintainability standpoint.
There was a problem hiding this 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 bugFiber'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 makingWithUser’s failure mode clearerNice improvement: both
NewBasicandNewCodenow callSetUser(c, *user), which matchesGetUser’s.(users.User)assertion, so the previous pointer/value mismatch is resolved.HasUsernow correctly delegates toGetUser.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
WithUserwithout 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:actionpattern, 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
📒 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 matchScopeList = "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 withinParseToken. The method callss.revoked.IsRevoked(ctx, claims.ID)and returnsErrTokenRevokedif the token is revoked. TheRevokedStoragedependency 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 consistentSwitch to
users.Userpluspermissions.RequireScope(ScopeRead)+userauth.WithUsermatches the new auth model; SwaggerJWTAuthannotation 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 modelMigration to
users.Userpluspermissions.RequireScope(ScopeRead/ScopeWrite)anduserauth.WithUseris consistent and preserves existing business logic aroundsettingsSvc. No issues spotted.internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
1-36: Permission middleware is straightforward and consistent
SetScopes,HasScope, andRequireScopeare simple and do what the handlers expect (including honoringScopeAll). 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 goodThe switch to
users.Userplus per‑route scopes (ScopeList,ScopeWrite,ScopeDelete) anduserauth.WithUseris 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
/authendpoints are for token management only (POST/tokento create managed tokens, DELETE/token/:jtito revoke). Both are already protected byuserauth.WithUser()middleware in their handlers, and requiring group-level authentication is the correct design. No issues detected.
There was a problem hiding this 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 fromnanoid.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 whenh.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/:jtiendpoint is registered but returnsErrNotImplemented. Given this is a draft PR, this is expected.When ready to implement this handler, it should:
- Extract the
jtiparameter from the route- Call
h.jwtSvc.RevokeToken(c.Context(), jti)- 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
usernameusesid[:6](indices 0-5) andpasswordusesid[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
📒 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:managescope and user authentication via middleware. The past review concern about missing@Securityannotations 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 ApiAuthand@Security JWTAuthannotations (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
ttlis omitted from the request,GenerateToken(line 58–60 ofinternal/sms-gateway/jwt/service.go) explicitly defaults it tos.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 flowThe Delete method implementation is correct and properly integrated. Cache invalidation occurs at
internal/sms-gateway/users/service.go:102within 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 tousers.User.The signature change from
models.Usertousers.Useris consistent with the broader migration. The function correctly usesuser.IDwhich exists in the new type.
230-232: LGTM! Correct delegation to users service.The change properly delegates password changes to
usersSvc.ChangePasswordwith 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
userauthprotection instead ofdeviceauth, 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.IDis always equal to the login username:
- In
internal/sms-gateway/users/service.go:51, theCreatemethod explicitly setsID: username, using the provided username parameter as the user's ID.- The
Userstruct contains only anIDfield with no separate Login or Username field.- For new users, the generated 6-character string is passed as
usernametoCreate(), which becomes theuser.ID.- For existing users,
user.IDIS the login username.Both code paths return the correct login username in the response.
Likely an incorrect or invalid review comment.
There was a problem hiding this 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 tokenscopesagainst permission constants (especiallydevices:write).The addition of
@jwtTokenand the JWT‑secured examples for messages and devices look good, and the new/3rdparty/v1/auth/tokenand/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
tokenModelshape and tags line up with thetokensDDL in20251121071748_add_tokens.sql, but the SQL migration also defines thefk_tokens_userforeign key while the GORM model does not. SinceMigrateusesAutoMigrateand 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
Migrateis 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 indexingexpires_atif you’ll query/cleanup by expiry.The tokens schema looks consistent with the Go model and has good indexes on
user_idandrevoked_at. If you plan to frequently select or delete tokens based onexpires_at(e.g., cleanup of expired tokens), consider adding an index onexpires_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 vsnewDisabled()depending onConfig.Secret) and the migration registration viadb.RegisterMigration(Migrate)both look good.One minor thing: the private
cache.Cacheprovider created withfactory.New("jwt")isn’t consumed by any other provider in this module as shown. If nothing injwtcurrently depends oncache.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
Revokealways returns nil even when no row matches(id,user_id,revoked_at is null), and it usesgorm.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’sRowsAffectedand surfacing a domain error. Also, using a Gotime.Now()value (e.g.,Update("revoked_at", time.Now())) would avoid tying this to dialects that don’t supportNOW().internal/sms-gateway/handlers/thirdparty/auth.go (3)
36-40: Middleware wiring is sound; consider renamingerrorHanlderand clarifying responsibilitiesThe route setup with
router.Use(h.errorHanlder)pluspermissions.RequireScopeanduserauth.WithUsergives a clear protection story. Two small polish points:
- The method name
errorHanlderlooks like a typo; renaming toerrorHandlerwill make usages easier to grep.- This middleware currently only maps
ErrDisabled,ErrInitFailed, andErrInvalidConfig. 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.TTLis optional (omitempty) andGenerateTokenfalls back toconfig.TTLwhen 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
postTokento explicitly state that:
scopesare required, andttlis 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
deleteTokendelegates tojwtSvc.RevokeToken, which in turn calls the repository’sRevokewithout surfacing whether any row was actually updated. As a result, a request with a non‑existentjti(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.Durationschema may not match actual JSON shape
internal_sms-gateway_handlers_thirdparty.tokenRequest.ttlreferencestime.Duration, which this spec models as anintegerwith nanosecond‑based enum values. In Go,time.Durationis 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.Durationvia 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 builtinminThe validation around
userID,scopes, and non‑negative TTL plus the “0 means default config TTL” behavior all look solid, and capping expiry withmin(ttl, s.config.TTL)enforces a max TTL.The only caveat is that the generic builtin
minrequires Go 1.21+. If yourgo.modstill 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 asErrInvalidTokenfor callersRight now, parse/validation failures from
jwt.ParseWithClaimsare wrapped asfmt.Errorf("failed to parse token: %w", err), while only the!ok || !parsedToken.Validbranch returnsErrInvalidToken. That means callers can’t reliably useerrors.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
📒 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
Serviceand consistently returnsErrDisabledfrom 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‑shapedThe Service contract plus Claims/TokenInfo structs are coherent with the implementation in
service.goand the third‑party handlers; fields and JSON tags give consumers what they need without overexposing internals.
There was a problem hiding this 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 indevices/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 goUpdate 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
Claimsstruct storesUserIDseparately (line 86) even thoughRegisteredClaims.Subjectalready holds the same value (line 82). This duplication may cause maintenance issues if they ever diverge.If the custom
UserIDfield serves no distinct purpose, consider removing it from theClaimsstruct injwt.goand using onlySubjectthroughout the codebase.
63-66: Consider validating that scopes are from an allowed set.The function requires
len(scopes) > 0but 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
deferensures the duration is recorded even iff()panics.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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
statuslabel keeps cardinality low.Note: The absence of a
reasonlabel 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).
There was a problem hiding this 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", buttokensRevokedCounterininternal/sms-gateway/jwt/metrics.go(line 50-55) only exposes astatuslabel. The alert will never fire.
34-42: Critical: Label mismatch persists, and inconsistent division protection.Two issues:
Label mismatch (same as previously identified): The alert queries
reason=~"expired|revoked", buttokensValidatedCounterininternal/sms-gateway/jwt/metrics.go(lines 43-48) only exposes astatuslabel. This alert will never fire.Inconsistent division protection: This alert uses
max(..., 0.00001)while line 5 usesclamp_min(..., 1e-9). Both work, but the inconsistency makes the codebase harder to maintain.Consider standardizing on
clamp_minfor 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.1Note: This also removes the non-existent
reasonlabel filter. If you need per-reason alerting, add thereasonlabel to the metric inmetrics.gofirst.
🧹 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
📒 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_minto prevent division by zero and queries thestatuslabel 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 ininternal/sms-gateway/jwt/metrics.go.
146-156: LGTM! Validation success rate correctly calculated.The panel correctly uses
sms_auth_jwt_tokens_validated_totalwith proper division-by-zero protection viaclamp_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_totalbystatus="error", which is the proper approach given the metric definition inmetrics.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_bucketfor p99, p95, and p50 percentiles.
603-613: LGTM! Validation success rate timeseries correctly configured.The query correctly uses
sms_auth_jwt_tokens_validated_totalwith proper division protection.
667-681: LGTM! Panel correctly uses the existing metric aggregated by status.The query correctly aggregates
sms_auth_jwt_tokens_validated_totalbystatus, 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
statusto 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_totalbystatus="error"and includes proper division protection withclamp_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
CounterVecfor labeled counters andHistogramfor 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
WithLabelValuesandIncmethods.
98-117: LGTM! Observation methods correctly use timer pattern.The observe methods correctly use
prometheus.NewTimerwith deferredObserveDurationto measure operation duration, which ensures timing is captured even if the operation panics.
There was a problem hiding this 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
📒 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 correctstatuslabel that exists in the metrics.Also applies to: 15-15, 25-25, 35-35
There was a problem hiding this 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 usesmax(..., 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.05Or 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.1Also applies to: 35-35
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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-existentreasonlabel references in earlier commits) and both division operations are protected against zero denominators viaclamp_min()andmax().
24-32: Verify Alert 3 sensitivity for production operations.The
JWT_Revocation_Failuresalert fires onrate(...) > 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.
c533392 to
a66a972
Compare
There was a problem hiding this 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 onmemoryItem.validUntilwhen updating TTL inGetIn the
Getpath with options,item.validUntilis mutated underm.mux.Lock, butgetItemlater callsitem.isExpired(time.Time)without holding any lock. Other goroutines can concurrently callGetwith TTL‑updating options on the same key, leading to unsynchronized reads/writes ofvalidUntiland a data race under the Go memory model (and likely flagged by the race detector).A minimal fix is to avoid mutating
memoryItemin place and instead create a newmemoryItemwith 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
Getis: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:WithTTLimplementation contradicts its own documentationDocs say “A TTL of zero or negative means the item will not expire”, but the implementation always sets
validUntil = time.Now().Add(ttl). Forttl <= 0this 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 errorsSwitching
getUserCodeto useusers.Useris aligned with the new users module anduserauth.WithUser, that looks fine.In
changePassword, every error fromusersSvc.ChangePasswordis logged and then returned as401 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: MakeWithUserpanic message explicit instead of a nil‑pointer deref
WithUserstill doesreturn handler(*GetUser(c), c), so if it’s ever used without a preceding auth middleware that callsSetUser, 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
makeKeyhashesusernameandpassworddirectly 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 andidGenis used later (e.g., inpostDevice). If initialization fails and returns a nil function, callingh.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 validatingIssuer(or documenting that it is optional)
Config.Validatedoesn’t currently enforce thatIssueris non‑empty, even though it’s written into tokens and is later required inParseTokenviajwt.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 handleRowsAffected == 0inUpdatePassword
UpdatePasswordwraps onlyresult.Errorand treats “no rows updated” as success, so a non‑existent user ID silently passes, unlikeGetByIDwhich returnsErrNotFound. A past review already suggested checkingRowsAffected; 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
Updatesemantics in your current version (specifically thatRowsAffected == 0indicates “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)
UnmarshalTextcurrently feeds the raw string intotime.ParseDuration, which will fail on an empty string. If you expect configs likettl: ""orttl: null(or want “missing or empty” to mean “0 / disabled”), you may want to treat empty input specially, e.g. map it to0instead 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 consistentReturning
ErrDisabledfrom allServicemethods is a clean way to represent a globally disabled JWT subsystem, andnewDisabledhiding the concrete type behind theServiceinterface 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 straightforwardThe
Userstruct is minimal and thenewUserhelper cleanly maps fromuserModelto the domain type. One thing to decide explicitly: ifmodelshould never benil, it may be worth documenting that contract; if it’s possible for repository code to passnil, adding a quick guard (or returningnil/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
NewCodenow correctly usesAuthorizeUserByCodeandSetUser, but it doesn’t set any scopes. Any routes guarded only bypermissions.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 paramThe added
@Security JWTAuthandpermissions.RequireScope(ScopeRead)plususerauth.WithUserare consistent and tighten access control without changing the 501 behavior. Theuser users.Userargument ingetis currently unused; if that’s intentional for future use, consider naming it_ users.Userto keep linters quiet. Also, if your OpenAPI tooling supports it, documenting that this route requires theScopeReadscope (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, andRequireScopecorrectly wire scopes via Fiber locals, andScopeAllbehaves as expected viaslices.ContainsFunc. For typical small scope lists this is perfectly fine. Longer‑term, you might want to add logging/metrics on forbidden responses inRequireScope(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
errorHandlercan return501 Not Implementedforjwt.ErrDisabled, and potentially400 Bad Requestforjwt.ErrInvalidParams. The swagger for both/3rdparty/v1/auth/tokenand/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 501entries 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 TODOUsing
userauth.NewBasic(h.usersSvc)for/mobile/v1/deviceand/mobile/v1/user/codealigns these endpoints with the new users service and theusers.Userdomain type, which looks good.There’s still a TODO noting that
/mobile/v1/user/password“Should be underuserauth.NewBasicprotection instead ofdeviceauth”. 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 lookThe
Loginflow (cache → DB → cache.Set) is sensible for performance, but two aspects are worth tightening:
- You’re passing the raw
passwordinto theloginCacheAPI (Get,Set,Delete). That’s fine ifloginCacheonly 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 fromcache.Delete(ctx, username, currentPassword)aborts the password change, even though the cache is only an optimization. It may be more robust to treatcache.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 inLogin.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 combinedApiAuthandJWTAuthIn Swagger 2.0, a
securityarray 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.Durationrepresentation may be confusing to API consumers
internal_sms-gateway_handlers_thirdparty.tokenRequest.ttlis modeled astime.Durationwith 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 forttl(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 supportsminontime.DurationThis code relies on
min(ttl, s.config.TTL)overtime.Duration. If you are using the Go 1.21+ builtinmin, confirm thatgo.modand CI are set to a compatible Go version; otherwise, provide a local helperfunc minDuration(a, b time.Duration) time.Durationinstead.internal/sms-gateway/modules/auth/service.go (2)
62-88:GenerateUserCodeloop depends on Go’s integerrangeand could use a clearer retry patternThe
for range 3construct is valid only on newer Go versions that support ranging over integers. Please ensure yourgo.modand CI toolchain target a compatible Go release; otherwise, rewrite as an explicitfor i := 0; i < 3; i++ { ... }loop. The retry logic itself (up to 3 attempts onrand.Read/SetOrFail) looks fine.
90-97: User lookup viaAuthorizeUserByCoderelies on usernames; consider clarifying naming
AuthorizeUserByCodepulls a string fromcodesCacheand passes it tousersSvc.GetByUsername, whileGenerateUserCode’s parameter is nameduserID. If the stored value is actually the username (which seems likely), renaming the parameter and local variable tousernamewould 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 forTThe
newItem[T]helper correctly enforces thatTis a pointer type and fixes the earlier nil‑pointerUnmarshalissue, but that requirement is only enforced at runtime and only on read paths (Get,GetAndDelete,Drain). A misconfiguredNewTyped[SomeStruct]will happily compile and evenSet, but will start failing with"cache: type ... must be a pointer"once something callsGet.To make this easier to discover for callers, consider either:
- Explicitly documenting in the exported comments for
Typed/NewTypedthatTmust be a pointer type implementingItem(e.g.NewTyped[*User], notNewTyped[User]), or- Adding a cheap validation in
NewTyped(e.g. callingnewItem[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/authroute protectionThe new wiring makes all
/3rdparty/v1routes (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
UserRequiredenforces it), but it relies on bothuserauth.NewBasicandjwtauth.NewJWTbehaving as no‑ops when their respective credentials are absent (e.g. noAuthorizationheader 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
⛔ Files ignored due to path filters (1)
go.sumis 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.gopkg/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.gopkg/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:actionpattern and Go naming conventions. The three scopes (list, write, delete) provide a sensible access control model for webhook operations.One optional consideration: verify that
ScopeWriteappropriately 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
migrationsvariable 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
migrationsis 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:
- User migration is properly handled in the new jwt or users module
- 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', fieldparameters. The migration is complete and consistent.internal/config/types.go (1)
11-27: Duration wrapper and nil‑safe accessors look goodThe
Durationalias and itsDuration()/String()methods are straightforward, handlenilsafely, and are appropriate for config use as a thin wrapper overtime.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 solidThe
memoryCache/memoryItemdesign, default TTL handling inNewMemoryandnewItem, and theisExpiredhelper form a clear, easy‑to‑reason‑about in‑memory implementation. Zero TTL meaning “no expiration by default” is consistent with the docs, andErrKeyNotFoundvsErrKeyExpiredseparation viagetItem/getValueis clean.Also applies to: 58-92, 331-353
93-176: Cleanup/Drain semantics correctly preserve non‑expired items only
Cleanupremoves expired entries first and only then invokes the callback, andDrainusescleanupto snapshotm.itemsand swap in a fresh map before copying out values. This matches the prior contract that:
- expired items are dropped before
Drainruns, andDrainreturns only non‑expired items and leaves the cache empty afterward.This keeps the behavior aligned with the existing Redis cache behavior and previous
memoryCache.cleanupsemantics. 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: anfx.Modulewith a package‑specific name, a scoped named logger vialogger.WithNamedLogger("3rdparty"), andfx.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: Confirmusers.UserGORM mapping matches previousmodels.UserSwapping the association type to
users.Useris consistent with the new users module and should work as long asusers.Userkeeps the same table name/PK type thatUserIDpoints to. Please double‑check that the new model’s GORM config (table name, primary key column/type) still matches the existingwebhooksFK/constraints soAutoMigratedoesn’t unexpectedly rewrite them.internal/sms-gateway/handlers/module.go (1)
9-10: thirdparty handlers module wiring looks consistentImporting
handlers/thirdpartyand addingthirdparty.Module()to the handlersfx.Modulecleanly integrates the new submodule; this aligns with the existingnewThirdPartyHandlerusage 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 appAdding
users.Module()andjwt.Module()to the top‑levelModuleis 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 (orfx.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 andusers.User– verify upstream auth pipelineSwitching the handler signatures to
users.Userand wrapping the routes withpermissions.RequireScope(ScopeList|ScopeDelete)plususerauth.WithUseris 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.SetUserbeforeWithUserruns; otherwiseWithUserwill panic.Please double‑check the 3rd‑party router setup (
newThirdPartyHandlerand 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
NewBasicnow logs in viausers.Service, stores ausers.UserviaSetUser, and assignspermissions.ScopeAll, whileSetUser/GetUserconsistently use a value type inLocals. 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 tousers.Userlooks consistent and non-breakingUpdating
SelectStatesandGetStateto acceptusers.Userwhile still keying everything offuser.IDkeeps 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-scopedThe exported error variables cover the main JWT failure modes cleanly and are suitable for use with
errors.Isacross the JWT subsystem and callers.internal/sms-gateway/jwt/jwt.go (1)
10-27: JWT service interface and models are well-shapedThe
Serviceinterface and theClaims/TokenInfotypes 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 stackThe
AuthHandlerconstructor andRegistermethod are wired cleanly: sharedbase.Handler, per‑routererrorHandler, and per‑routepermissions.RequireScope(ScopeTokensManage)plususerauth.WithUsermatch 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 consistentThe 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
*userintoauthSvc.RegisterDeviceand returningLogin/Passwordonly for the newly created‑user path matches the endpoint’s description. I don’t see functional problems in this section (aside from theidGeninitialization issue called out separately).internal/sms-gateway/users/service.go (2)
13-33: Service wiring is straightforward and idiomaticThe
Servicestruct andNewServiceconstructor 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
Createcorrectly checks for existing users, hashes the password with BCrypt, and wraps repository errors with useful context.GetByUsernameis a simple and appropriate wrapper overGetByID. 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 schemasThe
/3rdparty/v1/auth/tokenPOST and/3rdparty/v1/auth/token/{jti}DELETE operations now include bothApiAuthandJWTAuthsecurity 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 consistentThe new
/health/live,/health/ready, and/health/startupendpoints, together withsmsgateway.HealthResponseand 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 titleDefining
JWTAuthas anapiKeyin theAuthorizationheader 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
Validatecorrectly 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 safeConstructor 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
ParseTokenenforces 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, andIsRevokeduseWithContext, narrow queries ontokenModel, and wrap underlying GORM errors with descriptive messages. The revocation update is idempotent and theIsRevokedcheck is simple and efficient.internal/sms-gateway/modules/auth/service.go (1)
24-36: Auth service wiring to users/devices/online modules looks cleanThe updated
Servicestruct andNewconstructor neatly injectusersSvc,devicesSvc,onlineSvc, andlogger, 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 idiomaticThe 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
statuslabel, this is good to go.Please confirm that your Prometheus alert rules and dashboards reference these exact metric names and the
statuslabel only (or update them accordingly). For example:Also applies to: 83-117
There was a problem hiding this 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” semanticsThe data model and helpers are straightforward and match the comments:
WithTTLusingtime.Now().Add(ttl)on apply and treatingttl <= 0as “no expiry” viatime.Time{}is consistent with the documentation, andWithValidUntilis 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 interprettime.Time{}as “no expiration”), and perhaps mention thatWithValidUntilcannot currently express “no expiry” and thatWithTTL(0)is the supported way to get that behavior.
107-208: GetOption helpers are implemented correctly; consider documenting interactions and edge casesThe
AndSetTTL/AndUpdateTTL/AndSetValidUntil/AndDefaultTTL/AndDeletehelpers are all thin, correct setters ongetOptionsand the pointer‑capture pattern forttl/validUntilis 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
AndSetTTLandAndUpdateTTLdon’t mention how non‑positive TTLs are interpreted, even thoughWithTTLclearly definesttl <= 0as “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+AndDefaultTTLorAndDeleteplus a TTL update), the effective precedence is delegated to the consumers ofgetOptions. It might be worth documenting the intended precedence or combination rules somewhere nearGetOptionso 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
📒 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:getOptionsplumbing andisEmptyhelper are solidThe
getOptionsstruct andapply/isEmptyhelpers are minimal and correctly model optional behavior via pointer fields and booleans.isEmptychecks all flags, so consumers can cheaply skip post‑processing when noGetOptions were supplied.No functional issues here from what’s visible.
70e753c to
d32510b
Compare
There was a problem hiding this 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 checksEach endpoint now uses
permissions.RequireScope(...), which will return403 Forbiddenwhen the user lacks the required scope. The Swagger annotations for list/post/delete webhooks still only declare 401 and 500 failures.Consider adding a
403error 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/exportlacks a permission scope guard unlike other message endpointsIn
Register, all 3rd‑party message routes except inbox export are wrapped withpermissions.RequireScope(...):
GET ""→ScopeListPOST ""→ScopeSendGET ":id"→ScopeReadPOST "inbox/export"→ onlyuserauth.WithUser(h.postInboxExport)So
/3rdparty/v1/messages/inbox/exportis 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.,
ScopeReador 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 TTLIn
memoryCache.Get, the TTL update block has two problematic edge cases:
- When an item has no TTL (
item.validUntil.IsZero()),AndUpdateTTLdoesitem.validUntil = item.validUntil.Add(*o.updateTTL). This computes a timestamp relative to the zero time, so the entry becomes effectively “always expired” andgetItemwill returnErrKeyExpiredinstead of extending its lifetime.- When the cache was created with
NewMemory(0)(documented as “no expiration by default”),AndDefaultTTLstill executesitem.validUntil = time.Now().Add(m.ttl). Withm.ttl == 0, that setsvalidUntilto “now”, making the entry expire almost immediately, which contradicts the “no expiration” default.A minimal fix is to special‑case these two situations:
- Treat
AndUpdateTTLon an item with no existing TTL as “set a fresh TTL from now”.- Treat
AndDefaultTTLwithm.ttl <= 0as “clear the TTL” (no expiration), matchingNewMemoryandWithTTLsemantics.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/AndDefaultTTLbehave sensibly for “no TTL” entries andNewMemory(0)caches. Based on learnings, this also keeps cleanup/Drain semantics intact because expiration is still driven solely byvalidUntil.pkg/cache/redis.go (1)
24-48: Fix Redis TTL edge cases:AndDefaultTTLwith zero TTL andAndUpdateTTLwithout existing TTLTwo TTL bugs confirmed in the Redis cache implementation:
AndDefaultTTLwithRedisConfig{TTL: 0}(lines 306–307)
SettingttlTimestamp = time.Now().Add(r.ttl).Unix()whenr.ttl == 0produces the current Unix timestamp. This causesHExpireAtto set the field expiration to "now", immediately deleting it on next access—opposite of the intended "no default TTL" behavior.
AndUpdateTTLwhen field has no TTL (lines 34–36 in Lua script)
WhenHTTLreturns-1(field exists but has no TTL),newTtl = -1 + ttlDeltaproducesttlDelta - 1or a negative value. This is unintuitive; when a field has no TTL, extending byttlDeltashould set a fresh TTL ofttlDeltaseconds, not treat it as offset arithmetic.Missing Redis version requirement (type comment)
TheredisCachetype 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 inGetByIDand detect missing users inUpdatePasswordTwo important issues here, both previously flagged:
- Incorrect pointer passed to GORM in
GetByIDuser := new(userModel) if err := r.db.Where("id = ?", id).Take(&user).Error; err != nil {
useris already of type*userModel, so&useris**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)toTake(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 }
UpdatePasswordsilently succeeds when no rows are updatedGORM’s
Updatedoes not return an error when zero rows are affected (e.g. missing user ID). To keep behavior consistent withGetByIDreturningErrNotFound, you should checkRowsAffected: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
Takeshould receive*userModel(not**userModel) and thatRowsAffectedis the correct way to detect missing rows afterUpdate.internal/sms-gateway/jwt/config.go (1)
12-16: Optional: decide whetherIssuershould be validated or explicitly documented as optional
Config.Issueris present but not validated inValidate(). 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: Clarifyttlsemantics, add bounds, and align with OpenAPI definition
tokenRequest.TTLis auint64in (implicit) seconds and is passed as:time.Duration(req.TTL) * time.SecondThere are a few problems here:
- Very large
ttlvalues can overflowtime.Duration(int64) during the cast/multiply, silently producing incorrect expiries.ttl == 0is accepted but its meaning (“no expiry”, “use default”, or “immediately expired”) isn’t defined or validated.- The generated OpenAPI currently models
ttlastime.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.Durationand treat it as such, then regenerate Swagger).Also applies to: 76-76
internal/sms-gateway/handlers/mobile.go (1)
43-74: Still ignoringnanoid.Standarderror; handle init failure explicitly
idGen, _ := nanoid.Standard(21)ignores the returned error. If initialization ever fails and returns a nil function, subsequent calls toh.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
newMobileHandlerto 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 commandsThis implementation relies on Redis per-field hash TTL commands (
HEXPIRE*,HTTL, etc.) and theirFIELDSsub-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
redisCachetype 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 BackgroundUsing
user.IDconsistently for both cache keying and the repository filter keeps message state strictly scoped to the current user, and the negative-caching ofErrMessageNotFoundavoids repeated DB hits. The behavior is sound.Non-blocking: you might eventually want
GetStateto accept acontext.Contextand thread it through tocache.Get/cache.Setinstead of usingcontext.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 entriesIn
Enqueue, whenmessage.IDis empty you generate a new external ID:if msg.ExtID == "" { msg.ExtID = s.idgen() } state.ID = msg.ExtIDbut 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 thatGetState(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(orstate.ID) so it always matches whatGetStatewill 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
userModelitself andMigratelook fine and consistent withDevice.UserID(varchar(32)). The commented-outDevices []models.Devicerelation withOnDelete:CASCADEmeans 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:CASCADEtag ontoDevice.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;idgennow appears unusedInjecting
usersSvc,devicesSvc, andonlineSvcviaNewlooks clean and makes the dependencies explicit. However,idgenis 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
idgenfield and thenanoid.Standard(21)call, and- Dropping the
github.com/jaevor/go-nanoiddependency from this module if nothing else uses it.This will simplify the service and avoid unnecessary RNG work at startup.
Please confirm whether
idgenis used elsewhere in this package or can be safely removed.
90-97: Check semantics of user identifier passed through one-time codes
RegisterDevicenow takes ausers.Userand delegates device creation todevicesSvc.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 toGetByUsername. This is fine if, in your domain, the value cached inGenerateUserCodeis 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 acrossGenerateUserCodeandAuthorizeUserByCode.- If the cached value is a primary key/ID, consider calling a
GetByID‑style method onusersSvcinstead ofGetByUsername.Clarifying this will help prevent future bugs and confusion.
Please double-check what value your handlers pass into
GenerateUserCodeand ensure it matches whatGetByUsernameexpects.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
NewMetricsandNewRepository(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/jsonbut 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. IfGetByUsernamecan 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/ChangePasswordpass the rawpasswordintologinCache.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
loginCacheimplementation 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 robustThe parameter validation (non-empty
userID, non-empty scopes, non-negative TTL, zero TTL → default from config) plus capping expiry atmin(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()forIssuedAt/ExpiresAtto make the intended timezone of JWT timestamps explicit, but it’s not required for correctness.
114-161: Consider normalizing ParseToken errors toErrInvalidTokenThe 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 sentinelErrInvalidToken, while structurally invalid tokens or bad claims useErrInvalidToken.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) toErrInvalidTokenand log the underlyingparseErrinternally, 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 tighteningWithUserpanic semanticsThe switch to
SetUser+GetUserstoring ausers.Uservalue inLocalsfixes the previous pointer/value mismatch and makesHasUserandWithUserconsistent across both Basic and Code auth flows. Grantingpermissions.ScopeAllon successful Basic auth also aligns with the new permissions middleware.
WithUserintentionally panics when no user is present (handler(*GetUser(c), c)), as documented. Given that your routes usingWithUseralready chainUserRequired()beforehand, this should be safe in practice. If you ever expectWithUserto be reused more freely, a small guard with a clearer panic message (or a 401) aroundGetUser(c) == nilwould 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 fromuserauth.GetUserand only generating username/password for new users matches the route docs (“Returns user credentials only for new users”). Passing*userintoRegisterDeviceand usingusernamein the response is consistent with the newusers.Usermodel.- Updating
getUserCodeto acceptusers.Userand wiring the route throughuserauth.NewBasic(h.usersSvc),UserRequired(), andWithUser(h.getUserCode)creates a clean, type-safe flow from Basic auth to code generation.changePasswordnow correctly delegates tousersSvc.ChangePasswordusingdevice.UserIDand 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
usersSvcand 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 underuserauth.NewBasicprotection instead ofdeviceauth”; 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 semanticsThe interface shape and method docs align with both
memoryCacheandredisCacheimplementations. One small suggestion: in theGetcomment you state that an expired key yieldsErrKeyExpired, but for backends like Redis that eagerly drop expired fields, callers will effectively seeErrKeyNotFoundinstead. 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 untilCleanup, Redis removes them immediately).pkg/cache/options.go (1)
75-105: Clarify GetOption semantics for items without TTL and caches with zero default TTLThe
getOptionsstructure and helpers (AndSetTTL,AndUpdateTTL,AndSetValidUntil,AndDefaultTTL,AndDelete) give a nice, expressive surface. The one missing piece is documentation for edge cases:
- What should
AndUpdateTTLdo when an item has no existing TTL (e.g.,validUntilis zero in the in-memory cache, or HTTL returns “no TTL” in Redis)?- How should
AndDefaultTTLbehave when the cache’s default TTL is zero (meaning “no expiration” inNewMemory, and effectively “no default TTL” inRedisConfig)?Right now these behaviors are implicitly defined in
memoryCache.GetandredisCache.Get, and they’re a bit inconsistent. Once you address the TTL handling bugs in those implementations, consider adding 1–2 sentences to theAndUpdateTTLandAndDefaultTTLcomments 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
⛔ Files ignored due to path filters (1)
go.sumis 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.gopkg/cache/cache.gopkg/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.gopkg/cache/cache.gopkg/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 usageImporting
github.com/android-sms-gateway/server/internal/sms-gateway/usersis consistent with the updated method signatures usingusers.User, and there’s no conflict with existingmodelsusage. Givenusers.Userexposes anID stringfield, continuing to dereferenceuser.IDis type-safe. (Based on relevant_code_snippets forinternal/sms-gateway/users/domain.go.)
124-133: SelectStates: user scoping and error context look goodOverriding
filter.UserIDwithuser.IDcorrectly 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
Migratenow only migratesDevice, which is fine as long asinternal/sms-gateway/users.Migrate(and any jwt/token migrations) are wired into app startup separately. Otherwise new deployments will miss theusersschema.Please double-check your app/bootstrap wiring to confirm that
users.Migrateand jwt-related migrations are called alongsidemodels.Migrate.internal/sms-gateway/handlers/webhooks/permissions.go (1)
3-7: Webhooks scope constants are clear and reusableThe 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.Usertousers.Userand 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 solidThe move to an explicit constructor with
usersSvc,jwtSvc, and the various controllers (includingauthHandler) is clean and makes the handler’s dependencies very clear. The baseHandlerinitialization is consistent with the rest of the codebase, and keepingjwtServiceonly as a constructor parameter while storingjwtSvcis 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 theUse, which is typically desirable.Please double‑check that
/3rdparty/v1/authis 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‑structuredUsing Fiber locals plus
HasScope/RequireScopegives a clear, composable pattern for scope checks, and recognizingScopeAllinHasScopecentralizes 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 usageThe
Serviceinterface andClaims/TokenInfotypes are minimal and to the point, and they line up with howjwtauth.NewJWTusesParseTokenand theUserID/Scopesfields. Embeddingjwt.RegisteredClaimskeeps you compatible with the underlying library’s validation model.internal/sms-gateway/handlers/webhooks/3rdparty.go (1)
45-52: Route-level scope checks combined withWithUserare wired correctlyThe updated handlers take
user users.Userand are only invoked viauserauth.WithUser, while per-route authorization is enforced withpermissions.RequireScope(ScopeList/ScopeWrite/ScopeDelete). Given the upstream/3rdparty/v1middleware 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 robustThe
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 correctUser 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 permissionsUsing
users.Userin handler signatures, adding@Security JWTAuthto the docs, and wiring routes throughpermissions.RequireScope(ScopeRead/ScopeWrite)plususerauth.WithUsergives 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
NewvalidatesConfig, enforces non-nilRepository/Metrics, and correctly propagatesnanoid.Standarderrors instead of risking a nilidFactory. The resultingservicestruct wiring is cohesive and idiomatic for dependency injection; no changes needed here.
163-177: Revocation flow and metrics are straightforward
RevokeTokencleanly delegates toRepository.Revokeand 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 idiomaticThe GORM usage for insert, revocation (single UPDATE with
revoked_at is nullguard), 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 helpfulThe expanded comments for
ErrKeyNotFound,ErrKeyExpired, andErrKeyExistsaccurately describe how the in-memory and Redis caches use these errors, and the examples read well. No changes needed.
There was a problem hiding this 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: Validatettlto avoidtime.Durationoverflow and clarify zero/huge TTL behavior
req.TTLis converted with:time.Duration(req.TTL) * time.SecondWithout bounds, very large
ttlvalues can overflowtime.Duration(int64) and yield incorrect or even negative lifetimes. The semantics forttl == 0are 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
0should behave (either reject with 400 or treat it as “use configured default”).You can enforce this either via
validatortags onsmsgateway.TokenRequestor an explicit check here before callingGenerateToken.
🧹 Nitpick comments (3)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
24-35: Verify identity semantics between JWT claims and users service
claims.UserIDis passed intousersSvc.GetByUsername, and a similar pattern exists inauth.Service.AuthorizeUserByCode, where auserIDstring is also resolved viaGetByUsername. If this value is actually a stable user ID (not a login/username), consider:
- Switching to a dedicated
GetByID-style lookup onusersSvc, 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 3requires Go 1.22+; consider a more portable loopThe
for range 3construct 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.ReadorSetOrFailfailures) can remain unchanged.
125-138: Align one-time-code user lookup with users service identity model
AuthorizeUserByCodepulls auserIDstring fromcodesCacheand then calls:user, err := s.usersSvc.GetByUsername(userID)This mirrors the pattern in
jwtauth.NewJWT, whereclaims.UserIDis also resolved viaGetByUsername. 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
⛔ Files ignored due to path filters (1)
go.sumis 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.0is the latest stable release ofgolang-jwt/jwt/v5and 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/v5in all JWT-related imports (found ininternal/sms-gateway/jwt/service.goandinternal/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 flowThe new
/3rdparty/v1/auth/tokenand/3rdparty/v1/auth/token/{jti}paths,JWTAuthsecurity definition, andsmsgateway.TokenRequest/TokenResponseschemas all look consistent with the handlers inthirdparty/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 goodScope 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-scopedWrapping the auth routes with
errorHandlerand translatingjwt.ErrInvalidParams,ErrInitFailed/ErrInvalidConfig, andErrDisabledinto 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 consistentThe added
@Security JWTAuth+ 403 responses, updated handler signatures tousers.User, and router wiring:
ScopeSendfor POST/3rdparty/v1/messagesScopeListfor GET/3rdparty/v1/messagesScopeReadfor GET/3rdparty/v1/messages/{id}ScopeExportfor POST/3rdparty/v1/messages/inbox/exportare all consistent with the new
messagespermission 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
185514c to
5502158
Compare
There was a problem hiding this 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 fromnanoid.Standard.The error from
nanoid.Standard(21)is silently ignored. If initialization fails and returns a nil function, callingh.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 validatingPrivateTokenconfiguration for non‑public mode
AuthorizeRegistrationassumesconfig.PrivateTokenis correctly set whenMode != 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 butPrivateTokenis blank.
126-135: Clarify naming for value passed intoGetByUsername
userIDfromcodesCache.GetAndDelete(code)is passed tos.usersSvc.GetByUsername(userID), but perinternal/sms-gateway/users/service.gothis parameter is conceptually a username/login, not an internal ID. Renaming the parameter and local variable tousername(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/authroutesAll 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.NewJWTshould behave as an additive/optional authenticator: it should only attach a user when a valid JWT is present and avoid hard‑failing on a missingAuthorization: Bearerheader, so that Basic can still succeed and satisfyUserRequired().If any
/authendpoints 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
⛔ Files ignored due to path filters (1)
go.sumis 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.WithNamedLoggerfor 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:actionnaming 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:actionpattern 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.Usertousers.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.Usertousers.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.UserIDworks properly withGetByUsername(). The system design stores the username as theUser.IDfield (set during user creation in line 36:ID: username). While the naming could be clearer—UserIDsemantically suggests an identifier separate from username—the middleware functions correctly becauseUser.IDand username are equivalent in this codebase. TheGetByUsername()method internally callsGetByID()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.Usertousers.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:
ScopeReadfor GET operationsScopeWritefor 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:
ScopeListfor GET /messagesScopeSendfor POST /messagesScopeReadfor GET /messages/:idScopeExportfor POST /messages/inbox/exportAll 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:
NewBasicnow usesusers.Serviceand grantsScopeAllfor basic authSetUserprovides consistent user storage across middlewaresGetUsercorrectly returns*users.Userwith nil-safe type assertionWithUserconsistently 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 consistentThe added
usersSvc,devicesSvc, andonlineSvcfields and the updatedNewconstructor 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 correctUsing
users.Userand delegating persistence todevicesSvc.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 goodThe new
thirdPartyHandlerfields andnewThirdPartyHandlersignature (includingusersSvc,jwtSvc,authHandler, logger, and validator) are consistent and make the wiring clearer than fx‑based params. No problems spotted here.
2a6c1d8 to
c56a4ab
Compare
There was a problem hiding this 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
:idwithout a leading slash, which is inconsistent with Fiber routing conventions. Per the past review comment, this should be/:idto 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: GuardWithUseragainst missing user for clearer failures
WithUsercurrently 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 fromnanoid.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 whenh.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
Devicesrelation 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.SoftDeletableModelOr 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.TTLis already of typeDuration(which istime.Durationperinternal/config/types.go), so the explicittime.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
usersFX module is well‑structured: a named logger, a privateloginCachebuilt viacache.Factory, a private repository, and a publicNewService, with errors from cache creation correctly aborting startup. Theinit‑timedb.RegisterMigration(Migrate)also follows the existing migration pattern.If you expect to reuse
"users:login"elsewhere, consider extracting it into aconstto 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 betweenRequireScopeandWithUserRight 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 beforeuserauth.WithUser. That’s fine ifpermissions.HasScoperelies only on JWT‑attached claims (or other context) that are already set by upstream middleware, but it will fail if scope checks depend onuserauthhaving populated the user first.Please confirm that
HasScopedoes not depend onuserauth.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 justExistspre‑check
Createcurrently does a read (Exists) beforeInsert. 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 ofErrExists.Consider also mapping the repository’s unique‑constraint error from
InserttoErrExists(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
⛔ Files ignored due to path filters (1)
go.sumis 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
newUserconstructor correctly maps fields from theuserModel.internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
19-26: LGTM!The scope checking logic correctly handles both exact matches and the wildcard
ScopeAllscope 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
RegisterDevicecorrectly accepts the newusers.Userdomain type and properly delegates device creation to the devices service.
126-138: LGTM!The
AuthorizeUserByCodemethod correctly retrieves and returns the user viausers.Service, with proper error handling.internal/sms-gateway/modules/settings/models.go (1)
7-7: LGTM!The update to use
users.Useris 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 graphAdding
users.Module()andjwt.Module()to the top-levelModule(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 migrationinitruns.internal/sms-gateway/handlers/thirdparty/module.go (1)
1-16: Third‑party handler module wiring looks consistentUsing
logger.WithNamedLogger("3rdparty")to scope logs for this module and exposingNewAuthHandlerviafx.Provideintegrates cleanly with the existinghandlers.Modulecomposition; 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 consistentThe
ScopeListandScopeDeletevalues 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 tousers.Userand JWT/scopes on webhook endpoints is coherentSwitching the handler signatures to
func (h *ThirdPartyController) get/post/delete(user users.User, c *fiber.Ctx)and usinguser.IDwithwebhooksSvcaligns with the new users domain and theuserauth.WithUsermiddleware. The added@Security JWTAuthplus@Failure 403annotations 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
timeimport 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
Secretempty ensures users must explicitly configure it.
91-95: No action required—JWT secret validation already exists.The JWT
Secretfield is properly validated ininternal/sms-gateway/jwt/config.go. TheConfig.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.Usertype, 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
secretempty 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.Useraligns with the broader refactor, and the added error wrapping improves error context for debugging.
135-135: LGTM!The signature change to
users.Useris 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.Usertype, 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 consistentThe switch to
users.Userin handler signatures, plus wiring routes throughpermissions.RequireScope(...)anduserauth.WithUser, is coherent with the new users/jwt modules:
- All handlers only rely on
user.ID, whichusers.Userexposes.- Per‑route scopes (
ScopeList,ScopeSend,ScopeRead,ScopeExport) match the new permissions middleware contract.- Swagger additions for
JWTAuthand403 Forbiddenalign 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 changesThe generated OpenAPI now describes:
/3rdparty/v1/auth/tokenPOST and/3rdparty/v1/auth/token/{jti}DELETE with bothApiAuthandJWTAuthsecurity and appropriate 401/403/500 responses.- JWT‑protected 3rd‑party messages/webhooks endpoints with explicit 403 responses.
smsgateway.TokenRequest/TokenResponseschemas and aJWTAuthsecurityDefinition.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 scopesThe Basic and Code middlewares now:
- Authenticate via
users.Service.Login/auth.Service.AuthorizeUserByCode.- Store a
users.Uservalue inLocalsviaSetUser.- Expose retrieval through
GetUser/HasUserand, for Basic, grantpermissions.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 routesThe updated
thirdPartyHandlercomposition looks sound:
- Injects
usersSvcandjwtSvcand feeds them intouserauth.NewBasicandjwtauth.NewJWT, then enforcesuserauth.UserRequiredfor all 3rd‑party v1 routes registered after theUsecall.- Registers health endpoints before the auth middleware, keeping
/3rdparty/v1/healthas an unauthenticated readiness probe, which matches the OpenAPI.- Wires the new
/authsub‑router throughauthHandler, 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
userat 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 newusers.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.IDcorrectly represents the username.Verification confirms the code is correct: in
users.Service.Create, the username parameter is explicitly assigned to theIDfield (line 51 of service.go:ID: username). TheUserdomain model stores username asIDby design. The code at lines 127-129 properly extracts this for theLoginresponse field and aligns with the service contract.
38bd382 to
e9cfbac
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (6)
internal/sms-gateway/users/cache.go (1)
42-45: Avoid credential‑derived cache keys; key on non‑secret data instead
loginCache.makeKeycurrently hashesusername + "\x00" + passwordand all cache methods takeusername, password. Even though you hash, this still effectively uses credentials as the cache key and has the issues already called out in the earlier review:
- Deterministic, unsalted credential hashes are now stored in/cache-accessible (Redis, metrics, debugging) and become another password‑derived artifact to protect.
- The cache effectively turns into an authentication oracle keyed by credential tuple.
- It forces the rest of the code (e.g.,
Service.Login/ChangePassword) to pass plaintext passwords into the cache layer.Given this is just an optimization, it would be safer to redesign the cache API to never depend on the raw password — e.g. key on
username(or user ID) and, if you still want to cache authentication, cache the password hash or a short‑lived opaque login token instead, invalidated explicitly on password change.I’d strongly recommend changing
makeKeyand the cache methods to accept a non‑secret identifier and then adaptingServiceaccordingly, rather than keying onusername,password.Also applies to: 47-76
internal/sms-gateway/users/service.go (1)
71-94: Don’t push plaintext passwords into the cache API; make cache invalidation best‑effort
Service.LoginandService.ChangePasswordboth pass the rawpassword/currentPasswordinto the login cache (Get/Set/Delete), andChangePasswordhard‑fails ifcache.Deletereturns an error. This repeats the earlier concerns:
- The cache layer now sees plaintext passwords purely to compute a key, which widens the surface where secrets can accidentally leak (logs/metrics/instrumentation).
- Password change becomes dependent on cache health; a transient cache issue can prevent users from changing their password, even though the source of truth is the DB.
Consider instead:
- Refactoring the login cache API so it never needs the plaintext password (e.g. keying by username or a non‑secret token and/or caching the password hash or user data), and
- Treating cache deletion in
ChangePasswordas best‑effort — log a warning on failure but still proceed to hash and update the password in the repository so users aren’t blocked by cache issues.This keeps the cache as an optimization rather than part of the critical password‑change path.
Also applies to: 96-104
internal/sms-gateway/handlers/messages/3rdparty.go (1)
287-287: Fix route parameter syntax for:id
Registercurrently uses:router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)Per Fiber routing conventions and to stay consistent with the webhooks handler, this should include the leading slash so the final route matches
/3rdparty/v1/messages/{id}:- router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage) + router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)This mirrors the earlier review feedback on the same line.
internal/sms-gateway/handlers/devices/3rdparty.go (1)
88-88: Align Delete route parameter with Fiber conventions (/:id)
Registercurrently has:router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))To be consistent with other handlers (e.g., webhooks) and Fiber’s usual pattern for path parameters under a group, this should use
"/:id":- router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove)) + router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))Same issue was already noted in the earlier review.
internal/sms-gateway/handlers/thirdparty/auth.go (1)
65-68: TTL conversion may overflow for large values.The conversion
time.Duration(req.TTL) * time.Secondcan overflowtime.Duration(which isint64nanoseconds) for largeTTLvalues. Additionally,TTL == 0semantics are unclear—it could mean "immediately expired" or "use default."Consider adding validation in the handler or relying on the JWT service to enforce bounds:
+const maxTTLSeconds = 365 * 24 * 60 * 60 // 1 year max + func (h *AuthHandler) postToken(user users.User, c *fiber.Ctx) error { req := new(smsgateway.TokenRequest) if err := h.BodyParserValidator(c, req); err != nil { return err } + + if req.TTL > maxTTLSeconds { + return fiber.NewError(fiber.StatusBadRequest, "TTL exceeds maximum allowed value") + } token, err := h.jwtSvc.GenerateToken(c.Context(), user.ID, req.Scopes, time.Duration(req.TTL)*time.Second)internal/sms-gateway/handlers/mobile.go (1)
56-56: Handle the error fromnanoid.Standard.The error from
nanoid.Standard(21)is silently ignored. If initialization fails,h.idGenwill be nil and calling it at line 131 will cause a panic.- idGen, _ := nanoid.Standard(21) + idGen, err := nanoid.Standard(21) + if err != nil { + panic(fmt.Sprintf("failed to initialize ID generator: %v", err)) + }
🧹 Nitpick comments (3)
internal/sms-gateway/users/service.go (1)
35-59: Optional: rely on DB uniqueness rather than Exists+Insert TOCTOU
Createdoes anExistscheck and thenInsert. Between those two calls another concurrent insert can still succeed, leading to a duplicate‑key error that will be surfaced as a generic “failed to create user” instead ofErrExists.If the repository/DB enforces a unique constraint on the username (which it probably should), consider:
- Either dropping the pre‑check and mapping duplicate‑key errors from
InserttoErrExists, or- Keeping the pre‑check but still handling duplicate‑key errors explicitly.
Not critical, but it will make behavior under race conditions more predictable.
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
130-133: Potential nil dereference inWithUserif called without authentication.
GetUser(c)can returnnil, and dereferencing it at line 132 would panic. The comment on line 127 acknowledges this ("If the user is not present, it will panic"), but consider adding a defensive check for clearer error messages in case of middleware misconfiguration.func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler { return func(c *fiber.Ctx) error { + user := GetUser(c) + if user == nil { + panic("WithUser: no authenticated user in context; ensure auth middleware runs first") + } - return handler(*GetUser(c), c) + return handler(*user, c) } }This makes debugging easier if routes are misconfigured.
internal/sms-gateway/handlers/mobile.go (1)
230-233: Password change error handling could leak information.The error message "failed to change password" is returned for all errors, but logging the actual error while returning a generic message is good practice. However, returning 401 Unauthorized for any failure (including internal errors) may be misleading. Consider distinguishing between authentication failures and internal errors:
if err := h.usersSvc.ChangePassword(c.Context(), device.UserID, req.CurrentPassword, req.NewPassword); err != nil { h.Logger.Error("failed to change password", zap.Error(err)) - return fiber.NewError(fiber.StatusUnauthorized, "failed to change password") + if errors.Is(err, users.ErrInvalidCredentials) { + return fiber.NewError(fiber.StatusUnauthorized, "invalid current password") + } + return fiber.NewError(fiber.StatusInternalServerError, "failed to change password") }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (42)
cmd/sms-gateway/main.go(1 hunks)configs/config.example.yml(1 hunks)go.mod(2 hunks)internal/config/config.go(4 hunks)internal/config/module.go(2 hunks)internal/config/types.go(1 hunks)internal/sms-gateway/app.go(4 hunks)internal/sms-gateway/handlers/3rdparty.go(2 hunks)internal/sms-gateway/handlers/devices/3rdparty.go(6 hunks)internal/sms-gateway/handlers/devices/permissions.go(1 hunks)internal/sms-gateway/handlers/logs/3rdparty.go(3 hunks)internal/sms-gateway/handlers/logs/permissions.go(1 hunks)internal/sms-gateway/handlers/messages/3rdparty.go(8 hunks)internal/sms-gateway/handlers/messages/permissions.go(1 hunks)internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go(1 hunks)internal/sms-gateway/handlers/middlewares/permissions/permissions.go(1 hunks)internal/sms-gateway/handlers/middlewares/userauth/userauth.go(4 hunks)internal/sms-gateway/handlers/mobile.go(8 hunks)internal/sms-gateway/handlers/module.go(2 hunks)internal/sms-gateway/handlers/settings/3rdparty.go(8 hunks)internal/sms-gateway/handlers/settings/permissions.go(1 hunks)internal/sms-gateway/handlers/thirdparty/auth.go(1 hunks)internal/sms-gateway/handlers/thirdparty/module.go(1 hunks)internal/sms-gateway/handlers/thirdparty/permissions.go(1 hunks)internal/sms-gateway/handlers/webhooks/3rdparty.go(5 hunks)internal/sms-gateway/handlers/webhooks/permissions.go(1 hunks)internal/sms-gateway/models/migration.go(1 hunks)internal/sms-gateway/models/models.go(0 hunks)internal/sms-gateway/modules/auth/module.go(0 hunks)internal/sms-gateway/modules/auth/repository.go(0 hunks)internal/sms-gateway/modules/auth/service.go(4 hunks)internal/sms-gateway/modules/messages/service.go(3 hunks)internal/sms-gateway/modules/settings/models.go(1 hunks)internal/sms-gateway/modules/webhooks/models.go(2 hunks)internal/sms-gateway/openapi/docs.go(31 hunks)internal/sms-gateway/users/cache.go(1 hunks)internal/sms-gateway/users/domain.go(1 hunks)internal/sms-gateway/users/errors.go(1 hunks)internal/sms-gateway/users/models.go(1 hunks)internal/sms-gateway/users/module.go(1 hunks)internal/sms-gateway/users/repository.go(1 hunks)internal/sms-gateway/users/service.go(1 hunks)
💤 Files with no reviewable changes (3)
- internal/sms-gateway/models/models.go
- internal/sms-gateway/modules/auth/repository.go
- internal/sms-gateway/modules/auth/module.go
🚧 Files skipped from review as they are similar to previous changes (17)
- internal/sms-gateway/handlers/module.go
- internal/sms-gateway/users/models.go
- internal/sms-gateway/modules/webhooks/models.go
- internal/sms-gateway/handlers/thirdparty/permissions.go
- internal/sms-gateway/users/errors.go
- internal/config/types.go
- internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go
- internal/config/module.go
- internal/sms-gateway/handlers/middlewares/permissions/permissions.go
- internal/sms-gateway/handlers/messages/permissions.go
- internal/sms-gateway/users/module.go
- go.mod
- internal/sms-gateway/modules/settings/models.go
- internal/sms-gateway/handlers/webhooks/permissions.go
- internal/sms-gateway/handlers/logs/3rdparty.go
- internal/sms-gateway/users/repository.go
- internal/sms-gateway/handlers/settings/permissions.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T23:45:45.502Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: internal/sms-gateway/online/module.go:14-16
Timestamp: 2025-09-10T23:45:45.502Z
Learning: fx.Decorate in uber-go/fx creates module-scoped decorated dependencies, not global modifications. Using fx.Decorate(func(log *zap.Logger) *zap.Logger { return log.Named("module_name") }) is a correct pattern for creating module-specific loggers that are scoped only to that module's dependency graph.
Applied to files:
internal/sms-gateway/handlers/thirdparty/module.go
🧬 Code graph analysis (12)
internal/sms-gateway/users/cache.go (4)
pkg/cache/typed.go (2)
Typed(89-91)NewTyped(118-122)internal/sms-gateway/users/domain.go (1)
User(5-10)pkg/cache/options.go (2)
AndSetTTL(122-126)WithTTL(40-49)pkg/cache/errors.go (2)
ErrKeyNotFound(17-17)ErrKeyExpired(34-34)
internal/sms-gateway/handlers/webhooks/3rdparty.go (5)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/devices/permissions.go (2)
ScopeList(4-4)ScopeDelete(5-5)internal/sms-gateway/handlers/webhooks/permissions.go (3)
ScopeList(4-4)ScopeWrite(5-5)ScopeDelete(6-6)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)
internal/sms-gateway/handlers/3rdparty.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
Handler(15-18)internal/sms-gateway/handlers/thirdparty/auth.go (1)
AuthHandler(18-22)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
NewBasic(21-59)UserRequired(114-122)internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
NewJWT(14-42)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/messages/permissions.go (4)
ScopeList(10-10)ScopeSend(6-6)ScopeRead(8-8)ScopeExport(12-12)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)
internal/config/config.go (1)
internal/config/types.go (1)
Duration(11-11)
internal/sms-gateway/modules/messages/service.go (2)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/modules/messages/repository_filter.go (1)
MessagesSelectFilter(16-23)
internal/sms-gateway/handlers/thirdparty/auth.go (3)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/jwt/errors.go (4)
ErrInvalidParams(9-9)ErrInitFailed(7-7)ErrInvalidConfig(8-8)ErrDisabled(6-6)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
SetScopes(15-17)ScopeAll(10-10)internal/sms-gateway/users/domain.go (1)
User(5-10)
internal/sms-gateway/handlers/mobile.go (3)
internal/sms-gateway/handlers/base/handler.go (1)
Handler(15-18)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
GetUser(102-109)NewBasic(21-59)
internal/sms-gateway/modules/auth/service.go (3)
internal/sms-gateway/cache/factory.go (1)
Cache(14-14)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/models/models.go (1)
Device(17-28)
internal/sms-gateway/handlers/thirdparty/module.go (3)
internal/sms-gateway/app.go (1)
Module(35-60)internal/sms-gateway/handlers/module.go (1)
Module(16-41)internal/sms-gateway/handlers/thirdparty/auth.go (1)
NewAuthHandler(24-35)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/settings/permissions.go (2)
ScopeRead(4-4)ScopeWrite(5-5)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: E2E
- GitHub Check: Benchmark
- GitHub Check: Test
- GitHub Check: Analyze (go)
🔇 Additional comments (22)
internal/sms-gateway/models/migration.go (1)
12-13: The review comment is incorrect—users and jwt migrations are properly wired.Both
internal/sms-gateway/users/module.goandinternal/sms-gateway/jwt/module.gocontaininit()functions that calldb.RegisterMigration(Migrate), registering their respective migrations for execution during application startup. This follows the same pattern used inmodels/module.go. No action is needed; the refactoring maintains correct migration orchestration.Likely an incorrect or invalid review comment.
internal/sms-gateway/handlers/logs/permissions.go (1)
1-5: LGTM!The permissions constant definition is clean and follows the established pattern for scope definitions across other handlers in this PR.
internal/sms-gateway/handlers/devices/permissions.go (1)
1-6: LGTM!The permissions constants are well-defined and follow the established pattern for scope definitions across other handlers.
internal/sms-gateway/modules/messages/service.go (2)
124-133: LGTM! Good error wrapping added.The signature change from
models.Usertousers.Useraligns with the new users subsystem introduced in this PR. The added error wrapping on line 129 improves error context for debugging.
135-168: LGTM!The signature change is consistent with the broader refactoring to use the public
users.Usertype. The logic for cache retrieval and fallback to repository remains sound.internal/sms-gateway/modules/auth/service.go (3)
35-53: LGTM! Constructor refactored appropriately.The constructor signature change from fx.In-based parameters to explicit parameters aligns with the broader architectural shift. The initialization is clean and all dependencies are properly wired.
83-90: LGTM!The signature change to accept
users.Useris consistent with the new users subsystem. The device registration logic remains unchanged.
92-106: LGTM! Secure token comparison.The use of
subtle.ConstantTimeCompareon line 101 prevents timing attacks when validating the private token. The public/private mode logic is clear and correct.cmd/sms-gateway/main.go (1)
13-16: LGTM!The OpenAPI security definition for JWT authentication is correctly formatted and consistent with the existing security definitions. This is a documentation-only change that properly reflects the new JWT authentication capability.
internal/sms-gateway/app.go (1)
10-10: LGTM!The module wiring correctly integrates the new JWT and users modules into the application. The users module is registered before the auth module (which depends on it), and the JWT module is registered appropriately.
Also applies to: 24-24, 44-44, 59-59
internal/sms-gateway/handlers/thirdparty/module.go (1)
1-16: LGTM!The module definition follows the established fx pattern correctly. The use of
logger.WithNamedLogger("3rdparty")creates a properly scoped logger for the thirdparty handler module, consistent with the project's dependency injection practices.internal/config/config.go (1)
91-95: The review comment is incorrect. JWT Secret validation already exists and is enforced at startup.The
jwt.Configstruct has aValidate()method that checks if the Secret is empty. This validation is called in thejwt.Service.New()constructor during initialization—when the fx dependency injection framework constructs the service, it will invokeconfig.Validate()and fail application startup if the Secret is missing. The validation is already in place and working as intended.Likely an incorrect or invalid review comment.
configs/config.example.yml (1)
41-44: JWT config block and task interval comments look consistentThe new
jwtblock (secret,ttl,issuer) and the updatedtasks.*.interval/max_agecomments align with the rest of the configuration style and the duration literals already used in this file. No issues from my side.Also applies to: 50-56
internal/sms-gateway/users/domain.go (1)
5-19: Domain User mapping is straightforward and correct
Userencapsulates only the expected public fields andnewUsercleanly maps fromuserModel. Looks good.internal/sms-gateway/handlers/settings/3rdparty.go (1)
8-13: Settings handlers’ JWT + scope wiring looks correctSwitching handler signatures to
users.User, addingJWTAuthto Swagger, and registering routes withpermissions.RequireScope(ScopeRead/ScopeWrite)in combination withuserauth.WithUseris consistent and enforces the expected per‑user, per‑scope access control.Also applies to: 39-40, 48-55, 60-61, 72-90, 95-96, 107-124, 126-129
internal/sms-gateway/handlers/messages/3rdparty.go (1)
66-176: JWT + scope‑based access and users.User propagation look goodThe handlers correctly accept
users.User, passuser.IDinto devices/messages services, and are wrapped with the appropriatepermissions.RequireScopecalls (list/send/read/export). Swagger updates (JWTAuth, 403 responses) match the new auth model. No functional issues spotted here.Also applies to: 198-214, 231-244, 262-281, 285-290
internal/sms-gateway/handlers/devices/3rdparty.go (1)
33-55: Devices handlers’ user scoping and permissions look correctUsing
users.Useringet/remove, passinguser.IDintodevicesSvc, and wrapping the routes withpermissions.RequireScope(ScopeList/ScopeDelete)plususerauth.WithUsergives the expected per‑user, per‑scope behavior. Swagger’s JWTAuth additions are consistent with other 3rd‑party handlers.Also applies to: 57-84, 86-89
internal/sms-gateway/handlers/webhooks/3rdparty.go (1)
8-16: Webhooks handlers’ JWT + scope integration looks solidUsing
users.User, addingJWTAuth+ 403 responses in Swagger, and wrapping routes withpermissions.RequireScope(ScopeList/ScopeWrite/ScopeDelete)anduserauth.WithUseris consistent with the rest of the 3rd‑party surface. The/:idroute for delete is correctly formed.Also applies to: 33-52, 55-87, 89-111, 113-116
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
66-86:NewCodedoes not set any permission scopes, unlikeNewBasic.
NewBasicsetspermissions.ScopeAll(line 55) after authentication, butNewCodeonly callsSetUserwithout setting any scopes. This appears intentional since code-based auth is used for limited operations (device registration), but consider documenting this distinction in the function comment for clarity.internal/sms-gateway/handlers/3rdparty.go (1)
69-93: Well-structured middleware chain and route registration.The authentication middleware ordering (Basic → JWT → UserRequired) correctly allows either authentication method while ensuring a user is present. The auth routes are properly registered within the protected group.
internal/sms-gateway/handlers/thirdparty/auth.go (1)
100-120: Error handler provides good user-facing error translation.The error mapping correctly distinguishes between client errors (400 for invalid params), configuration issues (500/501), and passes through other errors. The user-friendly messages avoid leaking internal details.
internal/sms-gateway/handlers/mobile.go (1)
114-153: Device registration flow correctly integrates with new users service.The
postDevicehandler properly handles both authenticated and unauthenticated flows:
- Authenticated: uses existing user from context
- Unauthenticated: creates new user via
usersSvc.CreateThe flow correctly dereferences the user when calling
RegisterDeviceand returns the generated credentials.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (3)
internal/sms-gateway/users/service.go (1)
93-95: Cache deletion failure blocks password change unnecessarily.The
Loginmethod treats cache failures as non-fatal (logs and continues), butChangePasswordreturns immediately oncache.Deleteerror. This inconsistency could block password changes due to transient cache issues.As noted in a previous review, consider making cache deletion best-effort:
- if err := s.cache.Delete(ctx, username, currentPassword); err != nil { - return err - } + if err := s.cache.Delete(ctx, username, currentPassword); err != nil { + s.logger.Warn("failed to delete login cache on password change", zap.String("username", username), zap.Error(err)) + }internal/sms-gateway/users/repository.go (1)
59-67: UpdatePassword silently succeeds when user doesn't exist.GORM's
Updatereturns no error when zero rows are affected. If the user ID doesn't exist, this succeeds without updating anything—inconsistent withGetByIDreturningErrNotFound.As noted in a previous review, check
RowsAffected:func (r *repository) UpdatePassword(ID string, passwordHash string) error { - if err := r.db.Model((*userModel)(nil)). + result := r.db.Model((*userModel)(nil)). Where("id = ?", ID). - Update("password_hash", passwordHash).Error; err != nil { + Update("password_hash", passwordHash) + if err := result.Error; err != nil { return fmt.Errorf("can't update password: %w", err) } + if result.RowsAffected == 0 { + return ErrNotFound + } return nil }internal/sms-gateway/handlers/thirdparty/auth.go (1)
59-76: Guard against TTL overflow and clarify zero/huge TTL semantics.Line 65 converts
req.TTL(uint64 seconds) totime.Duration(int64 nanoseconds):time.Duration(req.TTL) * time.SecondThis can silently overflow for very large values, producing incorrect or negative expiries. Additionally,
TTL == 0semantics are undefined (does it mean "immediately expired" or "use default TTL"?).Consider:
- Adding validation on
TTL(e.g.,validate:"omitempty,gt=0"plus an upper bound check likereq.TTL <= math.MaxInt64/1e9)- Defining explicit behavior for
TTL == 0(reject with 400 or interpret as "use default TTL")
🧹 Nitpick comments (1)
internal/sms-gateway/modules/auth/service.go (1)
114-118: Consider adding error logging for async online status update.The goroutine silently discards any error from
SetOnline. While this may be intentional for fire-and-forget semantics, consider logging failures for observability:go func(id string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - s.onlineSvc.SetOnline(ctx, id) + if err := s.onlineSvc.SetOnline(ctx, id); err != nil { + s.logger.Debug("failed to set device online", zap.String("deviceId", id), zap.Error(err)) + } }(device.ID)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go(1 hunks)internal/sms-gateway/handlers/thirdparty/auth.go(1 hunks)internal/sms-gateway/modules/auth/service.go(4 hunks)internal/sms-gateway/openapi/docs.go(31 hunks)internal/sms-gateway/users/repository.go(1 hunks)internal/sms-gateway/users/service.go(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
SetUser(88-90)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
SetScopes(15-17)
internal/sms-gateway/handlers/thirdparty/auth.go (5)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/config/types.go (1)
Duration(11-11)internal/sms-gateway/jwt/errors.go (4)
ErrInvalidParams(9-9)ErrInitFailed(7-7)ErrInvalidConfig(8-8)ErrDisabled(6-6)
internal/sms-gateway/users/service.go (4)
internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/users/domain.go (1)
User(5-10)pkg/crypto/passwords.go (2)
MakeBCryptHash(14-20)CompareBCryptHash(22-27)pkg/cache/errors.go (1)
ErrKeyNotFound(17-17)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/errors.go (2)
ErrNotFound(6-6)ErrExists(7-7)pkg/mysql/errors.go (1)
IsDuplicateKeyViolation(9-15)
internal/sms-gateway/modules/auth/service.go (3)
internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/cache/factory.go (1)
Cache(14-14)internal/sms-gateway/users/domain.go (1)
User(5-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Benchmark
- GitHub Check: Analyze (go)
🔇 Additional comments (12)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
14-41: LGTM! Well-structured JWT middleware with proper error handling.The middleware correctly:
- Falls through when no Bearer token is present (allowing other auth mechanisms)
- Distinguishes between "user not found" (401) and internal errors (500)
- Sets both user and scopes in the request context
One consideration: the silent pass-through on missing/non-Bearer tokens (lines 18-20) is intentional for supporting multiple auth mechanisms, but ensure upstream middleware enforces authentication where required.
internal/sms-gateway/users/service.go (2)
35-51: LGTM! User creation with proper password hashing.Password is hashed before storage, errors are wrapped with context, and the method returns a public
Userrepresentation.
53-60: LGTM!Simple and clean delegation to repository.
internal/sms-gateway/openapi/docs.go (1)
1-6: Generated file - no manual changes needed.This file is auto-generated by swaggo/swag. The JWT security definitions, token endpoints, and 403 responses appear correctly generated. Any adjustments should be made to the source handler annotations, not this file directly.
internal/sms-gateway/users/repository.go (3)
37-37: LGTM! Correct GORM pointer usage.The
Take(user)call now correctly passes*userModelinstead of**userModel. The error handling properly mapsgorm.ErrRecordNotFoundto the domainErrNotFound.
22-31: LGTM!Clean existence check using COUNT query.
47-57: LGTM! Proper duplicate key handling.The Insert method correctly uses
mysql.IsDuplicateKeyViolationto detect conflicts and wraps both the domain error and original error for debugging.internal/sms-gateway/modules/auth/service.go (3)
35-53: LGTM! Clean constructor with explicit dependencies.The constructor now takes explicit parameters instead of fx.Params, improving testability and clarity. The users service is properly injected.
125-138: LGTM! Correct method name usage.The code now correctly uses
s.usersSvc.GetByID(userID)instead of the previously misnamedGetByUsername. Error handling is appropriate.
83-90: LGTM!Device registration correctly uses the users.User type and delegates to devicesSvc.
internal/sms-gateway/handlers/thirdparty/auth.go (2)
101-121: LGTM!The error handler properly maps JWT service errors to appropriate HTTP status codes with user-friendly messages for administrative issues.
37-41: No action required —ScopeTokensManageis correctly accessible.The constant is defined in the same package (
internal/sms-gateway/handlers/thirdparty/permissions.go) and used in the same package (auth.go). In Go, package-level identifiers are automatically accessible within the same package without imports or qualifiers. The code is correct as written.
1202a7f to
4e7b849
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
pkg/cache/redis.go (2)
295-315:AndDelete()without TTL options is silently ignored.If only
AndDelete()is passed without any TTL options, the code falls through to lines 306-314 which performs a simple HGet, ignoring the delete flag entirely. Theo.deletecheck is only used when TTL options trigger the Lua script path.Consider restructuring to handle delete-only case:
+ if o.delete && !o.hasTTLOptions() { + // Delete-only case: use the Lua script with no TTL changes + delArg := "1" + result, err := r.client.Eval(ctx, getAndUpdateTTLScript, []string{r.key}, key, delArg, 0, 0).Result() + if err != nil { + return nil, fmt.Errorf("can't get cache item: %w", err) + } + if value, ok := result.(string); ok { + return []byte(value), nil + } + return nil, ErrKeyNotFound + } + // Handle TTL options atomically using Lua script var ttlTimestamp, ttlDelta int64Alternatively, you could consolidate by always using the Lua script when any option is set, removing the fallback at lines 305-315.
423-446: Non-atomic HSetNX + HExpireAt creates race window.Unlike
Set()which uses pipelining, here HSetNX and HExpireAt are separate operations. If HSetNX succeeds but HExpireAt fails (line 440-442), the value persists in cache without TTL, potentially becoming stale indefinitely.Consider using a Lua script for atomicity:
+const setOrFailScript = ` +local success = redis.call('HSETNX', KEYS[1], ARGV[1], ARGV[2]) +if success == 1 and ARGV[3] ~= "0" then + redis.call('HExpireAt', KEYS[1], tonumber(ARGV[3]), 'FIELDS', '1', ARGV[1]) +end +return success +` func (r *redisCache) SetOrFail(ctx context.Context, key string, value []byte, opts ...Option) error { - val, err := r.client.HSetNX(ctx, r.key, key, value).Result() - if err != nil { - return fmt.Errorf("can't set cache item: %w", err) - } - - if !val { - return ErrKeyExists - } - options := new(options) if r.ttl > 0 { options.validUntil = time.Now().Add(r.ttl) } options.apply(opts...) - if !options.validUntil.IsZero() { - if err := r.client.HExpireAt(ctx, r.key, options.validUntil, key).Err(); err != nil { - return fmt.Errorf("can't set cache item ttl: %w", err) - } + ttlArg := "0" + if !options.validUntil.IsZero() { + ttlArg = fmt.Sprintf("%d", options.validUntil.Unix()) + } + + result, err := r.client.Eval(ctx, setOrFailScript, []string{r.key}, key, value, ttlArg).Result() + if err != nil { + return fmt.Errorf("can't set cache item: %w", err) + } + if result.(int64) == 0 { + return ErrKeyExists } return nil }
♻️ Duplicate comments (9)
pkg/cache/redis.go (1)
74-87: Document Redis version requirement.The implementation uses Redis 7.4.0+ commands (HEXPIREAT, HTTL, HEXPIRE with FIELDS parameter) for per-field hash TTL support. This minimum version requirement should be documented to prevent runtime errors with older Redis instances. Based on learnings, Redis 7.4.0 introduced these commands.
// redisCache implements the Cache interface using Redis as the backend. // +// Requires Redis 7.4.0 or later for per-field hash TTL support (HEXPIREAT, HTTL, HEXPIRE with FIELDS). +// // This implementation stores all data in a Redis hash, with each cache itemapi/requests.http (1)
206-219:"devices:write"is an invalid scope—must be replaced with"devices:delete".The verification confirms
"devices:write"does not exist in the permission definitions. The devices handler only defines"devices:list"and"devices:delete". Using an undefined scope will cause authorization failures."scopes": [ "messages:send", "messages:read", "devices:list", - "devices:write", + "devices:delete", "webhooks:list", "webhooks:write", "settings:read", "settings:write", "logs:read" ]internal/config/config.go (1)
22-23: Empty default JWT secret is unsafe; enforce non-empty secret or explicit disablement
Config.JWTanddefaultConfignow initialize TTL and Issuer but leaveJWT.Secretempty. Unless another layer explicitly:
- treats
Secret == ""as “JWT completely disabled” and refuses to issue/verify tokens, or- fails fast on startup when
Secretis empty,there’s a risk of accidentally running with an empty signing secret.
Consider:
- Adding validation in the config/jwt module to error out (or hard‑disable JWT) when
Secretis empty.- Optionally adding a comment near
JWT.Secretor indefaultConfigclarifying that a non‑empty secret must be provided via env/config for JWT to function.This mirrors the earlier bot review note and still appears unresolved in this file.
Also applies to: 91-95, 131-134
internal/sms-gateway/users/cache.go (1)
15-22: Reconsider credential-derived cache keys and treat cache misses as non-errorsTwo points here:
- Hashing username/password into the cache key is still risky
makeKeyderives the cache key fromusername + "\x00" + passwordhashed with SHA‑256. As noted in the earlier review, this effectively turns the cache into a credential oracle and has several security drawbacks (no salt, deterministic mapping, invalidation issues on password change, timing leakage).Prefer alternatives such as:
- Keying by user ID or by a server‑generated opaque login/session identifier, or
- If you must derive from credentials, using an HMAC with a server‑side secret and per‑user salt, and ensuring explicit invalidation on password changes.
This mirrors the prior bot comment and still appears unresolved.
- Cache “not found/expired” should probably be treated as a miss, not an error
In
Get, any error fromc.storage.Get(includingcache.ErrKeyNotFound/cache.ErrKeyExpired) is wrapped and returned as:return nil, fmt.Errorf("failed to get user from cache: %w", err)That forces callers to special‑case “cache miss via error”. It’s typically cleaner to do:
user, err := c.storage.Get(...) if errors.Is(err, cache.ErrKeyNotFound) || errors.Is(err, cache.ErrKeyExpired) { return nil, nil // cache miss } if err != nil { return nil, fmt.Errorf("failed to get user from cache: %w", err) }so cache misses behave as a normal “no cached login” path and only real cache/storage failures propagate as errors.
Also applies to: 32-45, 47-81
internal/sms-gateway/jwt/config.go (1)
12-16: Consider validating the Issuer field.The
Issuerfield is used in token creation but is not validated. While an empty issuer may be technically valid for JWT, validating it ensures consistent token configuration and prevents potential issues with token verification downstream.This was raised in a previous review. If an empty issuer is intentionally acceptable in your use case, consider adding a comment to document this decision.
internal/sms-gateway/users/service.go (1)
87-103: Cache deletion should be best-effort inChangePassword.The cache.Delete error at lines 93-95 causes the password change to fail entirely. This blocks users from changing passwords due to transient cache issues, even though the Login method treats cache failures as non-fatal.
This was raised in a previous review. Consider making cache deletion best-effort:
if err := s.cache.Delete(ctx, username, currentPassword); err != nil { - return err + s.logger.Warn("failed to delete login cache on password change", zap.String("username", username), zap.Error(err)) }internal/sms-gateway/handlers/devices/3rdparty.go (1)
87-88: Fix inconsistent route parameter syntax.Line 88 uses
:idwithout a leading slash, while the webhooks handler uses/:id. This was flagged in a previous review.Apply this diff:
- router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove)) + router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))internal/sms-gateway/users/repository.go (1)
22-67: UpdatePassword silently “succeeds” for non-existent users; align with GetByID semantics
GetByIDcorrectly mapsErrRecordNotFoundtoErrNotFound, butUpdatePasswordwill return nil even when no row matches the given ID. If callers expect a missing user to be reported (e.g., to be consistent withErrNotFound), capture the GORM result and checkRowsAffected: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 nilThis avoids silent no-ops when the user doesn’t exist.
#!/bin/bash # See how UpdatePassword errors are surfaced through the users service. rg -n 'UpdatePassword\(' internal/sms-gateway/users --type=go -C3internal/sms-gateway/handlers/mobile.go (1)
43-74: Handlenanoid.Standarderror instead of risking a nil id generator
newMobileHandlercurrently ignores the error fromnanoid.Standard(21):idGen, _ := nanoid.Standard(21)If initialization ever fails,
idGenwill be nil and any call toh.idGen()(e.g., inpostDevice) will panic. It’s safer to fail fast during handler construction:func newMobileHandler( @@ - validator *validator.Validate, -) *mobileHandler { - idGen, _ := nanoid.Standard(21) + validator *validator.Validate, +) *mobileHandler { + idGen, err := nanoid.Standard(21) + if err != nil { + panic(fmt.Sprintf("failed to initialize ID generator: %v", err)) + } @@ - idGen: idGen, + idGen: idGen, } }Alternatively, you could return an error from the constructor instead of panicking, depending on how this is wired into DI.
#!/bin/bash # Check all usages of idGen to confirm it's only initialized via newMobileHandler. rg -n 'idGen' internal/sms-gateway/handlers/mobile.go -n -C3
🧹 Nitpick comments (8)
internal/sms-gateway/users/domain.go (1)
12-19: Consider adding a nil check for defensive coding.The
newUserconstructor will panic ifmodelis nil. While this is an internal function and callers likely ensure non-nil input, a nil check could prevent unexpected panics during future refactoring.func newUser(model *userModel) *User { + if model == nil { + return nil + } return &User{ ID: model.ID, CreatedAt: model.CreatedAt, UpdatedAt: model.UpdatedAt, } }pkg/cache/options.go (1)
122-126: Consider handling non-positive TTL consistently withWithTTL.
WithTTL(line 40-49) explicitly handles non-positive TTL by settingvalidUntilto zero time (no expiration). However,AndSetTTLpasses the value through directly. If a negative duration is passed,time.Now().Add(*o.setTTL)inmemory.gowill set expiration in the past, causing immediate expiration.This inconsistency may be intentional (to allow forced expiration), but if not, consider aligning the behavior:
func AndSetTTL(ttl time.Duration) GetOption { return func(o *getOptions) { + if ttl <= 0 { + return // No-op for non-positive TTL, or set to nil + } o.setTTL = &ttl } }internal/config/types.go (1)
29-36: Nil receiver check missing inUnmarshalText.If
UnmarshalTextis called on a nil pointer, the assignment*d = Duration(t)at line 34 will panic. While typically unmarshaling is done on non-nil pointers, adding a nil check would make this more robust.func (d *Duration) UnmarshalText(text []byte) error { + if d == nil { + return fmt.Errorf("can't unmarshal into nil Duration") + } t, err := time.ParseDuration(string(text)) if err != nil { return fmt.Errorf("can't parse duration: %w", err) } *d = Duration(t) return nil }internal/sms-gateway/handlers/settings/3rdparty.go (1)
36-45: Consider adding 403 Forbidden response to Swagger documentation.With the new
permissions.RequireScopemiddleware, the endpoint can return a 403 status when the required scope is missing. The Swagger annotations don't document this response.// @Success 200 {object} smsgateway.DeviceSettings "Settings" // @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" +// @Failure 403 {object} smsgateway.ErrorResponse "Forbidden - missing required scope" // @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"Similar additions should be made for the PUT and PATCH endpoints (lines 57-69 and 92-104).
deployments/grafana/dashboards/jwt.json (1)
36-39: Hardcoded datasource UID reduces portability.The datasource UID
edqp0a73uh2bkais hardcoded throughout the dashboard. This will cause the dashboard to fail when imported into other Grafana instances with different datasource configurations.Consider using a template variable for the datasource or the default Prometheus datasource reference:
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }Then add a corresponding datasource variable to the templating section.
internal/sms-gateway/jwt/repository.go (1)
20-46: JWT token repository implementation looks correct; consider exposing revocation “no-op” if you ever need itInsert, Revoke, and IsRevoked are all using
WithContext, target the correct model, and handle errors consistently. The count-basedIsRevokedis fine for a uniquejti. If, in future, callers need to know whetherRevokeactually touched a row (e.g., to distinguish “already revoked / unknown” from success), you could capture the GORM result and inspectRowsAffected, but that’s not required for the current usage.pkg/cache/typed.go (1)
89-387: Typed cache correctly handles pointerT; reflection could be cached per instanceVerification confirms all usages pass pointer types (
*User,*loginCacheWrapper,*MyData), and thenewItem[T]reflection check correctly enforces this constraint. The design is functionally solid. If performance becomes a concern on hot paths, consider cachingreflect.Typeinside theTyped[T]instance duringNewTypedrather than recomputing it on everyGetorDraincall.internal/sms-gateway/handlers/mobile.go (1)
223-236: Fix error handling inchangePasswordto distinguish auth failures from internal errors.The current implementation maps all errors from
ChangePasswordto401 Unauthorized, but the method can fail in multiple ways:
- Auth failures:
Loginfails on invalid current password or user not found- Internal errors: cache deletion, password hashing, or DB update failures
These internal errors should return
500 Internal Server Error, not401.To fix this, the handler needs to differentiate error types. Consider:
- Adding custom error types in the users service (e.g.,
ErrInvalidPassword,ErrUserNotFound)- Wrapping internal errors distinctly so the handler can inspect them with
errors.AsExample:
if err := h.usersSvc.ChangePassword(c.Context(), device.UserID, req.CurrentPassword, req.NewPassword); err != nil { if errors.Is(err, users.ErrInvalidPassword) || errors.Is(err, users.ErrUserNotFound) { return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials") } h.Logger.Error("failed to change password", zap.Error(err)) return fiber.NewError(fiber.StatusInternalServerError, "failed to change password") }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (62)
api/mobile.http(1 hunks)api/requests.http(5 hunks)cmd/sms-gateway/main.go(1 hunks)configs/config.example.yml(1 hunks)deployments/grafana/dashboards/jwt.json(1 hunks)deployments/prometheus/alerts/jwt-alerts.yml(1 hunks)go.mod(2 hunks)internal/config/config.go(4 hunks)internal/config/module.go(2 hunks)internal/config/types.go(1 hunks)internal/sms-gateway/app.go(4 hunks)internal/sms-gateway/handlers/3rdparty.go(2 hunks)internal/sms-gateway/handlers/devices/3rdparty.go(6 hunks)internal/sms-gateway/handlers/devices/permissions.go(1 hunks)internal/sms-gateway/handlers/logs/3rdparty.go(3 hunks)internal/sms-gateway/handlers/logs/permissions.go(1 hunks)internal/sms-gateway/handlers/messages/3rdparty.go(8 hunks)internal/sms-gateway/handlers/messages/permissions.go(1 hunks)internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go(1 hunks)internal/sms-gateway/handlers/middlewares/permissions/permissions.go(1 hunks)internal/sms-gateway/handlers/middlewares/userauth/userauth.go(4 hunks)internal/sms-gateway/handlers/mobile.go(8 hunks)internal/sms-gateway/handlers/module.go(2 hunks)internal/sms-gateway/handlers/settings/3rdparty.go(8 hunks)internal/sms-gateway/handlers/settings/permissions.go(1 hunks)internal/sms-gateway/handlers/thirdparty/auth.go(1 hunks)internal/sms-gateway/handlers/thirdparty/module.go(1 hunks)internal/sms-gateway/handlers/thirdparty/permissions.go(1 hunks)internal/sms-gateway/handlers/webhooks/3rdparty.go(5 hunks)internal/sms-gateway/handlers/webhooks/permissions.go(1 hunks)internal/sms-gateway/jwt/config.go(1 hunks)internal/sms-gateway/jwt/disabled.go(1 hunks)internal/sms-gateway/jwt/errors.go(1 hunks)internal/sms-gateway/jwt/jwt.go(1 hunks)internal/sms-gateway/jwt/metrics.go(1 hunks)internal/sms-gateway/jwt/models.go(1 hunks)internal/sms-gateway/jwt/module.go(1 hunks)internal/sms-gateway/jwt/repository.go(1 hunks)internal/sms-gateway/jwt/service.go(1 hunks)internal/sms-gateway/models/migration.go(1 hunks)internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql(1 hunks)internal/sms-gateway/models/models.go(0 hunks)internal/sms-gateway/modules/auth/module.go(0 hunks)internal/sms-gateway/modules/auth/repository.go(0 hunks)internal/sms-gateway/modules/auth/service.go(4 hunks)internal/sms-gateway/modules/messages/service.go(3 hunks)internal/sms-gateway/modules/settings/models.go(1 hunks)internal/sms-gateway/modules/webhooks/models.go(2 hunks)internal/sms-gateway/openapi/docs.go(31 hunks)internal/sms-gateway/users/cache.go(1 hunks)internal/sms-gateway/users/domain.go(1 hunks)internal/sms-gateway/users/errors.go(1 hunks)internal/sms-gateway/users/models.go(1 hunks)internal/sms-gateway/users/module.go(1 hunks)internal/sms-gateway/users/repository.go(1 hunks)internal/sms-gateway/users/service.go(1 hunks)pkg/cache/cache.go(1 hunks)pkg/cache/errors.go(1 hunks)pkg/cache/memory.go(13 hunks)pkg/cache/options.go(4 hunks)pkg/cache/redis.go(10 hunks)pkg/cache/typed.go(1 hunks)
💤 Files with no reviewable changes (3)
- internal/sms-gateway/models/models.go
- internal/sms-gateway/modules/auth/repository.go
- internal/sms-gateway/modules/auth/module.go
🚧 Files skipped from review as they are similar to previous changes (19)
- internal/sms-gateway/handlers/thirdparty/module.go
- api/mobile.http
- deployments/prometheus/alerts/jwt-alerts.yml
- internal/sms-gateway/modules/settings/models.go
- internal/sms-gateway/users/module.go
- cmd/sms-gateway/main.go
- internal/config/module.go
- internal/sms-gateway/handlers/middlewares/permissions/permissions.go
- internal/sms-gateway/jwt/models.go
- internal/sms-gateway/modules/webhooks/models.go
- internal/sms-gateway/handlers/thirdparty/permissions.go
- internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go
- internal/sms-gateway/users/errors.go
- internal/sms-gateway/handlers/thirdparty/auth.go
- internal/sms-gateway/jwt/module.go
- internal/sms-gateway/handlers/webhooks/permissions.go
- internal/sms-gateway/handlers/settings/permissions.go
- go.mod
- internal/sms-gateway/jwt/disabled.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.
Applied to files:
pkg/cache/memory.gopkg/cache/cache.gopkg/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.gopkg/cache/cache.gopkg/cache/redis.go
📚 Learning: 2025-09-10T23:54:16.828Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:118-121
Timestamp: 2025-09-10T23:54:16.828Z
Learning: Redis 7.4.0 introduced HEXPIRE, HEXPIREAT, and related commands that support per-field TTL on hash data structures. go-redis v9.6.0+ includes support for these commands with methods like HExpireAt.
Applied to files:
pkg/cache/redis.go
🧬 Code graph analysis (21)
internal/sms-gateway/handlers/logs/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/logs/permissions.go (1)
ScopeRead(4-4)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)
internal/sms-gateway/users/models.go (1)
internal/sms-gateway/models/models.go (1)
SoftDeletableModel(12-15)
internal/config/config.go (1)
internal/config/types.go (1)
Duration(11-11)
internal/sms-gateway/users/service.go (4)
internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/users/domain.go (1)
User(5-10)pkg/crypto/passwords.go (2)
MakeBCryptHash(14-20)CompareBCryptHash(22-27)pkg/cache/errors.go (1)
ErrKeyNotFound(17-17)
internal/sms-gateway/modules/messages/service.go (2)
internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/users/domain.go (1)
User(5-10)
internal/sms-gateway/handlers/module.go (2)
internal/sms-gateway/app.go (1)
Module(35-60)internal/sms-gateway/handlers/thirdparty/module.go (1)
Module(8-16)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/devices/permissions.go (2)
ScopeList(4-4)ScopeDelete(5-5)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/errors.go (2)
ErrNotFound(6-6)ErrExists(7-7)pkg/mysql/errors.go (1)
IsDuplicateKeyViolation(9-15)
internal/sms-gateway/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
Device(17-28)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/settings/permissions.go (2)
ScopeRead(4-4)ScopeWrite(5-5)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)
internal/sms-gateway/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
ErrInvalidConfig(8-8)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
AndDelete(204-208)
internal/sms-gateway/users/cache.go (4)
pkg/cache/typed.go (2)
Typed(89-91)NewTyped(118-122)internal/sms-gateway/users/domain.go (1)
User(5-10)pkg/cache/options.go (1)
AndSetTTL(122-126)pkg/cache/errors.go (2)
ErrKeyNotFound(17-17)ErrKeyExpired(34-34)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
SetScopes(15-17)ScopeAll(10-10)internal/sms-gateway/users/domain.go (1)
User(5-10)
internal/sms-gateway/modules/auth/service.go (3)
internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/cache/factory.go (1)
Cache(14-14)internal/sms-gateway/users/domain.go (1)
User(5-10)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/messages/permissions.go (4)
ScopeList(10-10)ScopeSend(6-6)ScopeRead(8-8)ScopeExport(12-12)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-134)
internal/sms-gateway/handlers/3rdparty.go (4)
internal/sms-gateway/handlers/logs/3rdparty.go (1)
ThirdPartyController(21-23)internal/sms-gateway/handlers/thirdparty/auth.go (1)
AuthHandler(18-22)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
NewBasic(21-59)UserRequired(114-122)internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
NewJWT(14-42)
internal/sms-gateway/jwt/errors.go (1)
internal/sms-gateway/jwt/service.go (1)
New(24-51)
pkg/cache/cache.go (1)
pkg/cache/options.go (2)
Option(9-9)GetOption(89-89)
internal/sms-gateway/handlers/mobile.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
Handler(15-18)internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
GetUser(102-109)NewBasic(21-59)
pkg/cache/redis.go (1)
pkg/cache/options.go (1)
AndDelete(204-208)
🔇 Additional comments (53)
pkg/cache/errors.go (2)
6-17: Documentation enhancement verified and accurate.The API methods referenced in the examples (
GetandGetAndDelete) exist in the cache package with compatible signatures, andErrKeyNotFoundis correctly returned when keys don't exist. The documentation and examples are technically sound.
36-49: SetOrFail method verified and documentation is accurate.Verification confirms that
SetOrFailis properly implemented across multiple cache backends (memory, Redis, and typed cache) and is part of the public Cache interface. TheErrKeyExistsdocumentation at lines 36-49 accurately describes when this error is returned by SetOrFail operations.pkg/cache/redis.go (9)
51-72: LGTM!The configuration struct is well-documented with clear field descriptions.
132-159: LGTM!The constructor has good validation, proper handling of client ownership, and automatic prefix normalization.
177-179: LGTM!Correct implementation - Redis handles TTL-based expiration natively. Based on learnings.
197-203: LGTM!Simple and correct use of HDEL with proper error wrapping.
225-244: LGTM!Atomic drain via Lua script is correct. The fallback from UNLINK to DEL handles older Redis gracefully.
353-355: Note: depends on Get bug fix.This method will not work correctly until the
AndDelete()handling bug inGetis fixed. Once fixed, this delegation pattern is appropriate.
379-398: LGTM!Good use of pipelining to ensure HSet and HExpireAt execute atomically.
461-467: LGTM!Correct ownership semantics - only closes client if cache created it.
469-470: LGTM!Good practice to have compile-time interface verification.
internal/sms-gateway/handlers/logs/permissions.go (1)
1-5: LGTM!The permission constant follows the established
<resource>:<action>naming convention used across other handlers in this PR.internal/sms-gateway/users/models.go (2)
10-19: LGTM!The model structure is well-designed:
varchar(72)appropriately accommodates bcrypt hashes (60 chars) with headroom- Soft delete via
SoftDeletableModelembedding is consistent with other models- The unexported
userModelcorrectly limits visibility to the package
21-26: Migration function looks good.Error wrapping provides clear context for debugging migration failures.
pkg/cache/cache.go (2)
1-104: Excellent documentation coverage.The package-level documentation provides comprehensive examples for all cache backends and typed cache usage. This will significantly help developers understand the caching API.
108-283: Verification complete: all error sentinels are properly exported.All three error sentinels referenced in the Cache interface documentation are correctly defined and exported in
pkg/cache/errors.go:
ErrKeyNotFoundErrKeyExpiredErrKeyExistsThe interface contract is complete and accurate.
internal/sms-gateway/handlers/devices/permissions.go (1)
1-6: LGTM!The permission constants follow the established naming convention. Note that only
listanddeleteoperations are defined, which correctly reflects the available device operations (no create/update operations for devices via this API).api/requests.http (3)
4-4: LGTM!Good addition of the JWT token environment variable for testing Bearer authentication.
36-48: LGTM!The commented-out Basic auth with active Bearer auth provides a clear testing pattern for JWT authentication.
221-225: LGTM!The DELETE endpoint for token revocation follows RESTful conventions with the token ID in the path.
internal/sms-gateway/handlers/messages/permissions.go (1)
1-13: LGTM!Clean permission scope constants with consistent naming pattern (
messages:<action>) and appropriate documentation. The constants align well with the broader permission/JWT framework introduced in this PR.pkg/cache/memory.go (3)
207-241: LGTM! Well-structured Get method with proper locking strategy.The method correctly:
- Uses
RLockfor read-only access when no options are provided- Upgrades to
Lockwhen options require modifications- Checks expiration before applying TTL modifications (line 226)
- Handles delete-on-read atomically
The code is well-documented and follows Go concurrency best practices.
161-175: Drain implementation correctly handles atomic snapshot.The
cleanupcallback pattern ensures expired items are removed before the snapshot is taken, and the atomic swap ofm.itemsguarantees consistency. Based on learnings, this aligns with the established pattern where cleanup removes expired items first, then executes the callback.
409-421: Clean cleanup implementation with callback pattern.The callback is executed while holding the write lock, ensuring atomicity of cleanup operations combined with any additional actions (like the snapshot in
Drain). This matches the documented behavior from learnings.pkg/cache/options.go (2)
1-208: LGTM overall! Well-designed options pattern.The functional options pattern is cleanly implemented with:
- Clear separation between
Option(for Set) andGetOption(for Get)- Comprehensive documentation with usage examples
- Consistent naming conventions (
With*for Set,And*for Get)
98-105: The review comment is incorrect. TheisEmpty()method is actively used in the codebase—it's called at line 281 inpkg/cache/redis.go. The method should be kept as it serves a legitimate purpose.Likely an incorrect or invalid review comment.
internal/sms-gateway/handlers/module.go (1)
40-40: LGTM!The
thirdparty.Module()is correctly wired into the handlers module composition. This aligns with the JWT authentication infrastructure introduced in this PR, where the thirdparty module providesNewAuthHandlerfor 3rd-party authentication flows.internal/sms-gateway/handlers/logs/3rdparty.go (1)
3-8: JWT/logs handler wiring looks good; verify middleware stack for scopes & user loadingThe switch to
users.User, addedJWTAuthsecurity annotation, and routing viapermissions.RequireScope(ScopeRead)+userauth.WithUser(h.get)look consistent with the new auth model. Just make sure the router/group that mounts this controller also attaches the JWT middleware (that populates scopes and user) before these handlers soRequireScopeandWithUseralways see a populated context.Also applies to: 25-29, 40-46
internal/sms-gateway/app.go (1)
10-11: Fx module wiring for users/jwt modules is straightforwardRegistering
users.Module()andjwt.Module()in the mainModuleis clean and keeps composition centralized. No issues from this file alone; dependency injection should work as long as each submodule correctly provides its own config, migrations, and services.Also applies to: 24-25, 35-60
internal/sms-gateway/models/migration.go (1)
12-14: Device-only AutoMigrate fits modular schema split; ensure other domain migrations are registeredLimiting
Migrateto&Device{}aligns with moving users/JWT schema into their own modules. Just double‑check that:
internal/sms-gateway/users/models.goandinternal/sms-gateway/jwt/models.goare wired into the migration registration (via the DB module) so user/token tables are still created during bootstrap.internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql (1)
1-19: Tokens table migration looks consistent with JWT token storage requirementsThe
tokensschema (ID,user_id,expires_at, timestamps,revoked_at, indexes, FK tousers(id)withON DELETE CASCADE) is well‑structured and matches a typical JWT token persistence model. No issues from the migration itself.configs/config.example.yml (1)
41-44: Ensurejwt.secret== empty truly disables JWT and doesn’t allow tokens with an empty secretThe example config documents:
jwt: secret: # jwt secret (leave empty to disable JWT functionality)This is good UX, but it must match the implementation: the JWT module should not issue or validate tokens at all when
secretis empty (and ideally log clearly that JWT is disabled), rather than treating""as a valid signing key.Please verify that:
- Startup either fails or cleanly disables JWT when
jwt.secretis empty.- No code path signs/verifies tokens with an empty secret.
The updated task interval comments below are clear and consistent.
Also applies to: 50-56
internal/sms-gateway/modules/messages/service.go (1)
15-16: Switch tousers.Userin messages service is consistent and preserves behaviourUpdating
SelectStatesandGetStateto acceptusers.Userwhile continuing to useuser.IDfor filters and cache keys keeps semantics intact and aligns the messages domain with the new users model. The addedfmt.Errorf("can't select messages: %w", err)inSelectStatesalso improves error context. No issues from this file.Also applies to: 124-168
internal/sms-gateway/jwt/errors.go (1)
1-12: LGTM!Well-defined sentinel errors following Go conventions. Good organization with all JWT-related errors centralized in one file, enabling consistent error wrapping with
fmt.Errorf("%w: ...")throughout the package.internal/config/types.go (1)
47-48: Good use of compile-time interface assertions.These assertions ensure
Durationcorrectly implementsyaml.Unmarshalerandencoding.TextUnmarshalerat compile time rather than failing at runtime.internal/sms-gateway/jwt/jwt.go (1)
10-27: LGTM!Clean interface and type definitions. The
Serviceinterface provides a well-defined contract for JWT operations. TheClaimsstruct properly embedsjwt.RegisteredClaimswhile adding custom fields with appropriate JSON tags. TheTokenInfostruct exposes the necessary fields for token consumers.internal/sms-gateway/handlers/settings/3rdparty.go (1)
126-130: LGTM!The route registration correctly applies scope-based access control with appropriate permission levels: read access for GET and write access for PUT/PATCH. The
userauth.WithUserwrapper properly extracts the authenticated user for handler use.internal/sms-gateway/jwt/config.go (1)
18-32: LGTM!The validation logic is thorough and correctly handles edge cases:
- Empty secret check prevents misconfiguration
- Minimum length requirement (32 bytes) enforces adequate key strength for HMAC
- Non-positive TTL check prevents both zero and negative durations
Error messages are clear and consistently wrapped with
ErrInvalidConfig.internal/sms-gateway/users/service.go (2)
62-85: LGTM on login flow structure.The login flow correctly:
- Checks cache first for performance
- Falls back to repository lookup on cache miss
- Treats non-
ErrKeyNotFoundcache errors as non-fatal (logs warning, continues)- Caches successful authentications with error logged but not propagated
35-60: LGTM!The
Createmethod correctly hashes the password before storage. TheGetByIDmethod is a clean pass-through to the repository with proper domain type conversion vianewUser.internal/sms-gateway/handlers/messages/3rdparty.go (1)
66-176: LGTM!The migration to
users.Usertype and integration of permission scopes are correctly implemented. The handler logic remains intact and properly usesuser.IDfor service calls.deployments/grafana/dashboards/jwt.json (1)
146-156: Metric queries are now correct.The validation success rate query correctly uses
sms_auth_jwt_tokens_validated_totalwhich matches the metric defined ininternal/sms-gateway/jwt/metrics.go. The previous issues with incorrect metric names have been addressed.internal/sms-gateway/handlers/webhooks/3rdparty.go (1)
113-117: LGTM!The permission scope integration and migration to
users.Userare correctly implemented. Route parameter syntax/:idis correct and consistent with Fiber conventions.internal/sms-gateway/handlers/devices/3rdparty.go (1)
46-55: LGTM!The migration to
users.Usertype is correctly implemented. The handler properly usesuser.IDfor device service calls.internal/sms-gateway/handlers/3rdparty.go (2)
74-78: LGTM!The authentication middleware chain is correctly ordered: Basic auth and JWT auth both attempt to authenticate and set the user context, then
UserRequired()ensures at least one method succeeded. This allows clients to use either authentication method.
80-81: LGTM!The auth handler is correctly registered under
/authafter the authentication middleware, allowing token generation/revocation endpoints to require authentication.internal/sms-gateway/openapi/docs.go (2)
1-6: Generated file correctly reflects handler annotations.This file is auto-generated by swaggo/swag. The JWT authentication security definitions, token management endpoints, and 403 responses are correctly generated from the handler annotations. Previous issues regarding missing
jtipath parameter and security annotations have been addressed.
1916-1921: JWTAuth security definition is correctly configured.The JWT authentication is defined as an
apiKeytype in theAuthorizationheader, which aligns with the Bearer token format expected by thejwtauth.NewJWTmiddleware.internal/sms-gateway/modules/auth/service.go (1)
26-137: Auth service integration with usersSvc and code-based auth looks coherent
Servicenow cleanly depends on*users.Service,AuthorizeUserByCoderesolvesuserIDfromcodesCacheand thenGetByID, and the publicRegisterDevicesignature matchesusers.User. This removes the earlier GetByUsername/ID mismatch and keeps the code-based auth flow straightforward. If you ever need more context on cache failures, you could wrap theGetAndDeleteerror similar to the user lookup, but functionally this is fine as-is.internal/sms-gateway/handlers/mobile.go (1)
121-153: Device registration flow with existing/new users looks consistent
postDevicecorrectly prefers an authenticated user fromuserauth.GetUser(c)and falls back to generating a new username/password pair when unauthenticated, creating the user viausersSvc.Createand then registering the device viaauthSvc.RegisterDevice(*user, ...). ReturningLogin: usernameandPassword: passwordonly when a new user is created matches the documented “returns user credentials only for new users” behavior (password will naturally be empty for existing users). This flow looks good.internal/sms-gateway/jwt/metrics.go (1)
8-117: JWT metrics align correctly with alert rules and are production-readyVerification confirms all Prometheus alert expressions reference the correct metric names (
sms_auth_jwt_*), use the properstatuslabel, and apply histogram bucket syntax correctly. Alert thresholds and queries are properly wired to the metrics defined in the code.internal/sms-gateway/jwt/service.go (1)
24-177: JWT service behavior is correct; defensive scopes copy is optionalThe code is functionally sound and correctly generates, validates, and revokes tokens. The verification found no call sites within the
internal/sms-gatewaydirectory, but this does not prove all callers treat thescopesslice as immutable—they may exist in tests, other packages, or external code.The defensive copy suggestion remains optional but sensible as a best practice to prevent accidental mutation by any caller:
Scopes: append([]string(nil), scopes...),No changes are required; the code is correct as-is. If you adopt the defensive copy, consider it a minor robustness improvement rather than a fix for a current bug.
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
49-133: WithUser usages are correctly guarded by UserRequired middleware throughout codebaseVerification confirms the original analysis: all
WithUsercall sites are protected by either global or per-route auth middleware chains. Thirdparty routes applyrouter.Use(userauth.NewBasic, jwtauth.NewJWT, userauth.UserRequired)before handler registration, while mobile routes apply middleware per-route. This ensures a user is always present beforeWithUserdereferenciesGetUser(c), preventing nil panics. The documented panic-on-misuse behavior is correctly mitigated by middleware ordering.
| router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.list)) | ||
| router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUser(h.post)) | ||
| router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage) | ||
|
|
||
| router.Post("inbox/export", userauth.WithUser(h.postInboxExport)) | ||
| router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix inconsistent route parameter syntax.
Line 287 uses :id without a leading slash, while the webhooks handler (line 116) uses /:id. This inconsistency could cause routing issues in Fiber.
Apply this diff:
- router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
+ router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.list)) | |
| router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUser(h.post)) | |
| router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage) | |
| router.Post("inbox/export", userauth.WithUser(h.postInboxExport)) | |
| router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport)) | |
| router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.list)) | |
| router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUser(h.post)) | |
| router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage) | |
| router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport)) |
🤖 Prompt for AI Agents
internal/sms-gateway/handlers/messages/3rdparty.go around lines 285-289: the GET
route uses an inconsistent parameter syntax `:id` (no leading slash) which can
break Fiber routing; change the route path to include the leading slash so it
reads `"/:id"` (keep the same permissions, userauth wrapper and route name),
then run tests or start the server to confirm routing behaves like the other
handlers.
4e7b849 to
fce4a07
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
pkg/cache/memory.go (1)
225-236:AndUpdateTTLmay produce incorrect results when item has no expiration.When
o.updateTTL != niland the item'svalidUntilis the zero time (no expiration), adding a duration totime.Time{}produces an unexpected timestamp (year 0001 + duration).case o.updateTTL != nil: item.validUntil = item.validUntil.Add(*o.updateTTL) // If validUntil is zero, this is problematicConsider handling the zero-time case:
case o.updateTTL != nil: + if item.validUntil.IsZero() { + item.validUntil = time.Now().Add(*o.updateTTL) + } else { item.validUntil = item.validUntil.Add(*o.updateTTL) + }pkg/cache/redis.go (1)
420-443: Non-atomic TTL setting inSetOrFailmay leave orphaned entries.If
HSetNXsucceeds butHExpireAtfails (e.g., network error), the cache entry exists without a TTL and may persist indefinitely. UnlikeSetwhich uses pipelining,SetOrFailcannot easily pipeline since it needs theHSetNXresult first.Consider using a Lua script for atomicity, or document the risk:
local inserted = redis.call('HSETNX', KEYS[1], ARGV[1], ARGV[2]) if inserted == 1 and tonumber(ARGV[3]) > 0 then redis.call('HExpireAt', KEYS[1], tonumber(ARGV[3]), 'FIELDS', '1', ARGV[1]) end return insertedAlternatively, add a rollback on TTL failure:
if !options.validUntil.IsZero() { if expErr := r.client.HExpireAt(ctx, r.key, options.validUntil, key).Err(); expErr != nil { + // Rollback: delete the field since we couldn't set TTL + _ = r.client.HDel(ctx, r.key, key) return fmt.Errorf("failed to set cache item ttl: %w", expErr) } }internal/sms-gateway/handlers/base/handler_test.go (1)
272-335: ValidateStruct tests no longer assert failures for non‑OK casesIn
TestHandler_ValidateStruct, you changed invalid cases toexpectedStatus: fiber.StatusInternalServerError, but the assertions still only treatfiber.StatusBadRequestas the “error expected” sentinel:if test.expectedStatus == fiber.StatusOK && err != nil { ... } if test.expectedStatus == fiber.StatusBadRequest && err == nil { ... }With
expectedStatusnow set toStatusInternalServerError, the second condition never triggers, so invalid cases will pass even ifValidateStructstops returning an error.To restore the intended check with minimal change, treat any non‑OK status as “error expected”:
- if test.expectedStatus == fiber.StatusBadRequest && err == nil { - t.Errorf("Expected error, got nil") - } + if test.expectedStatus != fiber.StatusOK && err == nil { + t.Errorf("Expected error, got nil") + }This keeps the table the same while ensuring all invalid scenarios still assert that an error is returned.
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
124-139: UpdateWithUsercomment to match current behavior (no longer panics)The comment says “If the user is not present, it will panic.” but the implementation now returns
401 Unauthorizedinstead:user := GetUser(c) if user == nil { return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized") }Please update the comment so it doesn’t mislead readers.
♻️ Duplicate comments (10)
pkg/cache/redis.go (2)
78-91: Document Redis 7.4.0+ version requirement.The implementation uses Redis 7.4.0+ commands (
HEXPIREAT,HTTL,HEXPIREwithFIELDSparameter) for per-field hash TTL. This requirement should be documented to prevent runtime failures with older Redis versions.// RedisCache implements the Cache interface using Redis as the backend. // +// IMPORTANT: Requires Redis 7.4.0 or later for per-field hash TTL support +// (HEXPIREAT, HTTL, HEXPIRE commands with FIELDS parameter). +// // This implementation stores all data in a Redis hash, with each cache item
43-48: HTTL returns an array, causing Lua script failure.The
HTTLcommand returns an array of TTL values (one per field). The current code treatsttlas a scalar:local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field) if ttl < 0 then -- comparing array to number ttl = 0 end local newTtl = ttl + ttlDelta -- adding array to numberThis will cause a Lua runtime error when
updateTTLoption is used.Apply this fix to extract the first element from the array:
- local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field) - if ttl < 0 then + local ttlArr = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field) + local ttl = ttlArr[1] or -1 + if ttl == nil or ttl < 0 then ttl = 0 end local newTtl = ttl + ttlDeltaNote: This was flagged in a past review but appears unresolved in the current code.
internal/sms-gateway/handlers/devices/3rdparty.go (1)
92-95: Consider aligning Delete route path with other handlers (/:id).This route still uses
":id"without a leading slash. Previous review already suggested switching to"/:id"for consistency with other handlers (e.g., webhooks) and typical Fiber routing patterns:- router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove)) + router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))Please double-check the group prefix and confirm the desired final path.
internal/sms-gateway/users/repository.go (1)
59-67: CheckRowsAffectedto detect non-existent users.GORM's
Updatedoes not return an error when zero rows are affected. If the user ID doesn't exist, this method silently succeeds without updating anything, which is inconsistent withGetByIDreturningErrNotFound.Apply this diff:
func (r *repository) UpdatePassword(id string, passwordHash string) error { - if err := r.db.Model((*userModel)(nil)). + result := r.db.Model((*userModel)(nil)). Where("id = ?", id). - Update("password_hash", passwordHash).Error; err != nil { + Update("password_hash", passwordHash) + if err := result.Error; err != nil { return fmt.Errorf("can't update password: %w", err) } + if result.RowsAffected == 0 { + return ErrNotFound + } return nil }internal/sms-gateway/jwt/config.go (1)
18-32: Consider validating the Issuer field.The past review comment about missing Issuer validation remains unaddressed. While the
Issuerclaim is optional per RFC 7519, it is used in token creation (service.go:86) and validation (service.go:132). An empty issuer could lead to tokens with an empty "iss" claim, which may not align with security best practices.internal/sms-gateway/handlers/thirdparty/auth.go (2)
60-82: TTL overflow validation issue remains unresolved.The past review comment about potential TTL overflow when converting
uint64totime.Duration(int64) was not addressed. Line 70 includes anolint:goseccomment claiming validation happens in the service, but the service only validatesttl >= 0(service.go:73-75) and does not check for overflow.
97-105: jti parameter validation is still missing.The past review comment about validating the
jtiparameter was marked as addressed, but the current code (lines 98-100) still does not validate thatjtiis non-empty before callingjwtSvc.RevokeToken. While Fiber's routing ensures the parameter exists, an empty string could still be passed.Apply this diff:
func (h *AuthHandler) deleteToken(user users.User, c *fiber.Ctx) error { jti := c.Params("jti") + if jti == "" { + return fiber.NewError(fiber.StatusBadRequest, "jti parameter is required") + } if err := h.jwtSvc.RevokeToken(c.Context(), user.ID, jti); err != nil {internal/sms-gateway/handlers/messages/3rdparty.go (1)
329-329: Fix inconsistent route parameter syntax.Line 329 uses
:idwithout a leading slash, while other handlers (e.g., webhooks at line 126) use/:id. This inconsistency could cause routing issues in Fiber.Apply this diff:
- router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage) + router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
61-86: Code-based auth now stores user consistently with Basic path
NewCodenow callsSetUser(c, *user), matching the storage shape expected byGetUser. This resolves the earlier mismatch between storing*users.Userand assertingusers.User.internal/sms-gateway/handlers/mobile.go (1)
43-75: Handlenanoid.Standarderror to avoid nilidGenpanic
newMobileHandlerstill ignores the error fromnanoid.Standard:const idLength = 21 idGen, _ := nanoid.Standard(idLength)If initialization fails,
idGenwill be nil andh.idGen()will panic when registering a new device.Handle the error explicitly, e.g.:
- const idLength = 21 - idGen, _ := nanoid.Standard(idLength) + const idLength = 21 + idGen, err := nanoid.Standard(idLength) + if err != nil { + panic(fmt.Sprintf("failed to initialize ID generator: %v", err)) + }(or, alternatively, propagate the error out of the constructor instead of panicking, if you’re willing to adjust call sites).
🧹 Nitpick comments (6)
pkg/cache/typed.go (1)
338-358: Consider documenting partial failure behavior inDrain.If
storage.Drain()succeeds but unmarshalling fails for any item, the cache is already cleared but the caller receives anilmap. This is acceptable for corrupted data scenarios, but the documentation could clarify this behavior to set caller expectations.Consider adding to the method documentation:
// Returns: // - map[string]T: A map containing all non-expired key-typed value pairs -// - error: nil on success, or an error if the cache operation fails +// - error: nil on success, or an error if the cache operation or unmarshalling fails. +// Note: if unmarshalling fails, the cache will already be cleared.api/mobile.http (1)
70-73: Use distinct example values for current vs new password.Having both
currentPasswordandnewPasswordset to the same literal makes the example harder to reason about and could hide issues when manually testing. Consider using clearly different sample values (or placeholders) to illustrate a real password change.api/requests.http (1)
220-224: Consider removing unnecessary Content-Type header for DELETE request.The DELETE request at line 221 has no request body, so the
Content-Type: application/jsonheader on line 223 is unnecessary. This is a minor inconsistency.### DELETE {{baseUrl}}/3rdparty/v1/auth/token/w8pxz0a4Fwa4xgzyCvSeC HTTP/1.1 Authorization: Basic {{credentials}} -Content-Type: application/jsoninternal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
24-27: Consider logging token parse failures for debugging.While returning 401 is correct, silently discarding parse errors can make debugging difficult. Consider adding debug-level logging for operational visibility.
This would require passing a logger to the middleware:
claims, err := jwtSvc.ParseToken(c.Context(), token) if err != nil { // log.Debug("JWT parse failed", zap.Error(err)) return fiber.ErrUnauthorized }internal/sms-gateway/jwt/repository.go (1)
20-47: JWT token repository behavior looks sound; consider whether silent “no-op” revoke is desired
Insert,Revoke, andIsRevokedare correctly usingWithContextand GORM, and the revocation check viaCOUNT(revoked_at IS NOT NULL)is fine.One thing to double‑check is whether you intentionally treat “token not found / already revoked” as a successful
Revoke(noRowsAffectedcheck). That’s nice and idempotent, but if the API contract expects 404/409‑style behavior, you may want to inspectdb.RowsAffectedand surface that to callers instead.internal/sms-gateway/handlers/mobile.go (1)
228-241: Consider distinguishing auth failures from internal errors inchangePassword
changePasswordnow delegates toh.usersSvc.ChangePassword, but any error results in:h.Logger.Error("failed to change password", zap.Error(err)) return fiber.NewError(fiber.StatusUnauthorized, "failed to change password")This maps all failures (including DB or service outages) to
401 Unauthorized, which can mask server-side issues and doesn’t fully match the swagger docs that mention a500path.If
usersSvc.ChangePasswordexposes a well-known “invalid credentials” error, you might:
- return
401for that specific error, and- return
500for all other unexpected errors.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (65)
.golangci.yml(2 hunks)api/mobile.http(1 hunks)api/requests.http(5 hunks)cmd/sms-gateway/main.go(1 hunks)configs/config.example.yml(1 hunks)deployments/grafana/dashboards/jwt.json(1 hunks)deployments/prometheus/alerts/jwt-alerts.yml(1 hunks)go.mod(2 hunks)internal/config/config.go(4 hunks)internal/config/module.go(2 hunks)internal/config/types.go(1 hunks)internal/sms-gateway/app.go(4 hunks)internal/sms-gateway/handlers/3rdparty.go(2 hunks)internal/sms-gateway/handlers/base/handler.go(2 hunks)internal/sms-gateway/handlers/base/handler_test.go(8 hunks)internal/sms-gateway/handlers/devices/3rdparty.go(6 hunks)internal/sms-gateway/handlers/devices/permissions.go(1 hunks)internal/sms-gateway/handlers/logs/3rdparty.go(3 hunks)internal/sms-gateway/handlers/logs/permissions.go(1 hunks)internal/sms-gateway/handlers/messages/3rdparty.go(8 hunks)internal/sms-gateway/handlers/messages/permissions.go(1 hunks)internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go(1 hunks)internal/sms-gateway/handlers/middlewares/permissions/permissions.go(1 hunks)internal/sms-gateway/handlers/middlewares/userauth/userauth.go(4 hunks)internal/sms-gateway/handlers/mobile.go(8 hunks)internal/sms-gateway/handlers/module.go(2 hunks)internal/sms-gateway/handlers/settings/3rdparty.go(8 hunks)internal/sms-gateway/handlers/settings/permissions.go(1 hunks)internal/sms-gateway/handlers/thirdparty/auth.go(1 hunks)internal/sms-gateway/handlers/thirdparty/module.go(1 hunks)internal/sms-gateway/handlers/thirdparty/permissions.go(1 hunks)internal/sms-gateway/handlers/webhooks/3rdparty.go(5 hunks)internal/sms-gateway/handlers/webhooks/permissions.go(1 hunks)internal/sms-gateway/jwt/config.go(1 hunks)internal/sms-gateway/jwt/disabled.go(1 hunks)internal/sms-gateway/jwt/errors.go(1 hunks)internal/sms-gateway/jwt/jwt.go(1 hunks)internal/sms-gateway/jwt/metrics.go(1 hunks)internal/sms-gateway/jwt/models.go(1 hunks)internal/sms-gateway/jwt/module.go(1 hunks)internal/sms-gateway/jwt/repository.go(1 hunks)internal/sms-gateway/jwt/service.go(1 hunks)internal/sms-gateway/models/migration.go(1 hunks)internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql(1 hunks)internal/sms-gateway/models/models.go(0 hunks)internal/sms-gateway/modules/auth/module.go(0 hunks)internal/sms-gateway/modules/auth/repository.go(0 hunks)internal/sms-gateway/modules/auth/service.go(4 hunks)internal/sms-gateway/modules/messages/service.go(3 hunks)internal/sms-gateway/modules/settings/models.go(2 hunks)internal/sms-gateway/modules/webhooks/models.go(2 hunks)internal/sms-gateway/openapi/docs.go(25 hunks)internal/sms-gateway/users/cache.go(1 hunks)internal/sms-gateway/users/domain.go(1 hunks)internal/sms-gateway/users/errors.go(1 hunks)internal/sms-gateway/users/models.go(1 hunks)internal/sms-gateway/users/module.go(1 hunks)internal/sms-gateway/users/repository.go(1 hunks)internal/sms-gateway/users/service.go(1 hunks)pkg/cache/cache.go(1 hunks)pkg/cache/errors.go(1 hunks)pkg/cache/memory.go(13 hunks)pkg/cache/options.go(4 hunks)pkg/cache/redis.go(10 hunks)pkg/cache/typed.go(1 hunks)
💤 Files with no reviewable changes (3)
- internal/sms-gateway/models/models.go
- internal/sms-gateway/modules/auth/module.go
- internal/sms-gateway/modules/auth/repository.go
✅ Files skipped from review due to trivial changes (1)
- internal/sms-gateway/handlers/settings/permissions.go
🚧 Files skipped from review as they are similar to previous changes (21)
- cmd/sms-gateway/main.go
- internal/sms-gateway/modules/settings/models.go
- internal/sms-gateway/handlers/middlewares/permissions/permissions.go
- go.mod
- internal/sms-gateway/handlers/thirdparty/permissions.go
- internal/sms-gateway/modules/messages/service.go
- pkg/cache/errors.go
- internal/sms-gateway/users/domain.go
- internal/sms-gateway/app.go
- internal/sms-gateway/jwt/models.go
- internal/sms-gateway/handlers/thirdparty/module.go
- deployments/grafana/dashboards/jwt.json
- internal/sms-gateway/jwt/jwt.go
- internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
- deployments/prometheus/alerts/jwt-alerts.yml
- internal/sms-gateway/handlers/module.go
- internal/sms-gateway/users/cache.go
- internal/sms-gateway/users/service.go
- internal/sms-gateway/users/errors.go
- internal/config/config.go
- configs/config.example.yml
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.
Applied to files:
pkg/cache/cache.gopkg/cache/redis.gopkg/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.gopkg/cache/redis.gopkg/cache/memory.go
📚 Learning: 2025-09-10T23:54:16.828Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:118-121
Timestamp: 2025-09-10T23:54:16.828Z
Learning: Redis 7.4.0 introduced HEXPIRE, HEXPIREAT, and related commands that support per-field TTL on hash data structures. go-redis v9.6.0+ includes support for these commands with methods like HExpireAt.
Applied to files:
pkg/cache/redis.go
🧬 Code graph analysis (21)
internal/sms-gateway/users/models.go (5)
internal/sms-gateway/models/models.go (1)
SoftDeletableModel(12-16)internal/sms-gateway/jwt/models.go (1)
Migrate(33-38)internal/sms-gateway/models/migration.go (1)
Migrate(13-18)internal/sms-gateway/modules/settings/models.go (1)
Migrate(28-33)internal/sms-gateway/modules/webhooks/models.go (1)
Migrate(39-44)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
SetUser(88-90)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
SetScopes(15-17)
internal/sms-gateway/handlers/logs/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/logs/permissions.go (1)
ScopeRead(4-4)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-139)
internal/sms-gateway/handlers/webhooks/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/webhooks/permissions.go (3)
ScopeList(4-4)ScopeWrite(5-5)ScopeDelete(6-6)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-139)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/settings/permissions.go (2)
ScopeRead(4-4)ScopeWrite(5-5)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-139)
internal/sms-gateway/handlers/thirdparty/auth.go (5)
internal/sms-gateway/handlers/base/handler.go (1)
Handler(15-18)internal/sms-gateway/handlers/thirdparty/permissions.go (1)
ScopeTokensManage(4-4)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-139)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/jwt/errors.go (3)
ErrInvalidParams(9-9)ErrInitFailed(7-7)ErrDisabled(6-6)
internal/sms-gateway/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
ErrInvalidConfig(8-8)
internal/sms-gateway/modules/auth/service.go (5)
internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/modules/devices/service.go (1)
Service(15-24)internal/sms-gateway/online/service.go (2)
Service(14-17)New(28-37)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/models/models.go (2)
Device(18-29)NewDevice(31-37)
internal/sms-gateway/jwt/errors.go (1)
internal/sms-gateway/jwt/service.go (1)
New(24-51)
internal/sms-gateway/models/migration.go (1)
internal/sms-gateway/models/models.go (1)
Device(18-29)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/messages/permissions.go (4)
ScopeList(10-10)ScopeSend(6-6)ScopeRead(8-8)ScopeExport(12-12)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-139)
internal/sms-gateway/handlers/3rdparty.go (6)
internal/sms-gateway/handlers/base/handler.go (1)
Handler(15-18)internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/handlers/logs/3rdparty.go (1)
ThirdPartyController(21-23)internal/sms-gateway/handlers/thirdparty/auth.go (1)
AuthHandler(19-23)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
NewBasic(21-59)internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
NewJWT(14-42)
internal/sms-gateway/users/module.go (3)
internal/sms-gateway/cache/factory.go (1)
Factory(16-18)internal/sms-gateway/users/service.go (1)
NewService(21-33)internal/sms-gateway/users/models.go (1)
Migrate(29-34)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/devices/permissions.go (2)
ScopeList(4-4)ScopeDelete(5-5)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(130-139)
pkg/cache/typed.go (3)
pkg/cache/cache.go (1)
Cache(112-283)pkg/cache/options.go (3)
Option(9-9)GetOption(89-89)AndDelete(204-208)pkg/cache/errors.go (1)
ErrFailedToCreateZeroValue(53-53)
internal/sms-gateway/modules/webhooks/models.go (1)
internal/sms-gateway/users/domain.go (1)
User(5-10)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/errors.go (2)
ErrNotFound(6-6)ErrExists(7-7)pkg/mysql/errors.go (1)
IsDuplicateKeyViolation(13-19)
internal/sms-gateway/jwt/service.go (5)
internal/sms-gateway/jwt/config.go (1)
Config(12-16)internal/sms-gateway/jwt/repository.go (1)
Repository(10-12)internal/sms-gateway/jwt/metrics.go (3)
Metrics(24-31)StatusError(20-20)StatusSuccess(19-19)internal/sms-gateway/jwt/jwt.go (3)
Service(10-14)TokenInfo(23-27)Claims(16-21)internal/sms-gateway/jwt/errors.go (4)
ErrInitFailed(7-7)ErrInvalidParams(9-9)ErrInvalidToken(10-10)ErrTokenRevoked(11-11)
pkg/cache/redis.go (1)
pkg/cache/options.go (1)
AndDelete(204-208)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
SetScopes(15-17)ScopeAll(10-10)internal/sms-gateway/users/domain.go (1)
User(5-10)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
AndDelete(204-208)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: E2E
- GitHub Check: Benchmark
- GitHub Check: Test
- GitHub Check: Lint
- GitHub Check: Analyze (go)
🔇 Additional comments (44)
.golangci.yml (1)
255-260: Configuration updates align well with JWT integration.The addition of the JWT Claims exclusion pattern (line 260) is appropriate—external JWT claim types from
github.com/golang-jwt/jwt/v5should not be subject to exhaustruct's strict field initialization requirement, as the library design intentionally permits optional fields. The pattern correctly targets Claims types while being properly scoped to the v5 package.pkg/cache/cache.go (1)
1-283: Comprehensive interface documentation looks good.The expanded documentation for the
Cacheinterface provides clear usage examples for all methods, consistent parameter/return documentation, and helpful behavioral notes (e.g., Redis handling expiration automatically inCleanup). The examples correctly demonstrate error handling patterns withErrKeyNotFoundandErrKeyExpired.pkg/cache/typed.go (2)
184-195: Error wrapping inSetOrFailmay hideErrKeyExistsfrom callers.When the underlying cache returns
ErrKeyExists, wrapping it withfmt.Errorf("failed to set value in cache: %w", setErr)preserves the error chain forerrors.Is(). However, the documentation at line 181-182 suggests callers should check forcache.ErrKeyExists, so the wrapping is acceptable. The%wverb ensureserrors.Is(err, cache.ErrKeyExists)still works.
392-407: Reflection-based instantiation correctly addresses pointer type requirement.The
newItem[T]helper properly:
- Verifies
Tis a pointer type viat.Kind() != reflect.Ptr- Creates a new instance using
reflect.New(t.Elem())which allocates the underlying struct- Returns a meaningful error wrapping
ErrFailedToCreateZeroValueThis correctly fixes the previous issue where
var value Tfor pointer types resulted in nil receivers duringUnmarshal.pkg/cache/options.go (2)
38-49: Negative TTL semantics may be surprising.When
ttl < 0, the code setsvalidUntil = time.Now(), which means the item expires immediately upon insertion. This might be counterintuitive - a negative TTL could be expected to behave like zero (no expiration) or trigger an error.Is this intentional behavior? If so, consider documenting it explicitly:
// WithTTL sets the TTL (time to live) for an item. // // The item will expire after the given duration from the time of insertion. -// A TTL of zero or negative means the item will not expire. +// A TTL of zero means the item will not expire. +// A negative TTL means the item expires immediately (useful for invalidation).Or if immediate expiration isn't the intent:
case ttl < 0: - o.validUntil = time.Now() + o.validUntil = time.Time{}
76-105: Clean option pattern implementation.The use of pointer fields (
validUntil *time.Time,setTTL *time.Duration, etc.) correctly distinguishes between "not set" and "set to zero value". TheisEmpty()helper provides an efficient check for the common case of no options.pkg/cache/memory.go (2)
161-175: Drain correctly leverages cleanup pattern.Based on learnings, the
cleanupmethod removes expired items before executing the callback. This ensurescpycontains only non-expired items without additional filtering. The atomic swap of maps under the write lock is clean.
286-329: Set and SetOrFail implementations are correct.
Setproperly applies default TTL before custom optionsSetOrFailcorrectly checks expiration status before returningErrKeyExists, allowing expired keys to be overwritten as documentedpkg/cache/redis.go (1)
282-330: Get implementation correctly optimizes for common case.The
isEmpty()check enables a fast path using directHGetwhen no options are provided, avoiding Lua script overhead. The error mapping fromredis.NiltoErrKeyNotFoundis correct.Note: Unlike the memory cache, Redis never returns
ErrKeyExpiredsince Redis automatically removes expired fields. The documentation at lines 255-256 mentions this error but it won't occur in practice for Redis. Consider clarifying this in the method docs.internal/sms-gateway/handlers/base/handler.go (2)
20-42: Idiomatic wrapping of parse errors looks goodUsing
fmt.Errorf("failed to parse …: %w", err)for body/query/params parsing is idiomatic and preserves the underlying error for higher-level handling without changing control flow. No issues spotted here.
44-55: Consistent validation error wrapping; behavior now depends on upstream mappingWrapping both
validatorandValidatableerrors withfmt.Errorf("failed to validate: %w", err)is consistent and keeps the root cause accessible. Given the tests now expect generic 500s for these cases, this centralizes responsibility for mapping validation vs. server errors into your global error handler, which seems intentional.internal/sms-gateway/handlers/base/handler_test.go (1)
77-239: Status expectations updated to match new error semanticsThe updated
expectedStatusvalues for body, query, and params tests (500 for validation failures, 422 for the no‑body parse error, 404 for missing path segments) are consistent with the new behavior of the handler helpers and Fiber’s default error handling. The table‑driven tests remain clear and still exercise the main happy/invalid paths.internal/sms-gateway/handlers/logs/3rdparty.go (1)
5-8: JWT + scope wiring for logs looks consistent.Using
permissions.RequireScope(ScopeRead)together withuserauth.WithUserand documenting@Security JWTAuthmatches the new auth/permissions model, and the 501 Not Implemented response matches the swagger docs.Also applies to: 37-38, 49-54, 56-58
internal/sms-gateway/models/migration.go (1)
13-17: Confirm global migration wiring includes users (and other module) migrations.
Migratehere now only migratesDevice, relying on per-moduleMigratefunctions (e.g.,users.Migrate, webhooks, jwt, settings). Please double-check that the application’s migration entrypoint invokes all of these module-levelMigratefunctions so user tables and related schemas are actually created.internal/sms-gateway/handlers/logs/permissions.go (1)
1-5: Log scope constant is consistent with the permissions model.
ScopeRead = "logs:read"follows the same pattern as other handlers and cleanly centralizes the log-read scope.internal/sms-gateway/handlers/devices/permissions.go (1)
1-6: Device scope constants match intended usage.
ScopeListandScopeDeleteare clearly named and align with the permissions middleware and route usage in the devices handler.internal/sms-gateway/handlers/devices/3rdparty.go (1)
9-16: Devices 3rd‑party handler now correctly uses users + scopes.Switching to
users.Userwithuserauth.WithUser, adding@Security JWTAuth, and guarding list/delete withScopeList/ScopeDeleteare all consistent with the new JWT + permissions framework; the device queries remain properly scoped byuser.ID.Also applies to: 39-61, 63-90
internal/sms-gateway/users/models.go (1)
10-34: User ORM model and migration look sound and consistent.
userModel(ID varchar(32), PasswordHash varchar(72)) together withTableName() string { return "users" }and a module-levelMigratemirrors the pattern used by other modules and matchesDevice.UserID’s type, so the schema looks coherent.internal/sms-gateway/handlers/webhooks/permissions.go (1)
1-7: Webhook scope constants align with the shared permission scheme.
ScopeList,ScopeWrite, andScopeDeleteare clearly named and consistent with how other handler packages expose their scopes.internal/sms-gateway/handlers/messages/permissions.go (1)
1-13: LGTM!The permission scope constants are well-documented and follow a consistent naming pattern (
messages:<action>) that aligns with other permission scopes in the codebase (e.g.,settings:read,settings:write).internal/sms-gateway/handlers/settings/3rdparty.go (1)
137-141: LGTM!The permission middleware integration is well-structured:
ScopeReadcorrectly protects the GET endpointScopeWritecorrectly protects both PUT and PATCH endpoints- Middleware ordering is correct (permission check before user extraction)
internal/sms-gateway/users/repository.go (2)
22-45: LGTM!The
ExistsandGetByIDmethods are correctly implemented:
Existsuses an efficient COUNT queryGetByIDproperly mapsgorm.ErrRecordNotFoundto domainErrNotFound- The previous issue with
Take(&user)has been fixed toTake(user)
47-57: LGTM!The
Insertmethod correctly handles duplicate key violations by wrappingErrExistswith the underlying error, providing good error context for debugging while maintaining a clean domain error interface.internal/sms-gateway/users/module.go (2)
12-27: LGTM!The module wiring is well-structured:
- Private provisions for internal dependencies (
loginCache,repository)- Public provision for
Serviceto be consumed by other modules- Error wrapping in the cache factory provides good context
The previous review issue about the missing
newRepositoryprovision has been addressed.
29-32: Migration registration follows framework pattern.The
init()function with thenolint:gochecknoinitsdirective is appropriate here as it follows the framework's migration registration pattern.internal/sms-gateway/jwt/disabled.go (1)
1-28: LGTM!This is a clean implementation of the Null Object pattern for the JWT service. When JWT is disabled (empty secret), all operations consistently return
ErrDisabled, providing clear feedback to callers. The interface compliance is correct and the design allows graceful degradation.internal/sms-gateway/modules/webhooks/models.go (1)
8-8: LGTM! Type refactor tousers.Useris consistent with the PR's user domain changes.The import and type change align with the broader refactoring to consolidate user-related types into the dedicated
userspackage. The GORM foreign key relationship and constraints remain unchanged.Also applies to: 24-24
internal/config/module.go (1)
140-146: LGTM! JWT config provider follows established patterns.The provider correctly maps application configuration to the
jwt.Configtype, consistent with other configuration providers in this module. The secret validation is appropriately handled downstream in the JWT module (returns disabled service when empty).internal/sms-gateway/jwt/module.go (1)
9-22: LGTM! Well-structured fx module with appropriate conditional service initialization.The module correctly:
- Marks internal dependencies (Metrics, Repository) as private
- Provides a disabled service when no secret is configured, enabling graceful degradation
- Returns the full service with proper error propagation when configured
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
14-41: LGTM! Well-implemented JWT middleware with proper fallthrough for multi-auth support.The middleware correctly:
- Uses case-insensitive Bearer prefix check per RFC 7235
- Falls through to next handler when no Bearer token is present, allowing other auth methods (Basic auth) to coexist
- Distinguishes between user-not-found (401) and internal errors (500)
- Safely dereferences the user pointer after error check
internal/config/types.go (1)
11-48: LGTM!The Duration wrapper type is well-implemented with proper nil-safety, clean YAML/text unmarshaling delegation, and compile-time interface assertions.
internal/sms-gateway/jwt/errors.go (1)
5-12: LGTM!The error definitions follow standard Go patterns and provide clear, specific error types for the JWT subsystem.
internal/sms-gateway/openapi/docs.go (1)
1-1955: LGTM!The OpenAPI documentation has been regenerated with the JWT authentication security definitions and token management endpoints properly documented. Past review concerns about security annotations and parameter definitions have been addressed.
internal/sms-gateway/handlers/webhooks/3rdparty.go (1)
56-126: LGTM!The handler has been properly updated to use the new
users.Usertype and integrated with the permission-based authorization system. The routes are correctly wrapped with scope checks and the OpenAPI documentation reflects the JWT authentication requirements.internal/sms-gateway/jwt/service.go (1)
24-182: LGTM!The JWT service implementation is well-structured with comprehensive validation, proper error handling, and observability through metrics. Past review concerns about userID validation and parameter checks have been appropriately addressed.
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
16-57: Basic auth integration withusers.Servicelooks correctThe Basic middleware cleanly decodes credentials, delegates to
usersSvc.Login, and, on success, stores ausers.UserviaSetUserand grantsScopeAll. This is consistent with the new users domain and should work well with downstream middlewares.
88-109: Storingusers.Userby value is fine; pointer returned byGetUseris safe
SetUserstores ausers.Uservalue inLocals, andGetUserasserts that value and returns its address. Go will correctly heap-allocate the value when its address escapes, so the returned pointer remains valid. This is a reasonable pattern for this small struct.internal/sms-gateway/handlers/3rdparty.go (1)
20-80: Third‑party handler wiring and auth middleware composition look correctThe new constructor neatly injects
usersSvc,jwtSvc, andauthHandler, andRegisterapplies:
userauth.NewBasic(h.usersSvc)jwtauth.NewJWT(h.jwtSvc, h.usersSvc)userauth.UserRequired()after registering the health endpoint, so
/3rdparty/v1/healthstays open while/auth,/messages, etc. require either Basic or JWT. This is consistent with the new JWT and users modules.internal/sms-gateway/modules/auth/service.go (2)
85-145: User/device auth flows viausersSvcanddevicesSvclook consistent
RegisterDevicenow explicitly takes ausers.Userand passesuser.IDintodevicesSvc.Insert, matching theDevice.UserIDownership model.AuthorizeUserByCodecorrectly retrieves the user ID fromcodesCacheand then resolves the user viausersSvc.GetByID, returning*users.Useras expected by the updated middlewares.Both flows align with the new users domain and the code-based auth middleware.
55-83: Based on my verification, the review comment is incorrect. Here's what I found:
- Fact check: The
for range intsyntax (iterating over an integer directly) was indeed introduced in Go 1.22. ✓- Project verification: The main module's
go.modspecifiesgo 1.24.3, which is well above Go 1.22.- Conclusion: The syntax
for range 3is fully supported and compatible with the project's Go version (1.24.3).The review comment incorrectly frames this as a potential issue by suggesting "If your module still targets an earlier Go version, this won't compile." However, the module explicitly targets Go 1.24.3, so there is no compatibility issue.
No issue with
for range 3— project targets Go 1.24.3, which fully supports integer-based range loops introduced in Go 1.22.The syntax is valid and compatible with your project's Go version. No action is required.
internal/sms-gateway/jwt/metrics.go (1)
8-117: JWT metrics are well-structured and idiomaticMetric names, labels (
status), and histogram buckets are reasonable, and theObserve*helpers aroundprometheus.NewTimergive clean timing instrumentation. Wiring viaNewMetrics()should integrate smoothly with the jwt service.internal/sms-gateway/handlers/mobile.go (3)
123-155: Device registration flow correctly switches tousersSvcwhile preserving behaviorFor
/mobile/v1/device:
- Existing users:
userauth.GetUser(c)populatesuser *users.User, which you pass toRegisterDevice(*user, ...).- New users: you generate an ID, derive
username/password, create the user viah.usersSvc.Create, and reuse the returned*users.Userfor device registration.The response uses
Login: usernameandPassword: password, so credentials remain only non-empty for newly created users, matching the route description.
201-212:getUserCodesignature change tousers.Userfits WithUser middleware
getUserCodenow takesusers.User, and inRegisteryou useuserauth.WithUser(h.getUserCode), which dereferences*users.UserfromLocals. This keeps types aligned across userauth, auth.Service, and the mobile handler.
243-272: Mobile routes now correctly useuserauth.NewBasic(h.usersSvc)for user-based flowsIn
Register, the/mobile/v1/deviceand/mobile/v1/user/codeendpoints are now protected withuserauth.NewBasic(h.usersSvc)(and for/devicealsouserauth.NewCodeplus server key logic). This lines up with the new users service API and the updated userauth middleware.
🤖 Pull request artifacts
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4)
16-21: Update NewBasic comment to match actual Basic auth behaviorThe comment says that when the header “is not in the correct format, the request is passed through”, but malformed/undecodable credentials or missing
username:passwordcurrently result in401 Unauthorized, notNext(). Thebasic <username>:<password>wording is also slightly misleading given the base64 encoding.I suggest tightening the doc comment to reflect the real flow:
-// NewBasic returns a middleware that checks if the Authorization header is set to basic <username>:<password>>. -// If the header is not set, or is not in the correct format, the request is passed through. -// If the header is set, the middleware will decode the header contents, check if the credentials are in the correct format, -// and then authenticate the user using the given users service. -// If the user is authenticated, the middleware will set the user in the locals map. +// NewBasic returns a middleware that optionally performs HTTP Basic authentication. +// If the "Authorization" header is missing or does not start with "Basic ", the request is passed through unchanged. +// If the header is present, the middleware expects a base64-encoded "username:password" payload, decodes it, +// validates the credentials format, and authenticates the user using the given users service. +// On invalid or failed authentication it returns 401 Unauthorized; on success it stores the user in Locals. --- `77-83`: **Consider aligning scopes between Code auth and Basic auth** The Basic-auth path grants `permissions.ScopeAll`, but the Code-auth path only sets the user and does not assign any scopes. If code-based authorization is expected to confer the same permissions as a Basic login, the current behavior may leave code-authenticated users under‑scoped. If they should have full access, mirror the Basic path: ```diff - user, err := authSvc.AuthorizeUserByCode(code) + user, err := authSvc.AuthorizeUserByCode(code) if err != nil { return fiber.ErrUnauthorized } - SetUser(c, *user) + SetUser(c, *user) + permissions.SetScopes(c, []string{permissions.ScopeAll})If the asymmetry is intentional, adding a short comment here explaining why Code auth does not set scopes would help future readers.
99-108: Tighten GetUser documentation and clarify storage expectationsThe implementation now consistently asserts
users.Userand returns a pointer to that value, which pairs correctly withSetUserand fixes the earlier pointer/value mismatch.The comment still refers to
LocalsUserand doesn’t say that we expect a value, not a pointer. You could clarify it as:-// GetUser returns the user stored in the Locals of the given context. -// It returns nil if the Locals do not contain a user under the key LocalsUser. -// The user is stored in the Locals by the NewBasic and NewCode middlewares. +// GetUser returns the user stored in the Locals of the given context. +// It returns nil if the Locals do not contain a user under the key localsUser. +// The user is stored in Locals by the NewBasic and NewCode middlewares via SetUser, +// and is retrieved as a users.User value (exposed here as *users.User for convenience).No changes needed to the function body itself.
125-139: Sync WithUser docs with behavior and reuse fiber.ErrUnauthorizedThe comment still says “If the user is not present, it will panic”, but the implementation now (correctly) returns an unauthorized error instead, so the doc is stale. Also, you can simplify the unauthorized branch by reusing
fiber.ErrUnauthorizedfor consistency with the rest of this file.Suggested changes:
-// WithUser is a decorator that provides the current user to the handler. -// It assumes that the user is stored in the Locals under the key LocalsUser. -// If the user is not present, it will panic. -// -// It is a convenience function that wraps the call to GetUser and calls the -// handler with the user as the first argument. +// WithUser is a decorator that provides the current user to the handler. +// It assumes that the user is stored in Locals under the key localsUser. +// If the user is not present, it returns 401 Unauthorized. +// +// It is a convenience function that wraps the call to GetUser and calls the +// handler with the user as the first argument. func WithUser(handler func(users.User, *fiber.Ctx) error) fiber.Handler { return func(c *fiber.Ctx) error { user := GetUser(c) if user == nil { - return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized") + return fiber.ErrUnauthorized } return handler(*user, c) } }This keeps behavior the same but makes docs accurate and unauthorized handling consistent with the rest of the middleware.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
internal/sms-gateway/handlers/base/handler_test.go(9 hunks)internal/sms-gateway/handlers/middlewares/userauth/userauth.go(4 hunks)pkg/cache/memory.go(14 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/sms-gateway/handlers/base/handler_test.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.
Applied to files:
pkg/cache/memory.go
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.
Applied to files:
pkg/cache/memory.go
🧬 Code graph analysis (2)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
SetScopes(15-17)ScopeAll(10-10)internal/sms-gateway/users/domain.go (1)
User(5-10)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
AndDelete(204-208)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Benchmark
- GitHub Check: Test
- GitHub Check: E2E
- GitHub Check: Analyze (go)
🔇 Additional comments (6)
pkg/cache/memory.go (2)
9-56: LGTM! Well-documented cache implementation.The type definition and constructor are clean and well-documented. The use of
sync.RWMutexfor concurrent access is appropriate, and the zero-TTL semantic (no expiration) is clearly explained.
232-236: Based on my verification, I have confirmed that the review comment is accurate and raises a valid concern. Here are my findings:
Code Behavior Confirmed: Lines 232-236 (13-17 in the snippet) show exactly what the review describes - when
validUntil.IsZero()(non-expiring item),AndUpdateTTLconverts it to an expiring item by settingvalidUntil = time.Now().Add(*o.updateTTL).Documentation Gap: The
AndUpdateTTLdocumentation states it "adds the specified duration to the item's existing TTL" but does not explicitly address the edge case of items with no expiration.No Test Coverage: I searched all test files (
memory_test.go,memory_edge_test.go,memory_concurrency_test.go, etc.) and found no tests coveringAndUpdateTTLbehavior, particularly for non-expiring items.Semantic Ambiguity: The behavior could be surprising to users who might expect:
AndUpdateTTLto be a no-op on items with no expiration, or- The update to extend from an existing default TTL rather than starting fresh from now
Rewritten Review Comment:
Undocumented AndUpdateTTL behavior for non-expiring items may surprise users.
When
AndUpdateTTLis called on an item with no expiration (validUntil.IsZero()), this code converts it from never-expiring to expiring by settingvalidUntil = time.Now().Add(*o.updateTTL). The documentation does not explicitly describe this behavior, and there are no test cases covering it.Recommend:
- Update
AndUpdateTTLdocumentation to explicitly state the behavior for non-expiring items- Add test case demonstrating this behavior for clarity and regression prevention
- Verify this semantic is intentional (consider if
AndUpdateTTLshould be a no-op for non-expiring items instead)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (4)
7-9: Imports correctly reflect new permissions/users responsibilitiesThe added imports for
permissionsandusersare appropriate for the refactor toward user- and scope‑aware middlewares; no changes needed.
88-90: SetUser / GetUser value semantics look goodStoring
users.Userby value inLocalsand exposing it viaGetUserkeeps the middleware API simple and avoids sharing mutable pointers in context. Given the current small shape ofusers.User, this is a good trade‑off.
111-114: UserRequired behavior matches its documentation
UserRequiredsimply wrapsHasUserand returns401 Unauthorizedwhen no user is present, which is exactly what the comment describes. This is a clean, reusable guard to combine withWithUser.
49-56: Based on my verification of the codebase, I can now provide the rewritten review comment:
The Login method contract guarantees non-nil user when error is nil—no defensive check needed.
The
usersSvc.Loginmethod (internal/sms-gateway/users/service.go:59) explicitly returns(nil, error)on all error paths and(*User, nil)on success. Every branch—cache hit, GetByID success, and password validation—follows this pattern with no path returning(nil, nil). The existing error check at line 50 is sufficient; dereferencing user at line 55 is safe.
f2643ec to
10c5aa1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
pkg/cache/options.go (1)
38-49: Clarify: negative TTL causes immediate expiry, not "no expiry".The docstring states "A TTL of zero or negative means the item will not expire," but the implementation differs:
- Zero TTL →
time.Time{}(zero value, likely interpreted as no expiry)- Negative TTL →
time.Now()(immediate expiry)If this is intentional, update the documentation to clarify:
-// A TTL of zero or negative means the item will not expire. +// A TTL of zero means the item will not expire. +// A negative TTL means the item expires immediately.internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
66-86:NewCodemiddleware is missing thepermissions.SetScopes()call thatNewBasicimplements.Both
NewBasic(line 55) andNewCodeauthenticate users and store them viaSetUser, but onlyNewBasiccallspermissions.SetScopes(c, []string{permissions.ScopeAll}). TheNewCodemiddleware should set scopes identically since both represent full user authentication. Addpermissions.SetScopes(c, []string{permissions.ScopeAll})after line 82 inNewCodeto match the pattern used byNewBasicand ensure code-based auth users receive proper permissions.
♻️ Duplicate comments (11)
pkg/cache/redis.go (2)
43-48: Critical: HTTL returns an array, causing arithmetic failure.The
HTTLcommand withFIELDSparameter returns an array of TTL values (e.g.,{3600}), not a scalar. Line 47 attempts arithmetic on the array directly, which will fail at runtime.Apply this diff to extract the first element:
- local ttl = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field) + local ttlArr = redis.call('HTTL', KEYS[1], 'FIELDS', '1', field) + local ttl = ttlArr[1] if ttl < 0 then ttl = 0 end
78-91: Document Redis 7.4.0+ requirement.The implementation uses
HExpireAt,HTTL, andHExpirewith theFIELDSparameter, which require Redis 7.4.0 or later. This should be explicitly documented to prevent runtime errors with older Redis instances.Add the version requirement to the type documentation:
// RedisCache implements the Cache interface using Redis as the backend. // +// Requires Redis 7.4.0 or later for per-field hash TTL support. +// // This implementation stores all data in a Redis hash, with each cache itemapi/requests.http (1)
200-218: Verify scope name: "devices:write" may not exist.Based on past review comments, the scope
"devices:write"on line 211 does not match the defined permission constants indevices/permissions.go. The codebase reportedly defines only"devices:list"and"devices:delete", not"devices:write". If this is still accurate, using this scope will cause authorization failures.Run the following script to verify the currently defined device and webhook permission scopes:
#!/bin/bash # Description: Verify all defined permission scope constants echo "=== All Permission Scope Constants ===" rg -n 'Scope.*=.*".*:.*"' --type go echo "" echo "=== Devices Permissions ===" rg -n 'Scope' internal/sms-gateway/handlers/devices/permissions.go --type go 2>/dev/null || echo "File not found" echo "" echo "=== Webhooks Permissions ===" rg -n 'Scope' internal/sms-gateway/handlers/webhooks/permissions.go --type go 2>/dev/null || echo "File not found" echo "" echo "=== Settings Permissions ===" rg -n 'Scope' internal/sms-gateway/handlers/settings/permissions.go --type go 2>/dev/null || echo "File not found"If
"devices:write"is not defined, update line 211 to use the correct scope constant (likely"devices:delete"or whichever scope is appropriate for write operations).internal/sms-gateway/handlers/devices/3rdparty.go (1)
92-94: Fix route parameter syntax to include leading slash.The route parameter on line 94 uses
:idwithout a leading slash, which is inconsistent with Fiber routing conventions and other handlers in the codebase (e.g., webhooks handler uses/:id).Apply this diff:
- router.Delete(":id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove)) + router.Delete("/:id", permissions.RequireScope(ScopeDelete), userauth.WithUser(h.remove))internal/sms-gateway/users/service.go (2)
59-82: Security risk: plaintext passwords used as cache keys.The
Loginmethod passes plaintext passwords to the cache (lines 60 and 77). Depending on the cache implementation, this could expose passwords in logs, metrics, or external systems.Consider refactoring the cache to key only on username:
func (s *Service) Login(ctx context.Context, username, password string) (*User, error) { - cachedUser, err := s.cache.Get(ctx, username, password) + cachedUser, err := s.cache.Get(ctx, username) if err == nil { return cachedUser, nil } else if !errors.Is(err, cache.ErrKeyNotFound) { s.logger.Warn("failed to get user from cache", zap.String("username", username), zap.Error(err)) } user, err := s.users.GetByID(username) if err != nil { return nil, err } if compErr := crypto.CompareBCryptHash(user.PasswordHash, password); compErr != nil { return nil, fmt.Errorf("login failed: %w", compErr) } loggedInUser := newUser(user) - if setErr := s.cache.Set(ctx, username, password, *loggedInUser); setErr != nil { + if setErr := s.cache.Set(ctx, username, *loggedInUser); setErr != nil { s.logger.Error("failed to cache user", zap.String("username", username), zap.Error(setErr)) } return loggedInUser, nil }Note: This requires updating the cache interface to remove the password parameter.
84-100: Reliability issue: password change fails when cache is unavailable.
ChangePasswordreturns immediately ifcache.Deletefails (lines 90-92), preventing users from changing passwords when the cache is down. This is inconsistent withLogin, which treats cache failures as non-fatal.Make cache deletion best-effort:
func (s *Service) ChangePassword(ctx context.Context, username, currentPassword, newPassword string) error { _, err := s.Login(ctx, username, currentPassword) if err != nil { return err } - if delErr := s.cache.Delete(ctx, username, currentPassword); delErr != nil { - return delErr + if delErr := s.cache.Delete(ctx, username); delErr != nil { + s.logger.Warn("failed to delete login cache on password change", zap.String("username", username), zap.Error(delErr)) } passwordHash, err := crypto.MakeBCryptHash(newPassword) if err != nil { return fmt.Errorf("failed to hash password: %w", err) } return s.users.UpdatePassword(username, passwordHash) }internal/sms-gateway/handlers/messages/3rdparty.go (1)
327-331: Fix inconsistent route parameter syntax.Line 329 uses
:idwithout a leading slash, which is inconsistent with other handlers (e.g., webhooks uses/:id). Line 331 also lacks a leading slash forinbox/export.Apply this diff for consistency:
router.Get("", permissions.RequireScope(ScopeList), userauth.WithUser(h.list)) router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUser(h.post)) - router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage) + router.Get("/:id", permissions.RequireScope(ScopeRead), userauth.WithUser(h.get)).Name(route3rdPartyGetMessage) - router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport)) + router.Post("/inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUser(h.postInboxExport))internal/sms-gateway/handlers/thirdparty/auth.go (2)
97-105: Validatejtiparameter is non-empty.Line 98 extracts
jtifrom the path but does not validate it. An emptyjtishould be rejected with a 400 Bad Request before callingRevokeToken.Apply this diff:
func (h *AuthHandler) deleteToken(user users.User, c *fiber.Ctx) error { jti := c.Params("jti") + if jti == "" { + return fiber.NewError(fiber.StatusBadRequest, "jti parameter is required") + } if err := h.jwtSvc.RevokeToken(c.Context(), user.ID, jti); err != nil { return fmt.Errorf("failed to revoke token: %w", err) }
60-82: Validate TTL bounds before conversion to prevent overflow.Line 70 converts
req.TTL(auint64in seconds) totime.Durationviatime.Duration(req.TTL) * time.Second. Sincetime.Durationis anint64, values abovemath.MaxInt64will silently overflow, producing negative or incorrect durations. The//nolint:goseccomment says "validated in the service," but the overflow occurs before the service receives the value.Add validation in the handler to ensure
req.TTLis within safe bounds before the conversion.Apply this diff:
func (h *AuthHandler) postToken(user users.User, c *fiber.Ctx) error { req := new(smsgateway.TokenRequest) if err := h.BodyParserValidator(c, req); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } + // Prevent overflow: max int64 seconds is ~292 years + const maxTTLSeconds = uint64(1<<63 - 1) / uint64(time.Second) + if req.TTL > maxTTLSeconds { + return fiber.NewError(fiber.StatusBadRequest, "ttl exceeds maximum allowed value") + } + token, err := h.jwtSvc.GenerateToken( c.Context(), user.ID, req.Scopes, - time.Duration(req.TTL)*time.Second, //nolint:gosec // validated in the service + time.Duration(req.TTL)*time.Second, )internal/sms-gateway/users/repository.go (1)
59-67: Check RowsAffected to detect non-existent users.GORM's
Updatedoes not return an error when zero rows are affected. If the user ID doesn't exist, this method silently succeeds without updating anything, which is inconsistent withGetByIDreturningErrNotFound.Apply this diff:
func (r *repository) UpdatePassword(id string, passwordHash string) error { - if err := r.db.Model((*userModel)(nil)). + result := r.db.Model((*userModel)(nil)). Where("id = ?", id). - Update("password_hash", passwordHash).Error; err != nil { + Update("password_hash", passwordHash) + if err := result.Error; err != nil { return fmt.Errorf("can't update password: %w", err) } + if result.RowsAffected == 0 { + return ErrNotFound + } return nil }internal/sms-gateway/handlers/mobile.go (1)
43-75: Handle the error fromnanoid.Standard.Line 57 ignores the error from
nanoid.Standard(21). If initialization fails and returns a nil function, callingh.idGen()at line 134 will cause a nil pointer panic.Apply this diff:
func newMobileHandler( authSvc *auth.Service, usersSvc *users.Service, devicesSvc *devices.Service, messagesCtrl *messages.MobileController, webhooksCtrl *webhooks.MobileController, settingsCtrl *settings.MobileController, eventsCtrl *events.MobileController, logger *zap.Logger, validator *validator.Validate, ) *mobileHandler { const idLength = 21 - idGen, _ := nanoid.Standard(idLength) + idGen, err := nanoid.Standard(idLength) + if err != nil { + panic(fmt.Sprintf("failed to initialize ID generator: %v", err)) + } return &mobileHandler{
🧹 Nitpick comments (6)
deployments/prometheus/alerts/jwt-alerts.yml (2)
24-32: Consider raising the threshold for revocation failures.The alert condition
> 0will fire on any non-zero error rate, which may be too sensitive over a 5m evaluation window. Occasional transient errors during token revocation operations could trigger frequent critical alerts and cause alert fatigue. Consider using a small positive threshold (e.g.,> 0.01errors/s) to distinguish between occasional glitches and sustained failures.
5-5: Inconsistent division-by-zero protection approach.Line 5 uses
clamp_min(rate(...), 1e-9)while line 35 usesmax(rate(...), 0.00001)for division-by-zero protection. Both are functionally valid (though with slightly different epsilon values: 1e-9 vs 1e-5), but unifying the approach would improve consistency and maintainability.Apply this diff to unify the approach:
- expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / max(rate(sms_auth_jwt_tokens_validated_total[5m]), 0.00001) > 0.1 + expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / clamp_min(rate(sms_auth_jwt_tokens_validated_total[5m]), 1e-9) > 0.1Also applies to: 35-35
internal/sms-gateway/handlers/base/handler.go (2)
20-42: Error wrapping in parser helpers may change HTTP status behaviorSwitching
BodyParserValidator,QueryParserValidator, andParamsParserValidatorto returnfmt.Errorf("failed to parse ...: %w", err)instead of (presumably) returning*fiber.Errordirectly changes the error type seen by Fiber and any central error handler. If callers still justreturn h.BodyParserValidator(...), parse failures might now be treated as generic 500s unless your error mapping useserrors.Asto unwrap and inspect underlying*fiber.Erroror other typed causes.If your intent is to (a) keep using 4xx for client parse errors and (b) gain better cause preservation, consider either:
- translating these wrapped errors back into explicit HTTP-aware errors at the handler boundary, or
- updating your global error handler to use
errors.Asonerrto recover*fiber.Error/ validation error types and map them to the desired status codes.Please double‑check the end‑to‑end HTTP status behavior for bad JSON / query / param inputs after this change.
44-57: Validation error wrapping is good; consider a dedicatedValidationErrortypeWrapping both
h.ValidatorandValidatable.Validate()errors withfmt.Errorf("failed to validate: %w", err)is a nice improvement for preserving root causes while adding context. However, downstream code now only has the generic"failed to validate"surface unless it explicitly useserrors.Is/errors.Asagainst the wrapped cause.If you expect many callers or a central error handler to special‑case validation failures (e.g., always map them to 422 with structured details), it might be cleaner to introduce a small typed error such as:
type ValidationError struct { Err error } func (e *ValidationError) Error() string { return "validation failed: " + e.Err.Error() } func (e *ValidationError) Unwrap() error { return e.Err }and return
&ValidationError{Err: err}from here. That keeps the error chain intact and gives downstream code a stable type to match on, instead of relying on prefix strings.internal/sms-gateway/jwt/repository.go (2)
38-47: Consider distinguishing between non-existent and non-revoked tokens.
IsRevokedreturnsfalsefor both non-existent tokens and tokens that exist but aren't revoked. Depending on security requirements, you may want to treat unknown tokens as invalid/revoked.If stricter validation is desired:
func (r *Repository) IsRevoked(ctx context.Context, jti string) (bool, error) { - var count int64 - if err := r.db.WithContext(ctx).Model((*tokenModel)(nil)). - Where("id = ? and revoked_at is not null", jti). - Count(&count).Error; err != nil { - return false, fmt.Errorf("can't check if token is revoked: %w", err) + var token tokenModel + if err := r.db.WithContext(ctx).Where("id = ?", jti).First(&token).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return true, nil // Treat unknown tokens as revoked + } + return false, fmt.Errorf("can't check if token is revoked: %w", err) } - - return count > 0, nil + return token.RevokedAt != nil, nil }
28-36: Consider checking affected rows to detect no-op revocations.The
Revokemethod silently succeeds when the token doesn't exist or is already revoked. This may be intentional for idempotency, but if you need to distinguish these cases for logging or metrics, checkRowsAffected.func (r *Repository) Revoke(ctx context.Context, jti, userID string) error { - if err := r.db.WithContext(ctx).Model((*tokenModel)(nil)). + result := r.db.WithContext(ctx).Model((*tokenModel)(nil)). Where("id = ? and user_id = ? and revoked_at is null", jti, userID). - Update("revoked_at", gorm.Expr("NOW()")).Error; err != nil { + Update("revoked_at", gorm.Expr("NOW()")) + if result.Error != nil { - return fmt.Errorf("can't revoke token: %w", err) + return fmt.Errorf("can't revoke token: %w", result.Error) } + // Optionally log or return error if result.RowsAffected == 0 return nil }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (66)
.golangci.yml(2 hunks)api/mobile.http(1 hunks)api/requests.http(5 hunks)cmd/sms-gateway/main.go(1 hunks)configs/config.example.yml(1 hunks)deployments/grafana/dashboards/jwt.json(1 hunks)deployments/prometheus/alerts/jwt-alerts.yml(1 hunks)go.mod(2 hunks)internal/config/config.go(4 hunks)internal/config/module.go(2 hunks)internal/config/types.go(1 hunks)internal/sms-gateway/app.go(4 hunks)internal/sms-gateway/handlers/3rdparty.go(2 hunks)internal/sms-gateway/handlers/base/handler.go(2 hunks)internal/sms-gateway/handlers/base/handler_test.go(9 hunks)internal/sms-gateway/handlers/devices/3rdparty.go(6 hunks)internal/sms-gateway/handlers/devices/permissions.go(1 hunks)internal/sms-gateway/handlers/logs/3rdparty.go(3 hunks)internal/sms-gateway/handlers/logs/permissions.go(1 hunks)internal/sms-gateway/handlers/messages/3rdparty.go(8 hunks)internal/sms-gateway/handlers/messages/permissions.go(1 hunks)internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go(1 hunks)internal/sms-gateway/handlers/middlewares/permissions/permissions.go(1 hunks)internal/sms-gateway/handlers/middlewares/userauth/userauth.go(4 hunks)internal/sms-gateway/handlers/mobile.go(8 hunks)internal/sms-gateway/handlers/module.go(2 hunks)internal/sms-gateway/handlers/settings/3rdparty.go(8 hunks)internal/sms-gateway/handlers/settings/permissions.go(1 hunks)internal/sms-gateway/handlers/thirdparty/auth.go(1 hunks)internal/sms-gateway/handlers/thirdparty/module.go(1 hunks)internal/sms-gateway/handlers/thirdparty/permissions.go(1 hunks)internal/sms-gateway/handlers/webhooks/3rdparty.go(5 hunks)internal/sms-gateway/handlers/webhooks/permissions.go(1 hunks)internal/sms-gateway/jwt/config.go(1 hunks)internal/sms-gateway/jwt/disabled.go(1 hunks)internal/sms-gateway/jwt/errors.go(1 hunks)internal/sms-gateway/jwt/jwt.go(1 hunks)internal/sms-gateway/jwt/metrics.go(1 hunks)internal/sms-gateway/jwt/models.go(1 hunks)internal/sms-gateway/jwt/module.go(1 hunks)internal/sms-gateway/jwt/repository.go(1 hunks)internal/sms-gateway/jwt/service.go(1 hunks)internal/sms-gateway/models/migration.go(1 hunks)internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql(1 hunks)internal/sms-gateway/models/models.go(0 hunks)internal/sms-gateway/modules/auth/cache.go(0 hunks)internal/sms-gateway/modules/auth/module.go(0 hunks)internal/sms-gateway/modules/auth/repository.go(0 hunks)internal/sms-gateway/modules/auth/service.go(4 hunks)internal/sms-gateway/modules/messages/service.go(3 hunks)internal/sms-gateway/modules/settings/models.go(2 hunks)internal/sms-gateway/modules/webhooks/models.go(2 hunks)internal/sms-gateway/openapi/docs.go(25 hunks)internal/sms-gateway/users/cache.go(1 hunks)internal/sms-gateway/users/domain.go(1 hunks)internal/sms-gateway/users/errors.go(1 hunks)internal/sms-gateway/users/models.go(1 hunks)internal/sms-gateway/users/module.go(1 hunks)internal/sms-gateway/users/repository.go(1 hunks)internal/sms-gateway/users/service.go(1 hunks)pkg/cache/cache.go(1 hunks)pkg/cache/errors.go(1 hunks)pkg/cache/memory.go(14 hunks)pkg/cache/options.go(4 hunks)pkg/cache/redis.go(10 hunks)pkg/cache/typed.go(1 hunks)
💤 Files with no reviewable changes (4)
- internal/sms-gateway/models/models.go
- internal/sms-gateway/modules/auth/repository.go
- internal/sms-gateway/modules/auth/cache.go
- internal/sms-gateway/modules/auth/module.go
✅ Files skipped from review due to trivial changes (1)
- pkg/cache/cache.go
🚧 Files skipped from review as they are similar to previous changes (21)
- internal/sms-gateway/jwt/module.go
- cmd/sms-gateway/main.go
- internal/sms-gateway/handlers/settings/permissions.go
- internal/sms-gateway/handlers/thirdparty/module.go
- go.mod
- pkg/cache/errors.go
- internal/sms-gateway/users/module.go
- internal/config/types.go
- internal/sms-gateway/handlers/devices/permissions.go
- internal/sms-gateway/handlers/webhooks/permissions.go
- .golangci.yml
- internal/config/module.go
- internal/sms-gateway/modules/settings/models.go
- internal/sms-gateway/jwt/models.go
- internal/sms-gateway/modules/messages/service.go
- internal/sms-gateway/models/migrations/mysql/20251121071748_add_tokens.sql
- internal/sms-gateway/jwt/errors.go
- internal/sms-gateway/models/migration.go
- internal/sms-gateway/handlers/base/handler_test.go
- internal/sms-gateway/users/models.go
- deployments/grafana/dashboards/jwt.json
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.
Applied to files:
pkg/cache/redis.gopkg/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.gopkg/cache/memory.go
🧬 Code graph analysis (21)
internal/sms-gateway/handlers/module.go (1)
internal/sms-gateway/handlers/thirdparty/module.go (1)
Module(8-16)
internal/sms-gateway/jwt/disabled.go (2)
internal/sms-gateway/jwt/jwt.go (3)
Service(10-14)TokenInfo(23-27)Claims(16-21)internal/sms-gateway/jwt/errors.go (1)
ErrDisabled(6-6)
internal/sms-gateway/handlers/settings/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/settings/permissions.go (2)
ScopeRead(4-4)ScopeWrite(5-5)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(132-141)
internal/sms-gateway/jwt/service.go (5)
internal/sms-gateway/jwt/config.go (1)
Config(12-16)internal/sms-gateway/jwt/repository.go (1)
Repository(10-12)internal/sms-gateway/jwt/metrics.go (3)
Metrics(24-31)StatusError(20-20)StatusSuccess(19-19)internal/sms-gateway/jwt/jwt.go (3)
Service(10-14)TokenInfo(23-27)Claims(16-21)internal/sms-gateway/jwt/errors.go (4)
ErrInitFailed(7-7)ErrInvalidParams(9-9)ErrInvalidToken(10-10)ErrTokenRevoked(11-11)
internal/sms-gateway/jwt/config.go (1)
internal/sms-gateway/jwt/errors.go (1)
ErrInvalidConfig(8-8)
internal/config/config.go (2)
internal/config/types.go (1)
Duration(11-11)internal/worker/config/types.go (1)
Duration(11-11)
internal/sms-gateway/handlers/logs/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/logs/permissions.go (1)
ScopeRead(4-4)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(132-141)
internal/sms-gateway/handlers/3rdparty.go (4)
internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/handlers/thirdparty/auth.go (1)
AuthHandler(19-23)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
NewBasic(21-59)UserRequired(116-124)internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
NewJWT(14-42)
internal/sms-gateway/handlers/thirdparty/auth.go (8)
internal/sms-gateway/handlers/base/handler.go (1)
Handler(15-18)internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/thirdparty/permissions.go (1)
ScopeTokensManage(4-4)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(132-141)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/jwt/errors.go (3)
ErrInvalidParams(9-9)ErrInitFailed(7-7)ErrDisabled(6-6)
internal/sms-gateway/handlers/devices/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/devices/permissions.go (2)
ScopeList(4-4)ScopeDelete(5-5)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(132-141)
internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (2)
SetScopes(15-17)ScopeAll(10-10)internal/sms-gateway/users/domain.go (1)
User(5-10)
internal/sms-gateway/users/cache.go (5)
pkg/cache/typed.go (2)
Typed(89-91)NewTyped(118-122)internal/config/config.go (1)
Cache(83-85)internal/sms-gateway/users/domain.go (1)
User(5-10)pkg/cache/options.go (2)
AndSetTTL(122-126)WithTTL(38-49)pkg/cache/errors.go (2)
ErrKeyNotFound(19-19)ErrKeyExpired(36-36)
internal/sms-gateway/modules/auth/service.go (2)
internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/users/domain.go (1)
User(5-10)
internal/sms-gateway/modules/webhooks/models.go (1)
internal/sms-gateway/users/domain.go (1)
User(5-10)
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (6)
internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/modules/messages/service.go (1)
Service(27-39)internal/sms-gateway/handlers/base/handler.go (1)
Handler(15-18)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
SetUser(88-90)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
SetScopes(15-17)
internal/sms-gateway/users/service.go (5)
internal/sms-gateway/modules/messages/service.go (2)
NewService(41-64)Service(27-39)internal/sms-gateway/modules/auth/service.go (1)
Service(23-33)internal/sms-gateway/users/domain.go (1)
User(5-10)pkg/crypto/passwords.go (2)
MakeBCryptHash(14-20)CompareBCryptHash(22-27)pkg/cache/errors.go (1)
ErrKeyNotFound(19-19)
pkg/cache/redis.go (1)
pkg/cache/options.go (1)
AndDelete(204-208)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
AndDelete(204-208)
internal/sms-gateway/handlers/mobile.go (4)
internal/sms-gateway/handlers/base/handler.go (1)
Handler(15-18)internal/sms-gateway/users/service.go (1)
Service(13-19)internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (2)
GetUser(103-110)NewBasic(21-59)
internal/sms-gateway/users/repository.go (2)
internal/sms-gateway/users/errors.go (2)
ErrNotFound(6-6)ErrExists(7-7)pkg/mysql/errors.go (1)
IsDuplicateKeyViolation(13-19)
internal/sms-gateway/handlers/messages/3rdparty.go (4)
internal/sms-gateway/users/domain.go (1)
User(5-10)internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
RequireScope(28-36)internal/sms-gateway/handlers/messages/permissions.go (4)
ScopeList(10-10)ScopeSend(6-6)ScopeRead(8-8)ScopeExport(12-12)internal/sms-gateway/handlers/middlewares/userauth/userauth.go (1)
WithUser(132-141)
🪛 Gitleaks (8.29.1)
api/mobile.http
[high] 71-71: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
[high] 72-72: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: E2E
- GitHub Check: Test
- GitHub Check: Benchmark
- GitHub Check: Analyze (go)
🔇 Additional comments (53)
deployments/prometheus/alerts/jwt-alerts.yml (2)
4-12: Good division-by-zero protection and label usage.The alert correctly uses the
status="error"label and protects the division withclamp_min(...)to prevent NaN results. The 5% error rate threshold is reasonable for a warning-level alert.
14-22: Latency alert well-configured.Using p99 quantile to capture tail latency is a good observability practice. The 0.5s threshold for JWT issuance seems reasonable, and the 10m evaluation window allows for transient latency spikes to stabilize before triggering.
configs/config.example.yml (1)
41-44: Good security practice: empty Secret disables JWT.The configuration correctly documents that leaving the secret empty disables JWT functionality, which prevents accidental deployment with insecure tokens. This aligns with the disabled service implementation in the codebase.
internal/sms-gateway/handlers/logs/3rdparty.go (3)
5-7: LGTM! Import updates align with JWT integration.The new imports for permissions middleware and users domain are correctly integrated to support scope-based authorization.
37-49: LGTM! Handler correctly updated for JWT authentication.The JWTAuth security annotation and users.User parameter type properly reflect the new authentication mechanism.
57-57: LGTM! Permission enforcement properly integrated.The middleware chain correctly enforces the
logs:readscope before passing the authenticated user to the handler.internal/sms-gateway/users/errors.go (1)
1-8: LGTM! Clean sentinel error definitions.The error constants follow Go idioms and provide clear error semantics for user operations.
internal/sms-gateway/modules/webhooks/models.go (1)
8-24: LGTM! Consistent migration to users domain.The User field type update maintains existing foreign key constraints while adopting the new users package.
internal/sms-gateway/app.go (1)
10-60: LGTM! JWT and users modules properly wired.The fx module initialization correctly integrates the new users and JWT subsystems into the application dependency graph.
internal/sms-gateway/handlers/messages/permissions.go (1)
1-13: LGTM! Well-defined permission scopes.The scope constants are clearly documented and follow a consistent naming convention for message-related operations.
internal/sms-gateway/handlers/module.go (1)
9-41: LGTM! Thirdparty module correctly wired.The module registration follows the established fx pattern and properly integrates the thirdparty handlers.
internal/sms-gateway/handlers/thirdparty/permissions.go (1)
1-5: LGTM! Token management scope defined.The scope constant follows the established pattern and will be used to protect token-related endpoints.
api/requests.http (2)
4-4: LGTM! JWT Bearer authentication examples added.The jwtToken environment variable and Bearer auth examples properly demonstrate the new JWT authentication mechanism alongside existing Basic authentication.
Also applies to: 38-39, 83-84, 88-89, 108-109
220-224: LGTM! Token revocation endpoint example.The DELETE endpoint demonstrates the token revocation flow with proper authentication.
internal/sms-gateway/handlers/middlewares/jwtauth/jwtauth.go (1)
14-42: LGTM! Well-structured JWT authentication middleware.The middleware correctly:
- Validates the Authorization header format with proper length checks
- Uses case-insensitive comparison for the Bearer prefix
- Handles parse errors and missing users with appropriate HTTP status codes
- Integrates cleanly with the userauth and permissions middleware
The optional nature of the middleware (delegating to next handler when no Bearer token is present) allows handlers to explicitly enforce authentication via other middleware.
internal/sms-gateway/handlers/middlewares/permissions/permissions.go (1)
9-36: LGTM! Clean scope-based permissions implementation.The middleware provides a straightforward and effective way to enforce scope-based access control:
- Clear separation between setting and checking scopes
- The
ScopeAllconstant provides a convenient wildcard mechanism- Proper use of
slices.ContainsFuncfor flexible scope matching- Appropriate 403 Forbidden response with descriptive error message
internal/sms-gateway/jwt/jwt.go (1)
10-27: LGTM! Clean JWT service interface design.The interface and types are well-structured:
- Clear method signatures for token lifecycle operations
- Claims struct properly extends standard JWT claims with custom fields
- TokenInfo provides a complete representation of generated tokens
- Good separation of concerns between service operations
internal/sms-gateway/handlers/settings/3rdparty.go (1)
138-140: LGTM! Proper integration of permissions middleware.The route registration correctly:
- Uses empty string paths for group-based routing
- Applies appropriate scope requirements (ScopeRead for GET, ScopeWrite for PUT/PATCH)
- Wraps handlers with
userauth.WithUserfor user contextinternal/sms-gateway/jwt/config.go (1)
18-31: LGTM! Comprehensive configuration validation.The
Validate()method properly checks:
- Secret presence and minimum length (32 bytes)
- TTL is positive (rejects zero and negative values)
- Clear error messages wrapping
ErrInvalidConfiginternal/sms-gateway/openapi/docs.go (3)
21-137: LGTM! JWT authentication endpoints properly documented.The generated OpenAPI spec correctly includes:
- Security requirements (ApiAuth and JWTAuth) for both token endpoints
- Complete parameter definition for the
jtipath parameter in the DELETE endpoint- Proper request/response schemas for token operations
- Appropriate error responses (400, 401, 403, 500)
1796-1836: LGTM! Token request/response schemas properly defined.The
TokenRequestandTokenResponsedefinitions are well-structured:
TokenRequestrequires scopes array and has optional ttl (in seconds)TokenResponseincludes all necessary fields (id, access_token, expires_at, token_type)- Proper field descriptions and validation constraints
1916-1921: LGTM! JWTAuth security definition correctly added.The security definition properly configures JWT authentication as an API key in the Authorization header, consistent with standard Bearer token authentication patterns.
internal/sms-gateway/handlers/webhooks/3rdparty.go (2)
43-63: LGTM! JWT security and permission scope integration looks correct.The OpenAPI annotations properly document both
ApiAuthandJWTAuthsecurity methods, and the 403 Forbidden response is correctly added. The handler signature update tousers.Useraligns with the new users domain model.
123-127: LGTM! Route registration with permission scopes is correctly implemented.The permission middleware is properly chained before
userauth.WithUser, ensuring scope validation occurs after authentication but before the handler receives the user. The scope constants (ScopeList,ScopeWrite,ScopeDelete) are appropriately mapped to their respective endpoints.internal/sms-gateway/jwt/repository.go (1)
20-26: LGTM! Insert implementation is correct.Error wrapping is appropriate, and context is properly passed to GORM.
internal/sms-gateway/handlers/messages/3rdparty.go (1)
56-173: LGTM! Handler signature updates and JWT annotations are correct.The migration from
models.Usertousers.Useris consistent, and OpenAPI documentation properly includes bothApiAuthandJWTAuthsecurity with 403 Forbidden responses for scope-protected endpoints.pkg/cache/options.go (2)
75-105: LGTM! GetOption infrastructure is well-designed.The functional options pattern is correctly applied with clear separation between Set and Get behaviors. The
isEmpty()helper enables efficient short-circuit checks.
191-208: LGTM! AndDelete provides clean atomic get-and-delete semantics.Good documentation noting the equivalence to
GetAndDeletefor discoverability.internal/sms-gateway/handlers/middlewares/userauth/userauth.go (3)
88-110: LGTM! SetUser/GetUser pattern is correctly implemented.The consistent value storage and pointer retrieval pattern ensures type assertions work correctly across all middleware paths. This addresses the previous review concern about pointer vs value mismatches.
126-141: LGTM! WithUser now properly handles missing user with 401 response.The nil check with
fiber.ErrUnauthorizedprovides a clear error instead of the previous potential panic, addressing the feedback from the prior review.
49-58: LGTM! NewBasic authentication flow is correct.The middleware properly authenticates via
usersSvc.Login, stores the user viaSetUser, and grants full permissions withScopeAll. The early return on missing/invalid Authorization header enables middleware chaining.internal/sms-gateway/handlers/3rdparty.go (2)
35-67: LGTM!The constructor properly initializes all dependencies, and the new fields for
usersSvc,jwtSvc, andauthHandlerare correctly wired.
69-93: LGTM!Authentication middleware is correctly configured with both Basic auth (line 75) and JWT auth (line 76), and the auth handler routes are properly registered at line 80.
pkg/cache/typed.go (1)
1-407: LGTM!The typed cache implementation properly handles pointer types via the
newItemhelper (lines 392-407), which creates non-nil instances before unmarshalling. This addresses the previous pointer unmarshalling issue. The comprehensive documentation and consistent error handling throughout are excellent.internal/sms-gateway/modules/auth/service.go (3)
35-53: LGTM!The constructor properly injects the
usersSvcdependency and initializes all fields correctly.
85-96: LGTM!The method signature correctly uses the
users.Userdomain type, aligning with the new users module integration.
133-145: LGTM!The method correctly returns
*users.Userand delegates tousersSvc.GetByID, properly integrating with the new users service.internal/sms-gateway/users/repository.go (3)
22-31: LGTM!The
Existsmethod correctly uses a COUNT query and proper error handling.
33-45: LGTM!The
GetByIDmethod correctly passesuser(not&user) toTakeand properly mapsgorm.ErrRecordNotFoundtoErrNotFound.
47-57: LGTM!The
Insertmethod properly detects duplicate key violations and maps them toErrExists.internal/sms-gateway/jwt/service.go (4)
24-51: LGTM!The constructor properly validates all inputs (config, tokens repository, metrics) and initializes the nanoid factory with appropriate error handling.
53-117: LGTM!Token generation correctly validates inputs (userID non-empty at line 63, scopes non-empty at line 68, TTL non-negative at line 73), enforces maximum TTL via
min()at line 89, and properly integrates with metrics.
119-166: LGTM!Token parsing correctly validates JWT claims with required options (expiration, issued-at, issuer, signing method) and checks revocation status before returning claims.
168-182: LGTM!Token revocation properly delegates to the repository and records metrics.
internal/sms-gateway/jwt/metrics.go (1)
1-117: LGTM!The metrics implementation properly defines counters with status labels and histograms with appropriate latency buckets. The observe methods correctly use timers to record durations.
internal/sms-gateway/handlers/mobile.go (3)
116-156: LGTM!The device registration flow correctly integrates with the new users service, creating users via
usersSvc.Create(line 138) for unauthenticated requests and using authenticated users when available.
201-212: LGTM!The method signature correctly uses the
users.Userdomain type.
228-241: LGTM!Password change correctly delegates to
usersSvc.ChangePasswordwith proper error handling.pkg/cache/memory.go (5)
207-245: LGTM! Well-designed Get method with proper concurrency control.The implementation correctly:
- Uses RLock for the fast path (no options) to allow concurrent reads
- Upgrades to Lock when modifications are needed (delete or TTL updates)
- Handles the AndDelete option by removing items even if expired, then returning ErrKeyExpired through the getItem check - this provides auto-cleanup
- Implements updateTTL semantics correctly: for items without expiration, it sets an expiration from now; for items with expiration, it extends the expiration time by the specified duration
161-175: LGTM! Drain correctly leverages cleanup for atomic operation.The implementation correctly uses the cleanup helper to atomically remove expired items and execute the callback under a write lock. Based on learnings, expired items are removed before the callback executes, so the returned map contains only non-expired items without needing additional filtering.
321-333: LGTM! SetOrFail provides proper atomic check-and-set.The implementation correctly:
- Holds the write lock for the entire operation to prevent race conditions
- Treats expired items as non-existent (allows overwrite)
- Returns ErrKeyExists only for non-expired items
- Provides the expected semantics for conditional writes
413-425: LGTM! Cleanup helper correctly implements atomic cleanup with callback.The implementation correctly captures the timestamp once and uses it consistently for all expiration checks, ensuring a consistent view during cleanup. The callback is executed under the write lock after expired items are removed, which matches the documented behavior in the learnings and enables atomic compound operations.
132-138: LGTM! All remaining methods are correctly implemented.The helper methods and remaining public methods all follow proper patterns:
- Appropriate locking for thread safety
- Correct delegation and composition (e.g., GetAndDelete delegates to Get)
- Proper TTL precedence in newItem (default first, then options override)
- Consistent error handling in getItem/getValue
- Appropriate no-op Close for memory-based implementation
Also applies to: 265-267, 290-296, 346-356, 370-403, 439-441
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (1)
deployments/prometheus/alerts/jwt-alerts.yml (1)
24-32: Reconsider alert sensitivity: triggering on any revocation error may cause alert fatigue.The alert on line 25 triggers whenever
rate(sms_auth_jwt_tokens_revoked_total{status="error"}[5m]) > 0, meaning any single revocation error in the 5-minute window fires a critical alert. For operational stability, consider setting a threshold that allows minor transient errors (e.g.,> 0.001or> 0.01per second) before triggering, since occasional failures are typically tolerable and expected in distributed systems.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
deployments/prometheus/alerts/jwt-alerts.yml(1 hunks)pkg/cache/memory.go(14 hunks)pkg/cache/options.go(4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-10T11:11:43.462Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/memory.go:59-74
Timestamp: 2025-09-10T11:11:43.462Z
Learning: In pkg/cache/memory.go, the cleanup method removes expired items first, then executes the provided callback function. This means methods like Drain that use cleanup don't need additional expiration filtering since expired items are already removed before the callback runs.
Applied to files:
pkg/cache/memory.go
📚 Learning: 2025-09-10T23:53:42.006Z
Learnt from: capcom6
Repo: android-sms-gateway/server PR: 178
File: pkg/cache/redis.go:63-75
Timestamp: 2025-09-10T23:53:42.006Z
Learning: In pkg/cache/redis.go, the Redis cache implementation uses Redis's native field-level TTL via HExpireAt, meaning expired fields are automatically removed by Redis itself. This differs from the memory implementation where the application tracks expiration times. As a result, operations like Drain that call HGetAll will only return non-expired items since expired fields are already removed by Redis.
Applied to files:
pkg/cache/memory.go
🧬 Code graph analysis (1)
pkg/cache/memory.go (1)
pkg/cache/options.go (1)
AndDelete(205-209)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Benchmark
- GitHub Check: Test
- GitHub Check: E2E
- GitHub Check: Analyze (go)
🔇 Additional comments (5)
deployments/prometheus/alerts/jwt-alerts.yml (1)
1-42: Approve: Division-by-zero protection applied and alerts align with metrics.The alert expressions on lines 5 and 35 both correctly use division-by-zero protection (
clamp_minandmaxrespectively), preventing NaN results when rates drop to zero during low-traffic periods. The metric selectors (status="error") match the available labels in the metrics infrastructure.pkg/cache/options.go (1)
1-209: Well-structured options API with clear semantics.The use of pointer types in
getOptionsforvalidUntil,setTTL, andupdateTTLcorrectly distinguishes between "not set" and "set to zero value". TheisEmpty()helper enables efficient short-circuiting in cache implementations. Documentation is thorough with useful examples.pkg/cache/memory.go (3)
161-175: LGTM!The
Drainimplementation correctly leverages thecleanupcallback pattern. Based on learnings, the cleanup method removes expired items first before executing the callback, so no additional expiration filtering is needed when copying items to the result map.
207-245: Good optimization: read lock for simple gets, write lock only when options require mutation.The bifurcation between
RLockfor option-free reads andLockwhen options may modify state is a sensible optimization for read-heavy workloads.
232-236: Reasonable fallback forupdateTTLon never-expiring items.When an item has no expiration (
validUntil.IsZero()), treatingupdateTTLas "set TTL from now" is a sensible design choice rather than silently doing nothing or erroring.
| - name: jwt-alerts | ||
| rules: | ||
| - alert: JWT_Validation_ErrorRate_High | ||
| expr: rate(sms_auth_jwt_tokens_validated_total{status="error"}[5m]) / clamp_min(rate(sms_auth_jwt_tokens_validated_total[5m]), 1e-9) > 0.1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix annotation threshold mismatch: expression uses 10%, annotation claims 5%.
Line 5 sets the threshold to > 0.1 (10%), but the annotation on line 12 states "threshold: 5%". This discrepancy could confuse operators and make runbooks inaccurate.
Decide which threshold is intended and update accordingly:
- If 10% is correct, update the annotation to
"description: "JWT validation error rate is {{ $value | humanizePercentage }} (threshold: 10%)" - If 5% is correct, update the expression to
> 0.05
Also applies to: 12-12
🤖 Prompt for AI Agents
In deployments/prometheus/alerts/jwt-alerts.yml around lines 5 and 12, the
Prometheus expression uses a 10% threshold ("> 0.1") while the annotation text
claims a 5% threshold, causing a mismatch; choose the intended threshold and
make them consistent — either change the annotation on line 12 to read
"description: \"JWT validation error rate is {{ $value | humanizePercentage }}
(threshold: 10%)\"" if 10% is correct, or change the expression on line 5 to use
"> 0.05" if 5% is correct, ensuring both expression and annotation match
exactly.
| service: jwt | ||
| annotations: | ||
| summary: "JWT revocation failures detected" | ||
| description: "JWT revocation errors occurring at rate {{ $value }}/s" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarify alert description: does the metric expose reason labels?
The description states "database errors," but the query filters only on status="error" without a reason label. If the underlying metric does not distinguish database errors from other failure types, the description is misleading. Either:
- Update the description to accurately reflect "JWT revocation failures" (all error types), or
- If database-specific errors should be monitored separately, add a
reasonlabel to the metric ininternal/sms-gateway/jwt/metrics.goand update the alert query to filter byreason="database_error".
| case o.defaultTTL: | ||
| item.validUntil = time.Now().Add(m.ttl) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: AndDefaultTTL() causes immediate expiration when cache has no default TTL.
When m.ttl is 0 (no expiration), this sets validUntil = time.Now(), causing the item to expire immediately. This contradicts the documented behavior that "A TTL of zero means items do not expire by default." The newItem method (lines 350-352) handles this correctly by checking m.ttl > 0 first.
Apply this diff to align with newItem behavior:
case o.defaultTTL:
- item.validUntil = time.Now().Add(m.ttl)
+ if m.ttl > 0 {
+ item.validUntil = time.Now().Add(m.ttl)
+ } else {
+ item.validUntil = time.Time{}
+ }🤖 Prompt for AI Agents
In pkg/cache/memory.go around lines 237 to 238, the AndDefaultTTL() branch sets
item.validUntil = time.Now().Add(m.ttl) unconditionally, which makes items
expire immediately when m.ttl == 0; change this to mirror newItem by only
setting validUntil when m.ttl > 0 (i.e., if m.ttl > 0 then set validUntil =
time.Now().Add(m.ttl), otherwise leave item.validUntil zero/nil so items do not
expire).
Summary by CodeRabbit
New Features
Configuration
Security
Observability
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.