diff --git a/api/go.mod b/api/go.mod index 08f6a5c0..1fe7dca1 100644 --- a/api/go.mod +++ b/api/go.mod @@ -41,7 +41,6 @@ require ( github.com/redis/go-redis/extra/redisotel/v9 v9.19.0 github.com/redis/go-redis/v9 v9.19.0 github.com/rs/zerolog v1.35.1 - github.com/schollz/progressbar/v3 v3.19.0 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag v1.16.6 github.com/thedevsaddam/govalidator v1.9.10 @@ -142,7 +141,6 @@ require ( github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-sqlite3 v1.14.44 // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -155,7 +153,6 @@ require ( github.com/redis/go-redis/extra/rediscmd/v9 v9.19.0 // indirect github.com/richardlehane/mscfb v1.0.6 // indirect github.com/richardlehane/msoleps v1.0.6 // indirect - github.com/rivo/uniseg v0.4.7 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -196,7 +193,6 @@ require ( golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.44.0 // indirect - golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect diff --git a/api/go.sum b/api/go.sum index 0f6dcce5..fa1163a2 100644 --- a/api/go.sum +++ b/api/go.sum @@ -78,8 +78,6 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= -github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= @@ -247,8 +245,6 @@ github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3Ry github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -295,14 +291,10 @@ github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= -github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= -github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -485,8 +477,6 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 76304d3e..df2e5d6f 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -1264,6 +1264,7 @@ func (container *Container) PhoneAPIKeyHandler() (handler *handlers.PhoneAPIKeyH container.Tracer(), container.PhoneAPIKeyHandlerValidator(), container.PhoneAPIKeyService(), + container.EntitlementService(), ) } diff --git a/api/pkg/handlers/phone_api_key_handler.go b/api/pkg/handlers/phone_api_key_handler.go index c10df513..53df0ef3 100644 --- a/api/pkg/handlers/phone_api_key_handler.go +++ b/api/pkg/handlers/phone_api_key_handler.go @@ -17,10 +17,11 @@ import ( // PhoneAPIKeyHandler handles phone API key http requests type PhoneAPIKeyHandler struct { handler - logger telemetry.Logger - tracer telemetry.Tracer - validator *validators.PhoneAPIKeyHandlerValidator - service *services.PhoneAPIKeyService + logger telemetry.Logger + tracer telemetry.Tracer + validator *validators.PhoneAPIKeyHandlerValidator + service *services.PhoneAPIKeyService + entitlementService *services.EntitlementService } // NewPhoneAPIKeyHandler creates a new PhoneAPIKeyHandler @@ -29,12 +30,14 @@ func NewPhoneAPIKeyHandler( tracer telemetry.Tracer, validator *validators.PhoneAPIKeyHandlerValidator, service *services.PhoneAPIKeyService, + entitlementService *services.EntitlementService, ) *PhoneAPIKeyHandler { return &PhoneAPIKeyHandler{ - logger: logger.WithService(fmt.Sprintf("%T", &PhoneAPIKeyHandler{})), - tracer: tracer, - validator: validator, - service: service, + logger: logger.WithService(fmt.Sprintf("%T", &PhoneAPIKeyHandler{})), + tracer: tracer, + validator: validator, + service: service, + entitlementService: entitlementService, } } @@ -99,6 +102,7 @@ func (h *PhoneAPIKeyHandler) index(c *fiber.Ctx) error { // @Success 200 {object} responses.PhoneAPIKeyResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized +// @Failure 402 {object} responses.PaymentRequired // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /phone-api-keys [post] @@ -106,6 +110,19 @@ func (h *PhoneAPIKeyHandler) store(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() + userID := h.userIDFomContext(c) + + result, err := h.entitlementService.Check(ctx, userID, "PhoneAPIKey", func() (int, error) { + return h.service.CountByUser(ctx, userID) + }) + if err != nil { + ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot check entitlement for phone API keys for user [%s]", userID))) + return h.responseInternalServerError(c) + } + if !result.Allowed { + return h.responsePaymentRequired(c, result.Message) + } + var request requests.PhoneAPIKeyStoreRequest if err := c.BodyParser(&request); err != nil { msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request) diff --git a/api/pkg/repositories/gorm_phone_api_key_repository.go b/api/pkg/repositories/gorm_phone_api_key_repository.go index 3c306f5a..68692a04 100644 --- a/api/pkg/repositories/gorm_phone_api_key_repository.go +++ b/api/pkg/repositories/gorm_phone_api_key_repository.go @@ -61,6 +61,23 @@ WHERE user_id = ? AND array_position(phone_ids, ?) IS NOT NULL; return nil } +// CountByUser returns the number of phone API keys owned by a user. +func (repository *gormPhoneAPIKeyRepository) CountByUser(ctx context.Context, userID entities.UserID) (int, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + var count int64 + err := repository.db.WithContext(ctx). + Model(&entities.PhoneAPIKey{}). + Where("user_id = ?", userID). + Count(&count).Error + if err != nil { + return 0, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot count phone API keys for user [%s]", userID)) + } + + return int(count), nil +} + // Load an entities.PhoneAPIKey based on the entities.UserID func (repository *gormPhoneAPIKeyRepository) Load(ctx context.Context, userID entities.UserID, phoneAPIKeyID uuid.UUID) (*entities.PhoneAPIKey, error) { ctx, span := repository.tracer.Start(ctx) diff --git a/api/pkg/repositories/phone_api_key_repository.go b/api/pkg/repositories/phone_api_key_repository.go index 8894e4ac..ceecda85 100644 --- a/api/pkg/repositories/phone_api_key_repository.go +++ b/api/pkg/repositories/phone_api_key_repository.go @@ -31,6 +31,9 @@ type PhoneAPIKeyRepository interface { // RemovePhone removes an entities.Phone to an entities.PhoneAPIKey RemovePhone(ctx context.Context, phoneAPIKey *entities.PhoneAPIKey, phone *entities.Phone) error + // CountByUser returns the number of phone API keys owned by a user + CountByUser(ctx context.Context, userID entities.UserID) (int, error) + // DeleteAllForUser deletes all entities.PhoneAPIKey for a user DeleteAllForUser(ctx context.Context, userID entities.UserID) error diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index 59f077a2..c67d4803 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -19,6 +19,9 @@ var entityLimits = map[string]map[entities.SubscriptionName]int{ "MessageSendSchedule": { entities.SubscriptionNameFree: 1, }, + "PhoneAPIKey": { + entities.SubscriptionNameFree: 1, + }, } // EntitlementCheckResult holds the outcome of an entitlement check. diff --git a/api/pkg/services/phone_api_key_service.go b/api/pkg/services/phone_api_key_service.go index 5a148583..c9283fdd 100644 --- a/api/pkg/services/phone_api_key_service.go +++ b/api/pkg/services/phone_api_key_service.go @@ -40,6 +40,14 @@ func NewPhoneAPIKeyService( } } +// CountByUser returns the number of phone API keys owned by a user. +func (service *PhoneAPIKeyService) CountByUser(ctx context.Context, userID entities.UserID) (int, error) { + ctx, span := service.tracer.Start(ctx) + defer span.End() + + return service.repository.CountByUser(ctx, userID) +} + // Index fetches the entities.Webhook for an entities.UserID func (service *PhoneAPIKeyService) Index(ctx context.Context, userID entities.UserID, params repositories.IndexParams) ([]*entities.PhoneAPIKey, error) { ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)