[webhooks] model, DTO, repository, service, handler

This commit is contained in:
Aleksandr Soloshenko 2024-06-07 23:37:33 +07:00
parent ebb97a8238
commit c0ce84e89f
23 changed files with 425 additions and 49 deletions

View File

@ -57,3 +57,23 @@ Authorization: Basic {{localCredentials}}
###
GET {{localUrl}}/message/8GN2Pz-fzu73NL3398ROE HTTP/1.1
Authorization: Basic {{localCredentials}}
###
GET {{localUrl}}/webhooks HTTP/1.1
Authorization: Basic {{localCredentials}}
###
POST {{localUrl}}/webhooks HTTP/1.1
Authorization: Basic {{localCredentials}}
Content-Type: application/json
{
"id": "LreFUt-Z3sSq0JufY9uWB",
"url": "https://webhook.site/280a6655-eb68-40b9-b857-af5be37c5303",
"event": "sms:received"
}
###
DELETE {{localUrl}}/webhooks/LreFUt-Z3sSq0JufY9uWB HTTP/1.1
Authorization: Basic {{localCredentials}}

View File

@ -92,4 +92,24 @@ Content-Type: application/json
{
"token": "eTxx88nfSla87gZuJcW5mS:APA91bHGxVgSqqRtxwFHD1q9em5Oa6xSP4gO_OZRrqOoP1wjf_7UMfXKsc4uws6rWkqn73jYCc1owyATB1v61mqak4ntpqtmRkNtTey7NQXa0Wz3uQZBWY-Ecbn2rWG2VJRihOzXRId-"
}
]
]
###
GET {{baseUrl}}/api/3rdparty/v1/webhooks HTTP/1.1
Authorization: Basic {{credentials}}
###
POST {{baseUrl}}/api/3rdparty/v1/webhooks HTTP/1.1
Authorization: Basic {{credentials}}
Content-Type: application/json
{
"id": "MYofX8bTd5Bov0wWFZLRP",
"url": "https://webhook.site/280a6655-eb68-40b9-b857-af5be37c5303",
"event": "sms:received"
}
###
DELETE {{baseUrl}}/api/3rdparty/v1/webhooks/MYofX8bTd5Bov0wWFZLRP HTTP/1.1
Authorization: Basic {{credentials}}

View File

@ -12,9 +12,11 @@ import (
appconfig "github.com/capcom6/sms-gateway/internal/config"
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
appdb "github.com/capcom6/sms-gateway/internal/sms-gateway/modules/db"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/health"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/messages"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/push"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/webhooks"
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
"github.com/capcom6/sms-gateway/internal/sms-gateway/services"
"go.uber.org/fx"
@ -27,6 +29,7 @@ var Module = fx.Module(
"server",
logger.Module,
appconfig.Module,
appdb.Module,
http.Module,
validator.Module,
handlers.Module,
@ -37,6 +40,7 @@ var Module = fx.Module(
db.Module,
messages.Module,
health.Module,
webhooks.Module,
)
func Run() {

View File

@ -5,9 +5,11 @@ import (
"fmt"
"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/messages"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/webhooks"
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
"github.com/capcom6/sms-gateway/internal/sms-gateway/services"
"github.com/capcom6/sms-gateway/pkg/types"
@ -25,7 +27,8 @@ const (
type ThirdPartyHandlerParams struct {
fx.In
HealthHandler *healthHandler
HealthHandler *healthHandler
WebhooksHandler *webhooks.Handler
AuthSvc *auth.Service
MessagesSvc *messages.Service
@ -36,9 +39,10 @@ type ThirdPartyHandlerParams struct {
}
type thirdPartyHandler struct {
Handler
base.Handler
healthHandler *healthHandler
healthHandler *healthHandler
webhooksHandler *webhooks.Handler
authSvc *auth.Service
messagesSvc *messages.Service
@ -166,8 +170,16 @@ func (h *thirdPartyHandler) getMessage(user models.User, c *fiber.Ctx) error {
return c.JSON(state)
}
func (h *thirdPartyHandler) authorize(handler func(models.User, *fiber.Ctx) error) fiber.Handler {
return func(c *fiber.Ctx) error {
func (h *thirdPartyHandler) Register(router fiber.Router) {
router = router.Group("/3rdparty/v1")
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)
@ -179,33 +191,24 @@ func (h *thirdPartyHandler) authorize(handler func(models.User, *fiber.Ctx) erro
c.Locals("user", user)
return handler(user, c)
}
}
return c.Next()
})
func (h *thirdPartyHandler) Register(router fiber.Router) {
router = router.Group("/3rdparty/v1")
router.Get("/device", auth.WithUser(h.getDevice))
h.healthHandler.Register(router)
router.Post("/message", auth.WithUser(h.postMessage))
router.Get("/message/:id", auth.WithUser(h.getMessage)).Name(route3rdPartyGetMessage)
router.Use(basicauth.New(basicauth.Config{
Authorizer: func(username string, password string) bool {
return len(username) > 0 && len(password) > 0
},
}))
router.Get("/device", h.authorize(h.getDevice))
router.Post("/message", h.authorize(h.postMessage))
router.Get("/message/:id", h.authorize(h.getMessage)).Name(route3rdPartyGetMessage)
h.webhooksHandler.Register(router.Group("/webhooks"))
}
func newThirdPartyHandler(params ThirdPartyHandlerParams) *thirdPartyHandler {
return &thirdPartyHandler{
Handler: Handler{Logger: params.Logger.Named("ThirdPartyHandler"), Validator: params.Validator},
healthHandler: params.HealthHandler,
authSvc: params.AuthSvc,
messagesSvc: params.MessagesSvc,
devicesSvc: params.DevicesSvc,
Handler: base.Handler{Logger: params.Logger.Named("ThirdPartyHandler"), Validator: params.Validator},
healthHandler: params.HealthHandler,
webhooksHandler: params.WebhooksHandler,
authSvc: params.AuthSvc,
messagesSvc: params.MessagesSvc,
devicesSvc: params.DevicesSvc,
}
}

View File

@ -1,4 +1,4 @@
package handlers
package base
import (
"fmt"
@ -22,7 +22,7 @@ func (h *Handler) BodyParserValidator(c *fiber.Ctx, out any) error {
return fmt.Errorf("can't parse body: %w", err)
}
return h.validateStruct(out)
return h.ValidateStruct(out)
}
func (h *Handler) QueryParserValidator(c *fiber.Ctx, out any) error {
@ -30,7 +30,7 @@ func (h *Handler) QueryParserValidator(c *fiber.Ctx, out any) error {
return fmt.Errorf("can't parse query: %w", err)
}
return h.validateStruct(out)
return h.ValidateStruct(out)
}
func (h *Handler) ParamsParserValidator(c *fiber.Ctx, out any) error {
@ -38,10 +38,10 @@ func (h *Handler) ParamsParserValidator(c *fiber.Ctx, out any) error {
return fmt.Errorf("can't parse params: %w", err)
}
return h.validateStruct(out)
return h.ValidateStruct(out)
}
func (h *Handler) validateStruct(out any) error {
func (h *Handler) ValidateStruct(out any) error {
if h.Validator != nil {
if err := h.Validator.Struct(out); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())

View File

@ -1,4 +1,4 @@
package handlers
package base
import (
"bytes"
@ -197,7 +197,7 @@ func TestHandler_validateStruct(t *testing.T) {
Logger: tt.fields.Logger,
Validator: tt.fields.Validator,
}
if err := h.validateStruct(tt.args.out); (err != nil) != tt.wantErr {
if err := h.ValidateStruct(tt.args.out); (err != nil) != tt.wantErr {
t.Errorf("Handler.validateStruct() error = %v, wantErr %v", err, tt.wantErr)
}
})

View File

@ -2,6 +2,7 @@ package handlers
import (
"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/health"
"github.com/capcom6/sms-gateway/internal/version"
"github.com/capcom6/sms-gateway/pkg/maps"
@ -19,7 +20,7 @@ type healthHanlderParams struct {
}
type healthHandler struct {
Handler
base.Handler
healthSvc *health.Service
@ -72,7 +73,7 @@ func (h *healthHandler) Register(router fiber.Router) {
func newHealthHandler(params healthHanlderParams) *healthHandler {
return &healthHandler{
Handler: Handler{Logger: params.Logger.Named("HealthHandler"), Validator: nil},
Handler: base.Handler{Logger: params.Logger.Named("HealthHandler"), Validator: nil},
healthSvc: params.HealthSvc,
logger: params.Logger,
}

View File

@ -7,6 +7,7 @@ import (
"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/capcom6/go-infra-fx/http/apikey"
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/messages"
@ -20,7 +21,7 @@ import (
)
type mobileHandler struct {
Handler
base.Handler
authSvc *auth.Service
messagesSvc *messages.Service
@ -142,7 +143,7 @@ func (h *mobileHandler) patchMessage(device models.Device, c *fiber.Ctx) error {
}
for _, v := range req {
if err := h.validateStruct(v); err != nil {
if err := h.ValidateStruct(v); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
@ -205,7 +206,7 @@ func newMobileHandler(params MobileHandlerParams) *mobileHandler {
idGen, _ := nanoid.Standard(21)
return &mobileHandler{
Handler: Handler{Logger: params.Logger, Validator: params.Validator},
Handler: base.Handler{Logger: params.Logger, Validator: params.Validator},
authSvc: params.AuthSvc,
messagesSvc: params.MessagesSvc,
idGen: idGen,

View File

@ -4,6 +4,7 @@ import (
"time"
"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/push"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@ -13,7 +14,7 @@ import (
)
type upstreamHandler struct {
Handler
base.Handler
config Config
pushSvc *push.Service
@ -31,7 +32,7 @@ type upstreamHandlerParams struct {
func newUpstreamHandler(params upstreamHandlerParams) *upstreamHandler {
return &upstreamHandler{
Handler: Handler{Logger: params.Logger, Validator: params.Validator},
Handler: base.Handler{Logger: params.Logger, Validator: params.Validator},
config: params.Config,
pushSvc: params.PushSvc,
}
@ -62,7 +63,7 @@ func (h *upstreamHandler) postPush(c *fiber.Ctx) error {
}
for _, v := range req {
if err := h.validateStruct(v); err != nil {
if err := h.ValidateStruct(v); err != nil {
return err
}

View File

@ -0,0 +1,21 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE `webhooks` (
`id` BIGINT UNSIGNED AUTO_INCREMENT,
`ext_id` varchar(36) NOT NULL,
`user_id` varchar(32) NOT NULL,
`url` varchar(256) NOT NULL,
`event` varchar(32) NOT NULL,
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`deleted_at` datetime(3) NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `unq_webhooks_user_extid` (`user_id`, `ext_id`),
CONSTRAINT `fk_webhooks_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
-- +goose StatementEnd
---
-- +goose Down
-- +goose StatementBegin
DROP TABLE `webhooks`;
-- +goose StatementEnd

View File

@ -15,9 +15,9 @@ const (
)
type TimedModel struct {
CreatedAt time.Time `gorm:"not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"`
UpdatedAt time.Time `gorm:"not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"`
DeletedAt *time.Time
CreatedAt time.Time `gorm:"->;not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"`
UpdatedAt time.Time `gorm:"->;not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"`
DeletedAt *time.Time `gorm:"<-:update"`
}
type User struct {

View File

@ -0,0 +1,12 @@
package auth
import (
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
"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)
}
}

View File

@ -0,0 +1,15 @@
package db
import (
"github.com/jaevor/go-nanoid"
"go.uber.org/fx"
)
type IDGen func() string
var Module = fx.Module(
"db",
fx.Provide(func() (IDGen, error) {
return nanoid.Standard(21)
}),
)

View File

@ -10,10 +10,10 @@ import (
"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/capcom6/go-helpers/slices"
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/db"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/push"
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
"github.com/capcom6/sms-gateway/pkg/types"
"github.com/jaevor/go-nanoid"
"github.com/nyaruka/phonenumbers"
"go.uber.org/fx"
"go.uber.org/zap"
@ -37,6 +37,8 @@ type EnqueueOptions struct {
type ServiceParams struct {
fx.In
IDGen db.IDGen
Messages *repositories.MessagesRepository
HashingTask *HashingTask
@ -55,8 +57,6 @@ type Service struct {
}
func NewService(params ServiceParams) *Service {
idgen, _ := nanoid.Standard(21)
return &Service{
Messages: params.Messages,
HashingTask: params.HashingTask,
@ -64,7 +64,7 @@ func NewService(params ServiceParams) *Service {
PushSvc: params.PushSvc,
Logger: params.Logger.Named("Service"),
idgen: idgen,
idgen: params.IDGen,
}
}

View File

@ -0,0 +1,9 @@
package webhooks
func webhookToDTO(model *Webhook) WebhookDTO {
return WebhookDTO{
ID: model.ExtID,
URL: model.URL,
Event: model.Event,
}
}

View File

@ -0,0 +1,7 @@
package webhooks
type WebhookDTO struct {
ID string `json:"id" validate:"max=36" example:"123e4567-e89b-12d3-a456-426614174000"`
URL string `json:"url" validate:"required,http_url" example:"https://example.com/webhook"`
Event Event `json:"event" validate:"required" example:"sms:received"`
}

View File

@ -0,0 +1,80 @@
package webhooks
import (
"fmt"
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
"go.uber.org/zap"
)
type handlerParams struct {
fx.In
WebhooksSvc *Service
Validator *validator.Validate
Logger *zap.Logger
}
type Handler struct {
base.Handler
webhooksSvc *Service
logger *zap.Logger
}
func (h *Handler) get(user models.User, c *fiber.Ctx) error {
items, err := h.webhooksSvc.Select(user.ID)
if err != nil {
return fmt.Errorf("can't select webhooks: %w", err)
}
return c.JSON(items)
}
func (h *Handler) post(user models.User, c *fiber.Ctx) error {
dto := WebhookDTO{}
if err := h.BodyParserValidator(c, &dto); err != nil {
return err
}
if err := h.webhooksSvc.Replace(user.ID, dto); err != nil {
return fmt.Errorf("can't write webhook: %w", err)
}
return c.SendStatus(fiber.StatusCreated)
}
func (h *Handler) delete(user models.User, c *fiber.Ctx) error {
id := c.Params("id")
if err := h.webhooksSvc.Delete(user.ID, WithExtID(id)); err != nil {
return fmt.Errorf("can't delete webhook: %w", err)
}
return c.SendStatus(fiber.StatusNoContent)
}
func (h *Handler) Register(router fiber.Router) {
router.Get("/", auth.WithUser(h.get))
router.Post("/", auth.WithUser(h.post))
router.Delete("/:id", auth.WithUser(h.delete))
}
func NewHandler(params handlerParams) *Handler {
return &Handler{
Handler: base.Handler{
Logger: params.Logger.Named("webhooks"),
Validator: params.Validator,
},
webhooksSvc: params.WebhooksSvc,
logger: params.Logger,
}
}

View File

@ -0,0 +1,23 @@
package webhooks
import (
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
"gorm.io/gorm"
)
type Webhook struct {
ID uint64 `json:"-" gorm:"->;primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
ExtID string `json:"id" gorm:"not null;type:varchar(36);uniqueIndex:unq_webhooks_user_extid,priority:2"`
UserID string `json:"-" gorm:"<-:create;not null;type:varchar(32);uniqueIndex:unq_webhooks_user_extid,priority:1"`
URL string `json:"url" validate:"required,http_url" gorm:"not null;type:varchar(256)"`
Event Event `json:"event" gorm:"not null;type:varchar(32)"`
User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
models.TimedModel
}
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&Webhook{})
}

View File

@ -0,0 +1,23 @@
package webhooks
import (
"github.com/capcom6/go-infra-fx/db"
"go.uber.org/fx"
"go.uber.org/zap"
)
var Module = fx.Module(
"webhooks",
fx.Decorate(func(log *zap.Logger) *zap.Logger {
return log.Named("webhooks")
}),
fx.Provide(NewRepository, fx.Private),
fx.Provide(
NewService,
NewHandler,
),
)
func init() {
db.RegisterMigration(Migrate)
}

View File

@ -0,0 +1,35 @@
package webhooks
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Repository struct {
db *gorm.DB
}
func (r *Repository) Select(filters ...SelectFilter) ([]*Webhook, error) {
webhooks := []*Webhook{}
if err := newFilter(filters...).apply(r.db).Find(&webhooks).Error; err != nil {
return nil, err
}
return webhooks, nil
}
func (r *Repository) Replace(webhook *Webhook) error {
return r.db.
Clauses(clause.OnConflict{UpdateAll: true}).
Save(webhook).
Error
}
func (r *Repository) Delete(filters ...SelectFilter) error {
return newFilter(filters...).apply(r.db).Delete(&Webhook{}).Error
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{
db: db,
}
}

View File

@ -0,0 +1,42 @@
package webhooks
import "gorm.io/gorm"
type SelectFilter func(*selectFilter)
func WithExtID(extID string) SelectFilter {
return func(f *selectFilter) {
f.extID = &extID
}
}
func WithUserID(userID string) SelectFilter {
return func(f *selectFilter) {
f.userID = userID
}
}
type selectFilter struct {
userID string
extID *string
}
func newFilter(filters ...SelectFilter) *selectFilter {
f := &selectFilter{}
f.merge(filters...)
return f
}
func (f *selectFilter) merge(filters ...SelectFilter) {
for _, filter := range filters {
filter(f)
}
}
func (f *selectFilter) apply(query *gorm.DB) *gorm.DB {
query = query.Where("user_id = ?", f.userID)
if f.extID != nil {
query = query.Where("ext_id = ?", *f.extID)
}
return query
}

View File

@ -0,0 +1,52 @@
package webhooks
import (
"fmt"
"github.com/capcom6/go-helpers/slices"
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/db"
)
type Service struct {
idgen db.IDGen
webhooks *Repository
}
func NewService(idgen db.IDGen, webhooks *Repository) *Service {
return &Service{
idgen: idgen,
webhooks: webhooks,
}
}
func (s *Service) Select(userID string, filters ...SelectFilter) ([]WebhookDTO, error) {
filters = append(filters, WithUserID(userID))
items, err := s.webhooks.Select(filters...)
if err != nil {
return nil, fmt.Errorf("can't select webhooks: %w", err)
}
return slices.Map(items, webhookToDTO), nil
}
func (s *Service) Replace(userID string, webhook WebhookDTO) error {
if webhook.ID == "" {
webhook.ID = s.idgen()
}
model := Webhook{
ExtID: webhook.ID,
UserID: userID,
URL: webhook.URL,
Event: webhook.Event,
}
return s.webhooks.Replace(&model)
}
func (s *Service) Delete(userID string, filters ...SelectFilter) error {
filters = append(filters, WithUserID(userID))
return s.webhooks.Delete(filters...)
}

View File

@ -0,0 +1,7 @@
package webhooks
type Event string
const (
EventSmsReceived Event = "sms:received"
)