Skip to content

Commit 2dac7f7

Browse files
committed
Add api hanlders for the phone API key endpoint
1 parent ad4f82b commit 2dac7f7

12 files changed

+421
-22
lines changed

api/pkg/handlers/discord_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func (h *DiscordHandler) Delete(c *fiber.Ctx) error {
128128
defer span.End()
129129

130130
discordID := c.Params("discordID")
131-
if errors := h.validator.ValidateUUID(ctx, discordID, "discordID"); len(errors) != 0 {
131+
if errors := h.validator.ValidateUUID(discordID, "discordID"); len(errors) != 0 {
132132
msg := fmt.Sprintf("validation errors [%s], while deleting discord integration with ID [%s]", spew.Sdump(errors), discordID)
133133
ctxLogger.Warn(stacktrace.NewError(msg))
134134
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting discord integration")

api/pkg/handlers/handler.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,15 @@ func (h *handler) userIDFomContext(c *fiber.Ctx) entities.UserID {
115115
func (h *handler) computeRoute(middlewares []fiber.Handler, route fiber.Handler) []fiber.Handler {
116116
return append(append([]fiber.Handler{}, middlewares...), route)
117117
}
118+
119+
func (h *handler) mergeErrors(errors ...url.Values) url.Values {
120+
result := url.Values{}
121+
for _, item := range errors {
122+
for key, values := range item {
123+
for _, value := range values {
124+
result.Add(key, value)
125+
}
126+
}
127+
}
128+
return result
129+
}

api/pkg/handlers/message_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ func (h *MessageHandler) Delete(c *fiber.Ctx) error {
394394
ctxLogger := h.tracer.CtxLogger(h.logger, span)
395395

396396
messageID := c.Params("messageID")
397-
if errors := h.validator.ValidateUUID(ctx, messageID, "messageID"); len(errors) != 0 {
397+
if errors := h.validator.ValidateUUID(messageID, "messageID"); len(errors) != 0 {
398398
msg := fmt.Sprintf("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID)
399399
ctxLogger.Warn(stacktrace.NewError(msg))
400400
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")

api/pkg/handlers/message_thread_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (h *MessageThreadHandler) Delete(c *fiber.Ctx) error {
157157
defer span.End()
158158

159159
messageThreadID := c.Params("messageThreadID")
160-
if errors := h.validator.ValidateUUID(ctx, messageThreadID, "messageThreadID"); len(errors) != 0 {
160+
if errors := h.validator.ValidateUUID(messageThreadID, "messageThreadID"); len(errors) != 0 {
161161
msg := fmt.Sprintf("validation errors [%s], while deleting a thread thread with ID [%s]", spew.Sdump(errors), messageThreadID)
162162
ctxLogger.Warn(stacktrace.NewError(msg))
163163
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting a thread thread")
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package handlers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/repositories"
7+
"github.com/NdoleStudio/httpsms/pkg/requests"
8+
"github.com/NdoleStudio/httpsms/pkg/services"
9+
"github.com/NdoleStudio/httpsms/pkg/telemetry"
10+
"github.com/NdoleStudio/httpsms/pkg/validators"
11+
"github.com/davecgh/go-spew/spew"
12+
"github.com/gofiber/fiber/v2"
13+
"github.com/google/uuid"
14+
"github.com/palantir/stacktrace"
15+
)
16+
17+
// PhoneAPIKeyHandler handles phone API key http requests
18+
type PhoneAPIKeyHandler struct {
19+
handler
20+
logger telemetry.Logger
21+
tracer telemetry.Tracer
22+
validator *validators.PhoneAPIKeyHandlerValidator
23+
service *services.PhoneAPIKeyService
24+
phoneService *services.PhoneService
25+
}
26+
27+
// NewPhoneAPIKeyHandler creates a new PhoneAPIKeyHandler
28+
func NewPhoneAPIKeyHandler(
29+
logger telemetry.Logger,
30+
tracer telemetry.Tracer,
31+
validator *validators.PhoneAPIKeyHandlerValidator,
32+
service *services.PhoneAPIKeyService,
33+
phoneService *services.PhoneService,
34+
) *PhoneAPIKeyHandler {
35+
return &PhoneAPIKeyHandler{
36+
logger: logger.WithService(fmt.Sprintf("%T", &PhoneAPIKeyHandler{})),
37+
tracer: tracer,
38+
validator: validator,
39+
service: service,
40+
phoneService: phoneService,
41+
}
42+
}
43+
44+
// RegisterRoutes registers the routes for the PhoneAPIKeyHandler
45+
func (h *PhoneAPIKeyHandler) RegisterRoutes(app *fiber.App, middlewares ...fiber.Handler) {
46+
router := app.Group("/v1/api-keys/")
47+
router.Post("/", h.computeRoute(middlewares, h.Store)...)
48+
router.Delete("/:phoneAPIKeyID", h.computeRoute(middlewares, h.Delete)...)
49+
router.Delete("/:phoneAPIKeyID/phones/:phoneID", h.computeRoute(middlewares, h.DeletePhone)...)
50+
}
51+
52+
// Store a new Phone API key
53+
// @Summary Store phone API key
54+
// @Description Creates a new phone API key which can be used to log in to the httpSMS app on your Android phone
55+
// @Security ApiKeyAuth
56+
// @Tags PhoneAPIKeys
57+
// @Accept json
58+
// @Produce json
59+
// @Param payload body requests.PhoneAPIKeyStoreRequest true "Payload of new phone API key."
60+
// @Success 200 {object} responses.Ok[*entities.PhoneAPIKey]
61+
// @Failure 400 {object} responses.BadRequest
62+
// @Failure 401 {object} responses.Unauthorized
63+
// @Failure 422 {object} responses.UnprocessableEntity
64+
// @Failure 500 {object} responses.InternalServerError
65+
// @Router /api-keys [post]
66+
func (h *PhoneAPIKeyHandler) Store(c *fiber.Ctx) error {
67+
ctx, span := h.tracer.StartFromFiberCtx(c)
68+
defer span.End()
69+
70+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
71+
72+
var request requests.PhoneAPIKeyStoreRequest
73+
if err := c.BodyParser(&request); err != nil {
74+
msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request)
75+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
76+
return h.responseBadRequest(c, err)
77+
}
78+
79+
if errors := h.validator.ValidateStore(ctx, request.Sanitize()); len(errors) != 0 {
80+
msg := fmt.Sprintf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)
81+
ctxLogger.Warn(stacktrace.NewError(msg))
82+
return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones")
83+
}
84+
85+
phone, err := h.service.Create(ctx, h.userFromContext(c), request.Name)
86+
if err != nil {
87+
msg := fmt.Sprintf("cannot update phones with params [%+#v]", request)
88+
ctxLogger.Error(stacktrace.Propagate(err, msg))
89+
return h.responseInternalServerError(c)
90+
}
91+
92+
return h.responseOK(c, "phone updated successfully", phone)
93+
}
94+
95+
// Delete a phone API Key
96+
// @Summary Delete a phone API key from the database.
97+
// @Description Delete a phone API Key from the database and cannot be used for authentication anymore.
98+
// @Security ApiKeyAuth
99+
// @Tags PhoneAPIKeys
100+
// @Accept json
101+
// @Produce json
102+
// @Param phoneAPIKeyID path string true "ID of the phone API key" default(32343a19-da5e-4b1b-a767-3298a73703ca)
103+
// @Success 204 {object} responses.NoContent
104+
// @Failure 400 {object} responses.BadRequest
105+
// @Failure 401 {object} responses.Unauthorized
106+
// @Failure 404 {object} responses.NotFound
107+
// @Failure 422 {object} responses.UnprocessableEntity
108+
// @Failure 500 {object} responses.InternalServerError
109+
// @Router /messages/{phoneAPIKeyID} [delete]
110+
func (h *PhoneAPIKeyHandler) Delete(c *fiber.Ctx) error {
111+
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
112+
defer span.End()
113+
114+
phoneAPIKeyID := c.Params("phoneAPIKeyID")
115+
if errors := h.validator.ValidateUUID(phoneAPIKeyID, "phoneAPIKeyID"); len(errors) != 0 {
116+
msg := fmt.Sprintf("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID)
117+
ctxLogger.Warn(stacktrace.NewError(msg))
118+
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")
119+
}
120+
121+
err := h.service.Delete(ctx, h.userIDFomContext(c), uuid.MustParse(phoneAPIKeyID))
122+
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
123+
return h.responseNotFound(c, fmt.Sprintf("cannot find phone API key with ID [%s]", phoneAPIKeyID))
124+
}
125+
126+
if err != nil {
127+
msg := fmt.Sprintf("cannot delete phone API key with ID [%s] for user with ID [%s]", phoneAPIKeyID, h.userIDFomContext(c))
128+
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
129+
return h.responseInternalServerError(c)
130+
}
131+
132+
return h.responseNoContent(c, "phone API key deleted successfully")
133+
}
134+
135+
// DeletePhone removes a phone from a phone API key
136+
// @Summary Remove the association of a phone from the phone API key.
137+
// @Description You will need to login again to the httpSMS app on your Android phone with a new phone API key.
138+
// @Security ApiKeyAuth
139+
// @Tags PhoneAPIKeys
140+
// @Accept json
141+
// @Produce json
142+
// @Param phoneAPIKeyID path string true "ID of the phone API key" default(32343a19-da5e-4b1b-a767-3298a73703ca)
143+
// @Param phoneID path string true "ID of the phone" default(32343a19-da5e-4b1b-a767-3298a73703ca)
144+
// @Success 204 {object} responses.NoContent
145+
// @Failure 400 {object} responses.BadRequest
146+
// @Failure 401 {object} responses.Unauthorized
147+
// @Failure 404 {object} responses.NotFound
148+
// @Failure 422 {object} responses.UnprocessableEntity
149+
// @Failure 500 {object} responses.InternalServerError
150+
// @Router /messages/{phoneAPIKeyID}/phones/{phoneID} [delete]
151+
func (h *PhoneAPIKeyHandler) DeletePhone(c *fiber.Ctx) error {
152+
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
153+
defer span.End()
154+
155+
phoneAPIKeyID := c.Params("phoneAPIKeyID")
156+
phoneID := c.Params("phoneID")
157+
if errors := h.mergeErrors(h.validator.ValidateUUID(phoneAPIKeyID, "phoneAPIKeyID"), h.validator.ValidateUUID(phoneID, "phoneID")); len(errors) != 0 {
158+
msg := fmt.Sprintf("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID)
159+
ctxLogger.Warn(stacktrace.NewError(msg))
160+
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")
161+
}
162+
163+
err := h.service.RemovePhone(ctx, h.userIDFomContext(c), uuid.MustParse(phoneAPIKeyID), uuid.MustParse(phoneID))
164+
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
165+
return h.responseNotFound(c, fmt.Sprintf("cannot find phone with ID [%s] which is associated with phone API key with ID [%s]", phoneID, phoneAPIKeyID))
166+
}
167+
168+
if err != nil {
169+
msg := fmt.Sprintf("cannot remove phone with ID [%s] from phone API key with ID [%s] for user with ID [%s]", phoneID, phoneAPIKeyID, h.userIDFomContext(c))
170+
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
171+
return h.responseInternalServerError(c)
172+
}
173+
174+
return h.responseNoContent(c, "phone has been dissociated from phone API key successfully")
175+
}

api/pkg/handlers/webhook_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (h *WebhookHandler) Delete(c *fiber.Ctx) error {
111111
defer span.End()
112112

113113
webhookID := c.Params("webhookID")
114-
if errors := h.validator.ValidateUUID(ctx, webhookID, "webhookID"); len(errors) != 0 {
114+
if errors := h.validator.ValidateUUID(webhookID, "webhookID"); len(errors) != 0 {
115115
msg := fmt.Sprintf("validation errors [%s], while deleting webhook with ID [%s]", spew.Sdump(errors), webhookID)
116116
ctxLogger.Warn(stacktrace.NewError(msg))
117117
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting webhook")

api/pkg/repositories/gorm_phone_api_key_repository.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,26 @@ func NewGormPhoneAPIKeyRepository(
3737
}
3838
}
3939

40+
// Load an entities.Integration3CX based on the entities.UserID
41+
func (repository *gormPhoneAPIKeyRepository) Load(ctx context.Context, userID entities.UserID, phoneAPIKeyID uuid.UUID) (*entities.PhoneAPIKey, error) {
42+
ctx, span := repository.tracer.Start(ctx)
43+
defer span.End()
44+
45+
phoneAPIKey := new(entities.PhoneAPIKey)
46+
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", phoneAPIKeyID).First(&phoneAPIKey).Error
47+
if errors.Is(err, gorm.ErrRecordNotFound) {
48+
msg := fmt.Sprintf("[%T] with ID [%s] for user with ID [%s] does not exist", phoneAPIKey, phoneAPIKeyID, userID)
49+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg))
50+
}
51+
52+
if err != nil {
53+
msg := fmt.Sprintf("cannot load [%T] with ID [%s] for user with ID [%s]", phoneAPIKey, phoneAPIKeyID, userID)
54+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
55+
}
56+
57+
return phoneAPIKey, nil
58+
}
59+
4060
func (repository *gormPhoneAPIKeyRepository) Create(ctx context.Context, phoneAPIKey *entities.PhoneAPIKey) error {
4161
ctx, span := repository.tracer.Start(ctx)
4262
defer span.End()
@@ -77,7 +97,7 @@ func (repository *gormPhoneAPIKeyRepository) LoadAuthContext(ctx context.Context
7797
PhoneNumbers: phoneAPIKey.PhoneNumbers,
7898
}
7999

80-
if result := repository.cache.SetWithTTL(apiKey, authUser, 1, 2*time.Hour); !result {
100+
if result := repository.cache.SetWithTTL(apiKey, authUser, 1, 1*time.Hour); !result {
81101
msg := fmt.Sprintf("cannot cache [%T] with ID [%s] and result [%t]", authUser, phoneAPIKey.ID, result)
82102
ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewError(msg)))
83103
}
@@ -104,23 +124,21 @@ func (repository *gormPhoneAPIKeyRepository) Index(ctx context.Context, userID e
104124
return *phoneAPIKeys, nil
105125
}
106126

107-
func (repository *gormPhoneAPIKeyRepository) Delete(ctx context.Context, userID entities.UserID, phoneAPIKeyID uuid.UUID) error {
127+
func (repository *gormPhoneAPIKeyRepository) Delete(ctx context.Context, phoneAPIKey *entities.PhoneAPIKey) error {
108128
ctx, span := repository.tracer.Start(ctx)
109129
defer span.End()
110130

111-
err := repository.db.WithContext(ctx).
112-
Where("user_id = ?", userID).
113-
Where("id = ?", phoneAPIKeyID).
114-
Delete(&entities.PhoneAPIKey{}).Error
131+
err := repository.db.WithContext(ctx).Delete(phoneAPIKey).Error
115132
if err != nil {
116-
msg := fmt.Sprintf("cannot delete phone API key with ID [%s] and userID [%s]", phoneAPIKeyID, userID)
133+
msg := fmt.Sprintf("cannot delete phone API key with ID [%s] and userID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID)
117134
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
118135
}
136+
repository.cache.Del(phoneAPIKey.APIKey)
119137

120138
return nil
121139
}
122140

123-
func (repository *gormPhoneAPIKeyRepository) AddPhone(ctx context.Context, authContext entities.AuthContext, phone *entities.Phone) error {
141+
func (repository *gormPhoneAPIKeyRepository) AddPhone(ctx context.Context, authContext entities.AuthContext, phoneID uuid.UUID, phoneNumber string) error {
124142
ctx, span := repository.tracer.Start(ctx)
125143
defer span.End()
126144

@@ -132,17 +150,19 @@ WHERE array_position(phone_ids, ?) IS NULL AND id = ?;
132150
`
133151

134152
err := repository.db.WithContext(ctx).
135-
Raw(query, (entities.PhoneAPIKey{}).TableName(), phone.ID, phone.PhoneNumber, phone.ID, *authContext.PhoneAPIKeyID).
153+
Raw(query, (entities.PhoneAPIKey{}).TableName(), phoneID, phoneNumber, phoneID, *authContext.PhoneAPIKeyID).
136154
Error
137155
if err != nil {
138-
msg := fmt.Sprintf("cannot add phone with ID [%s] to phone API key with ID [%s]", phone.ID, *authContext.PhoneAPIKeyID)
156+
msg := fmt.Sprintf("cannot add phone with ID [%s] to phone API key with ID [%s]", phoneID, *authContext.PhoneAPIKeyID)
139157
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
140158
}
141159

160+
repository.cache.Clear()
161+
142162
return nil
143163
}
144164

145-
func (repository *gormPhoneAPIKeyRepository) RemovePhone(ctx context.Context, authContext entities.AuthContext, phone *entities.Phone) error {
165+
func (repository *gormPhoneAPIKeyRepository) RemovePhone(ctx context.Context, phoneAPIKey *entities.PhoneAPIKey, phone *entities.Phone) error {
146166
ctx, span := repository.tracer.Start(ctx)
147167
defer span.End()
148168

@@ -153,13 +173,15 @@ SET phone_ids = array_remove(phone_ids, ?),
153173
WHERE id = ?;
154174
`
155175
err := repository.db.WithContext(ctx).
156-
Raw(query, (entities.PhoneAPIKey{}).TableName(), phone.ID, phone.PhoneNumber, *authContext.PhoneAPIKeyID).
176+
Raw(query, (entities.PhoneAPIKey{}).TableName(), phone.ID, phone.PhoneNumber, phoneAPIKey.ID).
157177
Error
158178
if err != nil {
159-
msg := fmt.Sprintf("cannot remove phone with ID [%s] to phone API key with ID [%s]", phone.ID, *authContext.PhoneAPIKeyID)
179+
msg := fmt.Sprintf("cannot remove phone with ID [%s] from phone API key with ID [%s]", phone.ID, phoneAPIKey.ID)
160180
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
161181
}
162182

183+
repository.cache.Clear()
184+
163185
return nil
164186
}
165187

api/pkg/repositories/phone_api_key_repository.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@ type PhoneAPIKeyRepository interface {
1313
// Create a new entities.PhoneAPIKey
1414
Create(ctx context.Context, phone *entities.PhoneAPIKey) error
1515

16+
// Load an entities.PhoneAPIKey by userID and phoneAPIKeyID
17+
Load(ctx context.Context, userID entities.UserID, phoneAPIKeyID uuid.UUID) (*entities.PhoneAPIKey, error)
18+
1619
// LoadAuthContext fetches an entities.AuthContext by apiKey
1720
LoadAuthContext(ctx context.Context, apiKey string) (entities.AuthContext, error)
1821

1922
// Index entities.PhoneAPIKey of a user
2023
Index(ctx context.Context, userID entities.UserID, params IndexParams) ([]*entities.PhoneAPIKey, error)
2124

2225
// Delete an entities.PhoneAPIKey
23-
Delete(ctx context.Context, userID entities.UserID, phoneAPIKeyID uuid.UUID) error
26+
Delete(ctx context.Context, phoneAPIKey *entities.PhoneAPIKey) error
27+
28+
// AddPhone adds an entities.Phone to an entities.PhoneAPIKey
29+
AddPhone(ctx context.Context, authContext entities.AuthContext, phoneID uuid.UUID, phoneNumber string) error
2430

25-
// AddPhone an entities.Phone to an entities.PhoneAPIKey
26-
AddPhone(ctx context.Context, authContext entities.AuthContext, phone *entities.Phone) error
31+
// RemovePhone removes an entities.Phone to an entities.PhoneAPIKey
32+
RemovePhone(ctx context.Context, phoneAPIKey *entities.PhoneAPIKey, phone *entities.Phone) error
2733

2834
// DeleteAllForUser deletes all entities.PhoneAPIKey for a user
2935
DeleteAllForUser(ctx context.Context, userID entities.UserID) error
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package requests
2+
3+
// PhoneAPIKeyStoreRequest is the payload for storing a phone API key
4+
type PhoneAPIKeyStoreRequest struct {
5+
request
6+
Name string `json:"name" example:"My Phone API Key"`
7+
}
8+
9+
// Sanitize sets defaults to MessageReceive
10+
func (input *PhoneAPIKeyStoreRequest) Sanitize() PhoneAPIKeyStoreRequest {
11+
input.Name = input.sanitizeAddress(input.Name)
12+
return *input
13+
}

0 commit comments

Comments
 (0)