Skip to content
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

Add support for multiple devices for user #117

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
24 changes: 5 additions & 19 deletions internal/sms-gateway/handlers/3rdparty.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/devices"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/logs"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/messages"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/webhooks"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"go.uber.org/fx"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -46,24 +46,10 @@ func (h *thirdPartyHandler) Register(router fiber.Router) {

h.healthHandler.Register(router)

router.Use(basicauth.New(basicauth.Config{
Authorizer: func(username string, password string) bool {
return len(username) > 0 && len(password) > 0
},
}), func(c *fiber.Ctx) error {
username := c.Locals("username").(string)
password := c.Locals("password").(string)

user, err := h.authSvc.AuthorizeUser(username, password)
if err != nil {
h.Logger.Error("failed to authorize user", zap.Error(err))
return fiber.ErrUnauthorized
}

c.Locals("user", user)

return c.Next()
})
router.Use(
userauth.New(h.authSvc),
userauth.UserRequired(),
)

h.messagesHandler.Register(router.Group("/message")) // TODO: remove after 2025-12-31
h.messagesHandler.Register(router.Group("/messages"))
Expand Down
4 changes: 2 additions & 2 deletions internal/sms-gateway/handlers/devices/3rdparty.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (

"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices"
"github.com/android-sms-gateway/server/pkg/types"
"github.com/capcom6/go-helpers/slices"
Expand Down Expand Up @@ -62,7 +62,7 @@ func (h *ThirdPartyController) getDevices(user models.User, c *fiber.Ctx) error
}

func (h *ThirdPartyController) Register(router fiber.Router) {
router.Get("", auth.WithUser(h.getDevices))
router.Get("", userauth.WithUser(h.getDevices))
}

func NewThirdPartyController(params thirdPartyControllerParams) *ThirdPartyController {
Expand Down
4 changes: 2 additions & 2 deletions internal/sms-gateway/handlers/logs/3rdparty.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package logs

import (
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
Expand Down Expand Up @@ -40,7 +40,7 @@ func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error {
}

func (h *ThirdPartyController) Register(router fiber.Router) {
router.Get("", auth.WithUser(h.get))
router.Get("", userauth.WithUser(h.get))
}

func NewThirdPartyController(params thirdPartyControllerParams) *ThirdPartyController {
Expand Down
8 changes: 4 additions & 4 deletions internal/sms-gateway/handlers/messages/3rdparty.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (

"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages"
"github.com/go-playground/validator/v10"
Expand Down Expand Up @@ -163,10 +163,10 @@ func (h *ThirdPartyController) postInboxExport(user models.User, c *fiber.Ctx) e
}

func (h *ThirdPartyController) Register(router fiber.Router) {
router.Post("", auth.WithUser(h.post))
router.Get(":id", auth.WithUser(h.get))
router.Post("", userauth.WithUser(h.post))
router.Get(":id", userauth.WithUser(h.get))

router.Post("inbox/export", auth.WithUser(h.postInboxExport))
router.Post("inbox/export", userauth.WithUser(h.postInboxExport))
}

func NewThirdPartyController(params thirdPartyControllerParams) *ThirdPartyController {
Expand Down
100 changes: 100 additions & 0 deletions internal/sms-gateway/handlers/middlewares/userauth/userauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package userauth

import (
"encoding/base64"
"strings"

"github.com/android-sms-gateway/server/internal/sms-gateway/models"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
)

const LocalsUser = "user"

// New returns a middleware that will check if the request contains a valid
// "Authorization" header in the form of "Basic <base64 encoded username:password>".
// If the header is valid, the middleware will authorize the user and store the user
// in the request's Locals under the key LocalsUser. If the header is invalid, the
// middleware will call c.Next() and continue with the request.
func New(authSvc *auth.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
// Get authorization header
auth := c.Get(fiber.HeaderAuthorization)

// Check if the header contains content besides "basic".
if len(auth) <= 6 || !strings.EqualFold(auth[:6], "basic ") {
return c.Next()
}

// Decode the header contents
raw, err := base64.StdEncoding.DecodeString(auth[6:])
if err != nil {
return c.Next()
}

// Get the credentials
creds := utils.UnsafeString(raw)

// Check if the credentials are in the correct form
// which is "username:password".
index := strings.Index(creds, ":")
if index == -1 {
return c.Next()
}

// Get the username and password
username := creds[:index]
password := creds[index+1:]

user, err := authSvc.AuthorizeUser(username, password)
if err != nil {
return c.Next()
}

c.Locals(LocalsUser, user)

return c.Next()
}
}

// HasUser checks if a user is present in the Locals of the given context.
// It returns true if the Locals contain a user under the key LocalsUser,
// otherwise returns false.
func HasUser(c *fiber.Ctx) bool {
return c.Locals(LocalsUser) != nil
}

// GetUser returns the user stored in the Locals under the key LocalsUser.
// It is a convenience function that wraps the call to c.Locals(LocalsUser) and
// casts the result to models.User.
//
// It panics if the value stored in Locals is not a models.User.
func GetUser(c *fiber.Ctx) models.User {
return c.Locals(LocalsUser).(models.User)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for type assertion.

The type assertion in GetUser could panic. Consider adding error handling.

 func GetUser(c *fiber.Ctx) models.User {
-    return c.Locals(LocalsUser).(models.User)
+    user, ok := c.Locals(LocalsUser).(models.User)
+    if !ok {
+        panic("user not found in context or invalid type")
+    }
+    return user
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func GetUser(c *fiber.Ctx) models.User {
return c.Locals(LocalsUser).(models.User)
}
func GetUser(c *fiber.Ctx) models.User {
user, ok := c.Locals(LocalsUser).(models.User)
if !ok {
panic("user not found in context or invalid type")
}
return user
}


// UserRequired is a middleware that ensures a user is present in the request's Locals.
// If a user is not found, it returns an unauthorized error, otherwise it passes control
// to the next handler in the stack.
func UserRequired() fiber.Handler {
return func(c *fiber.Ctx) error {
if !HasUser(c) {
return fiber.ErrUnauthorized
}

return c.Next()
}
}

// 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.
func WithUser(handler func(models.User, *fiber.Ctx) error) fiber.Handler {
return func(c *fiber.Ctx) error {
return handler(GetUser(c), c)
}
}
Comment on lines +93 to +97
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling to prevent panics.

The WithUser decorator should handle potential panics from GetUser gracefully.

 func WithUser(handler func(models.User, *fiber.Ctx) error) fiber.Handler {
     return func(c *fiber.Ctx) error {
+        if !HasUser(c) {
+            return fiber.ErrUnauthorized
+        }
         return handler(GetUser(c), c)
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func WithUser(handler func(models.User, *fiber.Ctx) error) fiber.Handler {
return func(c *fiber.Ctx) error {
return handler(GetUser(c), c)
}
}
func WithUser(handler func(models.User, *fiber.Ctx) error) fiber.Handler {
return func(c *fiber.Ctx) error {
if !HasUser(c) {
return fiber.ErrUnauthorized
}
return handler(GetUser(c), c)
}
}

47 changes: 32 additions & 15 deletions internal/sms-gateway/handlers/mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/converters"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/webhooks"
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth"
Expand Down Expand Up @@ -65,33 +66,44 @@ func (h *mobileHandler) getDevice(device models.Device, c *fiber.Ctx) error {
// @Router /mobile/v1/device [post]
//
// Register device
func (h *mobileHandler) postDevice(c *fiber.Ctx) error {
func (h *mobileHandler) postDevice(c *fiber.Ctx) (err error) {
req := smsgateway.MobileRegisterRequest{}

if err := h.BodyParserValidator(c, &req); err != nil {
if err = h.BodyParserValidator(c, &req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}

id := h.idGen()
login := strings.ToUpper(id[:6])
password := strings.ToLower(id[7:])
var (
user models.User
login string
password string
)

if userauth.HasUser(c) {
user = userauth.GetUser(c)
} else {
id := h.idGen()
login = strings.ToUpper(id[:6])
password = strings.ToLower(id[7:])

user, err := h.authSvc.RegisterUser(login, password)
if err != nil {
return fmt.Errorf("can't create user: %w", err)
user, err = h.authSvc.RegisterUser(login, password)
if err != nil {
return fmt.Errorf("can't create user: %w", err)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential nil return of credentials for existing users.

When a user already exists (userauth.HasUser(c)), the login and password variables remain empty but are still returned in the response. This could lead to confusion or errors on the client side.

Consider this implementation:

 func (h *mobileHandler) postDevice(c *fiber.Ctx) (err error) {
     req := smsgateway.MobileRegisterRequest{}

     if err = h.BodyParserValidator(c, &req); err != nil {
         return fiber.NewError(fiber.StatusBadRequest, err.Error())
     }

-    var (
-        user     models.User
-        login    string
-        password string
-    )
+    var user models.User
+    var credentials struct {
+        login    string
+        password string
+    }

     if userauth.HasUser(c) {
         user = userauth.GetUser(c)
     } else {
         id := h.idGen()
-        login = strings.ToUpper(id[:6])
-        password = strings.ToLower(id[7:])
+        credentials.login = strings.ToUpper(id[:6])
+        credentials.password = strings.ToLower(id[7:])

-        user, err = h.authSvc.RegisterUser(login, password)
+        user, err = h.authSvc.RegisterUser(credentials.login, credentials.password)
         if err != nil {
             return fmt.Errorf("can't create user: %w", err)
         }
     }

Committable suggestion skipped: line range outside the PR's diff.


device, err := h.authSvc.RegisterDevice(user, req.Name, req.PushToken)
if err != nil {
return fmt.Errorf("can't register device: %w", err)
}

return c.Status(fiber.StatusCreated).JSON(smsgateway.MobileRegisterResponse{
Id: device.ID,
Token: device.AuthToken,
Login: login,
Password: password,
})
return c.Status(fiber.StatusCreated).
JSON(smsgateway.MobileRegisterResponse{
Id: device.ID,
Token: device.AuthToken,
Login: login,
Password: password,
})
Comment on lines +107 to +113
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Return appropriate response based on user context.

The response should be adjusted based on whether the user is new or existing.

Consider this implementation:

-    return c.Status(fiber.StatusCreated).
-        JSON(smsgateway.MobileRegisterResponse{
-            Id:       device.ID,
-            Token:    device.AuthToken,
-            Login:    login,
-            Password: password,
-        })
+    response := smsgateway.MobileRegisterResponse{
+        Id:    device.ID,
+        Token: device.AuthToken,
+    }
+    
+    if !userauth.HasUser(c) {
+        response.Login = credentials.login
+        response.Password = credentials.password
+    }
+    
+    return c.Status(fiber.StatusCreated).JSON(response)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return c.Status(fiber.StatusCreated).
JSON(smsgateway.MobileRegisterResponse{
Id: device.ID,
Token: device.AuthToken,
Login: login,
Password: password,
})
return c.Status(fiber.StatusCreated).JSON(func() smsgateway.MobileRegisterResponse {
response := smsgateway.MobileRegisterResponse{
Id: device.ID,
Token: device.AuthToken,
}
if !userauth.HasUser(c) {
response.Login = credentials.login
response.Password = credentials.password
}
return response
}())
Suggested change
return c.Status(fiber.StatusCreated).
JSON(smsgateway.MobileRegisterResponse{
Id: device.ID,
Token: device.AuthToken,
Login: login,
Password: password,
})
response := smsgateway.MobileRegisterResponse{
Id: device.ID,
Token: device.AuthToken,
}
if !userauth.HasUser(c) {
response.Login = credentials.login
response.Password = credentials.password
}
return c.Status(fiber.StatusCreated).JSON(response)

}

// @Summary Update device
Expand Down Expand Up @@ -212,8 +224,13 @@ func (h *mobileHandler) Register(router fiber.Router) {

router.Post("/device",
limiter.New(),
userauth.New(h.authSvc),
keyauth.New(keyauth.Config{
Next: func(c *fiber.Ctx) bool { return h.authSvc.IsPublic() },
Next: func(c *fiber.Ctx) bool {
// skip server key authorization...
return h.authSvc.IsPublic() || // ...if public mode
userauth.HasUser(c) // ...if registration with existing user
},
Validator: func(c *fiber.Ctx, token string) (bool, error) {
err := h.authSvc.AuthorizeRegistration(token)
return err == nil, err
Expand Down
8 changes: 4 additions & 4 deletions internal/sms-gateway/handlers/webhooks/3rdparty.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (

"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/webhooks"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
Expand Down Expand Up @@ -104,9 +104,9 @@ func (h *ThirdPartyController) delete(user models.User, c *fiber.Ctx) error {
}

func (h *ThirdPartyController) Register(router fiber.Router) {
router.Get("", auth.WithUser(h.get))
router.Post("", auth.WithUser(h.post))
router.Delete("/:id", auth.WithUser(h.delete))
router.Get("", userauth.WithUser(h.get))
router.Post("", userauth.WithUser(h.post))
router.Delete("/:id", userauth.WithUser(h.delete))
}

func NewThirdPartyController(params thirdPartyControllerParams) *ThirdPartyController {
Expand Down
6 changes: 0 additions & 6 deletions internal/sms-gateway/modules/auth/decorators.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import (
"github.com/gofiber/fiber/v2"
)

func WithUser(handler func(models.User, *fiber.Ctx) error) fiber.Handler {
return func(c *fiber.Ctx) error {
return handler(c.Locals("user").(models.User), c)
}
}

func WithDevice(handler func(models.Device, *fiber.Ctx) error) fiber.Handler {
return func(c *fiber.Ctx) error {
return handler(c.Locals("device").(models.Device), c)
Expand Down
2 changes: 1 addition & 1 deletion pkg/swagger/docs/local.http
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@ DELETE {{localUrl}}/webhooks/LreFUt-Z3sSq0JufY9uWB HTTP/1.1
Authorization: Basic {{localCredentials}}

###
GET {{localUrl}}/logs?from=2024-08-01T13:19:02.093%2B07:00 HTTP/1.1
GET {{localUrl}}/logs?from=2025-02-05T20:39:46.190%2B07:00 HTTP/1.1
Authorization: Basic {{localCredentials}}
8 changes: 5 additions & 3 deletions pkg/swagger/docs/mobile.http
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
@baseUrl={{$dotenv CLOUD__URL}}/api/mobile/v1
@baseUrl={{$dotenv CLOUD__URL}}/mobile/v1
@mobileToken={{$dotenv MOBILE__TOKEN}}
@phone={{$dotenv PHONE}}
@credentials={{$dotenv CLOUD__CREDENTIALS}}

###
GET {{baseUrl}}/device HTTP/1.1
Authorization: Bearer {{mobileToken}}

###
POST {{baseUrl}}/device HTTP/1.1
Authorization: Bearer 123456789
# Authorization: Bearer 123456789
Authorization: Basic {{credentials}}
Content-Type: application/json

{
"name": "Android Phone",
"pushToken": "eTxx88nfSla87gZuJcW5mS:APA91bHGxVgSqqRtxwFHD1q9em5Oa6xSP4gO_OZRrqOoP1wjf_7UMfXKsc4uws6rWkqn73jYCc1owyATB1v61mqak4ntpqtmRkNtTey7NQXa0Wz3uQZBWY-Ecbn2rWG2VJRihOzXRId-"
"pushToken": ""
}

###
Expand Down
10 changes: 5 additions & 5 deletions pkg/swagger/docs/requests.http
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
GET {{baseUrl}}/health HTTP/1.1

###
GET {{baseUrl}}/api/3rdparty/v1/health HTTP/1.1
GET {{baseUrl}}/3rdparty/v1/health HTTP/1.1

###
POST {{baseUrl}}/api/3rdparty/v1/messages?skipPhoneValidation=false HTTP/1.1
POST {{baseUrl}}/3rdparty/v1/messages?skipPhoneValidation=false HTTP/1.1
Content-Type: application/json
Authorization: Basic {{credentials}}

Expand All @@ -25,7 +25,7 @@ Authorization: Basic {{credentials}}
}

###
POST {{baseUrl}}/api/3rdparty/v1/messages HTTP/1.1
POST {{baseUrl}}/3rdparty/v1/messages HTTP/1.1
Content-Type: application/json
Authorization: Basic {{credentials}}

Expand All @@ -41,7 +41,7 @@ Authorization: Basic {{credentials}}
}

###
GET {{baseUrl}}/api/3rdparty/v1/messages/K56aIsVsQ2rECdv_ajzTd HTTP/1.1
GET {{baseUrl}}/3rdparty/v1/messages/K56aIsVsQ2rECdv_ajzTd HTTP/1.1
Authorization: Basic {{credentials}}

###
Expand All @@ -56,7 +56,7 @@ Content-Type: application/json
}

###
GET {{baseUrl}}/api/3rdparty/v1/devices HTTP/1.1
GET {{baseUrl}}/3rdparty/v1/devices HTTP/1.1
Authorization: Basic {{credentials}}

###
Expand Down