wip: password resets

This commit is contained in:
Ferdinand Mütsch 2021-04-05 16:25:13 +02:00
parent 1783858854
commit e6e134678a
4 changed files with 301 additions and 0 deletions

View File

@ -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()
}

111
services/mail/mailwhale.go Normal file
View File

@ -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))
}

View File

@ -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
}

View File

@ -0,0 +1,154 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Simple Transactional Email</title>
<style>
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #047857 !important;
}
.btn-primary a:hover {
background-color: #047857 !important;
border-color: #047857 !important;
}
}
</style>
</head>
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span>
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 18px; font-weight: 500; margin: 0; Margin-bottom: 15px;">Password Reset</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have requested to reset your Wakapi password. Please click the following link to proceed.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #2F855A; border-radius: 5px; text-align: center;"> <a href="{{ .ResetLink }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #2F855A; border: solid 1px #2F855A; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #2F855A;">Reset Password</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you did not request a password change, please just ignore this mail.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
Powered by <a href="https://wakapi.dev" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Wakapi.dev</a>.
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>