aboutsummaryrefslogtreecommitdiffstats
path: root/src/stores/UserStore.ts
diff options
context:
space:
mode:
authorLibravatar Ricardo Cino <ricardo@cino.io>2022-06-24 21:25:05 +0200
committerLibravatar Vijay Aravamudhan <vraravam@users.noreply.github.com>2022-06-25 05:50:00 +0530
commit2d71e61e46394d75d9f52ba1f4c273ed6d3c9cfd (patch)
tree7c0172945f962609637d03e7de885a254dbec8a4 /src/stores/UserStore.ts
parentchore: improve todo menu behaviour on fresh install (#359) (diff)
downloadferdium-app-2d71e61e46394d75d9f52ba1f4c273ed6d3c9cfd.tar.gz
ferdium-app-2d71e61e46394d75d9f52ba1f4c273ed6d3c9cfd.tar.zst
ferdium-app-2d71e61e46394d75d9f52ba1f4c273ed6d3c9cfd.zip
chore: convert the last few stores to typescript
Diffstat (limited to 'src/stores/UserStore.ts')
-rw-r--r--src/stores/UserStore.ts421
1 files changed, 421 insertions, 0 deletions
diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts
new file mode 100644
index 000000000..616ff29a6
--- /dev/null
+++ b/src/stores/UserStore.ts
@@ -0,0 +1,421 @@
1import { observable, computed, action } from 'mobx';
2import moment from 'moment';
3import jwt from 'jsonwebtoken';
4import localStorage from 'mobx-localstorage';
5import { ipcRenderer } from 'electron';
6
7import { ApiInterface } from 'src/api';
8import { Actions } from 'src/actions/lib/actions';
9import { Stores } from 'src/stores.types';
10import { TODOS_PARTITION_ID } from '../config';
11import { isDevMode } from '../environment-remote';
12import Request from './lib/Request';
13import CachedRequest from './lib/CachedRequest';
14import TypedStore from './lib/TypedStore';
15
16const debug = require('../preload-safe-debug')('Ferdium:UserStore');
17
18// TODO: split stores into UserStore and AuthStore
19export default class UserStore extends TypedStore {
20 BASE_ROUTE: string = '/auth';
21
22 WELCOME_ROUTE: string = `${this.BASE_ROUTE}/welcome`;
23
24 LOGIN_ROUTE: string = `${this.BASE_ROUTE}/login`;
25
26 LOGOUT_ROUTE: string = `${this.BASE_ROUTE}/logout`;
27
28 SIGNUP_ROUTE: string = `${this.BASE_ROUTE}/signup`;
29
30 SETUP_ROUTE: string = `${this.BASE_ROUTE}/signup/setup`;
31
32 IMPORT_ROUTE: string = `${this.BASE_ROUTE}/signup/import`;
33
34 INVITE_ROUTE: string = `${this.BASE_ROUTE}/signup/invite`;
35
36 PASSWORD_ROUTE: string = `${this.BASE_ROUTE}/password`;
37
38 CHANGE_SERVER_ROUTE: string = `${this.BASE_ROUTE}/server`;
39
40 @observable loginRequest: Request = new Request(this.api.user, 'login');
41
42 @observable signupRequest: Request = new Request(this.api.user, 'signup');
43
44 @observable passwordRequest: Request = new Request(this.api.user, 'password');
45
46 @observable inviteRequest: Request = new Request(this.api.user, 'invite');
47
48 @observable getUserInfoRequest: CachedRequest = new CachedRequest(
49 this.api.user,
50 'getInfo',
51 );
52
53 @observable updateUserInfoRequest: Request = new Request(
54 this.api.user,
55 'updateInfo',
56 );
57
58 @observable getLegacyServicesRequest: CachedRequest = new CachedRequest(
59 this.api.user,
60 'getLegacyServices',
61 );
62
63 @observable deleteAccountRequest: CachedRequest = new CachedRequest(
64 this.api.user,
65 'delete',
66 );
67
68 @observable isImportLegacyServicesExecuting: boolean = false;
69
70 @observable isImportLegacyServicesCompleted: boolean = false;
71
72 @observable isLoggingOut: boolean = false;
73
74 @observable id: string | null | undefined;
75
76 @observable authToken: string | null =
77 localStorage.getItem('authToken') || null;
78
79 @observable accountType: string | undefined;
80
81 @observable hasCompletedSignup: boolean = false;
82
83 @observable userData: object = {};
84
85 logoutReasonTypes = {
86 SERVER: 'SERVER',
87 };
88
89 @observable logoutReason: string | null = null;
90
91 fetchUserInfoInterval = null;
92
93 constructor(stores: Stores, api: ApiInterface, actions: Actions) {
94 super(stores, api, actions);
95
96 // Register action handlers
97 this.actions.user.login.listen(this._login.bind(this));
98 this.actions.user.retrievePassword.listen(
99 this._retrievePassword.bind(this),
100 );
101 this.actions.user.logout.listen(this._logout.bind(this));
102 this.actions.user.signup.listen(this._signup.bind(this));
103 this.actions.user.invite.listen(this._invite.bind(this));
104 this.actions.user.update.listen(this._update.bind(this));
105 this.actions.user.resetStatus.listen(this._resetStatus.bind(this));
106 this.actions.user.importLegacyServices.listen(
107 this._importLegacyServices.bind(this),
108 );
109 this.actions.user.delete.listen(this._delete.bind(this));
110
111 // Reactions
112 this.registerReactions([
113 this._requireAuthenticatedUser.bind(this),
114 this._getUserData.bind(this),
115 ]);
116 }
117
118 setup(): void {
119 // Data migration
120 this._migrateUserLocale();
121 }
122
123 // Routes
124 get loginRoute(): string {
125 return this.LOGIN_ROUTE;
126 }
127
128 get logoutRoute(): string {
129 return this.LOGOUT_ROUTE;
130 }
131
132 get signupRoute(): string {
133 return this.SIGNUP_ROUTE;
134 }
135
136 get setupRoute(): string {
137 return this.SETUP_ROUTE;
138 }
139
140 get inviteRoute(): string {
141 return this.INVITE_ROUTE;
142 }
143
144 get importRoute(): string {
145 return this.IMPORT_ROUTE;
146 }
147
148 get passwordRoute(): string {
149 return this.PASSWORD_ROUTE;
150 }
151
152 get changeServerRoute(): string {
153 return this.CHANGE_SERVER_ROUTE;
154 }
155
156 // Data
157 @computed get isLoggedIn(): boolean {
158 return Boolean(localStorage.getItem('authToken'));
159 }
160
161 @computed get isTokenExpired(): boolean {
162 if (!this.authToken) return false;
163 const parsedToken = this._parseToken(this.authToken);
164
165 return (
166 parsedToken !== false &&
167 this.authToken !== null &&
168 moment(parsedToken.tokenExpiry).isBefore(moment())
169 );
170 }
171
172 @computed get data() {
173 if (!this.isLoggedIn) return {};
174
175 return this.getUserInfoRequest.execute().result || {};
176 }
177
178 @computed get team(): any {
179 return this.data.team || null;
180 }
181
182 @computed get legacyServices(): any {
183 return this.getLegacyServicesRequest.execute() || {};
184 }
185
186 // Actions
187 @action async _login({ email, password }): Promise<void> {
188 const authToken = await this.loginRequest.execute(email, password)._promise;
189 this._setUserData(authToken);
190
191 this.stores.router.push('/');
192 }
193
194 @action _tokenLogin(authToken: string): void {
195 this._setUserData(authToken);
196
197 this.stores.router.push('/');
198 }
199
200 @action async _signup({
201 firstname,
202 lastname,
203 email,
204 password,
205 accountType,
206 company,
207 plan,
208 currency,
209 }): Promise<void> {
210 const authToken = await this.signupRequest.execute({
211 firstname,
212 lastname,
213 email,
214 password,
215 accountType,
216 company,
217 locale: this.stores.app.locale,
218 plan,
219 currency,
220 });
221
222 this.hasCompletedSignup = true;
223
224 this._setUserData(authToken);
225
226 this.stores.router.push(this.SETUP_ROUTE);
227 }
228
229 @action async _retrievePassword({ email }): Promise<void> {
230 const request = this.passwordRequest.execute(email);
231
232 await request._promise;
233 this.actionStatus = request.result.status || [];
234 }
235
236 @action async _invite({ invites }): Promise<void> {
237 const data = invites.filter(invite => invite.email !== '');
238
239 const response = await this.inviteRequest.execute(data)._promise;
240
241 this.actionStatus = response.status || [];
242
243 // we do not wait for a server response before redirecting the user ONLY DURING SIGNUP
244 if (this.stores.router.location.pathname.includes(this.INVITE_ROUTE)) {
245 this.stores.router.push('/');
246 }
247 }
248
249 @action async _update({ userData }): Promise<void> {
250 if (!this.isLoggedIn) return;
251
252 const response = await this.updateUserInfoRequest.execute(userData)
253 ._promise;
254
255 this.getUserInfoRequest.patch(() => response.data);
256 this.actionStatus = response.status || [];
257 }
258
259 @action _resetStatus(): void {
260 this.actionStatus = [];
261 }
262
263 @action _logout(): void {
264 // workaround mobx issue
265 localStorage.removeItem('authToken');
266 window.localStorage.removeItem('authToken');
267
268 this.getUserInfoRequest.invalidate().reset();
269 this.authToken = null;
270
271 this.stores.services.allServicesRequest.invalidate().reset();
272
273 if (this.stores.todos.isTodosEnabled) {
274 ipcRenderer.send('clear-storage-data', { sessionId: TODOS_PARTITION_ID });
275 }
276 }
277
278 @action async _importLegacyServices({ services }): Promise<void> {
279 this.isImportLegacyServicesExecuting = true;
280
281 // Reduces recipe duplicates
282 const recipes = services
283 .filter(
284 (obj, pos, arr) =>
285 arr.map(mapObj => mapObj.recipe.id).indexOf(obj.recipe.id) === pos,
286 )
287 .map(s => s.recipe.id);
288
289 // Install recipes
290 for (const recipe of recipes) {
291 // eslint-disable-next-line no-await-in-loop
292 await this.stores.recipes._install({ recipeId: recipe });
293 }
294
295 for (const service of services) {
296 this.actions.service.createFromLegacyService({
297 data: service,
298 });
299 // eslint-disable-next-line no-await-in-loop
300 await this.stores.services.createServiceRequest._promise;
301 }
302
303 this.isImportLegacyServicesExecuting = false;
304 this.isImportLegacyServicesCompleted = true;
305 }
306
307 @action async _delete(): Promise<void> {
308 this.deleteAccountRequest.execute();
309 }
310
311 // This is a mobx autorun which forces the user to login if not authenticated
312 _requireAuthenticatedUser = (): void => {
313 if (this.isTokenExpired) {
314 this._logout();
315 }
316
317 const { router } = this.stores;
318 const currentRoute = window.location.hash;
319 if (!this.isLoggedIn && currentRoute.includes('token=')) {
320 router.push(this.WELCOME_ROUTE);
321 const token = currentRoute.split('=')[1];
322
323 const data = this._parseToken(token);
324 if (data) {
325 // Give this some time to sink
326 setTimeout(() => {
327 this._tokenLogin(token);
328 }, 1000);
329 }
330 } else if (!this.isLoggedIn && !currentRoute.includes(this.BASE_ROUTE)) {
331 router.push(this.WELCOME_ROUTE);
332 } else if (this.isLoggedIn && currentRoute === this.LOGOUT_ROUTE) {
333 this.actions.user.logout();
334 router.push(this.LOGIN_ROUTE);
335 } else if (
336 this.isLoggedIn &&
337 currentRoute.includes(this.BASE_ROUTE) &&
338 (this.hasCompletedSignup || this.hasCompletedSignup === null) &&
339 !isDevMode
340 ) {
341 this.stores.router.push('/');
342 }
343 };
344
345 // Reactions
346 async _getUserData(): Promise<void> {
347 if (this.isLoggedIn) {
348 let data;
349 try {
350 data = await this.getUserInfoRequest.execute()._promise;
351 } catch {
352 return;
353 }
354
355 // We need to set the beta flag for the SettingsStore
356 this.actions.settings.update({
357 type: 'app',
358 data: {
359 beta: data.beta,
360 locale: data.locale,
361 },
362 });
363 }
364 }
365
366 // Helpers
367 _parseToken(authToken) {
368 try {
369 const decoded = jwt.decode(authToken);
370
371 return {
372 id: decoded.userId,
373 tokenExpiry: moment.unix(decoded.exp).toISOString(),
374 authToken,
375 };
376 } catch {
377 this._logout();
378 return false;
379 }
380 }
381
382 _setUserData(authToken: any): void {
383 const data = this._parseToken(authToken);
384 if (data !== false && data.authToken) {
385 localStorage.setItem('authToken', data.authToken);
386
387 this.authToken = data.authToken;
388 this.id = data.id;
389 } else {
390 this.authToken = null;
391 this.id = null;
392 }
393 }
394
395 getAuthURL(url: string): string {
396 const parsedUrl = new URL(url);
397 const params = new URLSearchParams(parsedUrl.search.slice(1));
398
399 // TODO: Remove the neccesity for `as string`
400 params.append('authToken', this.authToken as string);
401
402 return `${parsedUrl.origin}${parsedUrl.pathname}?${params.toString()}`;
403 }
404
405 async _migrateUserLocale(): Promise<void> {
406 try {
407 await this.getUserInfoRequest._promise;
408 } catch {
409 return;
410 }
411
412 if (!this.data.locale) {
413 debug('Migrate "locale" to user data');
414 this.actions.user.update({
415 userData: {
416 locale: this.stores.app.locale,
417 },
418 });
419 }
420 }
421}