From cdf3eb045443aa3380a9ec51238e694c3a88dd37 Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Sat, 24 May 2025 19:37:04 +0530 Subject: [PATCH] [server] Password reset email --- server/internal/command/user/cmd.go | 2 +- server/internal/command/user/invite.go | 40 +------------------ .../internal/command/user/password_reset.go | 35 ++++++++++++++++ server/internal/core/user/reset.go | 2 +- .../mail/emails/password_reset/body.html | 13 ++++++ .../mail/emails/password_reset/body.txt | 8 ++++ .../mail/emails/password_reset/subject | 1 + server/internal/mail/emails/welcome/body.html | 4 +- server/internal/mail/emails/welcome/body.txt | 4 +- server/internal/mail/mail.go | 5 ++- server/internal/mail/password_reset.go | 19 +++++++++ server/internal/mail/welcome.go | 18 ++++----- 12 files changed, 95 insertions(+), 56 deletions(-) create mode 100644 server/internal/command/user/password_reset.go create mode 100644 server/internal/mail/emails/password_reset/body.html create mode 100644 server/internal/mail/emails/password_reset/body.txt create mode 100644 server/internal/mail/emails/password_reset/subject create mode 100644 server/internal/mail/password_reset.go diff --git a/server/internal/command/user/cmd.go b/server/internal/command/user/cmd.go index f6b2b618..35151a8f 100644 --- a/server/internal/command/user/cmd.go +++ b/server/internal/command/user/cmd.go @@ -16,7 +16,7 @@ func SetupCommand() *cobra.Command { } cmd.AddCommand([]*cobra.Command{ setupInviteCommand(), - setupResetPasswordCommand(), + setupPasswordResetCommand(), setupUserModCommand(), setupUserListCommand(), setupUserPasswdCommand(), diff --git a/server/internal/command/user/invite.go b/server/internal/command/user/invite.go index 24079141..476241fd 100644 --- a/server/internal/command/user/invite.go +++ b/server/internal/command/user/invite.go @@ -79,7 +79,7 @@ func setupInviteCommand() *cobra.Command { os.Exit(1) } fmt.Println("User created") - if sendInviteEmail(u) != nil { + if mail.SendWelcomeEmail(u) != nil { fmt.Println("unable to send email: " + err.Error()) os.Exit(1) } @@ -90,41 +90,3 @@ func setupInviteCommand() *cobra.Command { cmd.Flags().BoolP("no-create-home", "M", false, "Do not make home directory") return cmd } - -func setupResetPasswordCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "pwreset email", - Short: "pwreset email", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - email := args[0] - - manager := user.ManagerFromContext(context.Background()) - if user, err := manager.UserByEmail(email); err != nil { - fmt.Println("unable to find user" + email + ": " + err.Error()) - os.Exit(1) - } else if token, err := manager.CreateResetToken(user); err != nil { - fmt.Println("unable to create reset token: " + err.Error()) - os.Exit(1) - } else if err := sendResetToken(user, token); err != nil { - fmt.Println("unable to send email: " + err.Error()) - os.Exit(1) - } - }, - } - return cmd -} - -func sendInviteEmail(user user.User) error { - return mail.SendWelcomeEmail( - mail.Recipient{Email: user.Email, Name: user.DisplayName}, - mail.WelcomeParams{Name: user.DisplayName}, - ) -} - -func sendResetToken(user user.User, token string) error { - return mail.SendWelcomeEmail( - mail.Recipient{Email: user.Email, Name: user.DisplayName}, - mail.WelcomeParams{Name: user.DisplayName}, - ) -} diff --git a/server/internal/command/user/password_reset.go b/server/internal/command/user/password_reset.go new file mode 100644 index 00000000..034c1756 --- /dev/null +++ b/server/internal/command/user/password_reset.go @@ -0,0 +1,35 @@ +package user + +import ( + "context" + "fmt" + "os" + + "github.com/shroff/phylum/server/internal/core/user" + "github.com/shroff/phylum/server/internal/mail" + "github.com/spf13/cobra" +) + +func setupPasswordResetCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "pwreset email", + Short: "pwreset email", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + email := args[0] + + manager := user.ManagerFromContext(context.Background()) + if user, err := manager.UserByEmail(email); err != nil { + fmt.Println("unable to find user" + email + ": " + err.Error()) + os.Exit(1) + } else if token, err := manager.CreateResetToken(user); err != nil { + fmt.Println("unable to create reset token: " + err.Error()) + os.Exit(1) + } else if err := mail.SendPasswordResetEmail(user, token); err != nil { + fmt.Println("unable to send email: " + err.Error()) + os.Exit(1) + } + }, + } + return cmd +} diff --git a/server/internal/core/user/reset.go b/server/internal/core/user/reset.go index bb2afc1e..bb96e6f1 100644 --- a/server/internal/core/user/reset.go +++ b/server/internal/core/user/reset.go @@ -7,7 +7,7 @@ import ( "github.com/shroff/phylum/server/internal/core/util/rand" ) -const resetTokenLength = 16 +const resetTokenLength = 64 const resetTokenDuration = 10 * time.Minute func (m manager) CreateResetToken(user User) (string, error) { diff --git a/server/internal/mail/emails/password_reset/body.html b/server/internal/mail/emails/password_reset/body.html new file mode 100644 index 00000000..7c85c826 --- /dev/null +++ b/server/internal/mail/emails/password_reset/body.html @@ -0,0 +1,13 @@ + + +Hi {{.name}}, + +You can reset your password by clicking here. + +If the link above doesn't work, then you can enter the following token in the password reset prompt, along with your email address: + +{{.token}} + +Thank you, + + \ No newline at end of file diff --git a/server/internal/mail/emails/password_reset/body.txt b/server/internal/mail/emails/password_reset/body.txt new file mode 100644 index 00000000..cfd291eb --- /dev/null +++ b/server/internal/mail/emails/password_reset/body.txt @@ -0,0 +1,8 @@ +Hi {{.name}}, + +You can reset your password at {{.base_url}}/reset_password?email={{.email}}&token={{.token}} + +If the link above doesn't work, then you can enter the following token in the password reset prompt, along with your email address: +{{.token}} + +Thank you, diff --git a/server/internal/mail/emails/password_reset/subject b/server/internal/mail/emails/password_reset/subject new file mode 100644 index 00000000..e2149675 --- /dev/null +++ b/server/internal/mail/emails/password_reset/subject @@ -0,0 +1 @@ +Reset your password \ No newline at end of file diff --git a/server/internal/mail/emails/welcome/body.html b/server/internal/mail/emails/welcome/body.html index bf86a8d6..0d9780a6 100644 --- a/server/internal/mail/emails/welcome/body.html +++ b/server/internal/mail/emails/welcome/body.html @@ -1,8 +1,8 @@ -Hi {{.Name}}, +Hi {{.name}}, -Someone invited you to use Phylum at {{.URL}} +Someone invited you to use Phylum at {{.base_url}} Please visit the URL above and reset your password to get started. diff --git a/server/internal/mail/emails/welcome/body.txt b/server/internal/mail/emails/welcome/body.txt index b1c27809..378997b4 100644 --- a/server/internal/mail/emails/welcome/body.txt +++ b/server/internal/mail/emails/welcome/body.txt @@ -1,6 +1,6 @@ -Hi {{.Name}}, +Hi {{.name}}, -Someone invited you to use Phylum at {{.URL}} +Someone invited you to use Phylum at {{.base_url}} Please visit the URL above and reset your password to get started. diff --git a/server/internal/mail/mail.go b/server/internal/mail/mail.go index d4772a5a..8f4cce4a 100644 --- a/server/internal/mail/mail.go +++ b/server/internal/mail/mail.go @@ -3,6 +3,7 @@ package mail import ( "io" + "github.com/shroff/phylum/server/internal/core/user" gomail "gopkg.in/mail.v2" ) @@ -21,11 +22,11 @@ func dialer() *gomail.Dialer { return d } -func send(e Email, rcpt Recipient, params any) error { +func send(e Email, rcpt user.User, params any) error { d := dialer() msg := gomail.NewMessage() msg.SetAddressHeader("From", Cfg.From.Email, Cfg.From.Name) - msg.SetAddressHeader("To", rcpt.Email, rcpt.Name) + msg.SetAddressHeader("To", rcpt.Email, rcpt.DisplayName) msg.SetHeader("Subject", e.Subject) msg.SetBodyWriter("text/plain", func(w io.Writer) error { return e.Plain.Execute(w, params) diff --git a/server/internal/mail/password_reset.go b/server/internal/mail/password_reset.go new file mode 100644 index 00000000..ee92a223 --- /dev/null +++ b/server/internal/mail/password_reset.go @@ -0,0 +1,19 @@ +package mail + +import "github.com/shroff/phylum/server/internal/core/user" + +type ResetPasswordParams struct { + Name string + URL string +} + +func SendPasswordResetEmail(u user.User, token string) error { + email := emails["password_reset"] + params := map[string]string{ + "name": u.DisplayName, + "email": u.Email, + "base_url": Cfg.URL, + "token": token, + } + return send(email, u, params) +} diff --git a/server/internal/mail/welcome.go b/server/internal/mail/welcome.go index b390e2a2..ef154dba 100644 --- a/server/internal/mail/welcome.go +++ b/server/internal/mail/welcome.go @@ -1,15 +1,15 @@ package mail -type WelcomeParams struct { - Name string - URL string -} +import "github.com/shroff/phylum/server/internal/core/user" -func SendWelcomeEmail(rcpt Recipient, params WelcomeParams) error { +func SendWelcomeEmail(u user.User) error { email := emails["welcome"] - if params.Name == "" { - params.Name = "there" + params := map[string]string{ + "name": u.DisplayName, + "url": Cfg.URL, } - params.URL = Cfg.URL - return send(email, rcpt, params) + if u.DisplayName == "" { + params["name"] = "there" + } + return send(email, u, params) }