Skip to content

Commit a89c323

Browse files
committed
Add bility to store multiple phone numbers per webhook
1 parent e083f23 commit a89c323

File tree

9 files changed

+141
-54
lines changed

9 files changed

+141
-54
lines changed

api/pkg/di/container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ func (container *Container) WebhookHandlerValidator() (validator *validators.Web
449449
return validators.NewWebhookHandlerValidator(
450450
container.Logger(),
451451
container.Tracer(),
452+
container.PhoneService(),
452453
)
453454
}
454455

api/pkg/entities/webhook.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import (
99

1010
// Webhook stores the webhooks of a user
1111
type Webhook struct {
12-
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
13-
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
14-
URL string `json:"url" example:"https://example.com"`
15-
SigningKey string `json:"signing_key" example:"DGW8NwQp7mxKaSZ72Xq9v67SLqSbWQvckzzmK8D6rvd7NywSEkdMJtuxKyEkYnCY"`
16-
Events pq.StringArray `json:"events" example:"[message.phone.received]" gorm:"type:text[]" swaggertype:"array,string"`
17-
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
18-
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
12+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
13+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
14+
URL string `json:"url" example:"https://example.com"`
15+
SigningKey string `json:"signing_key" example:"DGW8NwQp7mxKaSZ72Xq9v67SLqSbWQvckzzmK8D6rvd7NywSEkdMJtuxKyEkYnCY"`
16+
PhoneNumbers pq.StringArray `json:"phone_numbers" example:"[+18005550199,+18005550100]" gorm:"type:text[]" swaggertype:"array,string"`
17+
Events pq.StringArray `json:"events" example:"[message.phone.received]" gorm:"type:text[]" swaggertype:"array,string"`
18+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
19+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
1920
}

api/pkg/handlers/webhook_handler.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,21 +154,21 @@ func (h *WebhookHandler) Store(c *fiber.Ctx) error {
154154
return h.responseBadRequest(c, err)
155155
}
156156

157-
if errors := h.validator.ValidateStore(ctx, request.Sanitize()); len(errors) != 0 {
157+
if errors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
158158
msg := fmt.Sprintf("validation errors [%s], while storing webhook [%+#v]", spew.Sdump(errors), request)
159159
ctxLogger.Warn(stacktrace.NewError(msg))
160160
return h.responseUnprocessableEntity(c, errors, "validation errors while storing webhook")
161161
}
162162

163-
webhooks, err := h.service.Index(ctx, h.userIDFomContext(c), repositories.IndexParams{Skip: 0, Limit: 1})
163+
webhooks, err := h.service.Index(ctx, h.userIDFomContext(c), repositories.IndexParams{Skip: 0, Limit: 3})
164164
if err != nil {
165165
ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot index webhooks for user [%s]", h.userIDFomContext(c))))
166166
return h.responsePaymentRequired(c, "You can't create more than 1 webhook contact us to upgrade your account.")
167167
}
168168

169-
if len(webhooks) > 0 {
170-
ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("user with ID [%s] wants to create more than 1 webhook", h.userIDFomContext(c))))
171-
return h.responsePaymentRequired(c, "You can't create more than 1 webhook contact us to upgrade your account.")
169+
if len(webhooks) > 1 {
170+
ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("user with ID [%s] wants to create more than 2 webhooks", h.userIDFomContext(c))))
171+
return h.responsePaymentRequired(c, "You can't create more than 2 webhooks contact us to upgrade your account.")
172172
}
173173

174174
webhook, err := h.service.Store(ctx, request.ToStoreParams(h.userFromContext(c)))
@@ -208,7 +208,7 @@ func (h *WebhookHandler) Update(c *fiber.Ctx) error {
208208
}
209209

210210
request.WebhookID = c.Params("webhookID")
211-
if errors := h.validator.ValidateUpdate(ctx, request.Sanitize()); len(errors) != 0 {
211+
if errors := h.validator.ValidateUpdate(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
212212
msg := fmt.Sprintf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request)
213213
ctxLogger.Warn(stacktrace.NewError(msg))
214214
return h.responseUnprocessableEntity(c, errors, "validation errors while updating webhook")

api/pkg/requests/webhook_store_request.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,32 @@ import (
1010
// WebhookStore is the payload for creating a new entities.Webhook
1111
type WebhookStore struct {
1212
request
13-
SigningKey string `json:"signing_key"`
14-
URL string `json:"url"`
15-
Events []string `json:"events"`
13+
SigningKey string `json:"signing_key"`
14+
URL string `json:"url"`
15+
PhoneNumbers []string `json:"phone_numbers" example:"+18005550100,+18005550100"`
16+
Events []string `json:"events"`
1617
}
1718

1819
// Sanitize sets defaults to WebhookStore
1920
func (input *WebhookStore) Sanitize() WebhookStore {
2021
input.URL = strings.TrimSpace(input.URL)
2122
input.Events = input.removeStringDuplicates(input.Events)
23+
24+
var phoneNumbers []string
25+
for _, address := range input.PhoneNumbers {
26+
phoneNumbers = append(phoneNumbers, input.sanitizeAddress(address))
27+
}
28+
2229
return *input
2330
}
2431

2532
// ToStoreParams converts WebhookStore to services.WebhookStoreParams
2633
func (input *WebhookStore) ToStoreParams(user entities.AuthUser) *services.WebhookStoreParams {
2734
return &services.WebhookStoreParams{
28-
UserID: user.ID,
29-
SigningKey: input.SigningKey,
30-
URL: input.URL,
31-
Events: input.Events,
35+
UserID: user.ID,
36+
SigningKey: input.SigningKey,
37+
URL: input.URL,
38+
PhoneNumbers: input.PhoneNumbers,
39+
Events: input.Events,
3240
}
3341
}
Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package requests
22

33
import (
4-
"strings"
5-
64
"github.com/NdoleStudio/httpsms/pkg/entities"
75
"github.com/NdoleStudio/httpsms/pkg/services"
86
"github.com/google/uuid"
@@ -16,18 +14,18 @@ type WebhookUpdate struct {
1614

1715
// Sanitize sets defaults to WebhookUpdate
1816
func (input *WebhookUpdate) Sanitize() WebhookUpdate {
19-
input.URL = strings.TrimSpace(input.URL)
20-
input.Events = input.removeStringDuplicates(input.Events)
17+
input.WebhookStore.Sanitize()
2118
return *input
2219
}
2320

2421
// ToUpdateParams converts WebhookUpdate to services.WebhookUpdateParams
2522
func (input *WebhookUpdate) ToUpdateParams(user entities.AuthUser) *services.WebhookUpdateParams {
2623
return &services.WebhookUpdateParams{
27-
UserID: user.ID,
28-
WebhookID: uuid.MustParse(input.WebhookID),
29-
SigningKey: input.SigningKey,
30-
URL: input.URL,
31-
Events: input.Events,
24+
UserID: user.ID,
25+
WebhookID: uuid.MustParse(input.WebhookID),
26+
SigningKey: input.SigningKey,
27+
URL: input.URL,
28+
PhoneNumbers: input.PhoneNumbers,
29+
Events: input.Events,
3230
}
3331
}

api/pkg/services/webhook_service.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ func (service *WebhookService) Delete(ctx context.Context, userID entities.UserI
8787

8888
// WebhookStoreParams are parameters for creating a new entities.Webhook
8989
type WebhookStoreParams struct {
90-
UserID entities.UserID
91-
SigningKey string
92-
URL string
93-
Events pq.StringArray
90+
UserID entities.UserID
91+
SigningKey string
92+
URL string
93+
PhoneNumbers pq.StringArray
94+
Events pq.StringArray
9495
}
9596

9697
// Store a new entities.Webhook
@@ -101,13 +102,14 @@ func (service *WebhookService) Store(ctx context.Context, params *WebhookStorePa
101102
ctxLogger := service.tracer.CtxLogger(service.logger, span)
102103

103104
webhook := &entities.Webhook{
104-
ID: uuid.New(),
105-
UserID: params.UserID,
106-
URL: params.URL,
107-
SigningKey: params.SigningKey,
108-
Events: params.Events,
109-
CreatedAt: time.Now().UTC(),
110-
UpdatedAt: time.Now().UTC(),
105+
ID: uuid.New(),
106+
UserID: params.UserID,
107+
URL: params.URL,
108+
PhoneNumbers: params.PhoneNumbers,
109+
SigningKey: params.SigningKey,
110+
Events: params.Events,
111+
CreatedAt: time.Now().UTC(),
112+
UpdatedAt: time.Now().UTC(),
111113
}
112114

113115
if err := service.repository.Save(ctx, webhook); err != nil {
@@ -121,11 +123,12 @@ func (service *WebhookService) Store(ctx context.Context, params *WebhookStorePa
121123

122124
// WebhookUpdateParams are parameters for updating an entities.Webhook
123125
type WebhookUpdateParams struct {
124-
UserID entities.UserID
125-
SigningKey string
126-
URL string
127-
Events pq.StringArray
128-
WebhookID uuid.UUID
126+
UserID entities.UserID
127+
SigningKey string
128+
URL string
129+
Events pq.StringArray
130+
PhoneNumbers pq.StringArray
131+
WebhookID uuid.UUID
129132
}
130133

131134
// Update an entities.Webhook
@@ -142,6 +145,7 @@ func (service *WebhookService) Update(ctx context.Context, params *WebhookUpdate
142145
webhook.URL = params.URL
143146
webhook.SigningKey = params.SigningKey
144147
webhook.Events = params.Events
148+
webhook.PhoneNumbers = params.PhoneNumbers
145149

146150
if err = service.repository.Save(ctx, webhook); err != nil {
147151
msg := fmt.Sprintf("cannot save webhook with id [%s] after update", webhook.ID)

api/pkg/validators/webhook_handler_validator.go

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import (
55
"fmt"
66
"net/url"
77

8+
"github.com/NdoleStudio/httpsms/pkg/entities"
9+
"github.com/NdoleStudio/httpsms/pkg/repositories"
10+
"github.com/NdoleStudio/httpsms/pkg/services"
11+
"github.com/palantir/stacktrace"
12+
813
"github.com/NdoleStudio/httpsms/pkg/requests"
914

1015
"github.com/NdoleStudio/httpsms/pkg/telemetry"
@@ -14,18 +19,21 @@ import (
1419
// WebhookHandlerValidator validates models used in handlers.WebhookHandler
1520
type WebhookHandlerValidator struct {
1621
validator
17-
logger telemetry.Logger
18-
tracer telemetry.Tracer
22+
logger telemetry.Logger
23+
tracer telemetry.Tracer
24+
phoneService *services.PhoneService
1925
}
2026

2127
// NewWebhookHandlerValidator creates a new handlers.WebhookHandler validator
2228
func NewWebhookHandlerValidator(
2329
logger telemetry.Logger,
2430
tracer telemetry.Tracer,
31+
phoneService *services.PhoneService,
2532
) (v *WebhookHandlerValidator) {
2633
return &WebhookHandlerValidator{
27-
logger: logger.WithService(fmt.Sprintf("%T", v)),
28-
tracer: tracer,
34+
logger: logger.WithService(fmt.Sprintf("%T", v)),
35+
tracer: tracer,
36+
phoneService: phoneService,
2937
}
3038
}
3139

@@ -54,7 +62,10 @@ func (validator *WebhookHandlerValidator) ValidateIndex(_ context.Context, reque
5462
}
5563

5664
// ValidateStore validates the requests.WebhookStore request
57-
func (validator *WebhookHandlerValidator) ValidateStore(_ context.Context, request requests.WebhookStore) url.Values {
65+
func (validator *WebhookHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, request requests.WebhookStore) url.Values {
66+
ctx, span := validator.tracer.Start(ctx)
67+
defer span.End()
68+
5869
v := govalidator.New(govalidator.Options{
5970
Data: &request,
6071
Rules: govalidator.MapData{
@@ -72,13 +83,32 @@ func (validator *WebhookHandlerValidator) ValidateStore(_ context.Context, reque
7283
"required",
7384
webhookEventsRule,
7485
},
86+
"phone_numbers": []string{
87+
"required",
88+
multipleContactPhoneNumberRule,
89+
},
7590
},
7691
})
77-
return v.ValidateStruct()
92+
93+
result := v.ValidateStruct()
94+
if len(result) > 0 {
95+
return result
96+
}
97+
98+
for _, address := range request.PhoneNumbers {
99+
_, err := validator.phoneService.Load(ctx, userID, address)
100+
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
101+
result.Add("from", fmt.Sprintf("The phone number [%s] is not available in your account. Install the android app on your phone to store a webhook with this phone number", address))
102+
}
103+
}
104+
return result
78105
}
79106

80107
// ValidateUpdate validates the requests.WebhookUpdate request
81-
func (validator *WebhookHandlerValidator) ValidateUpdate(_ context.Context, request requests.WebhookUpdate) url.Values {
108+
func (validator *WebhookHandlerValidator) ValidateUpdate(ctx context.Context, userID entities.UserID, request requests.WebhookUpdate) url.Values {
109+
ctx, span := validator.tracer.Start(ctx)
110+
defer span.End()
111+
82112
v := govalidator.New(govalidator.Options{
83113
Data: &request,
84114
Rules: govalidator.MapData{
@@ -100,7 +130,23 @@ func (validator *WebhookHandlerValidator) ValidateUpdate(_ context.Context, requ
100130
"required",
101131
webhookEventsRule,
102132
},
133+
"phone_numbers": []string{
134+
"required",
135+
multipleContactPhoneNumberRule,
136+
},
103137
},
104138
})
105-
return v.ValidateStruct()
139+
140+
result := v.ValidateStruct()
141+
if len(result) > 0 {
142+
return result
143+
}
144+
145+
for _, address := range request.PhoneNumbers {
146+
_, err := validator.phoneService.Load(ctx, userID, address)
147+
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
148+
result.Add("from", fmt.Sprintf("The phone number [%s] is not available in your account. Install the android app on your phone to store a webhook with this phone number", address))
149+
}
150+
}
151+
return result
106152
}

web/models/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ export interface EntitiesWebhook {
208208
events: string[]
209209
/** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */
210210
id: string
211+
/** @example ["[+18005550199","+18005550100]"] */
212+
phone_numbers: string[]
211213
/** @example "DGW8NwQp7mxKaSZ72Xq9v67SLqSbWQvckzzmK8D6rvd7NywSEkdMJtuxKyEkYnCY" */
212214
signing_key: string
213215
/** @example "2022-06-05T14:26:10.303278+03:00" */
@@ -327,12 +329,16 @@ export interface RequestsUserUpdate {
327329

328330
export interface RequestsWebhookStore {
329331
events: string[]
332+
/** @example ["+18005550100","+18005550100"] */
333+
phone_numbers: string[]
330334
signing_key: string
331335
url: string
332336
}
333337

334338
export interface RequestsWebhookUpdate {
335339
events: string[]
340+
/** @example ["+18005550100","+18005550100"] */
341+
phone_numbers: string[]
336342
signing_key: string
337343
url: string
338344
}

0 commit comments

Comments
 (0)