]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front.git/commitdiff
Password reset.
authorjloup <jeanloup.jamet@gmail.com>
Fri, 4 May 2018 09:55:15 +0000 (11:55 +0200)
committerjloup <jeanloup.jamet@gmail.com>
Fri, 4 May 2018 09:55:15 +0000 (11:55 +0200)
20 files changed:
Gopkg.lock
Gopkg.toml
api/api.go
api/auth_jwt.go
api/free_sms.go [new file with mode: 0644]
api/password_reset.go [new file with mode: 0644]
api/routes.go
api/user.go
cmd/ansible/conf.toml.j2
cmd/ansible/vars.yml
cmd/app/conf.toml
cmd/app/main.go
cmd/web/Makefile
cmd/web/js/api.js
cmd/web/js/change_password.jsx [new file with mode: 0644]
cmd/web/js/main.jsx
cmd/web/js/password_reset.jsx [new file with mode: 0644]
cmd/web/package.json
cmd/web/yarn.lock
db/user.go

index d8ceccf32e28edae7013e707fab92e2fd5c52f8b..91f891c9734b9c9a096fecea5ae5b61cbd3e2054 100644 (file)
   revision = "3cfea5ab600ae37946be2b763b8ec2c1cf2d272d"
   version = "v1.0.0"
 
+[[projects]]
+  branch = "master"
+  name = "github.com/dchest/authcookie"
+  packages = ["."]
+  revision = "fbdef6e998665bcb27a2227f39d840dbfc62a918"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/dchest/passwordreset"
+  packages = ["."]
+  revision = "642cf836ac8dfd4ddec7a28cbef7b00da0807812"
+
 [[projects]]
   name = "github.com/dgrijalva/jwt-go"
   packages = ["."]
@@ -67,8 +79,8 @@
     "orm",
     "types"
   ]
-  revision = "24dfe0572921e42ffe1035f7afbd40f9d97cb8c8"
-  version = "v6.10.0"
+  revision = "5b73ce88484575f3480edf393237f6bf79d5f166"
+  version = "v6.11.2"
 
 [[projects]]
   name = "github.com/go-redis/redis"
 [[projects]]
   name = "github.com/golang/protobuf"
   packages = ["proto"]
-  revision = "925541529c1fa6821df4e44ce2723319eb2be768"
-  version = "v1.0.0"
+  revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
+  version = "v1.1.0"
 
 [[projects]]
   branch = "master"
 [[projects]]
   name = "github.com/ugorji/go"
   packages = ["codec"]
-  revision = "9831f2c3ac1068a78f50999a30db84270f647af6"
-  version = "v1.1"
+  revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
+  version = "v1.1.1"
 
 [[projects]]
   branch = "master"
     "blowfish",
     "ssh/terminal"
   ]
-  revision = "88942b9c40a4c9d203b82b3731787b672d6e809b"
+  revision = "8b1d31080a7692e075c4681cb2458454a1fe0706"
 
 [[projects]]
   branch = "master"
   name = "golang.org/x/net"
   packages = ["websocket"]
-  revision = "6078986fec03a1dcc236c34816c71b0e05018fda"
+  revision = "640f4622ab692b87c2f3a94265e6f579fe38263d"
 
 [[projects]]
   branch = "master"
     "unix",
     "windows"
   ]
-  revision = "91ee8cde435411ca3f1cd365e8f20131aed4d0a1"
+  revision = "78d5f264b493f125018180c204871ecf58a2dce1"
 
 [[projects]]
   name = "gopkg.in/go-playground/validator.v8"
 [[projects]]
   name = "gopkg.in/yaml.v2"
   packages = ["."]
-  revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5"
-  version = "v2.1.1"
+  revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
+  version = "v2.2.1"
 
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "587bbc93998cd884863b3559ec6ef87ebf381b7bca10587a7e79c2c227c64ead"
+  inputs-digest = "5c987f56ef837352173d0a50f12d6c58ac72831b5e90d34ca0a283bee71fb1a2"
   solver-name = "gps-cdcl"
   solver-version = 1
index 9721956104f2d9a8aa1b1bbb99780353821e12da..5926748c2a5546c7194de93edca3c2da3fdffbcd 100644 (file)
 [[constraint]]
   name = "github.com/go-redis/redis"
   version = "6.10.2"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/dchest/passwordreset"
+
+[[override]]
+  name = "github.com/dchest/authcookie"
+  branch = "master"
index 7b7be49257a2949796b5435ccaddc6abd63893bf..42b9923986484cf896d7587efaa8c508f2bde379 100644 (file)
@@ -7,6 +7,22 @@ import (
        "github.com/gin-gonic/gin"
 )
 
+var CONFIG Config
+
+type Config struct {
+       JwtSecret           string `toml:"jwt_secret"`
+       PasswordResetSecret string `toml:"password_reset_secret"`
+       FreeSMSUser         string `toml:"free_sms_user"`
+       FreeSMSPass         string `toml:"free_sms_pass"`
+}
+
+func SetConfig(config Config) {
+       CONFIG = config
+
+       JWT_SECRET = []byte(config.JwtSecret)
+       PASSWORD_RESET_SECRET = []byte(config.PasswordResetSecret)
+}
+
 type Error struct {
        Code        ErrorCode
        UserMessage string
index 5ce159369a89180650aa706bf65db1d3290a5d16..db7e3f4e4437d4c38b494ec749464c22ec9b74ca 100644 (file)
@@ -20,10 +20,6 @@ type JwtClaims struct {
        jwt.StandardClaims
 }
 
-func SetJwtSecretKey(secret string) {
-       JWT_SECRET = []byte(secret)
-}
-
 func VerifyJwtToken(token string) (JwtClaims, error) {
        if len(JWT_SECRET) == 0 {
                return JwtClaims{}, fmt.Errorf("not initialized jwt secret")
diff --git a/api/free_sms.go b/api/free_sms.go
new file mode 100644 (file)
index 0000000..f09a1d1
--- /dev/null
@@ -0,0 +1,26 @@
+package api
+
+import (
+       "fmt"
+       "net/http"
+       "net/url"
+)
+
+func SendSMS(user, pass, msg string) error {
+       form := url.Values{
+               "user": []string{user},
+               "pass": []string{pass},
+               "msg":  []string{msg},
+       }
+
+       response, err := http.Get(fmt.Sprintf("https://smsapi.free-mobile.fr/sendmsg?%s", form.Encode()))
+       if err != nil {
+               return err
+       }
+
+       if response.StatusCode != 200 {
+               return fmt.Errorf("Cannot send sms: status code %v", response.StatusCode)
+       }
+
+       return nil
+}
diff --git a/api/password_reset.go b/api/password_reset.go
new file mode 100644 (file)
index 0000000..82aaaef
--- /dev/null
@@ -0,0 +1,103 @@
+package api
+
+import (
+       "fmt"
+       "time"
+
+       "github.com/dchest/passwordreset"
+       "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db"
+)
+
+var PASSWORD_RESET_SECRET []byte
+
+type PasswordResetQuery struct {
+       In struct {
+               Email string
+       }
+}
+
+func (q PasswordResetQuery) ValidateParams() *Error {
+       if q.In.Email == "" {
+               return &Error{InvalidEmail, "invalid email", fmt.Errorf("invalid email")}
+       }
+
+       return nil
+}
+
+func (q PasswordResetQuery) Run() (interface{}, *Error) {
+       user, err := db.GetUserByEmail(q.In.Email)
+       if err != nil {
+               return nil, NewInternalError(err)
+       }
+
+       if user == nil {
+               return nil, &Error{NotFound, "account not found", fmt.Errorf("'%v' is not registered", q.In.Email)}
+       }
+
+       token := passwordreset.NewToken(q.In.Email, time.Hour*24*1, []byte(user.PasswordHash), PASSWORD_RESET_SECRET)
+       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 "OK", nil
+}
+
+type ChangePasswordQuery struct {
+       In struct {
+               Token    string
+               Password string
+       }
+}
+
+func (q ChangePasswordQuery) ValidateParams() *Error {
+       if q.In.Password == "" {
+               return &Error{InvalidPassword, "invalid password", fmt.Errorf("invalid password")}
+       }
+
+       if q.In.Token == "" {
+               return &Error{BadRequest, "invalid token", fmt.Errorf("invalid token")}
+       }
+
+       return nil
+}
+
+func (q ChangePasswordQuery) 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(user.PasswordHash), 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.SetPassword(user, q.In.Password)
+       if err != nil {
+               return nil, NewInternalError(err)
+       }
+
+       return "OK", nil
+}
index cdf3dd97cd5d67cb8e73ba6c7c6bc89ae2fe4816..22af0e728c9e0c6fe3c365592466512ad42886c1 100644 (file)
@@ -25,6 +25,8 @@ var Groups = []Group{
                []Route{
                        {"POST", []gin.HandlerFunc{Signup}, "/signup"},
                        {"POST", []gin.HandlerFunc{Signin}, "/signin"},
+                       {"POST", []gin.HandlerFunc{PasswordReset}, "/passwordreset"},
+                       {"POST", []gin.HandlerFunc{ChangePassword}, "/changepassword"},
                },
        },
        {
@@ -132,3 +134,20 @@ func UpdateMarketConfig(c *gin.Context) {
 
        RunQuery(query, c)
 }
+
+func PasswordReset(c *gin.Context) {
+       query := &PasswordResetQuery{}
+
+       query.In.Email = c.PostForm("email")
+
+       RunQuery(query, c)
+}
+
+func ChangePassword(c *gin.Context) {
+       query := &ChangePasswordQuery{}
+
+       query.In.Token = c.PostForm("token")
+       query.In.Password = c.PostForm("password")
+
+       RunQuery(query, c)
+}
index 1dc69e470d176e29cda92c281ed1bac4edffa06b..9fd947985210e186608cc290572078a12c8388aa 100644 (file)
@@ -74,6 +74,13 @@ func (q SignupQuery) Run() (interface{}, *Error) {
                return nil, NewInternalError(fmt.Errorf("cannot create jwt token %v", err))
        }
 
+       if CONFIG.FreeSMSUser != "" {
+               err := SendSMS(CONFIG.FreeSMSUser, CONFIG.FreeSMSPass, fmt.Sprintf("New user signup '%v'", q.In.Email))
+               if err != nil {
+                       return nil, NewInternalError(err)
+               }
+       }
+
        return SignResult{token}, nil
 }
 
index b2c3137cfaf6eaf1fc3e151bc08c859e9faf50ad..7b048af3c25f9698d2365baed8234f4f8870233d 100644 (file)
@@ -17,6 +17,9 @@ database=0
 [api]
 domain="{{ app_domain }}"
 jwt_secret="{{ jwt_secret }}"
+password_reset_secret="{{ reset_password_reset }}"
+free_sms_user="20996747"
+free_sms_pass="bM2ZPETB4zzWg3"
 
 [app]
 public_dir="/var/cryptoportfolio-app/static"
index 1de7413be02ca6ce858676748ee5ce4676e9349b..59a5eb89daa62f945b053911b79fecc5464fbfad 100644 (file)
@@ -1,22 +1,30 @@
 $ANSIBLE_VAULT;1.1;AES256
-63613535333830393037646665363566636635366534636261623839326130663431653839346266
-3832643338623561313362663837323234663537663439350a313034326663383235663964626132
-38343964396265323539396439383731336464393337383833653666643736303539626136383431
-6536316338376538360a343862626636363031353037626462333364623433613861393137353336
-37396664663030363530333364633266653862393538313835326138663465626638326363656561
-30393836386664633834663838666432383836623432363936343635313835303166393531643966
-33313361383565363232373066306534613465386534386266306564383365373762613361366365
-61366530623863623336643531346463323233323539333139336335383439373132373233663031
-39666535633362383135376534376532333663636136366130653762643164333436313261646137
-37353139633361636163326366616234613466393731373631616138386263383131663537633533
-31393763316561623134623063623735356334363833623939313437386330323837626131356332
-30383863373535366137366138633832623566613061313138396539306536633763633934313562
-35383763653532336539346632623935303634353866636264373262363839326439313837313765
-36303539613734646238636432393166616438666665363363323331373437633362613838653564
-64393639346661646333383466363162633638643838386666383564366665656266333836363435
-35643231323362323566303535303561626139333830393538383635326631656666323166343863
-31393566346531653535393738326166303261376238316532373833616432306638326139353234
-32653132323764316231393634663262313765393230656232343833373438636430643663353965
-36333931303731646333316430646534383531313264353936396565336338663530303434643036
-34356663373533663137636235386164646334356262336464363862643332636661313339303531
-35663833656564393331636139663738323834373862623436633666306661373166
+66653734343938333339346336333430336239376539663338333431626461656430393931353165
+3564303163396337646466376265633537636362346434330a376630333634366165353832613063
+36353931626633306565613862396566353138353439666336376231643432343637613636633634
+6531666131343361340a633839363864643037373638356433613830666137626665343362303363
+64323766333830646463313130316234316161383031646132346633316162366335643137663835
+63663866613335643330653839393762373163366530376538643666653630636462363535613137
+63663966343335346564316462323463666130313733396164663761613966366165653162663432
+66616232633733613766336465336632373765326362383035613465303463306330623632356232
+38636136633661323137336431646666323464383033346239633032336561613433646632363564
+33323737313235313463643866373465366436376138303430633535623335393163343862366264
+35626263336462353264656637643437326635633237333932613666646666313235396561663762
+36373762363662613635633065383037336634323665333865363533616136373565633436653566
+34393033356566663433343864623664386330376339653463623834393036643733666237393964
+32316134653666306134623135636664643461303831376336373839356531663764636433323539
+62663938363437623235383666363163353562646165633564323635343534316130663062373332
+39303239363166366538623763396563616664663038346465336263666365306435376364366337
+66363137303731613638323839393731326539356262626634373136316265323937303863363637
+37313565663534353035313834626139353333386337643263663264376238393030386363613435
+33616432383131303761306265653566306266366263616162323363383365333363363334653132
+32343235666434636361656133636332303131313766326665393233316135323566633433303362
+62383431313861323036616331633134386238313034633936353931313838383038373435653235
+32333230646439613134613337333762313062613839613232663037363761353664373935356264
+61386239366330653939613637333264313532666363626633386632376138643265373432373233
+39313234326430663561343961313732666465613939343839663263353964663963356639633231
+64663330323761393932613039326339643737303939363033333666346439393631623038316561
+34313233626666663234613339306539343030616266333565326565346563353739613363363464
+65323763653436303030643765663739383965313335373265636336633762636134653536326364
+65353366643639663765663566663065316334383463343464366662373939343763356333343731
+6361
index 13e3e0b7e931e6fe41ed4a57d218785d2a29d626..16087e5edf79437a3d90a64cc9fb19d1aff8947d 100644 (file)
@@ -16,6 +16,9 @@ database=0
 [api]
 domain="localhost"
 jwt_secret="secret"
+password_reset_secret="resetsecret"
+free_sms_user="20996747"
+free_sms_pass="bM2ZPETB4zzWg3"
 
 [app]
 public_dir="../web/build/static"
index 65e8b5a49af92b4dbda73472595b33883aebea77..a0463d260d0d839151ff18ee7f52ff57f622f249 100644 (file)
@@ -21,8 +21,8 @@ type AppConfig struct {
 }
 
 type ApiConfig struct {
-       Domain    string `toml:"domain"`
-       JwtSecret string `toml:"jwt_secret"`
+       api.Config
+       Domain string `toml:"domain"`
 }
 
 type Config struct {
@@ -45,9 +45,6 @@ func (c *Config) SetToDefaults() {
                App: AppConfig{
                        PublicDir: "./public",
                },
-               Api: ApiConfig{
-                       JwtSecret: "secret",
-               },
        }
 
        c.LogConfiguration.SetToDefaults()
@@ -63,7 +60,7 @@ func init() {
                panic(err)
        }
 
-       api.SetJwtSecretKey(C.Api.JwtSecret)
+       api.SetConfig(C.Api.Config)
 
        db.Init(C.Db, C.Redis)
 
@@ -142,6 +139,8 @@ func main() {
                "/",
                "/signup",
                "/signin",
+               "/reset-password",
+               "/change-password",
                "/signout",
                "/me",
                "/otp/enroll",
index c6bc2bd0b9376ffcc45734bda9a3989be4db0511..c5d5d62b99233c700b359fe04d129d1968245362 100644 (file)
@@ -4,7 +4,7 @@ export PATH := $(PATH):./node_modules/.bin
 
 SRC_DIR=js
 BUILD_DIR=build/js
-JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx
+JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx password_reset.jsx change_password.jsx
 JS_SRC= cookies.js app.js api.js
 STATIC_FILES= index.html style.css
 JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js))
index 5cbf5eb07094921b709ba457a559a257a9d7c347..c9b4ef5eda1d87bbecb2d3f93d2ce253f31b7e83 100644 (file)
@@ -43,6 +43,27 @@ var ApiEndpoints = {
       return '/signin';
     }
   },
+  'RESET_PASSWORD': {
+    'type': 'POST',
+    'auth': false,
+    'parameters': [
+      {'name': 'email', 'mandatory': true, 'inquery': true},
+    ],
+    'buildUrl': function() {
+      return '/passwordreset';
+    }
+  },
+  'CHANGE_PASSWORD': {
+    'type': 'POST',
+    'auth': false,
+    'parameters': [
+      {'name': 'token',    'mandatory': true, 'inquery': true},
+      {'name': 'password', 'mandatory': true, 'inquery': true},
+    ],
+    'buildUrl': function() {
+      return '/changepassword';
+    }
+  },
   'MARKET': {
     'type': 'GET',
     'auth': true,
diff --git a/cmd/web/js/change_password.jsx b/cmd/web/js/change_password.jsx
new file mode 100644 (file)
index 0000000..aedf4af
--- /dev/null
@@ -0,0 +1,62 @@
+import Api from './api.js';
+import App from './app.js';
+import classNames from 'classnames';
+import React from 'react';
+
+class ChangePasswordForm extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {'hideMsg': true, 'msg': '', 'msgOk': false, 'password': ''};
+  }
+
+  handleSubmit = (e) => {
+    Api.Call(
+    'CHANGE_PASSWORD',
+    {
+      'password': this.state.password,
+      'token': this.props.token
+    },
+    function(err, status, data) {
+      if (err) {
+        console.error(err, data);
+        this.displayMessage(App.errorCodeToMessage(err.code), false);
+        return;
+      }
+
+      this.displayMessage('You password has been reset.', true);
+      this.props.onSuccess();
+
+    }.bind(this)
+);
+    e.preventDefault();
+  }
+
+  handlePasswordChange = (event) => {
+    this.setState({'password': event.target.value});
+  }
+
+  hideMessage = () => {
+    this.setState({'hideMsg': true});
+  }
+
+  displayMessage = (msg, ok) => {
+    this.setState({'msg': msg, 'msgOk': ok, 'hideMsg': false});
+  }
+
+  render = () => {
+    var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk});
+    return (
+        <div className="row sign-in">
+          <div className="offset-4 col-4 col-xs-offset-1 col-xs-10 text-center">
+            <form role="form" onSubmit={this.handleSubmit}>
+              <input className="form-control" type="password" placeholder="password" onChange={this.handlePasswordChange} />
+              <input className="form-control submit" type="submit" value="Change password" />
+              <div className={cName}>{this.state.msg}</div>
+            </form>
+          </div>
+        </div>
+       );
+  }
+}
+
+export default ChangePasswordForm;
index e64adc777c06dda795c5296d813be74b0e81bcb5..909f1bd9a26f5b8037069b2d96a91dd98d900e29 100644 (file)
@@ -1,11 +1,14 @@
 import SignupForm from './signup.js';
 import SigninForm from './signin.js';
+import PasswordResetForm from './password_reset.js';
+import ChangePasswordForm from './change_password.js';
 import OtpEnrollForm from './otp.js';
 import PoloniexController from './poloniex.js';
 import App from './app.js';
 import Api from './api.js';
 import cookies from './cookies.js';
 import React from 'react';
+import qs from 'qs';
 
 class Header extends React.Component {
   render = () => {
@@ -60,6 +63,37 @@ App.page('/signin', false, function(context) {
     </div>);
 });
 
+App.page('/reset-password', false, function(context) {
+  if (App.isUserSignedIn()) {
+    App.go('/me');
+    return;
+  }
+
+  App.mount(<div>
+      <Header />
+      <PasswordResetForm />
+    </div>);
+});
+
+App.page('/change-password', false, function(context) {
+  if (App.isUserSignedIn()) {
+    App.go('/me');
+    return;
+  }
+
+  var token = qs.parse(context.querystring).token;
+
+  if (token === undefined) {
+    App.go('/');
+    return;
+  }
+
+  App.mount(<div>
+      <Header />
+      <ChangePasswordForm token={token} onSuccess={App.go.bind(App, '/signin')}/>
+    </div>);
+});
+
 App.page('/signout', true, function(context) {
   cookies.removeItem('jwt');
 
diff --git a/cmd/web/js/password_reset.jsx b/cmd/web/js/password_reset.jsx
new file mode 100644 (file)
index 0000000..8cbdc60
--- /dev/null
@@ -0,0 +1,57 @@
+import Api from './api.js';
+import App from './app.js';
+import classNames from 'classnames';
+import React from 'react';
+
+class PasswordResetForm extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {'hideMsg': true, 'msg': '', 'msgOk': false, 'email': ''};
+  }
+
+  handleSubmit = (e) => {
+    Api.Call('RESET_PASSWORD', {'email': this.state.email}, function(err, status, data) {
+      if (err) {
+        console.error(err, data);
+        this.displayMessage(App.errorCodeToMessage(err.code), false);
+        return;
+      }
+
+      this.displayMessage('You will receive a reset link to reset your password.', true);
+      if (this.props.onSuccess) {
+        this.props.onSuccess();
+      }
+
+    }.bind(this));
+    e.preventDefault();
+  }
+
+  handleEmailChange = (event) => {
+    this.setState({'email': event.target.value});
+  }
+
+  hideMessage = () => {
+    this.setState({'hideMsg': true});
+  }
+
+  displayMessage = (msg, ok) => {
+    this.setState({'msg': msg, 'msgOk': ok, 'hideMsg': false});
+  }
+
+  render = () => {
+    var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk});
+    return (
+        <div className="row sign-in">
+          <div className="offset-4 col-4 col-xs-offset-1 col-xs-10 text-center">
+            <form role="form" onSubmit={this.handleSubmit}>
+              <input className="form-control" type="email" placeholder="email" onChange={this.handleEmailChange} />
+              <input className="form-control submit" type="submit" value="Reset" />
+              <div className={cName}>{this.state.msg}</div>
+            </form>
+          </div>
+        </div>
+       );
+  }
+}
+
+export default PasswordResetForm;
\ No newline at end of file
index c9241f14cd58c4d0fb7c4aadf4593dd3719214f4..a86731336baf186f5a7d2eeffbb555fa0d551fc4 100644 (file)
@@ -14,6 +14,7 @@
     "localenvify": "^1.0.1",
     "page": "^1.8.3",
     "path-to-regexp": "^1.2.1",
+    "qs": "^6.5.1",
     "react": "^16.2.0"
   },
   "devDependencies": {
index 0d162a97a3324739894d66109085757fe31de00d..b2218ee570d28bf9f6caebcf6405b229d395308b 100644 (file)
@@ -3536,6 +3536,10 @@ q@~1.0.0, q@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.0.1.tgz#11872aeedee89268110b10a718448ffb10112a14"
 
+qs@^6.5.1:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
 qs@~1.2.0:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.2.tgz#19b57ff24dc2a99ce1f8bdf6afcda59f8ef61f88"
index aed0ac173c92652338c213629abbe5d276fff663..7a0a32b37105a1fac3f83b421c986ab684bd80ab 100644 (file)
@@ -70,3 +70,13 @@ func SetOtpSecret(user *User, secret string, temporary bool) error {
 
        return DB.Update(user)
 }
+
+func SetPassword(user *User, password string) error {
+       var err error
+       user.PasswordHash, err = HashPassword(password)
+       if err != nil {
+               return err
+       }
+
+       return DB.Update(user)
+}