From e6e134678a18620d56f8671eb97032d02da70454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Mon, 5 Apr 2021 16:25:13 +0200 Subject: [PATCH] wip: password resets --- config/config.go | 32 ++++++ services/mail/mailwhale.go | 111 +++++++++++++++++++++ services/services.go | 4 + views/mail/reset_password.tpl.html | 154 +++++++++++++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 services/mail/mailwhale.go create mode 100644 views/mail/reset_password.tpl.html diff --git a/config/config.go b/config/config.go index 56d3730..104edfb 100644 --- a/config/config.go +++ b/config/config.go @@ -49,6 +49,8 @@ const ( WakatimeApiMachineNamesUrl = "/users/current/machine_names" ) +var emailProviders = []string{"mailwhale"} + var cfg *Config var cFlag = flag.String("config", defaultConfigPath, "config file location") @@ -89,6 +91,7 @@ type serverConfig struct { ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"` ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"` BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"` + PublicUrl string `yaml:"public_url" default:"https://wakapi.dev/" env:"WAKAPI_PUBLIC_URL"` TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"` TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"` } @@ -100,6 +103,17 @@ type sentryConfig struct { SampleRateHeartbeats float32 `yaml:"sample_rate_heartbeats" default:"0.1" env:"WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS"` } +type mailConfig struct { + Provider string `env:"WAKAPI_MAIL_PROVIDER"` + MailWhale *MailwhaleMailConfig `yaml:"mailwhale"` +} + +type MailwhaleMailConfig struct { + Url string `env:"WAKAPI_MAIL_MAILWHALE_URL"` + ClientId string `yaml:"client_id" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_ID"` + ClientSecret string `yaml:"client_secret" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET"` +} + type Config struct { Env string `default:"dev" env:"ENVIRONMENT"` Version string `yaml:"-"` @@ -108,6 +122,7 @@ type Config struct { Db dbConfig Server serverConfig Sentry sentryConfig + Mail mailConfig } func (c *Config) CreateCookie(name, value, path string) *http.Cookie { @@ -238,6 +253,10 @@ func (c *appConfig) GetOSColors() map[string]string { return cloneStringMap(c.Colors["operating_systems"], true) } +func (c *serverConfig) GetPublicUrl() string { + return strings.TrimSuffix(c.PublicUrl, "/") +} + func IsDev(env string) bool { return env == "dev" || env == "development" } @@ -337,6 +356,15 @@ func initSentry(config sentryConfig, debug bool) { } } +func findString(needle string, haystack []string, defaultVal string) string { + for _, s := range haystack { + if s == needle { + return s + } + } + return defaultVal +} + func Set(config *Config) { cfg = config } @@ -385,6 +413,10 @@ func Load() *Config { initSentry(config.Sentry, config.IsDev()) } + if config.Mail.Provider != "" && findString(config.Mail.Provider, emailProviders, "") == "" { + logbuch.Fatal("unknown mail provider '%s'", config.Mail.Provider) + } + Set(config) return Get() } diff --git a/services/mail/mailwhale.go b/services/mail/mailwhale.go new file mode 100644 index 0000000..85ddc90 --- /dev/null +++ b/services/mail/mailwhale.go @@ -0,0 +1,111 @@ +package mail + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/markbates/pkger" + conf "github.com/muety/wakapi/config" + "github.com/muety/wakapi/models" + "io/ioutil" + "net/http" + "text/template" + "time" +) + +const ( + tplPath = "/views/mail" + tplNamePasswordReset = "reset_password" +) + +type MailWhaleService struct { + config *conf.MailwhaleMailConfig + httpClient *http.Client +} + +type MailWhaleSendRequest struct { + To []string `json:"to"` + Subject string `json:"subject"` + Text string `json:"text"` + Html string `json:"html"` + TemplateId string `json:"template_id"` + TemplateVars map[string]string `json:"template_vars"` +} + +func NewMailWhaleService(config *conf.MailwhaleMailConfig) *MailWhaleService { + return &MailWhaleService{ + config: config, + httpClient: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +func (m *MailWhaleService) SendPasswordResetMail(recipient *models.User, resetLink string) error { + tpl, err := m.loadTemplate(tplNamePasswordReset) + if err != nil { + return err + } + + type data struct { + ResetLink string + } + + var rendered bytes.Buffer + if err := tpl.Execute(&rendered, data{ResetLink: resetLink}); err != nil { + return err + } + + return m.send(recipient.Email, "Wakapi – Password Reset", rendered.String(), true) +} + +func (m *MailWhaleService) send(to, subject, body string, isHtml bool) error { + if to == "" { + return errors.New("no recipient mail address set, cannot send password reset link") + } + + sendRequest := &MailWhaleSendRequest{ + To: []string{to}, + Subject: subject, + } + if isHtml { + sendRequest.Html = body + } else { + sendRequest.Text = body + } + payload, _ := json.Marshal(sendRequest) + + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/mail", m.config.Url), bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.SetBasicAuth(m.config.ClientId, m.config.ClientSecret) + req.Header.Set("Content-Type", "application/json") + + res, err := m.httpClient.Do(req) + if err != nil { + return err + } + if res.StatusCode >= 400 { + return errors.New(fmt.Sprintf("failed to send password reset mail to %v, got status %d from mailwhale", to, res.StatusCode)) + } + + return nil +} + +func (m *MailWhaleService) loadTemplate(tplName string) (*template.Template, error) { + tplFile, err := pkger.Open(fmt.Sprintf("%s/%s.tpl.html", tplPath, tplName)) + if err != nil { + return nil, err + } + defer tplFile.Close() + + tplData, err := ioutil.ReadAll(tplFile) + if err != nil { + return nil, err + } + + return template.New(tplName).Parse(string(tplData)) +} diff --git a/services/services.go b/services/services.go index 2414a20..c4856da 100644 --- a/services/services.go +++ b/services/services.go @@ -75,3 +75,7 @@ type IUserService interface { MigrateMd5Password(*models.User, *models.Login) (*models.User, error) FlushCache() } + +type IMailService interface { + SendPasswordResetMail(recipient *models.User, resetLink string) error +} diff --git a/views/mail/reset_password.tpl.html b/views/mail/reset_password.tpl.html new file mode 100644 index 0000000..7636e5c --- /dev/null +++ b/views/mail/reset_password.tpl.html @@ -0,0 +1,154 @@ + + + + + + Simple Transactional Email + + + + + + + + + + +
  +
+ + + + + + + + + + +
+ + + + +
+

Password Reset

+

You have requested to reset your Wakapi password. Please click the following link to proceed.

+ + + + + + +
+ + + + + + +
Reset Password
+
+

If you did not request a password change, please just ignore this mail.

+
+
+ + + + + + +
+
 
+ +