aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gopkg.lock36
-rw-r--r--Gopkg.toml8
-rw-r--r--api/api.go16
-rw-r--r--api/auth_jwt.go4
-rw-r--r--api/free_sms.go26
-rw-r--r--api/password_reset.go103
-rw-r--r--api/routes.go19
-rw-r--r--api/user.go7
-rw-r--r--cmd/ansible/conf.toml.j23
-rw-r--r--cmd/ansible/vars.yml50
-rw-r--r--cmd/app/conf.toml3
-rw-r--r--cmd/app/main.go11
-rw-r--r--cmd/web/Makefile2
-rw-r--r--cmd/web/js/api.js21
-rw-r--r--cmd/web/js/change_password.jsx62
-rw-r--r--cmd/web/js/main.jsx34
-rw-r--r--cmd/web/js/password_reset.jsx57
-rw-r--r--cmd/web/package.json1
-rw-r--r--cmd/web/yarn.lock4
-rw-r--r--db/user.go10
20 files changed, 433 insertions, 44 deletions
diff --git a/Gopkg.lock b/Gopkg.lock
index d8ceccf..91f891c 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -24,6 +24,18 @@
24 version = "v1.0.0" 24 version = "v1.0.0"
25 25
26[[projects]] 26[[projects]]
27 branch = "master"
28 name = "github.com/dchest/authcookie"
29 packages = ["."]
30 revision = "fbdef6e998665bcb27a2227f39d840dbfc62a918"
31
32[[projects]]
33 branch = "master"
34 name = "github.com/dchest/passwordreset"
35 packages = ["."]
36 revision = "642cf836ac8dfd4ddec7a28cbef7b00da0807812"
37
38[[projects]]
27 name = "github.com/dgrijalva/jwt-go" 39 name = "github.com/dgrijalva/jwt-go"
28 packages = ["."] 40 packages = ["."]
29 revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" 41 revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
@@ -67,8 +79,8 @@
67 "orm", 79 "orm",
68 "types" 80 "types"
69 ] 81 ]
70 revision = "24dfe0572921e42ffe1035f7afbd40f9d97cb8c8" 82 revision = "5b73ce88484575f3480edf393237f6bf79d5f166"
71 version = "v6.10.0" 83 version = "v6.11.2"
72 84
73[[projects]] 85[[projects]]
74 name = "github.com/go-redis/redis" 86 name = "github.com/go-redis/redis"
@@ -88,8 +100,8 @@
88[[projects]] 100[[projects]]
89 name = "github.com/golang/protobuf" 101 name = "github.com/golang/protobuf"
90 packages = ["proto"] 102 packages = ["proto"]
91 revision = "925541529c1fa6821df4e44ce2723319eb2be768" 103 revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
92 version = "v1.0.0" 104 version = "v1.1.0"
93 105
94[[projects]] 106[[projects]]
95 branch = "master" 107 branch = "master"
@@ -134,8 +146,8 @@
134[[projects]] 146[[projects]]
135 name = "github.com/ugorji/go" 147 name = "github.com/ugorji/go"
136 packages = ["codec"] 148 packages = ["codec"]
137 revision = "9831f2c3ac1068a78f50999a30db84270f647af6" 149 revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
138 version = "v1.1" 150 version = "v1.1.1"
139 151
140[[projects]] 152[[projects]]
141 branch = "master" 153 branch = "master"
@@ -145,13 +157,13 @@
145 "blowfish", 157 "blowfish",
146 "ssh/terminal" 158 "ssh/terminal"
147 ] 159 ]
148 revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" 160 revision = "8b1d31080a7692e075c4681cb2458454a1fe0706"
149 161
150[[projects]] 162[[projects]]
151 branch = "master" 163 branch = "master"
152 name = "golang.org/x/net" 164 name = "golang.org/x/net"
153 packages = ["websocket"] 165 packages = ["websocket"]
154 revision = "6078986fec03a1dcc236c34816c71b0e05018fda" 166 revision = "640f4622ab692b87c2f3a94265e6f579fe38263d"
155 167
156[[projects]] 168[[projects]]
157 branch = "master" 169 branch = "master"
@@ -160,7 +172,7 @@
160 "unix", 172 "unix",
161 "windows" 173 "windows"
162 ] 174 ]
163 revision = "91ee8cde435411ca3f1cd365e8f20131aed4d0a1" 175 revision = "78d5f264b493f125018180c204871ecf58a2dce1"
164 176
165[[projects]] 177[[projects]]
166 name = "gopkg.in/go-playground/validator.v8" 178 name = "gopkg.in/go-playground/validator.v8"
@@ -171,12 +183,12 @@
171[[projects]] 183[[projects]]
172 name = "gopkg.in/yaml.v2" 184 name = "gopkg.in/yaml.v2"
173 packages = ["."] 185 packages = ["."]
174 revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5" 186 revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
175 version = "v2.1.1" 187 version = "v2.2.1"
176 188
177[solve-meta] 189[solve-meta]
178 analyzer-name = "dep" 190 analyzer-name = "dep"
179 analyzer-version = 1 191 analyzer-version = 1
180 inputs-digest = "587bbc93998cd884863b3559ec6ef87ebf381b7bca10587a7e79c2c227c64ead" 192 inputs-digest = "5c987f56ef837352173d0a50f12d6c58ac72831b5e90d34ca0a283bee71fb1a2"
181 solver-name = "gps-cdcl" 193 solver-name = "gps-cdcl"
182 solver-version = 1 194 solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
index 9721956..5926748 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -68,3 +68,11 @@
68[[constraint]] 68[[constraint]]
69 name = "github.com/go-redis/redis" 69 name = "github.com/go-redis/redis"
70 version = "6.10.2" 70 version = "6.10.2"
71
72[[constraint]]
73 branch = "master"
74 name = "github.com/dchest/passwordreset"
75
76[[override]]
77 name = "github.com/dchest/authcookie"
78 branch = "master"
diff --git a/api/api.go b/api/api.go
index 7b7be49..42b9923 100644
--- a/api/api.go
+++ b/api/api.go
@@ -7,6 +7,22 @@ import (
7 "github.com/gin-gonic/gin" 7 "github.com/gin-gonic/gin"
8) 8)
9 9
10var CONFIG Config
11
12type Config struct {
13 JwtSecret string `toml:"jwt_secret"`
14 PasswordResetSecret string `toml:"password_reset_secret"`
15 FreeSMSUser string `toml:"free_sms_user"`
16 FreeSMSPass string `toml:"free_sms_pass"`
17}
18
19func SetConfig(config Config) {
20 CONFIG = config
21
22 JWT_SECRET = []byte(config.JwtSecret)
23 PASSWORD_RESET_SECRET = []byte(config.PasswordResetSecret)
24}
25
10type Error struct { 26type Error struct {
11 Code ErrorCode 27 Code ErrorCode
12 UserMessage string 28 UserMessage string
diff --git a/api/auth_jwt.go b/api/auth_jwt.go
index 5ce1593..db7e3f4 100644
--- a/api/auth_jwt.go
+++ b/api/auth_jwt.go
@@ -20,10 +20,6 @@ type JwtClaims struct {
20 jwt.StandardClaims 20 jwt.StandardClaims
21} 21}
22 22
23func SetJwtSecretKey(secret string) {
24 JWT_SECRET = []byte(secret)
25}
26
27func VerifyJwtToken(token string) (JwtClaims, error) { 23func VerifyJwtToken(token string) (JwtClaims, error) {
28 if len(JWT_SECRET) == 0 { 24 if len(JWT_SECRET) == 0 {
29 return JwtClaims{}, fmt.Errorf("not initialized jwt secret") 25 return JwtClaims{}, fmt.Errorf("not initialized jwt secret")
diff --git a/api/free_sms.go b/api/free_sms.go
new file mode 100644
index 0000000..f09a1d1
--- /dev/null
+++ b/api/free_sms.go
@@ -0,0 +1,26 @@
1package api
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7)
8
9func SendSMS(user, pass, msg string) error {
10 form := url.Values{
11 "user": []string{user},
12 "pass": []string{pass},
13 "msg": []string{msg},
14 }
15
16 response, err := http.Get(fmt.Sprintf("https://smsapi.free-mobile.fr/sendmsg?%s", form.Encode()))
17 if err != nil {
18 return err
19 }
20
21 if response.StatusCode != 200 {
22 return fmt.Errorf("Cannot send sms: status code %v", response.StatusCode)
23 }
24
25 return nil
26}
diff --git a/api/password_reset.go b/api/password_reset.go
new file mode 100644
index 0000000..82aaaef
--- /dev/null
+++ b/api/password_reset.go
@@ -0,0 +1,103 @@
1package api
2
3import (
4 "fmt"
5 "time"
6
7 "github.com/dchest/passwordreset"
8 "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db"
9)
10
11var PASSWORD_RESET_SECRET []byte
12
13type PasswordResetQuery struct {
14 In struct {
15 Email string
16 }
17}
18
19func (q PasswordResetQuery) ValidateParams() *Error {
20 if q.In.Email == "" {
21 return &Error{InvalidEmail, "invalid email", fmt.Errorf("invalid email")}
22 }
23
24 return nil
25}
26
27func (q PasswordResetQuery) Run() (interface{}, *Error) {
28 user, err := db.GetUserByEmail(q.In.Email)
29 if err != nil {
30 return nil, NewInternalError(err)
31 }
32
33 if user == nil {
34 return nil, &Error{NotFound, "account not found", fmt.Errorf("'%v' is not registered", q.In.Email)}
35 }
36
37 token := passwordreset.NewToken(q.In.Email, time.Hour*24*1, []byte(user.PasswordHash), PASSWORD_RESET_SECRET)
38 if CONFIG.FreeSMSUser != "" {
39 err := SendSMS(CONFIG.FreeSMSUser, CONFIG.FreeSMSPass, fmt.Sprintf("'%v' request a password reset. Token '/change-password?token=%v'", q.In.Email, token))
40 if err != nil {
41 return nil, NewInternalError(err)
42 }
43 }
44
45 return "OK", nil
46}
47
48type ChangePasswordQuery struct {
49 In struct {
50 Token string
51 Password string
52 }
53}
54
55func (q ChangePasswordQuery) ValidateParams() *Error {
56 if q.In.Password == "" {
57 return &Error{InvalidPassword, "invalid password", fmt.Errorf("invalid password")}
58 }
59
60 if q.In.Token == "" {
61 return &Error{BadRequest, "invalid token", fmt.Errorf("invalid token")}
62 }
63
64 return nil
65}
66
67func (q ChangePasswordQuery) Run() (interface{}, *Error) {
68 var user *db.User
69
70 email, err := passwordreset.VerifyToken(q.In.Token, func(email string) ([]byte, error) {
71 var err error
72 user, err = db.GetUserByEmail(email)
73 if err != nil {
74 return nil, err
75 }
76
77 if user == nil {
78 return nil, fmt.Errorf("'%v' is not registered", email)
79 }
80
81 return []byte(user.PasswordHash), nil
82
83 }, PASSWORD_RESET_SECRET)
84
85 if err != nil && (err == passwordreset.ErrExpiredToken) {
86 return nil, &Error{BadRequest, "expired token", fmt.Errorf("expired token")}
87 } else if err != nil && (err == passwordreset.ErrMalformedToken || err == passwordreset.ErrWrongSignature) {
88 return nil, &Error{BadRequest, "wrong token", fmt.Errorf("wrong token")}
89 } else if err != nil {
90 return nil, NewInternalError(err)
91 }
92
93 if user == nil {
94 return nil, &Error{BadRequest, "bad request", fmt.Errorf("no user found for email '%v'", email)}
95 }
96
97 err = db.SetPassword(user, q.In.Password)
98 if err != nil {
99 return nil, NewInternalError(err)
100 }
101
102 return "OK", nil
103}
diff --git a/api/routes.go b/api/routes.go
index cdf3dd9..22af0e7 100644
--- a/api/routes.go
+++ b/api/routes.go
@@ -25,6 +25,8 @@ var Groups = []Group{
25 []Route{ 25 []Route{
26 {"POST", []gin.HandlerFunc{Signup}, "/signup"}, 26 {"POST", []gin.HandlerFunc{Signup}, "/signup"},
27 {"POST", []gin.HandlerFunc{Signin}, "/signin"}, 27 {"POST", []gin.HandlerFunc{Signin}, "/signin"},
28 {"POST", []gin.HandlerFunc{PasswordReset}, "/passwordreset"},
29 {"POST", []gin.HandlerFunc{ChangePassword}, "/changepassword"},
28 }, 30 },
29 }, 31 },
30 { 32 {
@@ -132,3 +134,20 @@ func UpdateMarketConfig(c *gin.Context) {
132 134
133 RunQuery(query, c) 135 RunQuery(query, c)
134} 136}
137
138func PasswordReset(c *gin.Context) {
139 query := &PasswordResetQuery{}
140
141 query.In.Email = c.PostForm("email")
142
143 RunQuery(query, c)
144}
145
146func ChangePassword(c *gin.Context) {
147 query := &ChangePasswordQuery{}
148
149 query.In.Token = c.PostForm("token")
150 query.In.Password = c.PostForm("password")
151
152 RunQuery(query, c)
153}
diff --git a/api/user.go b/api/user.go
index 1dc69e4..9fd9479 100644
--- a/api/user.go
+++ b/api/user.go
@@ -74,6 +74,13 @@ func (q SignupQuery) Run() (interface{}, *Error) {
74 return nil, NewInternalError(fmt.Errorf("cannot create jwt token %v", err)) 74 return nil, NewInternalError(fmt.Errorf("cannot create jwt token %v", err))
75 } 75 }
76 76
77 if CONFIG.FreeSMSUser != "" {
78 err := SendSMS(CONFIG.FreeSMSUser, CONFIG.FreeSMSPass, fmt.Sprintf("New user signup '%v'", q.In.Email))
79 if err != nil {
80 return nil, NewInternalError(err)
81 }
82 }
83
77 return SignResult{token}, nil 84 return SignResult{token}, nil
78} 85}
79 86
diff --git a/cmd/ansible/conf.toml.j2 b/cmd/ansible/conf.toml.j2
index b2c3137..7b048af 100644
--- a/cmd/ansible/conf.toml.j2
+++ b/cmd/ansible/conf.toml.j2
@@ -17,6 +17,9 @@ database=0
17[api] 17[api]
18domain="{{ app_domain }}" 18domain="{{ app_domain }}"
19jwt_secret="{{ jwt_secret }}" 19jwt_secret="{{ jwt_secret }}"
20password_reset_secret="{{ reset_password_reset }}"
21free_sms_user="20996747"
22free_sms_pass="bM2ZPETB4zzWg3"
20 23
21[app] 24[app]
22public_dir="/var/cryptoportfolio-app/static" 25public_dir="/var/cryptoportfolio-app/static"
diff --git a/cmd/ansible/vars.yml b/cmd/ansible/vars.yml
index 1de7413..59a5eb8 100644
--- a/cmd/ansible/vars.yml
+++ b/cmd/ansible/vars.yml
@@ -1,22 +1,30 @@
1$ANSIBLE_VAULT;1.1;AES256 1$ANSIBLE_VAULT;1.1;AES256
263613535333830393037646665363566636635366534636261623839326130663431653839346266 266653734343938333339346336333430336239376539663338333431626461656430393931353165
33832643338623561313362663837323234663537663439350a313034326663383235663964626132 33564303163396337646466376265633537636362346434330a376630333634366165353832613063
438343964396265323539396439383731336464393337383833653666643736303539626136383431 436353931626633306565613862396566353138353439666336376231643432343637613636633634
56536316338376538360a343862626636363031353037626462333364623433613861393137353336 56531666131343361340a633839363864643037373638356433613830666137626665343362303363
637396664663030363530333364633266653862393538313835326138663465626638326363656561 664323766333830646463313130316234316161383031646132346633316162366335643137663835
730393836386664633834663838666432383836623432363936343635313835303166393531643966 763663866613335643330653839393762373163366530376538643666653630636462363535613137
833313361383565363232373066306534613465386534386266306564383365373762613361366365 863663966343335346564316462323463666130313733396164663761613966366165653162663432
961366530623863623336643531346463323233323539333139336335383439373132373233663031 966616232633733613766336465336632373765326362383035613465303463306330623632356232
1039666535633362383135376534376532333663636136366130653762643164333436313261646137 1038636136633661323137336431646666323464383033346239633032336561613433646632363564
1137353139633361636163326366616234613466393731373631616138386263383131663537633533 1133323737313235313463643866373465366436376138303430633535623335393163343862366264
1231393763316561623134623063623735356334363833623939313437386330323837626131356332 1235626263336462353264656637643437326635633237333932613666646666313235396561663762
1330383863373535366137366138633832623566613061313138396539306536633763633934313562 1336373762363662613635633065383037336634323665333865363533616136373565633436653566
1435383763653532336539346632623935303634353866636264373262363839326439313837313765 1434393033356566663433343864623664386330376339653463623834393036643733666237393964
1536303539613734646238636432393166616438666665363363323331373437633362613838653564 1532316134653666306134623135636664643461303831376336373839356531663764636433323539
1664393639346661646333383466363162633638643838386666383564366665656266333836363435 1662663938363437623235383666363163353562646165633564323635343534316130663062373332
1735643231323362323566303535303561626139333830393538383635326631656666323166343863 1739303239363166366538623763396563616664663038346465336263666365306435376364366337
1831393566346531653535393738326166303261376238316532373833616432306638326139353234 1866363137303731613638323839393731326539356262626634373136316265323937303863363637
1932653132323764316231393634663262313765393230656232343833373438636430643663353965 1937313565663534353035313834626139353333386337643263663264376238393030386363613435
2036333931303731646333316430646534383531313264353936396565336338663530303434643036 2033616432383131303761306265653566306266366263616162323363383365333363363334653132
2134356663373533663137636235386164646334356262336464363862643332636661313339303531 2132343235666434636361656133636332303131313766326665393233316135323566633433303362
2235663833656564393331636139663738323834373862623436633666306661373166 2262383431313861323036616331633134386238313034633936353931313838383038373435653235
2332333230646439613134613337333762313062613839613232663037363761353664373935356264
2461386239366330653939613637333264313532666363626633386632376138643265373432373233
2539313234326430663561343961313732666465613939343839663263353964663963356639633231
2664663330323761393932613039326339643737303939363033333666346439393631623038316561
2734313233626666663234613339306539343030616266333565326565346563353739613363363464
2865323763653436303030643765663739383965313335373265636336633762636134653536326364
2965353366643639663765663566663065316334383463343464366662373939343763356333343731
306361
diff --git a/cmd/app/conf.toml b/cmd/app/conf.toml
index 13e3e0b..16087e5 100644
--- a/cmd/app/conf.toml
+++ b/cmd/app/conf.toml
@@ -16,6 +16,9 @@ database=0
16[api] 16[api]
17domain="localhost" 17domain="localhost"
18jwt_secret="secret" 18jwt_secret="secret"
19password_reset_secret="resetsecret"
20free_sms_user="20996747"
21free_sms_pass="bM2ZPETB4zzWg3"
19 22
20[app] 23[app]
21public_dir="../web/build/static" 24public_dir="../web/build/static"
diff --git a/cmd/app/main.go b/cmd/app/main.go
index 65e8b5a..a0463d2 100644
--- a/cmd/app/main.go
+++ b/cmd/app/main.go
@@ -21,8 +21,8 @@ type AppConfig struct {
21} 21}
22 22
23type ApiConfig struct { 23type ApiConfig struct {
24 Domain string `toml:"domain"` 24 api.Config
25 JwtSecret string `toml:"jwt_secret"` 25 Domain string `toml:"domain"`
26} 26}
27 27
28type Config struct { 28type Config struct {
@@ -45,9 +45,6 @@ func (c *Config) SetToDefaults() {
45 App: AppConfig{ 45 App: AppConfig{
46 PublicDir: "./public", 46 PublicDir: "./public",
47 }, 47 },
48 Api: ApiConfig{
49 JwtSecret: "secret",
50 },
51 } 48 }
52 49
53 c.LogConfiguration.SetToDefaults() 50 c.LogConfiguration.SetToDefaults()
@@ -63,7 +60,7 @@ func init() {
63 panic(err) 60 panic(err)
64 } 61 }
65 62
66 api.SetJwtSecretKey(C.Api.JwtSecret) 63 api.SetConfig(C.Api.Config)
67 64
68 db.Init(C.Db, C.Redis) 65 db.Init(C.Db, C.Redis)
69 66
@@ -142,6 +139,8 @@ func main() {
142 "/", 139 "/",
143 "/signup", 140 "/signup",
144 "/signin", 141 "/signin",
142 "/reset-password",
143 "/change-password",
145 "/signout", 144 "/signout",
146 "/me", 145 "/me",
147 "/otp/enroll", 146 "/otp/enroll",
diff --git a/cmd/web/Makefile b/cmd/web/Makefile
index c6bc2bd..c5d5d62 100644
--- a/cmd/web/Makefile
+++ b/cmd/web/Makefile
@@ -4,7 +4,7 @@ export PATH := $(PATH):./node_modules/.bin
4 4
5SRC_DIR=js 5SRC_DIR=js
6BUILD_DIR=build/js 6BUILD_DIR=build/js
7JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx 7JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx password_reset.jsx change_password.jsx
8JS_SRC= cookies.js app.js api.js 8JS_SRC= cookies.js app.js api.js
9STATIC_FILES= index.html style.css 9STATIC_FILES= index.html style.css
10JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js)) 10JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js))
diff --git a/cmd/web/js/api.js b/cmd/web/js/api.js
index 5cbf5eb..c9b4ef5 100644
--- a/cmd/web/js/api.js
+++ b/cmd/web/js/api.js
@@ -43,6 +43,27 @@ var ApiEndpoints = {
43 return '/signin'; 43 return '/signin';
44 } 44 }
45 }, 45 },
46 'RESET_PASSWORD': {
47 'type': 'POST',
48 'auth': false,
49 'parameters': [
50 {'name': 'email', 'mandatory': true, 'inquery': true},
51 ],
52 'buildUrl': function() {
53 return '/passwordreset';
54 }
55 },
56 'CHANGE_PASSWORD': {
57 'type': 'POST',
58 'auth': false,
59 'parameters': [
60 {'name': 'token', 'mandatory': true, 'inquery': true},
61 {'name': 'password', 'mandatory': true, 'inquery': true},
62 ],
63 'buildUrl': function() {
64 return '/changepassword';
65 }
66 },
46 'MARKET': { 67 'MARKET': {
47 'type': 'GET', 68 'type': 'GET',
48 'auth': true, 69 'auth': true,
diff --git a/cmd/web/js/change_password.jsx b/cmd/web/js/change_password.jsx
new file mode 100644
index 0000000..aedf4af
--- /dev/null
+++ b/cmd/web/js/change_password.jsx
@@ -0,0 +1,62 @@
1import Api from './api.js';
2import App from './app.js';
3import classNames from 'classnames';
4import React from 'react';
5
6class ChangePasswordForm extends React.Component {
7 constructor(props) {
8 super(props);
9 this.state = {'hideMsg': true, 'msg': '', 'msgOk': false, 'password': ''};
10 }
11
12 handleSubmit = (e) => {
13 Api.Call(
14 'CHANGE_PASSWORD',
15 {
16 'password': this.state.password,
17 'token': this.props.token
18 },
19 function(err, status, data) {
20 if (err) {
21 console.error(err, data);
22 this.displayMessage(App.errorCodeToMessage(err.code), false);
23 return;
24 }
25
26 this.displayMessage('You password has been reset.', true);
27 this.props.onSuccess();
28
29 }.bind(this)
30);
31 e.preventDefault();
32 }
33
34 handlePasswordChange = (event) => {
35 this.setState({'password': event.target.value});
36 }
37
38 hideMessage = () => {
39 this.setState({'hideMsg': true});
40 }
41
42 displayMessage = (msg, ok) => {
43 this.setState({'msg': msg, 'msgOk': ok, 'hideMsg': false});
44 }
45
46 render = () => {
47 var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk});
48 return (
49 <div className="row sign-in">
50 <div className="offset-4 col-4 col-xs-offset-1 col-xs-10 text-center">
51 <form role="form" onSubmit={this.handleSubmit}>
52 <input className="form-control" type="password" placeholder="password" onChange={this.handlePasswordChange} />
53 <input className="form-control submit" type="submit" value="Change password" />
54 <div className={cName}>{this.state.msg}</div>
55 </form>
56 </div>
57 </div>
58 );
59 }
60}
61
62export default ChangePasswordForm;
diff --git a/cmd/web/js/main.jsx b/cmd/web/js/main.jsx
index e64adc7..909f1bd 100644
--- a/cmd/web/js/main.jsx
+++ b/cmd/web/js/main.jsx
@@ -1,11 +1,14 @@
1import SignupForm from './signup.js'; 1import SignupForm from './signup.js';
2import SigninForm from './signin.js'; 2import SigninForm from './signin.js';
3import PasswordResetForm from './password_reset.js';
4import ChangePasswordForm from './change_password.js';
3import OtpEnrollForm from './otp.js'; 5import OtpEnrollForm from './otp.js';
4import PoloniexController from './poloniex.js'; 6import PoloniexController from './poloniex.js';
5import App from './app.js'; 7import App from './app.js';
6import Api from './api.js'; 8import Api from './api.js';
7import cookies from './cookies.js'; 9import cookies from './cookies.js';
8import React from 'react'; 10import React from 'react';
11import qs from 'qs';
9 12
10class Header extends React.Component { 13class Header extends React.Component {
11 render = () => { 14 render = () => {
@@ -60,6 +63,37 @@ App.page('/signin', false, function(context) {
60 </div>); 63 </div>);
61}); 64});
62 65
66App.page('/reset-password', false, function(context) {
67 if (App.isUserSignedIn()) {
68 App.go('/me');
69 return;
70 }
71
72 App.mount(<div>
73 <Header />
74 <PasswordResetForm />
75 </div>);
76});
77
78App.page('/change-password', false, function(context) {
79 if (App.isUserSignedIn()) {
80 App.go('/me');
81 return;
82 }
83
84 var token = qs.parse(context.querystring).token;
85
86 if (token === undefined) {
87 App.go('/');
88 return;
89 }
90
91 App.mount(<div>
92 <Header />
93 <ChangePasswordForm token={token} onSuccess={App.go.bind(App, '/signin')}/>
94 </div>);
95});
96
63App.page('/signout', true, function(context) { 97App.page('/signout', true, function(context) {
64 cookies.removeItem('jwt'); 98 cookies.removeItem('jwt');
65 99
diff --git a/cmd/web/js/password_reset.jsx b/cmd/web/js/password_reset.jsx
new file mode 100644
index 0000000..8cbdc60
--- /dev/null
+++ b/cmd/web/js/password_reset.jsx
@@ -0,0 +1,57 @@
1import Api from './api.js';
2import App from './app.js';
3import classNames from 'classnames';
4import React from 'react';
5
6class PasswordResetForm extends React.Component {
7 constructor(props) {
8 super(props);
9 this.state = {'hideMsg': true, 'msg': '', 'msgOk': false, 'email': ''};
10 }
11
12 handleSubmit = (e) => {
13 Api.Call('RESET_PASSWORD', {'email': this.state.email}, function(err, status, data) {
14 if (err) {
15 console.error(err, data);
16 this.displayMessage(App.errorCodeToMessage(err.code), false);
17 return;
18 }
19
20 this.displayMessage('You will receive a reset link to reset your password.', true);
21 if (this.props.onSuccess) {
22 this.props.onSuccess();
23 }
24
25 }.bind(this));
26 e.preventDefault();
27 }
28
29 handleEmailChange = (event) => {
30 this.setState({'email': event.target.value});
31 }
32
33 hideMessage = () => {
34 this.setState({'hideMsg': true});
35 }
36
37 displayMessage = (msg, ok) => {
38 this.setState({'msg': msg, 'msgOk': ok, 'hideMsg': false});
39 }
40
41 render = () => {
42 var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk});
43 return (
44 <div className="row sign-in">
45 <div className="offset-4 col-4 col-xs-offset-1 col-xs-10 text-center">
46 <form role="form" onSubmit={this.handleSubmit}>
47 <input className="form-control" type="email" placeholder="email" onChange={this.handleEmailChange} />
48 <input className="form-control submit" type="submit" value="Reset" />
49 <div className={cName}>{this.state.msg}</div>
50 </form>
51 </div>
52 </div>
53 );
54 }
55}
56
57export default PasswordResetForm; \ No newline at end of file
diff --git a/cmd/web/package.json b/cmd/web/package.json
index c9241f1..a867313 100644
--- a/cmd/web/package.json
+++ b/cmd/web/package.json
@@ -14,6 +14,7 @@
14 "localenvify": "^1.0.1", 14 "localenvify": "^1.0.1",
15 "page": "^1.8.3", 15 "page": "^1.8.3",
16 "path-to-regexp": "^1.2.1", 16 "path-to-regexp": "^1.2.1",
17 "qs": "^6.5.1",
17 "react": "^16.2.0" 18 "react": "^16.2.0"
18 }, 19 },
19 "devDependencies": { 20 "devDependencies": {
diff --git a/cmd/web/yarn.lock b/cmd/web/yarn.lock
index 0d162a9..b2218ee 100644
--- a/cmd/web/yarn.lock
+++ b/cmd/web/yarn.lock
@@ -3536,6 +3536,10 @@ q@~1.0.0, q@~1.0.1:
3536 version "1.0.1" 3536 version "1.0.1"
3537 resolved "https://registry.yarnpkg.com/q/-/q-1.0.1.tgz#11872aeedee89268110b10a718448ffb10112a14" 3537 resolved "https://registry.yarnpkg.com/q/-/q-1.0.1.tgz#11872aeedee89268110b10a718448ffb10112a14"
3538 3538
3539qs@^6.5.1:
3540 version "6.5.1"
3541 resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
3542
3539qs@~1.2.0: 3543qs@~1.2.0:
3540 version "1.2.2" 3544 version "1.2.2"
3541 resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.2.tgz#19b57ff24dc2a99ce1f8bdf6afcda59f8ef61f88" 3545 resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.2.tgz#19b57ff24dc2a99ce1f8bdf6afcda59f8ef61f88"
diff --git a/db/user.go b/db/user.go
index aed0ac1..7a0a32b 100644
--- a/db/user.go
+++ b/db/user.go
@@ -70,3 +70,13 @@ func SetOtpSecret(user *User, secret string, temporary bool) error {
70 70
71 return DB.Update(user) 71 return DB.Update(user)
72} 72}
73
74func SetPassword(user *User, password string) error {
75 var err error
76 user.PasswordHash, err = HashPassword(password)
77 if err != nil {
78 return err
79 }
80
81 return DB.Update(user)
82}