diff --git a/api/local.http b/api/local.http index fbf4804..55b313d 100644 --- a/api/local.http +++ b/api/local.http @@ -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}} + diff --git a/api/requests.http b/api/requests.http index aaafc26..366c805 100644 --- a/api/requests.http +++ b/api/requests.http @@ -92,4 +92,24 @@ Content-Type: application/json { "token": "eTxx88nfSla87gZuJcW5mS:APA91bHGxVgSqqRtxwFHD1q9em5Oa6xSP4gO_OZRrqOoP1wjf_7UMfXKsc4uws6rWkqn73jYCc1owyATB1v61mqak4ntpqtmRkNtTey7NQXa0Wz3uQZBWY-Ecbn2rWG2VJRihOzXRId-" } -] \ No newline at end of file +] + +### +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}} + diff --git a/internal/sms-gateway/app.go b/internal/sms-gateway/app.go index 70fc0d3..af2cb2a 100644 --- a/internal/sms-gateway/app.go +++ b/internal/sms-gateway/app.go @@ -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() { diff --git a/internal/sms-gateway/handlers/3rdparty.go b/internal/sms-gateway/handlers/3rdparty.go index 74907a8..c8be0a7 100644 --- a/internal/sms-gateway/handlers/3rdparty.go +++ b/internal/sms-gateway/handlers/3rdparty.go @@ -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, } } diff --git a/internal/sms-gateway/handlers/handler.go b/internal/sms-gateway/handlers/base/handler.go similarity index 87% rename from internal/sms-gateway/handlers/handler.go rename to internal/sms-gateway/handlers/base/handler.go index bec4914..4853a40 100644 --- a/internal/sms-gateway/handlers/handler.go +++ b/internal/sms-gateway/handlers/base/handler.go @@ -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()) diff --git a/internal/sms-gateway/handlers/handler_test.go b/internal/sms-gateway/handlers/base/handler_test.go similarity index 98% rename from internal/sms-gateway/handlers/handler_test.go rename to internal/sms-gateway/handlers/base/handler_test.go index 4791ff9..b71c988 100644 --- a/internal/sms-gateway/handlers/handler_test.go +++ b/internal/sms-gateway/handlers/base/handler_test.go @@ -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) } }) diff --git a/internal/sms-gateway/handlers/health.go b/internal/sms-gateway/handlers/health.go index 45d14fe..5a805f7 100644 --- a/internal/sms-gateway/handlers/health.go +++ b/internal/sms-gateway/handlers/health.go @@ -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, } diff --git a/internal/sms-gateway/handlers/mobile.go b/internal/sms-gateway/handlers/mobile.go index 5048991..4e53ad4 100644 --- a/internal/sms-gateway/handlers/mobile.go +++ b/internal/sms-gateway/handlers/mobile.go @@ -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, diff --git a/internal/sms-gateway/handlers/upstream.go b/internal/sms-gateway/handlers/upstream.go index ad99bc2..0e7690e 100644 --- a/internal/sms-gateway/handlers/upstream.go +++ b/internal/sms-gateway/handlers/upstream.go @@ -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 } diff --git a/internal/sms-gateway/models/migrations/mysql/20240607230753_webhooks.sql b/internal/sms-gateway/models/migrations/mysql/20240607230753_webhooks.sql new file mode 100644 index 0000000..c8555cd --- /dev/null +++ b/internal/sms-gateway/models/migrations/mysql/20240607230753_webhooks.sql @@ -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 \ No newline at end of file diff --git a/internal/sms-gateway/models/models.go b/internal/sms-gateway/models/models.go index ffc2b24..f629e8c 100644 --- a/internal/sms-gateway/models/models.go +++ b/internal/sms-gateway/models/models.go @@ -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 { diff --git a/internal/sms-gateway/modules/auth/decorators.go b/internal/sms-gateway/modules/auth/decorators.go new file mode 100644 index 0000000..8608aea --- /dev/null +++ b/internal/sms-gateway/modules/auth/decorators.go @@ -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) + } +} diff --git a/internal/sms-gateway/modules/db/module.go b/internal/sms-gateway/modules/db/module.go new file mode 100644 index 0000000..2ecf53f --- /dev/null +++ b/internal/sms-gateway/modules/db/module.go @@ -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) + }), +) diff --git a/internal/sms-gateway/modules/messages/service.go b/internal/sms-gateway/modules/messages/service.go index 658edbb..5be345e 100644 --- a/internal/sms-gateway/modules/messages/service.go +++ b/internal/sms-gateway/modules/messages/service.go @@ -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, } } diff --git a/internal/sms-gateway/modules/webhooks/converters.go b/internal/sms-gateway/modules/webhooks/converters.go new file mode 100644 index 0000000..795e661 --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/converters.go @@ -0,0 +1,9 @@ +package webhooks + +func webhookToDTO(model *Webhook) WebhookDTO { + return WebhookDTO{ + ID: model.ExtID, + URL: model.URL, + Event: model.Event, + } +} diff --git a/internal/sms-gateway/modules/webhooks/dto.go b/internal/sms-gateway/modules/webhooks/dto.go new file mode 100644 index 0000000..869d559 --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/dto.go @@ -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"` +} diff --git a/internal/sms-gateway/modules/webhooks/handler.go b/internal/sms-gateway/modules/webhooks/handler.go new file mode 100644 index 0000000..8b08a2a --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/handler.go @@ -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, + } +} diff --git a/internal/sms-gateway/modules/webhooks/models.go b/internal/sms-gateway/modules/webhooks/models.go new file mode 100644 index 0000000..663f5cd --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/models.go @@ -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{}) +} diff --git a/internal/sms-gateway/modules/webhooks/module.go b/internal/sms-gateway/modules/webhooks/module.go new file mode 100644 index 0000000..8c0ed37 --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/module.go @@ -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) +} diff --git a/internal/sms-gateway/modules/webhooks/repository.go b/internal/sms-gateway/modules/webhooks/repository.go new file mode 100644 index 0000000..d0f63f1 --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/repository.go @@ -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, + } +} diff --git a/internal/sms-gateway/modules/webhooks/repository_filter.go b/internal/sms-gateway/modules/webhooks/repository_filter.go new file mode 100644 index 0000000..f7f9a8a --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/repository_filter.go @@ -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 +} diff --git a/internal/sms-gateway/modules/webhooks/service.go b/internal/sms-gateway/modules/webhooks/service.go new file mode 100644 index 0000000..e8ef2cc --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/service.go @@ -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...) +} diff --git a/internal/sms-gateway/modules/webhooks/types.go b/internal/sms-gateway/modules/webhooks/types.go new file mode 100644 index 0000000..475ec95 --- /dev/null +++ b/internal/sms-gateway/modules/webhooks/types.go @@ -0,0 +1,7 @@ +package webhooks + +type Event string + +const ( + EventSmsReceived Event = "sms:received" +)