Skip to content

Commit 2144dc5

Browse files
committed
Add email when API key is rotated
1 parent 99dc33a commit 2144dc5

6 files changed

Lines changed: 135 additions & 2 deletions

File tree

api/pkg/emails/hermes_user_email_factory.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,54 @@ type hermesUserEmailFactory struct {
1515
generator hermes.Hermes
1616
}
1717

18+
func (factory *hermesUserEmailFactory) APIKeyRotated(emailAddress string, timestamp time.Time, timezone string) (*Email, error) {
19+
location, err := time.LoadLocation(timezone)
20+
if err != nil {
21+
location = time.UTC
22+
}
23+
24+
email := hermes.Email{
25+
Body: hermes.Body{
26+
Intros: []string{
27+
fmt.Sprintf("This is a confirmation email that your httpSMS API Key has been successfully rotated at %s.", timestamp.In(location).Format(time.RFC1123)),
28+
},
29+
Actions: []hermes.Action{
30+
{
31+
Instructions: "You can see your new API key in the httpSMS settings page.",
32+
Button: hermes.Button{
33+
Color: "#329ef4",
34+
TextColor: "#FFFFFF",
35+
Text: "httpSMS Settings",
36+
Link: "https://httpsms.com/settings/",
37+
},
38+
},
39+
},
40+
Title: "Hey,",
41+
Signature: "Cheers",
42+
Outros: []string{
43+
fmt.Sprintf("If you did not trigger this API key rotation please contact us immediately by replying to this email."),
44+
},
45+
},
46+
}
47+
48+
html, err := factory.generator.GenerateHTML(email)
49+
if err != nil {
50+
return nil, stacktrace.Propagate(err, "cannot generate html email")
51+
}
52+
53+
text, err := factory.generator.GeneratePlainText(email)
54+
if err != nil {
55+
return nil, stacktrace.Propagate(err, "cannot generate text email")
56+
}
57+
58+
return &Email{
59+
ToEmail: emailAddress,
60+
Subject: "Your httpSMS API Key has been rotated successfully",
61+
HTML: html,
62+
Text: text,
63+
}, nil
64+
}
65+
1866
// UsageLimitExceeded is the email sent when the plan limit is reached
1967
func (factory *hermesUserEmailFactory) UsageLimitExceeded(user *entities.User) (*Email, error) {
2068
email := hermes.Email{

api/pkg/emails/user_email_factory.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ type UserEmailFactory interface {
1616

1717
// UsageLimitAlert sends an email when a user is approaching the limit
1818
UsageLimitAlert(user *entities.User, usage *entities.BillingUsage) (*Email, error)
19+
20+
// APIKeyRotated sends an email when the API key is rotated
21+
APIKeyRotated(email string, timestamp time.Time, timezone string) (*Email, error)
1922
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
)
8+
9+
// UserAPIKeyRotated is raised when a user's API key is rotated
10+
const UserAPIKeyRotated = "user.api-key.rotated"
11+
12+
// UserAPIKeyRotatedPayload stores the data for the UserAPIKeyRotated event
13+
type UserAPIKeyRotatedPayload struct {
14+
UserID entities.UserID `json:"user_id"`
15+
Email string `json:"email"`
16+
Timestamp time.Time `json:"timestamp"`
17+
Timezone string `json:"timezone"`
18+
}

api/pkg/handlers/user_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ func (h *UserHandler) DeleteAPIKey(c *fiber.Ctx) error {
241241
return h.responseUnauthorized(c)
242242
}
243243

244-
user, err := h.service.RotateAPIKey(ctx, h.userIDFomContext(c))
244+
user, err := h.service.RotateAPIKey(ctx, c.OriginalURL(), h.userIDFomContext(c))
245245
if err != nil {
246246
msg := fmt.Sprintf("cannot rotate the api key for [%T] with ID [%s]", user, h.userIDFomContext(c))
247247
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))

api/pkg/listeners/user_listener.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func NewUserListener(
3838
events.UserSubscriptionCancelled: l.OnUserSubscriptionCancelled,
3939
events.UserSubscriptionUpdated: l.OnUserSubscriptionUpdated,
4040
events.UserSubscriptionExpired: l.OnUserSubscriptionExpired,
41+
events.UserAPIKeyRotated: l.onUserAPIKeyRotated,
4142
}
4243
}
4344

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

71+
// onAPIKeyRotated handles the events.UserAPIKeyRotated event
72+
func (listener *UserListener) onUserAPIKeyRotated(ctx context.Context, event cloudevents.Event) error {
73+
ctx, span := listener.tracer.Start(ctx)
74+
defer span.End()
75+
76+
payload := new(events.UserAPIKeyRotatedPayload)
77+
if err := event.DataAs(&payload); err != nil {
78+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
79+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
80+
}
81+
82+
if err := listener.service.SendAPIKeyRotatedEmail(ctx, payload); err != nil {
83+
msg := fmt.Sprintf("cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())
84+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
85+
}
86+
87+
return nil
88+
}
89+
7090
// OnUserSubscriptionCreated handles the events.UserSubscriptionCreated event
7191
func (listener *UserListener) OnUserSubscriptionCreated(ctx context.Context, event cloudevents.Event) error {
7292
ctx, span := listener.tracer.Start(ctx)

api/pkg/services/user_service.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type UserService struct {
2626
emailFactory emails.UserEmailFactory
2727
mailer emails.Mailer
2828
repository repositories.UserRepository
29+
dispatcher *EventDispatcher
2930
marketingService *MarketingService
3031
lemonsqueezyClient *lemonsqueezy.Client
3132
}
@@ -39,6 +40,7 @@ func NewUserService(
3940
emailFactory emails.UserEmailFactory,
4041
marketingService *MarketingService,
4142
lemonsqueezyClient *lemonsqueezy.Client,
43+
dispatcher *EventDispatcher,
4244
) (s *UserService) {
4345
return &UserService{
4446
logger: logger.WithService(fmt.Sprintf("%T", s)),
@@ -47,6 +49,7 @@ func NewUserService(
4749
marketingService: marketingService,
4850
emailFactory: emailFactory,
4951
repository: repository,
52+
dispatcher: dispatcher,
5053
lemonsqueezyClient: lemonsqueezyClient,
5154
}
5255
}
@@ -150,7 +153,7 @@ func (service *UserService) UpdateNotificationSettings(ctx context.Context, user
150153
}
151154

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

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

163166
ctxLogger.Info(fmt.Sprintf("rotated the api key for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository))
167+
168+
event, err := service.createEvent(events.UserAPIKeyRotated, source, &events.UserAPIKeyRotatedPayload{
169+
UserID: user.ID,
170+
Email: user.Email,
171+
Timestamp: time.Now().UTC(),
172+
Timezone: user.Timezone,
173+
})
174+
if err != nil {
175+
msg := fmt.Sprintf("cannot create event [%s] for user [%s]", events.UserAPIKeyRotated, user.ID)
176+
ctxLogger.Error(stacktrace.Propagate(err, msg))
177+
return user, nil
178+
}
179+
180+
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
181+
msg := fmt.Sprintf("cannot dispatch [%s] event for user [%s]", event.Type(), user.ID)
182+
ctxLogger.Error(stacktrace.Propagate(err, msg))
183+
return user, nil
184+
}
185+
164186
return user, nil
165187
}
166188

189+
// SendAPIKeyRotatedEmail sends an email to an entities.User when the API key is rotated
190+
func (service *UserService) SendAPIKeyRotatedEmail(ctx context.Context, payload *events.UserAPIKeyRotatedPayload) error {
191+
ctx, span := service.tracer.Start(ctx)
192+
defer span.End()
193+
194+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
195+
196+
email, err := service.emailFactory.APIKeyRotated(payload.Email, payload.Timestamp, payload.Timezone)
197+
if err != nil {
198+
msg := fmt.Sprintf("cannot create api key rotated email for user [%s]", payload.UserID)
199+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
200+
}
201+
202+
if err = service.mailer.Send(ctx, email); err != nil {
203+
msg := fmt.Sprintf("canot create api key rotated email to user [%s]", payload.UserID)
204+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
205+
}
206+
207+
ctxLogger.Info(fmt.Sprintf("api key rotated email sent successfully to [%s] with user ID [%s]", payload.Email, payload.UserID))
208+
return nil
209+
}
210+
167211
// UserSendPhoneDeadEmailParams are parameters for notifying a user when a phone is dead
168212
type UserSendPhoneDeadEmailParams struct {
169213
UserID entities.UserID

0 commit comments

Comments
 (0)