diff options
-rw-r--r-- | api/routes.go | 15 | ||||
-rw-r--r-- | api/user.go | 59 | ||||
-rw-r--r-- | cmd/web/js/account.jsx | 120 | ||||
-rw-r--r-- | cmd/web/js/api.js | 8 | ||||
-rw-r--r-- | cmd/web/js/main.jsx | 4 |
5 files changed, 152 insertions, 54 deletions
diff --git a/api/routes.go b/api/routes.go index d0e8cec..404f821 100644 --- a/api/routes.go +++ b/api/routes.go | |||
@@ -48,6 +48,13 @@ var Groups = []Group{ | |||
48 | {"GET", []gin.HandlerFunc{GetPortfolio}, "/:name/portfolio"}, | 48 | {"GET", []gin.HandlerFunc{GetPortfolio}, "/:name/portfolio"}, |
49 | }, | 49 | }, |
50 | }, | 50 | }, |
51 | { | ||
52 | "/user", | ||
53 | []Middleware{JwtAuth, UserConfirmed, OtpAuth}, | ||
54 | []Route{ | ||
55 | {"GET", []gin.HandlerFunc{UserAccount}, "/account"}, | ||
56 | }, | ||
57 | }, | ||
51 | } | 58 | } |
52 | 59 | ||
53 | func Signup(c *gin.Context) { | 60 | func Signup(c *gin.Context) { |
@@ -169,3 +176,11 @@ func ConfirmEmail(c *gin.Context) { | |||
169 | 176 | ||
170 | RunQuery(query, c) | 177 | RunQuery(query, c) |
171 | } | 178 | } |
179 | |||
180 | func UserAccount(c *gin.Context) { | ||
181 | query := &UserAccountQuery{} | ||
182 | |||
183 | query.In.User = GetUser(c) | ||
184 | |||
185 | RunQuery(query, c) | ||
186 | } | ||
diff --git a/api/user.go b/api/user.go index 2848696..a2737fd 100644 --- a/api/user.go +++ b/api/user.go | |||
@@ -16,6 +16,26 @@ const ( | |||
16 | VALID_EMAIL_REGEX = `(?i)^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$` | 16 | VALID_EMAIL_REGEX = `(?i)^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$` |
17 | ) | 17 | ) |
18 | 18 | ||
19 | func UserConfirmed(c *gin.Context) *Error { | ||
20 | user, exists := c.Get("user") | ||
21 | |||
22 | if !exists { | ||
23 | return &Error{NotAuthorized, "not authorized", fmt.Errorf("no user key in context")} | ||
24 | } | ||
25 | |||
26 | if user.(db.User).Status != db.Confirmed { | ||
27 | return &Error{UserNotConfirmed, "user awaiting admin validation", fmt.Errorf("user '%v' not confirmed", user)} | ||
28 | } | ||
29 | |||
30 | return nil | ||
31 | } | ||
32 | |||
33 | func GetUser(c *gin.Context) db.User { | ||
34 | user, _ := c.Get("user") | ||
35 | |||
36 | return user.(db.User) | ||
37 | } | ||
38 | |||
19 | func IsValidEmailAddress(email string) bool { | 39 | func IsValidEmailAddress(email string) bool { |
20 | r := regexp.MustCompile(VALID_EMAIL_REGEX) | 40 | r := regexp.MustCompile(VALID_EMAIL_REGEX) |
21 | 41 | ||
@@ -142,26 +162,6 @@ func (q SigninQuery) Run() (interface{}, *Error) { | |||
142 | return SignResult{token}, nil | 162 | return SignResult{token}, nil |
143 | } | 163 | } |
144 | 164 | ||
145 | func UserConfirmed(c *gin.Context) *Error { | ||
146 | user, exists := c.Get("user") | ||
147 | |||
148 | if !exists { | ||
149 | return &Error{NotAuthorized, "not authorized", fmt.Errorf("no user key in context")} | ||
150 | } | ||
151 | |||
152 | if user.(db.User).Status != db.Confirmed { | ||
153 | return &Error{UserNotConfirmed, "user awaiting admin validation", fmt.Errorf("user '%v' not confirmed", user)} | ||
154 | } | ||
155 | |||
156 | return nil | ||
157 | } | ||
158 | |||
159 | func GetUser(c *gin.Context) db.User { | ||
160 | user, _ := c.Get("user") | ||
161 | |||
162 | return user.(db.User) | ||
163 | } | ||
164 | |||
165 | type ConfirmEmailQuery struct { | 165 | type ConfirmEmailQuery struct { |
166 | In struct { | 166 | In struct { |
167 | Token string | 167 | Token string |
@@ -214,3 +214,22 @@ func (q ConfirmEmailQuery) Run() (interface{}, *Error) { | |||
214 | 214 | ||
215 | return nil, nil | 215 | return nil, nil |
216 | } | 216 | } |
217 | |||
218 | type UserAccountQuery struct { | ||
219 | In struct { | ||
220 | User db.User | ||
221 | } | ||
222 | Out struct { | ||
223 | Email string `json:"email"` | ||
224 | } | ||
225 | } | ||
226 | |||
227 | func (q UserAccountQuery) ValidateParams() *Error { | ||
228 | return nil | ||
229 | } | ||
230 | |||
231 | func (q UserAccountQuery) Run() (interface{}, *Error) { | ||
232 | q.Out.Email = q.In.User.Email | ||
233 | |||
234 | return q.Out, nil | ||
235 | } | ||
diff --git a/cmd/web/js/account.jsx b/cmd/web/js/account.jsx index c80be33..3dc8afd 100644 --- a/cmd/web/js/account.jsx +++ b/cmd/web/js/account.jsx | |||
@@ -1,15 +1,68 @@ | |||
1 | import Api from './api.js'; | 1 | import Api from './api.js'; |
2 | import React from 'react'; | 2 | import React from 'react'; |
3 | import classnames from 'classnames'; | ||
4 | |||
5 | class Panel extends React.Component { | ||
6 | render = () => { | ||
7 | if (this.props.component === null) { | ||
8 | return <div></div>; | ||
9 | } | ||
10 | |||
11 | var className = classnames('row', this.props.topClassName); | ||
12 | |||
13 | return ( | ||
14 | <div className={className}> | ||
15 | <div className="box col-12"> | ||
16 | <div className="row"> | ||
17 | <div className="col-4">{this.props.title}</div> | ||
18 | </div> | ||
19 | <hr/> | ||
20 | {this.props.component} | ||
21 | </div> | ||
22 | </div>); | ||
23 | } | ||
24 | } | ||
25 | |||
26 | class AccountInformation extends React.Component { | ||
27 | constructor(props) { | ||
28 | super(props); | ||
29 | this.state = {'email': null}; | ||
30 | } | ||
31 | |||
32 | loadAccount = () => { | ||
33 | Api.Call('USER_ACCOUNT', {}, function(err, status, data) { | ||
34 | if (err) { | ||
35 | console.error(err, data); | ||
36 | return; | ||
37 | } | ||
38 | |||
39 | this.setState({'email': data.email}); | ||
40 | }.bind(this)); | ||
41 | } | ||
42 | |||
43 | componentDidMount = () => { | ||
44 | this.loadAccount(); | ||
45 | } | ||
46 | |||
47 | render = () => { | ||
48 | var component = <p>Loading...</p>; | ||
49 | if (this.state.email !== null) { | ||
50 | component = <p>Email: {this.state.email}</p>; | ||
51 | } | ||
52 | |||
53 | return component; | ||
54 | } | ||
55 | |||
56 | } | ||
3 | 57 | ||
4 | class PoloniexConfiguration extends React.Component { | 58 | class PoloniexConfiguration extends React.Component { |
5 | constructor(props) { | 59 | constructor(props) { |
6 | super(props); | 60 | super(props); |
7 | this.state = {'apiKey': '', 'apiSecret': '', 'apiRequested': false, 'status': 'loading', 'editMode': false}; | 61 | this.state = {'apiKey': '', 'apiSecret': '', 'status': 'loading', 'editMode': false}; |
8 | } | 62 | } |
9 | 63 | ||
10 | checkCredentials = () => { | 64 | checkCredentials = () => { |
11 | Api.Call('MARKET_TEST_CREDENTIALS', {'name': 'poloniex'}, function(err, status, data) { | 65 | Api.Call('MARKET_TEST_CREDENTIALS', {'name': 'poloniex'}, function(err, status, data) { |
12 | this.setState({'apiRequested': true}); | ||
13 | if (err) { | 66 | if (err) { |
14 | console.error(err, data); | 67 | console.error(err, data); |
15 | if (err.code === 'invalid_market_credentials') { | 68 | if (err.code === 'invalid_market_credentials') { |
@@ -53,7 +106,7 @@ class PoloniexConfiguration extends React.Component { | |||
53 | 106 | ||
54 | onEditClick = () => { | 107 | onEditClick = () => { |
55 | Api.Call('MARKET', {'name': 'poloniex'}, function(err, status, data) { | 108 | Api.Call('MARKET', {'name': 'poloniex'}, function(err, status, data) { |
56 | this.setState({'apiRequested': true, 'editMode': true}); | 109 | this.setState({'editMode': true}); |
57 | if (err) { | 110 | if (err) { |
58 | console.error(err, data); | 111 | console.error(err, data); |
59 | return; | 112 | return; |
@@ -90,11 +143,8 @@ class PoloniexConfiguration extends React.Component { | |||
90 | console.error('unknown status', this.state.status); | 143 | console.error('unknown status', this.state.status); |
91 | displayText = null; | 144 | displayText = null; |
92 | } | 145 | } |
93 | if (this.state.apiRequested === false) { | 146 | |
94 | return <div></div>; | ||
95 | } | ||
96 | return ( | 147 | return ( |
97 | <div> | ||
98 | <PoloniexCredentialsForm onLoadCredentials={this.onLoadCredentials} | 148 | <PoloniexCredentialsForm onLoadCredentials={this.onLoadCredentials} |
99 | onCredentialsSubmit={this.handleCredentialsSubmit} | 149 | onCredentialsSubmit={this.handleCredentialsSubmit} |
100 | onCredentialsChange={this.handleCredentialsChange} | 150 | onCredentialsChange={this.handleCredentialsChange} |
@@ -104,7 +154,6 @@ class PoloniexConfiguration extends React.Component { | |||
104 | statusMessage={displayText} | 154 | statusMessage={displayText} |
105 | editMode={this.state.editMode} | 155 | editMode={this.state.editMode} |
106 | onEditClick={this.onEditClick}/> | 156 | onEditClick={this.onEditClick}/> |
107 | </div> | ||
108 | ); | 157 | ); |
109 | } | 158 | } |
110 | } | 159 | } |
@@ -140,33 +189,40 @@ class PoloniexCredentialsForm extends React.Component { | |||
140 | } | 189 | } |
141 | 190 | ||
142 | return ( | 191 | return ( |
143 | <div className="row api-credentials-form"> | 192 | <React.Fragment> |
144 | <div className="offset-2 col-8 box"> | 193 | <div className="row config-status"> |
145 | <span className="text-center">Poloniex credentials</span> | 194 | <div className="col-12"> |
146 | <hr/> | 195 | <span><i className={iconName}></i>{this.props.statusMessage}</span> |
147 | <div className="row config-status"> | ||
148 | <div className="col-12"> | ||
149 | <span><i className={iconName}></i>{this.props.statusMessage}</span> | ||
150 | </div> | ||
151 | </div> | ||
152 | <div className="row"> | ||
153 | <div className="col-12"> | ||
154 | <form role="form" onSubmit={this.handleSubmit}> | ||
155 | <label className="w-100">Key: | ||
156 | <input className="form-control" type="text" placeholder="key" value={keyDisplayed} onChange={this.handleApiKeyChange} disabled={!this.props.editMode}/> | ||
157 | </label> | ||
158 | <label className="w-100">Secret: | ||
159 | <input className="form-control" type="text" placeholder="secret" value={secretDisplayed} onChange={this.handleApiSecretChange} disabled={!this.props.editMode}/> | ||
160 | </label> | ||
161 | <input className="form-control submit" type={submitType} value="Save" /> | ||
162 | <button className="form-control submit" style={{display: buttonDisplay}} onSubmit={null} onClick={this.props.onEditClick} type="button">Show/Edit</button> | ||
163 | </form> | ||
164 | </div> | ||
165 | </div> | ||
166 | </div> | 196 | </div> |
167 | </div> | 197 | </div> |
198 | <div className="row"> | ||
199 | <div className="col-12"> | ||
200 | <form role="form" onSubmit={this.handleSubmit}> | ||
201 | <label className="w-100">Key: | ||
202 | <input className="form-control" type="text" placeholder="key" value={keyDisplayed} onChange={this.handleApiKeyChange} disabled={!this.props.editMode}/> | ||
203 | </label> | ||
204 | <label className="w-100">Secret: | ||
205 | <input className="form-control" type="text" placeholder="secret" value={secretDisplayed} onChange={this.handleApiSecretChange} disabled={!this.props.editMode}/> | ||
206 | </label> | ||
207 | <input className="form-control submit" type={submitType} value="Save" /> | ||
208 | <button className="form-control submit" style={{display: buttonDisplay}} onSubmit={null} onClick={this.props.onEditClick} type="button">Show/Edit</button> | ||
209 | </form> | ||
210 | </div> | ||
211 | </div> | ||
212 | </React.Fragment> | ||
168 | ); | 213 | ); |
169 | } | 214 | } |
170 | } | 215 | } |
171 | 216 | ||
172 | export default PoloniexConfiguration; | 217 | class UserAccount extends React.Component { |
218 | render = () => { | ||
219 | return ( | ||
220 | <React.Fragment> | ||
221 | <Panel component={<AccountInformation/>} title="Account" /> | ||
222 | <Panel component={<PoloniexConfiguration/>} title="Poloniex credentials" topClassName="api-credentials-form" /> | ||
223 | </React.Fragment> | ||
224 | ); | ||
225 | } | ||
226 | } | ||
227 | |||
228 | export default UserAccount; | ||
diff --git a/cmd/web/js/api.js b/cmd/web/js/api.js index 62530ba..63355f0 100644 --- a/cmd/web/js/api.js +++ b/cmd/web/js/api.js | |||
@@ -116,6 +116,14 @@ var ApiEndpoints = { | |||
116 | return '/market/' + params.name + '/update'; | 116 | return '/market/' + params.name + '/update'; |
117 | } | 117 | } |
118 | }, | 118 | }, |
119 | 'USER_ACCOUNT': { | ||
120 | 'type': 'GET', | ||
121 | 'auth': true, | ||
122 | 'parameters': [], | ||
123 | 'buildUrl': function(params) { | ||
124 | return '/user/account'; | ||
125 | } | ||
126 | }, | ||
119 | 'OTP_ENROLL': { | 127 | 'OTP_ENROLL': { |
120 | 'type': 'GET', | 128 | 'type': 'GET', |
121 | 'auth': true, | 129 | 'auth': true, |
diff --git a/cmd/web/js/main.jsx b/cmd/web/js/main.jsx index dfc3337..7fa2d26 100644 --- a/cmd/web/js/main.jsx +++ b/cmd/web/js/main.jsx | |||
@@ -4,7 +4,7 @@ import PasswordResetForm from './password_reset.js'; | |||
4 | import ChangePasswordForm from './change_password.js'; | 4 | import ChangePasswordForm from './change_password.js'; |
5 | import OtpEnrollForm from './otp.js'; | 5 | import OtpEnrollForm from './otp.js'; |
6 | import PoloniexController from './poloniex.js'; | 6 | import PoloniexController from './poloniex.js'; |
7 | import PoloniexConfiguration from './account.js'; | 7 | import UserAccount from './account.js'; |
8 | import App from './app.js'; | 8 | import App from './app.js'; |
9 | import Api from './api.js'; | 9 | import Api from './api.js'; |
10 | import cookies from './cookies.js'; | 10 | import cookies from './cookies.js'; |
@@ -97,7 +97,7 @@ App.page('/me', true, function(context) { | |||
97 | 97 | ||
98 | App.page('/account', true, function(context) { | 98 | App.page('/account', true, function(context) { |
99 | App.mount(<div> | 99 | App.mount(<div> |
100 | <PoloniexConfiguration/> | 100 | <UserAccount/> |
101 | </div>); | 101 | </div>); |
102 | }); | 102 | }); |
103 | 103 | ||