From: jloup Date: Fri, 11 May 2018 11:57:29 +0000 (+0200) Subject: Mails. X-Git-Tag: v0.0.10~1 X-Git-Url: https://git.immae.eu/?p=perso%2FImmae%2FProjets%2FCryptomonnaies%2FCryptoportfolio%2FFront.git;a=commitdiff_plain;h=2da5b12c31074591eaf16929b760322b98f189e8 Mails. --- diff --git a/Gopkg.lock b/Gopkg.lock index 91f891c..2a6ae4f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -7,12 +7,30 @@ revision = "b26d9c308763d68093482582cea63d69be07a0f0" version = "v0.3.0" +[[projects]] + name = "github.com/Masterminds/semver" + packages = ["."] + revision = "c7af12943936e8c39859482e61f0574c2fd7fc75" + version = "v1.4.2" + +[[projects]] + name = "github.com/Masterminds/sprig" + packages = ["."] + revision = "6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c" + version = "v2.15.0" + [[projects]] name = "github.com/Sirupsen/logrus" packages = ["."] revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" version = "v1.0.5" +[[projects]] + name = "github.com/aokoli/goutils" + packages = ["."] + revision = "3391d3790d23d03408670993e957e8f408993c34" + version = "v1.0.1" + [[projects]] name = "github.com/boombuler/barcode" packages = [ @@ -103,6 +121,30 @@ revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" version = "v1.1.0" +[[projects]] + name = "github.com/google/uuid" + packages = ["."] + revision = "064e2069ce9c359c118179501254f67d7d37ba24" + version = "0.2" + +[[projects]] + name = "github.com/huandu/xstrings" + packages = ["."] + revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1" + version = "v1.0.0" + +[[projects]] + name = "github.com/imdario/mergo" + packages = ["."] + revision = "7fe0c75c13abdee74b09fcacef5ea1c6bba6a874" + version = "0.2.4" + +[[projects]] + branch = "master" + name = "github.com/jaytaylor/html2text" + packages = ["."] + revision = "64a82a6d140778896f13303121a49d8cb8007034" + [[projects]] branch = "master" name = "github.com/jinzhu/inflection" @@ -121,12 +163,30 @@ packages = ["."] revision = "3e7b2ea67e9637d153f53ef5ff148f23ee5274d4" +[[projects]] + name = "github.com/matcornic/hermes" + packages = ["."] + revision = "23ab47deb5a321481be0c4936b810c1420da2262" + version = "1.1.1" + [[projects]] name = "github.com/mattn/go-isatty" packages = ["."] revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" +[[projects]] + name = "github.com/mattn/go-runewidth" + packages = ["."] + revision = "9e777a8366cce605130a531d2cd6363d07ad7317" + version = "v0.0.2" + +[[projects]] + branch = "master" + name = "github.com/olekukonko/tablewriter" + packages = ["."] + revision = "d4647c9c7a84d847478d890b816b7d8b62b0b279" + [[projects]] name = "github.com/pquerna/otp" packages = [ @@ -143,6 +203,18 @@ packages = ["."] revision = "2df3e6ddaf6e9531dd02d7b6337f2d310f5e4f22" +[[projects]] + branch = "master" + name = "github.com/shurcooL/sanitized_anchor_name" + packages = ["."] + revision = "86672fcb3f950f35f2e675df2240550f2a50762f" + +[[projects]] + branch = "master" + name = "github.com/ssor/bom" + packages = ["."] + revision = "6386211fdfcf24c0bfbdaceafd02849ed9a8a509" + [[projects]] name = "github.com/ugorji/go" packages = ["codec"] @@ -155,6 +227,8 @@ packages = [ "bcrypt", "blowfish", + "pbkdf2", + "scrypt", "ssh/terminal" ] revision = "8b1d31080a7692e075c4681cb2458454a1fe0706" @@ -162,7 +236,11 @@ [[projects]] branch = "master" name = "golang.org/x/net" - packages = ["websocket"] + packages = [ + "html", + "html/atom", + "websocket" + ] revision = "640f4622ab692b87c2f3a94265e6f579fe38263d" [[projects]] @@ -174,12 +252,30 @@ ] revision = "78d5f264b493f125018180c204871ecf58a2dce1" +[[projects]] + branch = "v3" + name = "gopkg.in/alexcesaro/quotedprintable.v3" + packages = ["."] + revision = "2caba252f4dc53eaf6b553000885530023f54623" + [[projects]] name = "gopkg.in/go-playground/validator.v8" packages = ["."] revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" version = "v8.18.2" +[[projects]] + name = "gopkg.in/gomail.v2" + packages = ["."] + revision = "41f3572897373c5538c50a2402db15db079fa4fd" + version = "2.0.0" + +[[projects]] + name = "gopkg.in/russross/blackfriday.v2" + packages = ["."] + revision = "cadec560ec52d93835bf2f15bd794700d3a2473b" + version = "v2.0.0" + [[projects]] name = "gopkg.in/yaml.v2" packages = ["."] @@ -189,6 +285,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "5c987f56ef837352173d0a50f12d6c58ac72831b5e90d34ca0a283bee71fb1a2" + inputs-digest = "9df92ae3bbf81638f86228e1daacd75bd4f6d0afbfc449742e7f28fdefffdc46" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 5926748..31e55bd 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -76,3 +76,7 @@ [[override]] name = "github.com/dchest/authcookie" branch = "master" + +[[constraint]] + name = "github.com/matcornic/hermes" + version = "1.1.1" diff --git a/api/api.go b/api/api.go index 79a13a5..e011811 100644 --- a/api/api.go +++ b/api/api.go @@ -8,8 +8,18 @@ import ( ) var CONFIG Config +var MAIL_CONFIG MailConfig + +type MailConfig struct { + IsEnabled bool + SmtpAddress string `toml:"smtp_address"` + AddressFrom string `toml:"address_from"` + Login string `toml:"login"` + Password string `toml:"password"` +} type Config struct { + Domain string `toml:"domain"` JwtSecret string `toml:"jwt_secret"` PasswordResetSecret string `toml:"password_reset_secret"` FreeSMSUser string `toml:"free_sms_user"` @@ -23,6 +33,15 @@ func SetConfig(config Config) { PASSWORD_RESET_SECRET = []byte(config.PasswordResetSecret) } +func SetMailConfig(config MailConfig) { + MAIL_CONFIG = config + + if config.Login != "" && config.AddressFrom != "" && config.Password != "" && config.SmtpAddress != "" { + MAIL_CONFIG.IsEnabled = true + ConfigureMailTemplateEngine() + } +} + type Error struct { Code ErrorCode UserMessage string diff --git a/api/mail.go b/api/mail.go new file mode 100644 index 0000000..e0f6ccb --- /dev/null +++ b/api/mail.go @@ -0,0 +1,103 @@ +package api + +import ( + "fmt" + "strings" + + "github.com/matcornic/hermes" + "gopkg.in/gomail.v2" +) + +var MailTemplateEngine hermes.Hermes + +func ConfigureMailTemplateEngine() { + var link string + if strings.Contains(CONFIG.Domain, "localhost") { + link = fmt.Sprintf("http://%s", CONFIG.Domain) + } else { + link = fmt.Sprintf("https://%s", CONFIG.Domain) + } + + MailTemplateEngine = hermes.Hermes{ + Product: hermes.Product{ + Name: "CryptoPF", + Link: link, + Copyright: "Copyright © 2017 CryptoPF. All rights reserved.", + }, + } +} + +func SendResetPasswordMail(to, token string) error { + mail := hermes.Email{ + Body: hermes.Body{ + Name: to, + Intros: []string{ + "You have received this email because a password reset request for your CryptoPF account was received.", + }, + Actions: []hermes.Action{ + { + Instructions: "Click the button below to reset your password:", + Button: hermes.Button{ + Color: "#DC4D2F", + Text: "Reset your password", + Link: fmt.Sprintf("%s/change-password?token=%s", MailTemplateEngine.Product.Link, token), + }, + }, + }, + Outros: []string{ + "If you did not request a password reset, no further action is required on your part.", + }, + Signature: "Thanks", + }, + } + + body, err := MailTemplateEngine.GenerateHTML(mail) + if err != nil { + return err + } + + return SendEmail(to, "Password reset", body) +} + +func SendConfirmationMail(to, token string) error { + mail := hermes.Email{ + Body: hermes.Body{ + Name: to, + Intros: []string{ + "Welcome to CryptoPF! We're very excited to have you on board.", + }, + Actions: []hermes.Action{ + { + Instructions: "To get started with CryptoPF, please click here:", + Button: hermes.Button{ + Text: "Confirm your account", + Link: fmt.Sprintf("%s/confirm?token=%s", MailTemplateEngine.Product.Link, token), + }, + }, + }, + Outros: []string{ + "Need help, or have questions? Just reply to this email, we'd love to help.", + }, + Signature: "Thanks", + }, + } + + body, err := MailTemplateEngine.GenerateHTML(mail) + if err != nil { + return err + } + + return SendEmail(to, "Confirm your email", body) +} + +func SendEmail(to, subject, body string) error { + m := gomail.NewMessage() + m.SetAddressHeader("From", MAIL_CONFIG.AddressFrom, "CryptoPF") + m.SetAddressHeader("To", to, to) + m.SetHeader("Subject", subject) + m.SetBody("text/html", body) + + d := gomail.NewPlainDialer(MAIL_CONFIG.SmtpAddress, 587, MAIL_CONFIG.Login, MAIL_CONFIG.Password) + + return d.DialAndSend(m) +} diff --git a/api/password_reset.go b/api/password_reset.go index 4b002cd..c7931d4 100644 --- a/api/password_reset.go +++ b/api/password_reset.go @@ -42,6 +42,13 @@ func (q PasswordResetQuery) Run() (interface{}, *Error) { } } + if MAIL_CONFIG.IsEnabled { + err = SendResetPasswordMail(q.In.Email, token) + if err != nil { + return nil, NewInternalError(err) + } + } + return nil, nil } diff --git a/api/routes.go b/api/routes.go index d7b316d..d0e8cec 100644 --- a/api/routes.go +++ b/api/routes.go @@ -27,6 +27,7 @@ var Groups = []Group{ {"POST", []gin.HandlerFunc{Signin}, "/signin"}, {"POST", []gin.HandlerFunc{PasswordReset}, "/passwordreset"}, {"POST", []gin.HandlerFunc{ChangePassword}, "/changepassword"}, + {"POST", []gin.HandlerFunc{ConfirmEmail}, "/confirmemail"}, }, }, { @@ -160,3 +161,11 @@ func ChangePassword(c *gin.Context) { RunQuery(query, c) } + +func ConfirmEmail(c *gin.Context) { + query := &ConfirmEmailQuery{} + + query.In.Token = c.PostForm("token") + + RunQuery(query, c) +} diff --git a/api/user.go b/api/user.go index c1d9d6c..2848696 100644 --- a/api/user.go +++ b/api/user.go @@ -3,7 +3,10 @@ package api import ( "fmt" "regexp" + "strconv" + "time" + "github.com/dchest/passwordreset" "github.com/gin-gonic/gin" "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" @@ -90,6 +93,21 @@ func (q SignupQuery) Run() (interface{}, *Error) { return nil, NewInternalError(err) } + if MAIL_CONFIG.IsEnabled { + mailConfirmationToken := passwordreset.NewToken(q.In.Email, time.Hour*24*1, []byte(strconv.FormatUint(uint64(newUser.Status), 10)), PASSWORD_RESET_SECRET) + err = SendConfirmationMail(q.In.Email, mailConfirmationToken) + if err != nil { + return nil, NewInternalError(err) + } + } + + if CONFIG.FreeSMSUser != "" { + err := SendSMS(CONFIG.FreeSMSUser, CONFIG.FreeSMSPass, fmt.Sprintf("'%v' request a password reset. Token '/change-password?token=%v'", q.In.Email, token)) + if err != nil { + return nil, NewInternalError(err) + } + } + return SignResult{token}, nil } @@ -143,3 +161,56 @@ func GetUser(c *gin.Context) db.User { return user.(db.User) } + +type ConfirmEmailQuery struct { + In struct { + Token string + } +} + +func (q ConfirmEmailQuery) ValidateParams() *Error { + + if q.In.Token == "" { + return &Error{BadRequest, "invalid token", fmt.Errorf("invalid token")} + } + + return nil +} + +func (q ConfirmEmailQuery) Run() (interface{}, *Error) { + var user *db.User + + email, err := passwordreset.VerifyToken(q.In.Token, func(email string) ([]byte, error) { + var err error + user, err = db.GetUserByEmail(email) + if err != nil { + return nil, err + } + + if user == nil { + return nil, fmt.Errorf("'%v' is not registered", email) + } + + return []byte(strconv.FormatUint(uint64(user.Status), 10)), nil + + }, PASSWORD_RESET_SECRET) + + if err != nil && (err == passwordreset.ErrExpiredToken) { + return nil, &Error{BadRequest, "expired token", fmt.Errorf("expired token")} + } else if err != nil && (err == passwordreset.ErrMalformedToken || err == passwordreset.ErrWrongSignature) { + return nil, &Error{BadRequest, "wrong token", fmt.Errorf("wrong token")} + } else if err != nil { + return nil, NewInternalError(err) + } + + if user == nil { + return nil, &Error{BadRequest, "bad request", fmt.Errorf("no user found for email '%v'", email)} + } + + err = db.SetUserStatus(user, db.Confirmed) + if err != nil { + return nil, NewInternalError(err) + } + + return nil, nil +} diff --git a/cmd/app/main.go b/cmd/app/main.go index 28eb775..e414bf2 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -22,12 +22,12 @@ type AppConfig struct { type ApiConfig struct { api.Config - Domain string `toml:"domain"` } type Config struct { App AppConfig Api ApiConfig + Mail api.MailConfig Db db.DBConfig Redis db.RedisConfig @@ -61,6 +61,7 @@ func init() { } api.SetConfig(C.Api.Config) + api.SetMailConfig(C.Mail) db.Init(C.Db, C.Redis) @@ -139,6 +140,7 @@ func main() { "/", "/signup", "/signin", + "/confirm", "/reset-password", "/change-password", "/signout", diff --git a/cmd/web/js/api.js b/cmd/web/js/api.js index f892a6b..62530ba 100644 --- a/cmd/web/js/api.js +++ b/cmd/web/js/api.js @@ -64,6 +64,16 @@ var ApiEndpoints = { return '/changepassword'; } }, + 'CONFIRM_EMAIL': { + 'type': 'POST', + 'auth': false, + 'parameters': [ + {'name': 'token', 'mandatory': true, 'inquery': true}, + ], + 'buildUrl': function() { + return '/confirmemail'; + } + }, 'MARKET': { 'type': 'GET', 'auth': true, diff --git a/cmd/web/js/main.jsx b/cmd/web/js/main.jsx index 84b5848..5dc45eb 100644 --- a/cmd/web/js/main.jsx +++ b/cmd/web/js/main.jsx @@ -62,6 +62,27 @@ App.page('/change-password', false, function(context) { ); }); +App.page('/confirm', false, function(context) { + var token = qs.parse(context.querystring).token; + + if (token === undefined) { + App.go('/'); + return; + } + + Api.Call( + 'CONFIRM_EMAIL', + {'token': token}, + function(err, status, data) { + if (err) { + console.error(err, data); + } + + App.go('/me'); + } + ); +}); + App.page('/signout', true, function(context) { cookies.removeItem('jwt'); @@ -84,8 +105,8 @@ App.page('/not_confirmed', true, function(context) { App.mount(
-

Please be patient, you account is being confirmed...

-

Refresh

+

An email has now been sent to your email address.

+

Please click the "Confirm your account" button to validate your email.

); diff --git a/db/user.go b/db/user.go index 7a0a32b..64ca6a6 100644 --- a/db/user.go +++ b/db/user.go @@ -80,3 +80,9 @@ func SetPassword(user *User, password string) error { return DB.Update(user) } + +func SetUserStatus(user *User, status UserStatus) error { + user.Status = status + + return DB.Update(user) +}