diff options
Diffstat (limited to 'src/stores/UserStore.js')
-rw-r--r-- | src/stores/UserStore.js | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js new file mode 100644 index 000000000..4927d615f --- /dev/null +++ b/src/stores/UserStore.js | |||
@@ -0,0 +1,272 @@ | |||
1 | import { observable, computed, action } from 'mobx'; | ||
2 | import moment from 'moment'; | ||
3 | import jwt from 'jsonwebtoken'; | ||
4 | |||
5 | import Store from './lib/Store'; | ||
6 | import Request from './lib/Request'; | ||
7 | import CachedRequest from './lib/CachedRequest'; | ||
8 | import { gaEvent } from '../lib/analytics'; | ||
9 | |||
10 | // TODO: split stores into UserStore and AuthStore | ||
11 | export default class UserStore extends Store { | ||
12 | BASE_ROUTE = '/auth'; | ||
13 | WELCOME_ROUTE = `${this.BASE_ROUTE}/welcome`; | ||
14 | LOGIN_ROUTE = `${this.BASE_ROUTE}/login`; | ||
15 | LOGOUT_ROUTE = `${this.BASE_ROUTE}/logout`; | ||
16 | SIGNUP_ROUTE = `${this.BASE_ROUTE}/signup`; | ||
17 | PRICING_ROUTE = `${this.BASE_ROUTE}/signup/pricing`; | ||
18 | IMPORT_ROUTE = `${this.BASE_ROUTE}/signup/import`; | ||
19 | INVITE_ROUTE = `${this.BASE_ROUTE}/signup/invite`; | ||
20 | PASSWORD_ROUTE = `${this.BASE_ROUTE}/password`; | ||
21 | |||
22 | @observable loginRequest = new Request(this.api.user, 'login'); | ||
23 | @observable signupRequest = new Request(this.api.user, 'signup'); | ||
24 | @observable passwordRequest = new Request(this.api.user, 'password'); | ||
25 | @observable inviteRequest = new Request(this.api.user, 'invite'); | ||
26 | @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo'); | ||
27 | @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo'); | ||
28 | @observable getLegacyServicesRequest = new CachedRequest(this.api.user, 'getLegacyServices'); | ||
29 | |||
30 | @observable isImportLegacyServicesExecuting = false; | ||
31 | @observable isImportLegacyServicesCompleted = false; | ||
32 | |||
33 | @observable id; | ||
34 | @observable authToken = localStorage.getItem('authToken') || null; | ||
35 | @observable accountType; | ||
36 | |||
37 | @observable hasCompletedSignup = null; | ||
38 | |||
39 | @observable userData = {}; | ||
40 | |||
41 | @observable actionStatus = []; | ||
42 | |||
43 | logoutReasonTypes = { | ||
44 | SERVER: 'SERVER', | ||
45 | }; | ||
46 | @observable logoutReason = null; | ||
47 | |||
48 | constructor(...args) { | ||
49 | super(...args); | ||
50 | |||
51 | // Register action handlers | ||
52 | this.actions.user.login.listen(this._login.bind(this)); | ||
53 | this.actions.user.retrievePassword.listen(this._retrievePassword.bind(this)); | ||
54 | this.actions.user.logout.listen(this._logout.bind(this)); | ||
55 | this.actions.user.signup.listen(this._signup.bind(this)); | ||
56 | this.actions.user.invite.listen(this._invite.bind(this)); | ||
57 | this.actions.user.update.listen(this._update.bind(this)); | ||
58 | this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); | ||
59 | this.actions.user.importLegacyServices.listen(this._importLegacyServices.bind(this)); | ||
60 | |||
61 | // Reactions | ||
62 | this.registerReactions([ | ||
63 | this._requireAuthenticatedUser, | ||
64 | this._getUserData.bind(this), | ||
65 | ]); | ||
66 | } | ||
67 | |||
68 | // Routes | ||
69 | get loginRoute() { | ||
70 | return this.LOGIN_ROUTE; | ||
71 | } | ||
72 | |||
73 | get logoutRoute() { | ||
74 | return this.LOGOUT_ROUTE; | ||
75 | } | ||
76 | |||
77 | get signupRoute() { | ||
78 | return this.SIGNUP_ROUTE; | ||
79 | } | ||
80 | |||
81 | get pricingRoute() { | ||
82 | return this.PRICING_ROUTE; | ||
83 | } | ||
84 | |||
85 | get inviteRoute() { | ||
86 | return this.INVITE_ROUTE; | ||
87 | } | ||
88 | |||
89 | get importRoute() { | ||
90 | return this.IMPORT_ROUTE; | ||
91 | } | ||
92 | |||
93 | get passwordRoute() { | ||
94 | return this.PASSWORD_ROUTE; | ||
95 | } | ||
96 | |||
97 | // Data | ||
98 | @computed get isLoggedIn() { | ||
99 | return this.authToken !== null && this.authToken !== undefined; | ||
100 | } | ||
101 | |||
102 | // @computed get isTokenValid() { | ||
103 | // return this.authToken !== null && moment(this.tokenExpiry).isAfter(moment()); | ||
104 | // } | ||
105 | |||
106 | @computed get isTokenExpired() { | ||
107 | if (!this.authToken) return false; | ||
108 | |||
109 | const { tokenExpiry } = this._parseToken(this.authToken); | ||
110 | return this.authToken !== null && moment(tokenExpiry).isBefore(moment()); | ||
111 | } | ||
112 | |||
113 | @computed get data() { | ||
114 | this.getUserInfoRequest.execute(); | ||
115 | return this.getUserInfoRequest.result || {}; | ||
116 | } | ||
117 | |||
118 | @computed get legacyServices() { | ||
119 | this.getLegacyServicesRequest.execute(); | ||
120 | return this.getLegacyServicesRequest.result || []; | ||
121 | } | ||
122 | |||
123 | // Actions | ||
124 | @action async _login({ email, password }) { | ||
125 | const authToken = await this.loginRequest.execute(email, password)._promise; | ||
126 | this._setUserData(authToken); | ||
127 | |||
128 | this.stores.router.push('/'); | ||
129 | |||
130 | gaEvent('User', 'login'); | ||
131 | } | ||
132 | |||
133 | @action async _signup({ firstname, lastname, email, password, accountType, company }) { | ||
134 | const authToken = await this.signupRequest.execute({ | ||
135 | firstname, | ||
136 | lastname, | ||
137 | email, | ||
138 | password, | ||
139 | accountType, | ||
140 | company, | ||
141 | }); | ||
142 | |||
143 | this.hasCompletedSignup = false; | ||
144 | |||
145 | this._setUserData(authToken); | ||
146 | |||
147 | this.stores.router.push(this.PRICING_ROUTE); | ||
148 | |||
149 | gaEvent('User', 'signup'); | ||
150 | } | ||
151 | |||
152 | @action async _retrievePassword({ email }) { | ||
153 | const request = this.passwordRequest.execute(email); | ||
154 | |||
155 | await request._promise; | ||
156 | this.actionStatus = request.result.status || []; | ||
157 | |||
158 | gaEvent('User', 'retrievePassword'); | ||
159 | } | ||
160 | |||
161 | @action _invite({ invites }) { | ||
162 | const data = invites.filter(invite => invite.email !== ''); | ||
163 | |||
164 | this.inviteRequest.execute(data); | ||
165 | |||
166 | // we do not wait for a server response before redirecting the user | ||
167 | this.stores.router.push('/'); | ||
168 | |||
169 | gaEvent('User', 'inviteUsers'); | ||
170 | } | ||
171 | |||
172 | @action async _update({ userData }) { | ||
173 | const response = await this.updateUserInfoRequest.execute(userData)._promise; | ||
174 | |||
175 | this.getUserInfoRequest.patch(() => response.data); | ||
176 | this.actionStatus = response.status || []; | ||
177 | |||
178 | gaEvent('User', 'update'); | ||
179 | } | ||
180 | |||
181 | @action _resetStatus() { | ||
182 | this.actionStatus = []; | ||
183 | } | ||
184 | |||
185 | @action _logout() { | ||
186 | localStorage.removeItem('authToken'); | ||
187 | this.getUserInfoRequest.invalidate().reset(); | ||
188 | this.authToken = null; | ||
189 | // this.data = {}; | ||
190 | } | ||
191 | |||
192 | @action async _importLegacyServices({ services }) { | ||
193 | this.isImportLegacyServicesExecuting = true; | ||
194 | |||
195 | for (const service of services) { | ||
196 | this.actions.service.createFromLegacyService({ | ||
197 | data: service, | ||
198 | }); | ||
199 | await this.stores.services.createServiceRequest._promise; // eslint-disable-line | ||
200 | } | ||
201 | |||
202 | this.isImportLegacyServicesExecuting = false; | ||
203 | this.isImportLegacyServicesCompleted = true; | ||
204 | } | ||
205 | |||
206 | // This is a mobx autorun which forces the user to login if not authenticated | ||
207 | _requireAuthenticatedUser = () => { | ||
208 | if (this.isTokenExpired) { | ||
209 | this._logout(); | ||
210 | } | ||
211 | |||
212 | const { router } = this.stores; | ||
213 | const currentRoute = router.location.pathname; | ||
214 | if (!this.isLoggedIn | ||
215 | && !currentRoute.includes(this.BASE_ROUTE)) { | ||
216 | router.push(this.WELCOME_ROUTE); | ||
217 | } else if (this.isLoggedIn | ||
218 | && currentRoute === this.LOGOUT_ROUTE) { | ||
219 | this.actions.user.logout(); | ||
220 | router.push(this.LOGIN_ROUTE); | ||
221 | } else if (this.isLoggedIn | ||
222 | && currentRoute.includes(this.BASE_ROUTE) | ||
223 | && (this.hasCompletedSignup | ||
224 | || this.hasCompletedSignup === null)) { | ||
225 | this.stores.router.push('/'); | ||
226 | } | ||
227 | }; | ||
228 | |||
229 | // Reactions | ||
230 | async _getUserData() { | ||
231 | if (this.isLoggedIn) { | ||
232 | const data = await this.getUserInfoRequest.execute()._promise; | ||
233 | |||
234 | // We need to set the beta flag for the SettingsStore | ||
235 | this.actions.settings.update({ | ||
236 | settings: { | ||
237 | beta: data.beta, | ||
238 | }, | ||
239 | }); | ||
240 | } | ||
241 | } | ||
242 | |||
243 | // Helpers | ||
244 | _parseToken(authToken) { | ||
245 | try { | ||
246 | const decoded = jwt.decode(authToken); | ||
247 | |||
248 | return ({ | ||
249 | id: decoded.userId, | ||
250 | tokenExpiry: moment.unix(decoded.exp).toISOString(), | ||
251 | authToken, | ||
252 | }); | ||
253 | } catch (err) { | ||
254 | console.error('AccessToken Invalid'); | ||
255 | |||
256 | return false; | ||
257 | } | ||
258 | } | ||
259 | |||
260 | _setUserData(authToken) { | ||
261 | const data = this._parseToken(authToken); | ||
262 | if (data.authToken) { | ||
263 | localStorage.setItem('authToken', data.authToken); | ||
264 | |||
265 | this.authToken = data.authToken; | ||
266 | this.id = data.id; | ||
267 | } else { | ||
268 | this.authToken = null; | ||
269 | this.id = null; | ||
270 | } | ||
271 | } | ||
272 | } | ||