2025-08-16 20:20:31 +07:00

282 lines
8.5 KiB
Go

package handlers
import (
"fmt"
"strings"
"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/events"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/messages"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/deviceauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/settings"
"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"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices"
"github.com/capcom6/go-helpers/anys"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/keyauth"
"github.com/jaevor/go-nanoid"
"go.uber.org/fx"
"go.uber.org/zap"
)
type mobileHandlerParams struct {
fx.In
Logger *zap.Logger
Validator *validator.Validate
AuthSvc *auth.Service
DevicesSvc *devices.Service
MessagesCtrl *messages.MobileController
WebhooksCtrl *webhooks.MobileController
SettingsCtrl *settings.MobileController
EventsCtrl *events.MobileController
}
type mobileHandler struct {
base.Handler
authSvc *auth.Service
devicesSvc *devices.Service
messagesCtrl *messages.MobileController
webhooksCtrl *webhooks.MobileController
settingsCtrl *settings.MobileController
eventsCtrl *events.MobileController
idGen func() string
}
// @Summary Get device information
// @Description Returns device information
// @Tags Device
// @Produce json
// @Success 200 {object} smsgateway.MobileDeviceResponse "Device information"
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
// @Router /mobile/v1/device [get]
//
// Get device information
func (h *mobileHandler) getDevice(device models.Device, c *fiber.Ctx) error {
res := smsgateway.MobileDeviceResponse{
ExternalIP: c.IP(),
}
if !device.IsEmpty() {
res.Device = anys.AsPointer(converters.DeviceToDTO(device))
}
return c.JSON(res)
}
// @Summary Register device
// @Description Registers new device for new or existing user. Returns user credentials only for new users
// @Security ApiAuth
// @Security UserCode
// @Security ServerKey
// @Tags Device
// @Accept json
// @Produce json
// @Param request body smsgateway.MobileRegisterRequest true "Device registration request"
// @Success 201 {object} smsgateway.MobileRegisterResponse "Device registered"
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized (private mode only)"
// @Failure 429 {object} smsgateway.ErrorResponse "Too many requests"
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
// @Router /mobile/v1/device [post]
//
// Register device
func (h *mobileHandler) postDevice(c *fiber.Ctx) (err error) {
req := smsgateway.MobileRegisterRequest{}
if err = h.BodyParserValidator(c, &req); err != nil {
return err
}
var (
user models.User
login string
password string
)
if userauth.HasUser(c) {
user = userauth.GetUser(c)
login = user.ID
} 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)
}
}
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,
})
}
// @Summary Update device
// @Description Updates push token for device
// @Security MobileToken
// @Tags Device
// @Accept json
// @Param request body smsgateway.MobileUpdateRequest true "Device update request"
// @Success 204 "Successfully updated"
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
// @Failure 403 {object} smsgateway.ErrorResponse "Forbidden (wrong device ID)"
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
// @Router /mobile/v1/device [patch]
//
// Update device
func (h *mobileHandler) patchDevice(device models.Device, c *fiber.Ctx) error {
req := smsgateway.MobileUpdateRequest{}
if err := h.BodyParserValidator(c, &req); err != nil {
return err
}
if req.Id != device.ID {
return fiber.ErrForbidden
}
if err := h.devicesSvc.UpdatePushToken(req.Id, req.PushToken); err != nil {
return err
}
return c.SendStatus(fiber.StatusNoContent)
}
// @Summary Get one-time code for device registration
// @Description Returns one-time code for device registration
// @Security ApiAuth
// @Tags Device
// @Accept json
// @Produce json
// @Success 200 {object} smsgateway.MobileUserCodeResponse "User code"
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
// @Router /mobile/v1/user/code [get]
//
// Get user code
func (h *mobileHandler) getUserCode(user models.User, c *fiber.Ctx) error {
code, err := h.authSvc.GenerateUserCode(user.ID)
if err != nil {
return err
}
return c.JSON(smsgateway.MobileUserCodeResponse{
Code: code.Code,
ValidUntil: code.ValidUntil,
})
}
// @Summary Change password
// @Description Changes the user's password
// @Security MobileToken
// @Tags Device
// @Accept json
// @Produce json
// @Param request body smsgateway.MobileChangePasswordRequest true "Password change request"
// @Success 204 {object} nil "Password changed successfully"
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
// @Router /mobile/v1/user/password [patch]
//
// Change password
func (h *mobileHandler) changePassword(device models.Device, c *fiber.Ctx) error {
req := smsgateway.MobileChangePasswordRequest{}
if err := h.BodyParserValidator(c, &req); err != nil {
return err
}
if err := h.authSvc.ChangePassword(device.UserID, req.CurrentPassword, req.NewPassword); err != nil {
h.Logger.Error("failed to change password", zap.Error(err))
return fiber.NewError(fiber.StatusUnauthorized, "Invalid current password")
}
return c.SendStatus(fiber.StatusNoContent)
}
func (h *mobileHandler) Register(router fiber.Router) {
router = router.Group("/mobile/v1")
router.Post("/device",
userauth.NewBasic(h.authSvc),
userauth.NewCode(h.authSvc),
keyauth.New(keyauth.Config{
Next: func(c *fiber.Ctx) bool {
// Skip server key authorization in the following cases:
// 1. Public mode is enabled - allowing open registration
// 2. User is already authenticated - allowing device registration for existing users
return h.authSvc.IsPublic() || userauth.HasUser(c)
},
Validator: func(c *fiber.Ctx, token string) (bool, error) {
err := h.authSvc.AuthorizeRegistration(token)
return err == nil, err
},
}),
h.postDevice,
)
router.Get("/user/code",
userauth.NewBasic(h.authSvc),
userauth.UserRequired(),
userauth.WithUser(h.getUserCode),
)
router.Use(
deviceauth.New(h.authSvc),
)
router.Get("/device", deviceauth.WithDevice(h.getDevice))
router.Use(deviceauth.DeviceRequired())
router.Patch("/device", deviceauth.WithDevice(h.patchDevice))
// Should be under `userauth.NewBasic` protection instead of `deviceauth`
router.Patch("/user/password", deviceauth.WithDevice(h.changePassword))
h.messagesCtrl.Register(router.Group("/message"))
h.messagesCtrl.Register(router.Group("/messages"))
h.webhooksCtrl.Register(router.Group("/webhooks"))
h.settingsCtrl.Register(router.Group("/settings"))
h.eventsCtrl.Register(router.Group("/events"))
}
func newMobileHandler(params mobileHandlerParams) *mobileHandler {
idGen, _ := nanoid.Standard(21)
return &mobileHandler{
Handler: base.Handler{Logger: params.Logger, Validator: params.Validator},
authSvc: params.AuthSvc,
messagesCtrl: params.MessagesCtrl,
devicesSvc: params.DevicesSvc,
webhooksCtrl: params.WebhooksCtrl,
settingsCtrl: params.SettingsCtrl,
eventsCtrl: params.EventsCtrl,
idGen: idGen,
}
}