From 2ef1b4beff6c0b3fe54b9980200b003fbd0ee6ad Mon Sep 17 00:00:00 2001 From: Bobby Rullo Date: Fri, 30 Oct 2015 14:41:00 -0700 Subject: [PATCH] user: introduce "invite" emails Invite emails are essentially just reset password emails with a different template (though this can and probably will change (slightly) in the near future) --- integration/user_api_test.go | 16 ++++++++++++++++ server/password_test.go | 4 ++-- static/email/invite.html | 7 +++++++ static/email/invite.txt | 4 ++++ user/api/api.go | 3 ++- user/api/api_test.go | 11 +++++++++++ user/email/email.go | 22 +++++++++++++++++++++- 7 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 static/email/invite.html create mode 100644 static/email/invite.txt diff --git a/integration/user_api_test.go b/integration/user_api_test.go index 7c321d12..0b4c1431 100644 --- a/integration/user_api_test.go +++ b/integration/user_api_test.go @@ -519,6 +519,7 @@ func TestCreateUser(t *testing.T) { cantEmail: tt.cantEmail, lastEmail: tt.req.User.Email, lastClientID: "XXX", + lastWasInvite: true, lastRedirectURL: *urlParsed, } if diff := pretty.Compare(wantEmalier, f.emailer); diff != "" { @@ -578,6 +579,7 @@ type testEmailer struct { lastEmail string lastClientID string lastRedirectURL url.URL + lastWasInvite bool } // SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer. @@ -585,6 +587,20 @@ func (t *testEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, t.lastEmail = email t.lastRedirectURL = redirectURL t.lastClientID = clientID + t.lastWasInvite = false + + var retURL *url.URL + if t.cantEmail { + retURL = &testResetPasswordURL + } + return retURL, nil +} + +func (t *testEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) { + t.lastEmail = email + t.lastRedirectURL = redirectURL + t.lastClientID = clientID + t.lastWasInvite = true var retURL *url.URL if t.cantEmail { diff --git a/server/password_test.go b/server/password_test.go index 4c0e02ce..c4dfbca5 100644 --- a/server/password_test.go +++ b/server/password_test.go @@ -102,7 +102,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { wantEmailer: &testEmailer{ to: str("Email-1@example.com"), from: "noreply@example.com", - subject: "Reset your password.", + subject: "Reset Your Password", }, wantPRUserID: "ID-1", wantPRRedirect: &testRedirectURL, @@ -138,7 +138,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { wantEmailer: &testEmailer{ to: str("Email-1@example.com"), from: "noreply@example.com", - subject: "Reset your password.", + subject: "Reset Your Password", }, wantPRPassword: "password", wantPRUserID: "ID-1", diff --git a/static/email/invite.html b/static/email/invite.html new file mode 100644 index 00000000..a9bdd7e0 --- /dev/null +++ b/static/email/invite.html @@ -0,0 +1,7 @@ + + + Welcome to Dex! Click below to set your password: + + Set Password + + diff --git a/static/email/invite.txt b/static/email/invite.txt new file mode 100644 index 00000000..02259718 --- /dev/null +++ b/static/email/invite.txt @@ -0,0 +1,4 @@ +Welcome to Dex! Click below to set your password: + +Link: +{{ .link }} diff --git a/user/api/api.go b/user/api/api.go index a05cc746..8c27173e 100644 --- a/user/api/api.go +++ b/user/api/api.go @@ -89,6 +89,7 @@ type UsersAPI struct { type Emailer interface { SendResetPasswordEmail(string, url.URL, string) (*url.URL, error) + SendInviteEmail(string, url.URL, string) (*url.URL, error) } type Creds struct { @@ -169,7 +170,7 @@ func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (s usr = userToSchemaUser(userUser) - url, err := u.emailer.SendResetPasswordEmail(usr.Email, validRedirURL, creds.ClientID) + url, err := u.emailer.SendInviteEmail(usr.Email, validRedirURL, creds.ClientID) // An email is sent only if we don't get a link and there's no error. emailSent := err == nil && url == nil diff --git a/user/api/api_test.go b/user/api/api_test.go index fa5f85b1..f6b3255a 100644 --- a/user/api/api_test.go +++ b/user/api/api_test.go @@ -20,13 +20,23 @@ type testEmailer struct { lastEmail string lastClientID string lastRedirectURL url.URL + lastWasInvite bool } // SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer. func (t *testEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) { + return t.sendEmail(email, redirectURL, clientID, false) +} + +func (t *testEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) { + return t.sendEmail(email, redirectURL, clientID, true) +} + +func (t *testEmailer) sendEmail(email string, redirectURL url.URL, clientID string, invite bool) (*url.URL, error) { t.lastEmail = email t.lastRedirectURL = redirectURL t.lastClientID = clientID + t.lastWasInvite = invite var retURL *url.URL if t.cantEmail { @@ -369,6 +379,7 @@ func TestCreateUser(t *testing.T) { lastEmail: tt.usr.Email, lastClientID: tt.creds.ClientID, lastRedirectURL: tt.redirURL, + lastWasInvite: true, } if diff := pretty.Compare(wantEmalier, emailer); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff --git a/user/email/email.go b/user/email/email.go index 9203327d..2d671167 100644 --- a/user/email/email.go +++ b/user/email/email.go @@ -53,6 +53,16 @@ func NewUserEmailer(ur user.UserRepo, // This method DOES NOT check for client ID, redirect URL validity - it is expected that upstream users have already done so. // If there is no emailer is configured, the URL of the aforementioned link is returned, otherwise nil is returned. func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) { + return u.sendResetPasswordOrInviteEmail(email, redirectURL, clientID, false) +} + +// SendInviteEmail is exactly the same as SendResetPasswordEmail, except that it uses the invite template and subject name. +// In the near future, invite emails might diverge further. +func (u *UserEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) { + return u.sendResetPasswordOrInviteEmail(email, redirectURL, clientID, true) +} + +func (u *UserEmailer) sendResetPasswordOrInviteEmail(email string, redirectURL url.URL, clientID string, invite bool) (*url.URL, error) { usr, err := u.ur.GetByEmail(nil, email) if err == user.ErrorNotFound { log.Errorf("No Such user for email: %q", email) @@ -95,8 +105,17 @@ func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, q.Set("token", token) resetURL.RawQuery = q.Encode() + var tmplName, subj string + if invite { + tmplName = "invite" + subj = "Activate Your Account" + } else { + tmplName = "password-reset" + subj = "Reset Your Password" + } + if u.emailer != nil { - err = u.emailer.SendMail(u.fromAddress, "Reset your password.", "password-reset", + err = u.emailer.SendMail(u.fromAddress, subj, tmplName, map[string]interface{}{ "email": usr.Email, "link": resetURL.String(), @@ -107,6 +126,7 @@ func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, return nil, err } return &resetURL, nil + } // SendEmailVerification sends an email to the user with the given userID containing a link which when visited marks the user as having had their email verified.