Skip to content

Commit

Permalink
Add email when API key is rotated
Browse files Browse the repository at this point in the history
  • Loading branch information
AchoArnold committed Apr 23, 2024
1 parent 99dc33a commit 2144dc5
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 2 deletions.
48 changes: 48 additions & 0 deletions api/pkg/emails/hermes_user_email_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,54 @@ type hermesUserEmailFactory struct {
generator hermes.Hermes
}

func (factory *hermesUserEmailFactory) APIKeyRotated(emailAddress string, timestamp time.Time, timezone string) (*Email, error) {
location, err := time.LoadLocation(timezone)
if err != nil {
location = time.UTC
}

email := hermes.Email{
Body: hermes.Body{
Intros: []string{
fmt.Sprintf("This is a confirmation email that your httpSMS API Key has been successfully rotated at %s.", timestamp.In(location).Format(time.RFC1123)),
},
Actions: []hermes.Action{
{
Instructions: "You can see your new API key in the httpSMS settings page.",
Button: hermes.Button{
Color: "#329ef4",
TextColor: "#FFFFFF",
Text: "httpSMS Settings",
Link: "https://httpsms.com/settings/",
},
},
},
Title: "Hey,",
Signature: "Cheers",
Outros: []string{
fmt.Sprintf("If you did not trigger this API key rotation please contact us immediately by replying to this email."),
},
},
}

html, err := factory.generator.GenerateHTML(email)
if err != nil {
return nil, stacktrace.Propagate(err, "cannot generate html email")
}

text, err := factory.generator.GeneratePlainText(email)
if err != nil {
return nil, stacktrace.Propagate(err, "cannot generate text email")
}

return &Email{
ToEmail: emailAddress,
Subject: "Your httpSMS API Key has been rotated successfully",
HTML: html,
Text: text,
}, nil
}

// UsageLimitExceeded is the email sent when the plan limit is reached
func (factory *hermesUserEmailFactory) UsageLimitExceeded(user *entities.User) (*Email, error) {
email := hermes.Email{
Expand Down
3 changes: 3 additions & 0 deletions api/pkg/emails/user_email_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ type UserEmailFactory interface {

// UsageLimitAlert sends an email when a user is approaching the limit
UsageLimitAlert(user *entities.User, usage *entities.BillingUsage) (*Email, error)

// APIKeyRotated sends an email when the API key is rotated
APIKeyRotated(email string, timestamp time.Time, timezone string) (*Email, error)
}
18 changes: 18 additions & 0 deletions api/pkg/events/user_api_key_rotated_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package events

import (
"time"

"github.com/NdoleStudio/httpsms/pkg/entities"
)

// UserAPIKeyRotated is raised when a user's API key is rotated
const UserAPIKeyRotated = "user.api-key.rotated"

// UserAPIKeyRotatedPayload stores the data for the UserAPIKeyRotated event
type UserAPIKeyRotatedPayload struct {
UserID entities.UserID `json:"user_id"`
Email string `json:"email"`
Timestamp time.Time `json:"timestamp"`
Timezone string `json:"timezone"`
}
2 changes: 1 addition & 1 deletion api/pkg/handlers/user_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func (h *UserHandler) DeleteAPIKey(c *fiber.Ctx) error {
return h.responseUnauthorized(c)
}

user, err := h.service.RotateAPIKey(ctx, h.userIDFomContext(c))
user, err := h.service.RotateAPIKey(ctx, c.OriginalURL(), h.userIDFomContext(c))
if err != nil {
msg := fmt.Sprintf("cannot rotate the api key for [%T] with ID [%s]", user, h.userIDFomContext(c))
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
Expand Down
20 changes: 20 additions & 0 deletions api/pkg/listeners/user_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func NewUserListener(
events.UserSubscriptionCancelled: l.OnUserSubscriptionCancelled,
events.UserSubscriptionUpdated: l.OnUserSubscriptionUpdated,
events.UserSubscriptionExpired: l.OnUserSubscriptionExpired,
events.UserAPIKeyRotated: l.onUserAPIKeyRotated,
}
}

Expand Down Expand Up @@ -67,6 +68,25 @@ func (listener *UserListener) onPhoneHeartbeatDead(ctx context.Context, event cl
return nil
}

// onAPIKeyRotated handles the events.UserAPIKeyRotated event
func (listener *UserListener) onUserAPIKeyRotated(ctx context.Context, event cloudevents.Event) error {
ctx, span := listener.tracer.Start(ctx)
defer span.End()

payload := new(events.UserAPIKeyRotatedPayload)
if err := event.DataAs(&payload); err != nil {
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
}

if err := listener.service.SendAPIKeyRotatedEmail(ctx, payload); err != nil {
msg := fmt.Sprintf("cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
}

return nil
}

// OnUserSubscriptionCreated handles the events.UserSubscriptionCreated event
func (listener *UserListener) OnUserSubscriptionCreated(ctx context.Context, event cloudevents.Event) error {
ctx, span := listener.tracer.Start(ctx)
Expand Down
46 changes: 45 additions & 1 deletion api/pkg/services/user_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type UserService struct {
emailFactory emails.UserEmailFactory
mailer emails.Mailer
repository repositories.UserRepository
dispatcher *EventDispatcher
marketingService *MarketingService
lemonsqueezyClient *lemonsqueezy.Client
}
Expand All @@ -39,6 +40,7 @@ func NewUserService(
emailFactory emails.UserEmailFactory,
marketingService *MarketingService,
lemonsqueezyClient *lemonsqueezy.Client,
dispatcher *EventDispatcher,
) (s *UserService) {
return &UserService{
logger: logger.WithService(fmt.Sprintf("%T", s)),
Expand All @@ -47,6 +49,7 @@ func NewUserService(
marketingService: marketingService,
emailFactory: emailFactory,
repository: repository,
dispatcher: dispatcher,
lemonsqueezyClient: lemonsqueezyClient,
}
}
Expand Down Expand Up @@ -150,7 +153,7 @@ func (service *UserService) UpdateNotificationSettings(ctx context.Context, user
}

// RotateAPIKey for an entities.User
func (service *UserService) RotateAPIKey(ctx context.Context, userID entities.UserID) (*entities.User, error) {
func (service *UserService) RotateAPIKey(ctx context.Context, source string, userID entities.UserID) (*entities.User, error) {
ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
defer span.End()

Expand All @@ -161,9 +164,50 @@ func (service *UserService) RotateAPIKey(ctx context.Context, userID entities.Us
}

ctxLogger.Info(fmt.Sprintf("rotated the api key for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository))

event, err := service.createEvent(events.UserAPIKeyRotated, source, &events.UserAPIKeyRotatedPayload{
UserID: user.ID,
Email: user.Email,
Timestamp: time.Now().UTC(),
Timezone: user.Timezone,
})
if err != nil {
msg := fmt.Sprintf("cannot create event [%s] for user [%s]", events.UserAPIKeyRotated, user.ID)
ctxLogger.Error(stacktrace.Propagate(err, msg))
return user, nil
}

if err = service.dispatcher.Dispatch(ctx, event); err != nil {
msg := fmt.Sprintf("cannot dispatch [%s] event for user [%s]", event.Type(), user.ID)
ctxLogger.Error(stacktrace.Propagate(err, msg))
return user, nil
}

return user, nil
}

// SendAPIKeyRotatedEmail sends an email to an entities.User when the API key is rotated
func (service *UserService) SendAPIKeyRotatedEmail(ctx context.Context, payload *events.UserAPIKeyRotatedPayload) error {
ctx, span := service.tracer.Start(ctx)
defer span.End()

ctxLogger := service.tracer.CtxLogger(service.logger, span)

email, err := service.emailFactory.APIKeyRotated(payload.Email, payload.Timestamp, payload.Timezone)
if err != nil {
msg := fmt.Sprintf("cannot create api key rotated email for user [%s]", payload.UserID)
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
}

if err = service.mailer.Send(ctx, email); err != nil {
msg := fmt.Sprintf("canot create api key rotated email to user [%s]", payload.UserID)
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
}

ctxLogger.Info(fmt.Sprintf("api key rotated email sent successfully to [%s] with user ID [%s]", payload.Email, payload.UserID))
return nil
}

// UserSendPhoneDeadEmailParams are parameters for notifying a user when a phone is dead
type UserSendPhoneDeadEmailParams struct {
UserID entities.UserID
Expand Down

0 comments on commit 2144dc5

Please sign in to comment.