Skip to content

Commit

Permalink
moving all the mail templates stuff to notification package
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasmenendez committed Nov 22, 2024
1 parent 4130ff1 commit 98f3cbf
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 203 deletions.
21 changes: 9 additions & 12 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,15 @@ const (
)

type APIConfig struct {
Host string
Port int
Secret string
Chain string
DB *db.MongoStorage
Client *apiclient.HTTPclient
Account *account.Account
MailTemplates map[notifications.MailTemplate]string
MailService notifications.NotificationService
WebAppURL string
Host string
Port int
Secret string
Chain string
DB *db.MongoStorage
Client *apiclient.HTTPclient
Account *account.Account
MailService notifications.NotificationService
WebAppURL string
// FullTransparentMode if true allows signing all transactions and does not
// modify any of them.
FullTransparentMode bool
Expand All @@ -53,7 +52,6 @@ type API struct {
client *apiclient.HTTPclient
account *account.Account
mail notifications.NotificationService
mailTemplates map[notifications.MailTemplate]string
secret string
webAppURL string
transparentMode bool
Expand All @@ -74,7 +72,6 @@ func New(conf *APIConfig) *API {
client: conf.Client,
account: conf.Account,
mail: conf.MailService,
mailTemplates: conf.MailTemplates,
secret: conf.Secret,
webAppURL: conf.WebAppURL,
transparentMode: conf.FullTransparentMode,
Expand Down
3 changes: 2 additions & 1 deletion api/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var VerificationCodeExpiration = 3 * time.Minute
const (
// VerificationCodeLength is the length of the verification code in bytes
VerificationCodeLength = 3
// InvitationExpiration is the duration of the invitation code before it is invalidated
// InvitationExpiration is the duration of the invitation code before it is
// invalidated
InvitationExpiration = 5 * 24 * time.Hour // 5 days
)
83 changes: 16 additions & 67 deletions api/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,82 +6,31 @@ import (
"time"

"github.com/vocdoni/saas-backend/internal"
"github.com/vocdoni/saas-backend/notifications"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
)

// apiNotification is an internal struct that represents a notification to be
// sent via notifications package. It contains the mail template, the link path
// and the notification to be sent.
type apiNotification struct {
Template notifications.MailTemplate
LinkPath string
Notification notifications.Notification
}

// VerifyAccountNotification is the notification to be sent when a user creates
// an account and needs to verify it.
var VerifyAccountNotification = apiNotification{
Template: "verification_account",
LinkPath: "/account/verify",
Notification: notifications.Notification{
Subject: "Vocdoni verification code",
PlainBody: `Your Vocdoni password reset code is: {{.Code}}
You can also use this link to reset your password: {{.Link}}`,
},
}

// PasswordResetNotification is the notification to be sent when a user requests
// a password reset.
var PasswordResetNotification = apiNotification{
Template: "forgot_password",
LinkPath: "/account/password/reset",
Notification: notifications.Notification{
Subject: "Vocdoni password reset",
PlainBody: `Your Vocdoni password reset code is: {{.Code}}
You can also use this link to reset your password: {{.Link}}`,
},
}

// InviteAdminNotification is the notification to be sent when a user is invited
// to be an admin of an organization.
var InviteAdminNotification = apiNotification{
Template: "invite_admin",
LinkPath: "/account/invite",
Notification: notifications.Notification{
Subject: "Vocdoni organization invitation",
PlainBody: `You code to join to '{{.Organization}}' organization is: {{.Code}}
You can also use this link to join the organization: {{.Link}}`,
},
}

// sendNotification method sends a notification to the email provided. It
// requires the email the API notification definition and the data to fill it.
// It clones the notification included in the API notification and fills it
// with the recipient email address and the data provided, using the template
// defined in the API notification. It returns an error if the mail service is
// available and the notification could not be sent. If the mail service is not
// available, the notification is not sent but the function returns nil.
func (a *API) sendNotification(ctx context.Context, to string, an apiNotification, data any) error {
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
// send the verification code via email if the mail service is available
// sendMail method sends a notification to the email provided. It requires the
// email template and the data to fill it. It executes the mail template with
// the data to get the notification and sends it with the recipient email
// address provided. It returns an error if the mail service is available and
// the notification could not be sent or the email address is invalid. If the
// mail service is not available, it does nothing.
func (a *API) sendMail(ctx context.Context, to string, mail mailtemplates.MailTemplate, data any) error {
if a.mail != nil {
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
// check if the email address is valid
if !internal.ValidEmail(to) {
return fmt.Errorf("invalid email address")
}
// clone the notification and create a pointer to it
notification := &an.Notification
// set the recipient email address
notification.ToAddress = to
// execute the template with the data provided
if err := notification.ExecTemplate(a.mailTemplates[an.Template], data); err != nil {
// execute the mail template to get the notification
notification, err := mail.ExecTemplate(data)
if err != nil {
return err
}
// send the notification
// set the recipient email address
notification.ToAddress = to
// send the mail notification
if err := a.mail.SendNotification(ctx, notification); err != nil {
return err
}
Expand Down
25 changes: 17 additions & 8 deletions api/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/vocdoni/saas-backend/account"
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/internal"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
"go.vocdoni.io/dvote/log"
)

Expand Down Expand Up @@ -297,19 +298,27 @@ func (a *API) inviteOrganizationMemberHandler(w http.ResponseWriter, r *http.Req
return
}
// send the invitation verification code to the user email
inviteLink, err := a.buildWebAppURL(InviteAdminNotification.LinkPath,
map[string]any{"email": invite.Email, "code": inviteCode, "address": org.Address})
inviteLink, err := a.buildWebAppURL(mailtemplates.InviteNotification.WebAppURI,
map[string]any{
"email": invite.Email,
"code": inviteCode,
"address": org.Address,
})
if err != nil {
log.Warnw("could not build verification link", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
if err := a.sendNotification(r.Context(), invite.Email, InviteAdminNotification, struct {
Organization string
Code string
Link string
}{org.Address, inviteCode, inviteLink}); err != nil {
log.Warnw("could not send verification code", "error", err)
// send the invitation mail to invited user email with the invite code and
// the invite link
if err := a.sendMail(r.Context(), invite.Email, mailtemplates.InviteNotification,
struct {
Organization string
Code string
Link string
}{org.Address, inviteCode, inviteLink},
); err != nil {
log.Warnw("could not send verification code email", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
Expand Down
32 changes: 22 additions & 10 deletions api/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/internal"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/dvote/util"
)
Expand Down Expand Up @@ -98,15 +99,17 @@ func (a *API) registerHandler(w http.ResponseWriter, r *http.Request) {
return
}
// send the new verification code to the user email
verificationLink, err := a.buildWebAppURL(VerifyAccountNotification.LinkPath,
verificationLink, err := a.buildWebAppURL(verifyUserCodeEndpoint,
map[string]any{"email": newUser.Email, "code": code})
if err != nil {
log.Warnw("could not build verification link", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
if err := a.sendNotification(r.Context(), userInfo.Email, VerifyAccountNotification,
struct {
// send the verification mail to the user email with the verification code
// and the verification link
if err := a.sendMail(r.Context(), userInfo.Email,
mailtemplates.VerifyAccountNotification, struct {
Code string
Link string
}{code, verificationLink},
Expand Down Expand Up @@ -134,7 +137,6 @@ func (a *API) verifyUserAccountHandler(w http.ResponseWriter, r *http.Request) {
ErrMalformedBody.Write(w)
return
}

// check the email and verification code are not empty only if the mail
// service is available
if a.mail != nil && (verification.Code == "" || verification.Email == "") {
Expand Down Expand Up @@ -296,14 +298,19 @@ func (a *API) resendUserVerificationCodeHandler(w http.ResponseWriter, r *http.R
return
}
// send the new verification code to the user email
verificationLink, err := a.buildWebAppURL(VerifyAccountNotification.LinkPath,
map[string]any{"email": user.Email, "code": newCode})
verificationLink, err := a.buildWebAppURL(mailtemplates.VerifyAccountNotification.WebAppURI,
map[string]any{
"email": user.Email,
"code": newCode,
})
if err != nil {
log.Warnw("could not build verification link", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
if err := a.sendNotification(r.Context(), user.Email, VerifyAccountNotification,
// send the verification mail to the user email with the verification code
// and the verification link
if err := a.sendMail(r.Context(), user.Email, mailtemplates.VerifyAccountNotification,
struct {
Code string
Link string
Expand Down Expand Up @@ -492,14 +499,19 @@ func (a *API) recoverUserPasswordHandler(w http.ResponseWriter, r *http.Request)
return
}
// send the password reset code to the user email
resetLink, err := a.buildWebAppURL(PasswordResetNotification.LinkPath,
map[string]any{"email": user.Email, "code": code})
resetLink, err := a.buildWebAppURL(mailtemplates.PasswordResetNotification.WebAppURI,
map[string]any{
"email": user.Email,
"code": code,
})
if err != nil {
log.Warnw("could not build verification link", "error", err)
ErrGenericInternalServerError.Write(w)
return
}
if err := a.sendNotification(r.Context(), user.Email, PasswordResetNotification,
// send the password reset mail to the user email with the verification
// code and the verification link
if err := a.sendMail(r.Context(), user.Email, mailtemplates.PasswordResetNotification,
struct {
Code string
Link string
Expand Down
36 changes: 22 additions & 14 deletions api/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,41 @@ import (

qt "github.com/frankban/quicktest"
"github.com/vocdoni/saas-backend/notifications"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
)

var verificationCodeRgx, passwordResetRgx *regexp.Regexp

func init() {
// create a regex to find the verification code in the email
codeRgx := fmt.Sprintf(`(.{%d})`, VerificationCodeLength*2)
// compose notification with the verification code regex needle
verifyNotification := &notifications.Notification{
PlainBody: VerifyAccountNotification.Notification.PlainBody,
// load the email templates
mailtemplates.Load("../assets")

Check failure on line 25 in api/users_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `mailtemplates.Load` is not checked (errcheck)

Check failure on line 25 in api/users_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `mailtemplates.Load` is not checked (errcheck)
// wrap the mail template execution to force plain body and set the regex
// needle as the verification code
testTemplateExec := func(mt mailtemplates.MailTemplate) (*notifications.Notification, error) {
n, err := mt.ExecTemplate(struct {
Code string
Link string
}{codeRgx, ""})
if err != nil {
return nil, err
}
// force plain body
n.Body = n.PlainBody
return n, nil
}
if err := verifyNotification.ExecTemplate("", struct {
Code string
Link string
}{codeRgx, ""}); err != nil {
// compose notification with the verification code regex needle
verifyNotification, err := testTemplateExec(mailtemplates.VerifyAccountNotification)
if err != nil {
panic(err)
}
// clean the notification body to get only the verification code and
// compile the regex
verificationCodeRgx = regexp.MustCompile(strings.Split(verifyNotification.PlainBody, "\n")[0])
// compose notification with the password reset code regex needle
passwordResetNotification := &notifications.Notification{
PlainBody: PasswordResetNotification.Notification.PlainBody,
}
if err := passwordResetNotification.ExecTemplate("", struct {
Code string
Link string
}{codeRgx, ""}); err != nil {
passwordResetNotification, err := testTemplateExec(mailtemplates.PasswordResetNotification)
if err != nil {
panic(err)
}
// clean the notification body to get only the password reset code and
Expand Down
10 changes: 6 additions & 4 deletions cmd/service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/vocdoni/saas-backend/account"
"github.com/vocdoni/saas-backend/api"
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/notifications"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
"github.com/vocdoni/saas-backend/notifications/smtp"
"github.com/vocdoni/saas-backend/stripe"
"github.com/vocdoni/saas-backend/subscriptions"
Expand Down Expand Up @@ -136,12 +136,14 @@ func main() {
}); err != nil {
log.Fatalf("could not create the email service: %v", err)
}
// load email templates
// load email templates if the path is set
if emailTemplatesPath != "" {
apiConf.MailTemplates, err = notifications.GetMailTemplates(emailTemplatesPath)
if err != nil {
if err := mailtemplates.Load(emailTemplatesPath); err != nil {
log.Fatalf("could not load email templates: %v", err)
}
log.Infow("email templates loaded",
"path", emailTemplatesPath,
"templates", len(mailtemplates.AvailableTemplates))
}
log.Infow("email service created", "from", fmt.Sprintf("%s <%s>", emailFromName, emailFromAddress))
}
Expand Down
Loading

0 comments on commit 98f3cbf

Please sign in to comment.