97 lines
3.4 KiB
Go
97 lines
3.4 KiB
Go
// Package application is the notification service's entire job: listen
|
|
// for the events iam publishes when it needs an email sent, and send
|
|
// it. Replaces the parts of internal/handlers/auth.go that called
|
|
// mail.Mailer.Send inline (sendVerificationEmail, sendResetEmail),
|
|
// decoupled onto RabbitMQ per the task's requirement that asynchronous,
|
|
// no-reply-needed work use the message bus rather than a synchronous
|
|
// call — sending an email has no result the caller
|
|
// (iam's SignUp/ResendVerification/ForgotPassword RPCs) needs to wait
|
|
// for.
|
|
package application
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"enkly/pkg/eventbus"
|
|
"enkly/pkg/eventbus/events"
|
|
"enkly/pkg/mailkit"
|
|
)
|
|
|
|
type Service struct {
|
|
Mailer *mailkit.Mailer
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// HandleEvent is the single eventbus.Handler this service registers —
|
|
// see cmd/server/main.go's Consume call. It switches on the envelope's
|
|
// routing key the same way the monolith's handlers switched on which
|
|
// function (sendVerificationEmail vs. sendResetEmail) was calling
|
|
// mail.Send.
|
|
func (s *Service) HandleEvent(ctx context.Context, env eventbus.Envelope) error {
|
|
switch env.RoutingKey {
|
|
case eventbus.RoutingKeyVerificationCodeIssued:
|
|
var ev events.VerificationCodeIssued
|
|
if err := decode(env, &ev); err != nil {
|
|
return err
|
|
}
|
|
return s.sendVerificationEmail(ctx, ev)
|
|
|
|
case eventbus.RoutingKeyPasswordResetRequested:
|
|
var ev events.PasswordResetRequested
|
|
if err := decode(env, &ev); err != nil {
|
|
return err
|
|
}
|
|
return s.sendResetEmail(ctx, ev)
|
|
|
|
default:
|
|
// Bound to this queue by mistake (topology drift) — log and ack
|
|
// rather than requeue-looping forever on a message we'll never
|
|
// know how to handle.
|
|
s.Logger.Warn("notification: unhandled routing key", "routing_key", env.RoutingKey)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func decode(env eventbus.Envelope, v any) error {
|
|
if err := json.Unmarshal(env.Payload, v); err != nil {
|
|
return fmt.Errorf("decode %s payload: %w", env.RoutingKey, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sendVerificationEmail replaces Handlers.sendVerificationEmail.
|
|
func (s *Service) sendVerificationEmail(_ context.Context, ev events.VerificationCodeIssued) error {
|
|
subject := "Verify your enkly account"
|
|
body := fmt.Sprintf(
|
|
"Hi,\n\nYour enkly verification code is: %s\n\nThis code expires in 15 minutes. If you didn't request this, you can ignore this email.\n",
|
|
ev.Code)
|
|
if !s.Mailer.Configured() {
|
|
s.Logger.Info("mail not configured — verification code logged instead", "email", ev.Email, "code", ev.Code)
|
|
return nil
|
|
}
|
|
if err := s.Mailer.Send(ev.Email, subject, body); err != nil {
|
|
return fmt.Errorf("send verification email to %s: %w", ev.Email, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sendResetEmail replaces Handlers.sendResetEmail.
|
|
func (s *Service) sendResetEmail(_ context.Context, ev events.PasswordResetRequested) error {
|
|
subject := "Reset your enkly password"
|
|
body := fmt.Sprintf(
|
|
"Hi,\n\nSomeone (hopefully you) requested a password reset for your enkly account.\n\n"+
|
|
"Reset it here: %s\n\nThis link expires in an hour. If you didn't request this, you can ignore this email.\n",
|
|
ev.ResetLink)
|
|
if !s.Mailer.Configured() {
|
|
s.Logger.Info("mail not configured — reset link logged instead", "email", ev.Email, "link", ev.ResetLink)
|
|
return nil
|
|
}
|
|
if err := s.Mailer.Send(ev.Email, subject, body); err != nil {
|
|
return fmt.Errorf("send reset email to %s: %w", ev.Email, err)
|
|
}
|
|
return nil
|
|
}
|