]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front.git/commitdiff
Mails.
authorjloup <jeanloup.jamet@gmail.com>
Fri, 11 May 2018 11:57:29 +0000 (13:57 +0200)
committerjloup <jeanloup.jamet@gmail.com>
Fri, 11 May 2018 11:57:29 +0000 (13:57 +0200)
Gopkg.lock
Gopkg.toml
api/api.go
api/mail.go [new file with mode: 0644]
api/password_reset.go
api/routes.go
api/user.go
cmd/app/main.go
cmd/web/js/api.js
cmd/web/js/main.jsx
db/user.go

index 91f891c9734b9c9a096fecea5ae5b61cbd3e2054..2a6ae4fe7d111906abe36ab0295a7009143cb335 100644 (file)
@@ -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 = [
   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"
   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 = [
   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"]
   packages = [
     "bcrypt",
     "blowfish",
+    "pbkdf2",
+    "scrypt",
     "ssh/terminal"
   ]
   revision = "8b1d31080a7692e075c4681cb2458454a1fe0706"
 [[projects]]
   branch = "master"
   name = "golang.org/x/net"
-  packages = ["websocket"]
+  packages = [
+    "html",
+    "html/atom",
+    "websocket"
+  ]
   revision = "640f4622ab692b87c2f3a94265e6f579fe38263d"
 
 [[projects]]
   ]
   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 = ["."]
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "5c987f56ef837352173d0a50f12d6c58ac72831b5e90d34ca0a283bee71fb1a2"
+  inputs-digest = "9df92ae3bbf81638f86228e1daacd75bd4f6d0afbfc449742e7f28fdefffdc46"
   solver-name = "gps-cdcl"
   solver-version = 1
index 5926748c2a5546c7194de93edca3c2da3fdffbcd..31e55bda6b6c34a57a6539f49ad8f5fb7cfab706 100644 (file)
@@ -76,3 +76,7 @@
 [[override]]
   name = "github.com/dchest/authcookie"
   branch = "master"
+
+[[constraint]]
+  name = "github.com/matcornic/hermes"
+  version = "1.1.1"
index 79a13a5663fb5f2081458db268a0baefd7ed9731..e01181123633e4a7112dd1124375ae8d7f5e680b 100644 (file)
@@ -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 (file)
index 0000000..e0f6ccb
--- /dev/null
@@ -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)
+}
index 4b002cdf2f525fe5c4606c094ecaf3a446054b06..c7931d4c7d9083b488ba4a1421939959e3a1156a 100644 (file)
@@ -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
 }
 
index d7b316d885517e4a720aa3b1bdeb4ffa1037e7e0..d0e8cec151dbd550e2c406f06a7498747c9d1207 100644 (file)
@@ -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)
+}
index c1d9d6c608feed7e963a215c1fe05092e5a8dcec..28486961da0b7757e16533f96bac7c183ce5c6ab 100644 (file)
@@ -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
+}
index 28eb77522b0e2ff1c421b16292bae3290a58c514..e414bf2c45eb87f6a0a0879b2c154e5f4f5d4c4e 100644 (file)
@@ -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",
index f892a6b3affa49a5463c0165cab2b5593802ce65..62530ba8a82b0122016c0ece2ab5cc149fdda5b9 100644 (file)
@@ -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,
index 84b584895235ef7b426440d478e1f963784af344..5dc45eb9a74c79a3d2b4f545b915ace1b7ffbb66 100644 (file)
@@ -62,6 +62,27 @@ App.page('/change-password', false, function(context) {
     </div>);
 });
 
+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(<div>
       <div className="row">
         <div className="box offset-3 col-6 text-center">
-          <p>Please be patient, you account is being confirmed...</p>
-          <p><a href="/me"><u>Refresh</u></a></p>
+          <p>An email has now been sent to your email address.</p>
+          <p>Please click the "Confirm your account" button to validate your email.</p>
         </div>
       </div>
     </div>);
index 7a0a32b37105a1fac3f83b421c986ab684bd80ab..64ca6a611ee7609ae0607995c4820598372739f2 100644 (file)
@@ -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)
+}