diff options
-rw-r--r-- | Gopkg.lock | 36 | ||||
-rw-r--r-- | Gopkg.toml | 8 | ||||
-rw-r--r-- | api/api.go | 16 | ||||
-rw-r--r-- | api/auth_jwt.go | 4 | ||||
-rw-r--r-- | api/free_sms.go | 26 | ||||
-rw-r--r-- | api/password_reset.go | 103 | ||||
-rw-r--r-- | api/routes.go | 19 | ||||
-rw-r--r-- | api/user.go | 7 | ||||
-rw-r--r-- | cmd/ansible/conf.toml.j2 | 3 | ||||
-rw-r--r-- | cmd/ansible/vars.yml | 50 | ||||
-rw-r--r-- | cmd/app/conf.toml | 3 | ||||
-rw-r--r-- | cmd/app/main.go | 11 | ||||
-rw-r--r-- | cmd/web/Makefile | 2 | ||||
-rw-r--r-- | cmd/web/js/api.js | 21 | ||||
-rw-r--r-- | cmd/web/js/change_password.jsx | 62 | ||||
-rw-r--r-- | cmd/web/js/main.jsx | 34 | ||||
-rw-r--r-- | cmd/web/js/password_reset.jsx | 57 | ||||
-rw-r--r-- | cmd/web/package.json | 1 | ||||
-rw-r--r-- | cmd/web/yarn.lock | 4 | ||||
-rw-r--r-- | db/user.go | 10 |
20 files changed, 433 insertions, 44 deletions
@@ -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 |
@@ -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" | ||
@@ -7,6 +7,22 @@ import ( | |||
7 | "github.com/gin-gonic/gin" | 7 | "github.com/gin-gonic/gin" |
8 | ) | 8 | ) |
9 | 9 | ||
10 | var CONFIG Config | ||
11 | |||
12 | type 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 | |||
19 | func SetConfig(config Config) { | ||
20 | CONFIG = config | ||
21 | |||
22 | JWT_SECRET = []byte(config.JwtSecret) | ||
23 | PASSWORD_RESET_SECRET = []byte(config.PasswordResetSecret) | ||
24 | } | ||
25 | |||
10 | type Error struct { | 26 | type 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 | ||
23 | func SetJwtSecretKey(secret string) { | ||
24 | JWT_SECRET = []byte(secret) | ||
25 | } | ||
26 | |||
27 | func VerifyJwtToken(token string) (JwtClaims, error) { | 23 | func 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 @@ | |||
1 | package api | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "net/http" | ||
6 | "net/url" | ||
7 | ) | ||
8 | |||
9 | func 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 @@ | |||
1 | package api | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "time" | ||
6 | |||
7 | "github.com/dchest/passwordreset" | ||
8 | "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" | ||
9 | ) | ||
10 | |||
11 | var PASSWORD_RESET_SECRET []byte | ||
12 | |||
13 | type PasswordResetQuery struct { | ||
14 | In struct { | ||
15 | Email string | ||
16 | } | ||
17 | } | ||
18 | |||
19 | func (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 | |||
27 | func (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 | |||
48 | type ChangePasswordQuery struct { | ||
49 | In struct { | ||
50 | Token string | ||
51 | Password string | ||
52 | } | ||
53 | } | ||
54 | |||
55 | func (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 | |||
67 | func (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 | |||
138 | func PasswordReset(c *gin.Context) { | ||
139 | query := &PasswordResetQuery{} | ||
140 | |||
141 | query.In.Email = c.PostForm("email") | ||
142 | |||
143 | RunQuery(query, c) | ||
144 | } | ||
145 | |||
146 | func 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] |
18 | domain="{{ app_domain }}" | 18 | domain="{{ app_domain }}" |
19 | jwt_secret="{{ jwt_secret }}" | 19 | jwt_secret="{{ jwt_secret }}" |
20 | password_reset_secret="{{ reset_password_reset }}" | ||
21 | free_sms_user="20996747" | ||
22 | free_sms_pass="bM2ZPETB4zzWg3" | ||
20 | 23 | ||
21 | [app] | 24 | [app] |
22 | public_dir="/var/cryptoportfolio-app/static" | 25 | public_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 |
2 | 63613535333830393037646665363566636635366534636261623839326130663431653839346266 | 2 | 66653734343938333339346336333430336239376539663338333431626461656430393931353165 |
3 | 3832643338623561313362663837323234663537663439350a313034326663383235663964626132 | 3 | 3564303163396337646466376265633537636362346434330a376630333634366165353832613063 |
4 | 38343964396265323539396439383731336464393337383833653666643736303539626136383431 | 4 | 36353931626633306565613862396566353138353439666336376231643432343637613636633634 |
5 | 6536316338376538360a343862626636363031353037626462333364623433613861393137353336 | 5 | 6531666131343361340a633839363864643037373638356433613830666137626665343362303363 |
6 | 37396664663030363530333364633266653862393538313835326138663465626638326363656561 | 6 | 64323766333830646463313130316234316161383031646132346633316162366335643137663835 |
7 | 30393836386664633834663838666432383836623432363936343635313835303166393531643966 | 7 | 63663866613335643330653839393762373163366530376538643666653630636462363535613137 |
8 | 33313361383565363232373066306534613465386534386266306564383365373762613361366365 | 8 | 63663966343335346564316462323463666130313733396164663761613966366165653162663432 |
9 | 61366530623863623336643531346463323233323539333139336335383439373132373233663031 | 9 | 66616232633733613766336465336632373765326362383035613465303463306330623632356232 |
10 | 39666535633362383135376534376532333663636136366130653762643164333436313261646137 | 10 | 38636136633661323137336431646666323464383033346239633032336561613433646632363564 |
11 | 37353139633361636163326366616234613466393731373631616138386263383131663537633533 | 11 | 33323737313235313463643866373465366436376138303430633535623335393163343862366264 |
12 | 31393763316561623134623063623735356334363833623939313437386330323837626131356332 | 12 | 35626263336462353264656637643437326635633237333932613666646666313235396561663762 |
13 | 30383863373535366137366138633832623566613061313138396539306536633763633934313562 | 13 | 36373762363662613635633065383037336634323665333865363533616136373565633436653566 |
14 | 35383763653532336539346632623935303634353866636264373262363839326439313837313765 | 14 | 34393033356566663433343864623664386330376339653463623834393036643733666237393964 |
15 | 36303539613734646238636432393166616438666665363363323331373437633362613838653564 | 15 | 32316134653666306134623135636664643461303831376336373839356531663764636433323539 |
16 | 64393639346661646333383466363162633638643838386666383564366665656266333836363435 | 16 | 62663938363437623235383666363163353562646165633564323635343534316130663062373332 |
17 | 35643231323362323566303535303561626139333830393538383635326631656666323166343863 | 17 | 39303239363166366538623763396563616664663038346465336263666365306435376364366337 |
18 | 31393566346531653535393738326166303261376238316532373833616432306638326139353234 | 18 | 66363137303731613638323839393731326539356262626634373136316265323937303863363637 |
19 | 32653132323764316231393634663262313765393230656232343833373438636430643663353965 | 19 | 37313565663534353035313834626139353333386337643263663264376238393030386363613435 |
20 | 36333931303731646333316430646534383531313264353936396565336338663530303434643036 | 20 | 33616432383131303761306265653566306266366263616162323363383365333363363334653132 |
21 | 34356663373533663137636235386164646334356262336464363862643332636661313339303531 | 21 | 32343235666434636361656133636332303131313766326665393233316135323566633433303362 |
22 | 35663833656564393331636139663738323834373862623436633666306661373166 | 22 | 62383431313861323036616331633134386238313034633936353931313838383038373435653235 |
23 | 32333230646439613134613337333762313062613839613232663037363761353664373935356264 | ||
24 | 61386239366330653939613637333264313532666363626633386632376138643265373432373233 | ||
25 | 39313234326430663561343961313732666465613939343839663263353964663963356639633231 | ||
26 | 64663330323761393932613039326339643737303939363033333666346439393631623038316561 | ||
27 | 34313233626666663234613339306539343030616266333565326565346563353739613363363464 | ||
28 | 65323763653436303030643765663739383965313335373265636336633762636134653536326364 | ||
29 | 65353366643639663765663566663065316334383463343464366662373939343763356333343731 | ||
30 | 6361 | ||
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] |
17 | domain="localhost" | 17 | domain="localhost" |
18 | jwt_secret="secret" | 18 | jwt_secret="secret" |
19 | password_reset_secret="resetsecret" | ||
20 | free_sms_user="20996747" | ||
21 | free_sms_pass="bM2ZPETB4zzWg3" | ||
19 | 22 | ||
20 | [app] | 23 | [app] |
21 | public_dir="../web/build/static" | 24 | public_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 | ||
23 | type ApiConfig struct { | 23 | type 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 | ||
28 | type Config struct { | 28 | type 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 | ||
5 | SRC_DIR=js | 5 | SRC_DIR=js |
6 | BUILD_DIR=build/js | 6 | BUILD_DIR=build/js |
7 | JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx | 7 | JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx password_reset.jsx change_password.jsx |
8 | JS_SRC= cookies.js app.js api.js | 8 | JS_SRC= cookies.js app.js api.js |
9 | STATIC_FILES= index.html style.css | 9 | STATIC_FILES= index.html style.css |
10 | JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js)) | 10 | JSX_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 @@ | |||
1 | import Api from './api.js'; | ||
2 | import App from './app.js'; | ||
3 | import classNames from 'classnames'; | ||
4 | import React from 'react'; | ||
5 | |||
6 | class 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 | |||
62 | export 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 @@ | |||
1 | import SignupForm from './signup.js'; | 1 | import SignupForm from './signup.js'; |
2 | import SigninForm from './signin.js'; | 2 | import SigninForm from './signin.js'; |
3 | import PasswordResetForm from './password_reset.js'; | ||
4 | import ChangePasswordForm from './change_password.js'; | ||
3 | import OtpEnrollForm from './otp.js'; | 5 | import OtpEnrollForm from './otp.js'; |
4 | import PoloniexController from './poloniex.js'; | 6 | import PoloniexController from './poloniex.js'; |
5 | import App from './app.js'; | 7 | import App from './app.js'; |
6 | import Api from './api.js'; | 8 | import Api from './api.js'; |
7 | import cookies from './cookies.js'; | 9 | import cookies from './cookies.js'; |
8 | import React from 'react'; | 10 | import React from 'react'; |
11 | import qs from 'qs'; | ||
9 | 12 | ||
10 | class Header extends React.Component { | 13 | class 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 | ||
66 | App.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 | |||
78 | App.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 | |||
63 | App.page('/signout', true, function(context) { | 97 | App.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 @@ | |||
1 | import Api from './api.js'; | ||
2 | import App from './app.js'; | ||
3 | import classNames from 'classnames'; | ||
4 | import React from 'react'; | ||
5 | |||
6 | class 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 | |||
57 | export 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 | ||
3539 | qs@^6.5.1: | ||
3540 | version "6.5.1" | ||
3541 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" | ||
3542 | |||
3539 | qs@~1.2.0: | 3543 | qs@~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" |
@@ -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 | |||
74 | func 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 | } | ||