diff options
Diffstat (limited to 'src/api/server/ServerApi.js')
-rw-r--r-- | src/api/server/ServerApi.js | 610 |
1 files changed, 0 insertions, 610 deletions
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js deleted file mode 100644 index bcc72ffdc..000000000 --- a/src/api/server/ServerApi.js +++ /dev/null | |||
@@ -1,610 +0,0 @@ | |||
1 | /* eslint-disable import/no-import-module-exports */ | ||
2 | /* eslint-disable global-require */ | ||
3 | import { join } from 'path'; | ||
4 | import tar from 'tar'; | ||
5 | import { | ||
6 | readdirSync, | ||
7 | statSync, | ||
8 | writeFileSync, | ||
9 | copySync, | ||
10 | ensureDirSync, | ||
11 | pathExistsSync, | ||
12 | readJsonSync, | ||
13 | removeSync, | ||
14 | } from 'fs-extra'; | ||
15 | import fetch from 'electron-fetch'; | ||
16 | |||
17 | import ServiceModel from '../../models/Service'; | ||
18 | import RecipePreviewModel from '../../models/RecipePreview'; | ||
19 | import RecipeModel from '../../models/Recipe'; | ||
20 | import UserModel from '../../models/User'; | ||
21 | |||
22 | import { sleep } from '../../helpers/async-helpers'; | ||
23 | |||
24 | import { SERVER_NOT_LOADED } from '../../config'; | ||
25 | import { userDataRecipesPath, userDataPath } from '../../environment-remote'; | ||
26 | import { asarRecipesPath } from '../../helpers/asar-helpers'; | ||
27 | import apiBase from '../apiBase'; | ||
28 | import { prepareAuthRequest, sendAuthRequest } from '../utils/auth'; | ||
29 | |||
30 | import { | ||
31 | getRecipeDirectory, | ||
32 | getDevRecipeDirectory, | ||
33 | loadRecipeConfig, | ||
34 | } from '../../helpers/recipe-helpers'; | ||
35 | |||
36 | import { removeServicePartitionDirectory } from '../../helpers/service-helpers'; | ||
37 | |||
38 | const debug = require('debug')('Ferdi:ServerApi'); | ||
39 | |||
40 | module.paths.unshift(getDevRecipeDirectory(), getRecipeDirectory()); | ||
41 | |||
42 | export default class ServerApi { | ||
43 | recipePreviews = []; | ||
44 | |||
45 | recipes = []; | ||
46 | |||
47 | // User | ||
48 | async login(email, passwordHash) { | ||
49 | const request = await sendAuthRequest( | ||
50 | `${apiBase()}/auth/login`, | ||
51 | { | ||
52 | method: 'POST', | ||
53 | headers: { | ||
54 | Authorization: `Basic ${window.btoa(`${email}:${passwordHash}`)}`, | ||
55 | }, | ||
56 | }, | ||
57 | false, | ||
58 | ); | ||
59 | if (!request.ok) { | ||
60 | throw request; | ||
61 | } | ||
62 | const u = await request.json(); | ||
63 | |||
64 | debug('ServerApi::login resolves', u); | ||
65 | return u.token; | ||
66 | } | ||
67 | |||
68 | async signup(data) { | ||
69 | const request = await sendAuthRequest( | ||
70 | `${apiBase()}/auth/signup`, | ||
71 | { | ||
72 | method: 'POST', | ||
73 | body: JSON.stringify(data), | ||
74 | }, | ||
75 | false, | ||
76 | ); | ||
77 | if (!request.ok) { | ||
78 | throw request; | ||
79 | } | ||
80 | const u = await request.json(); | ||
81 | |||
82 | debug('ServerApi::signup resolves', u); | ||
83 | return u.token; | ||
84 | } | ||
85 | |||
86 | async inviteUser(data) { | ||
87 | const request = await sendAuthRequest(`${apiBase()}/invite`, { | ||
88 | method: 'POST', | ||
89 | body: JSON.stringify(data), | ||
90 | }); | ||
91 | if (!request.ok) { | ||
92 | throw request; | ||
93 | } | ||
94 | |||
95 | debug('ServerApi::inviteUser'); | ||
96 | return true; | ||
97 | } | ||
98 | |||
99 | async retrievePassword(email) { | ||
100 | const request = await sendAuthRequest( | ||
101 | `${apiBase()}/auth/password`, | ||
102 | { | ||
103 | method: 'POST', | ||
104 | body: JSON.stringify({ | ||
105 | email, | ||
106 | }), | ||
107 | }, | ||
108 | false, | ||
109 | ); | ||
110 | if (!request.ok) { | ||
111 | throw request; | ||
112 | } | ||
113 | const r = await request.json(); | ||
114 | |||
115 | debug('ServerApi::retrievePassword'); | ||
116 | return r; | ||
117 | } | ||
118 | |||
119 | async userInfo() { | ||
120 | if (apiBase() === SERVER_NOT_LOADED) { | ||
121 | throw new Error('Server not loaded'); | ||
122 | } | ||
123 | |||
124 | const request = await sendAuthRequest(`${apiBase()}/me`); | ||
125 | if (!request.ok) { | ||
126 | throw request; | ||
127 | } | ||
128 | const data = await request.json(); | ||
129 | |||
130 | const user = new UserModel(data); | ||
131 | debug('ServerApi::userInfo resolves', user); | ||
132 | |||
133 | return user; | ||
134 | } | ||
135 | |||
136 | async updateUserInfo(data) { | ||
137 | const request = await sendAuthRequest(`${apiBase()}/me`, { | ||
138 | method: 'PUT', | ||
139 | body: JSON.stringify(data), | ||
140 | }); | ||
141 | if (!request.ok) { | ||
142 | throw request; | ||
143 | } | ||
144 | const updatedData = await request.json(); | ||
145 | |||
146 | const user = Object.assign(updatedData, { | ||
147 | data: new UserModel(updatedData.data), | ||
148 | }); | ||
149 | debug('ServerApi::updateUserInfo resolves', user); | ||
150 | return user; | ||
151 | } | ||
152 | |||
153 | async deleteAccount() { | ||
154 | const request = await sendAuthRequest(`${apiBase()}/me`, { | ||
155 | method: 'DELETE', | ||
156 | }); | ||
157 | if (!request.ok) { | ||
158 | throw request; | ||
159 | } | ||
160 | const data = await request.json(); | ||
161 | |||
162 | debug('ServerApi::deleteAccount resolves', data); | ||
163 | return data; | ||
164 | } | ||
165 | |||
166 | // Services | ||
167 | async getServices() { | ||
168 | if (apiBase() === SERVER_NOT_LOADED) { | ||
169 | throw new Error('Server not loaded'); | ||
170 | } | ||
171 | |||
172 | const request = await sendAuthRequest(`${apiBase()}/me/services`); | ||
173 | if (!request.ok) { | ||
174 | throw request; | ||
175 | } | ||
176 | const data = await request.json(); | ||
177 | |||
178 | const services = await this._mapServiceModels(data); | ||
179 | const filteredServices = services.filter(service => !!service); | ||
180 | debug('ServerApi::getServices resolves', filteredServices); | ||
181 | return filteredServices; | ||
182 | } | ||
183 | |||
184 | async createService(recipeId, data) { | ||
185 | const request = await sendAuthRequest(`${apiBase()}/service`, { | ||
186 | method: 'POST', | ||
187 | body: JSON.stringify({ recipeId, ...data }), | ||
188 | }); | ||
189 | if (!request.ok) { | ||
190 | throw request; | ||
191 | } | ||
192 | const serviceData = await request.json(); | ||
193 | |||
194 | if (data.iconFile) { | ||
195 | const iconData = await this.uploadServiceIcon( | ||
196 | serviceData.data.id, | ||
197 | data.iconFile, | ||
198 | ); | ||
199 | |||
200 | serviceData.data = iconData; | ||
201 | } | ||
202 | |||
203 | const service = Object.assign(serviceData, { | ||
204 | data: await this._prepareServiceModel(serviceData.data), | ||
205 | }); | ||
206 | |||
207 | debug('ServerApi::createService resolves', service); | ||
208 | return service; | ||
209 | } | ||
210 | |||
211 | async updateService(serviceId, rawData) { | ||
212 | const data = rawData; | ||
213 | |||
214 | if (data.iconFile) { | ||
215 | await this.uploadServiceIcon(serviceId, data.iconFile); | ||
216 | } | ||
217 | |||
218 | const request = await sendAuthRequest(`${apiBase()}/service/${serviceId}`, { | ||
219 | method: 'PUT', | ||
220 | body: JSON.stringify(data), | ||
221 | }); | ||
222 | |||
223 | if (!request.ok) { | ||
224 | throw request; | ||
225 | } | ||
226 | |||
227 | const serviceData = await request.json(); | ||
228 | |||
229 | const service = Object.assign(serviceData, { | ||
230 | data: await this._prepareServiceModel(serviceData.data), | ||
231 | }); | ||
232 | |||
233 | debug('ServerApi::updateService resolves', service); | ||
234 | return service; | ||
235 | } | ||
236 | |||
237 | async uploadServiceIcon(serviceId, icon) { | ||
238 | const formData = new FormData(); | ||
239 | formData.append('icon', icon); | ||
240 | |||
241 | const requestData = prepareAuthRequest({ | ||
242 | method: 'PUT', | ||
243 | body: formData, | ||
244 | }); | ||
245 | |||
246 | delete requestData.headers['Content-Type']; | ||
247 | |||
248 | const request = await window.fetch( | ||
249 | `${apiBase()}/service/${serviceId}`, | ||
250 | requestData, | ||
251 | ); | ||
252 | |||
253 | if (!request.ok) { | ||
254 | throw request; | ||
255 | } | ||
256 | |||
257 | const serviceData = await request.json(); | ||
258 | |||
259 | return serviceData.data; | ||
260 | } | ||
261 | |||
262 | async reorderService(data) { | ||
263 | const request = await sendAuthRequest(`${apiBase()}/service/reorder`, { | ||
264 | method: 'PUT', | ||
265 | body: JSON.stringify(data), | ||
266 | }); | ||
267 | if (!request.ok) { | ||
268 | throw request; | ||
269 | } | ||
270 | const serviceData = await request.json(); | ||
271 | debug('ServerApi::reorderService resolves', serviceData); | ||
272 | return serviceData; | ||
273 | } | ||
274 | |||
275 | async deleteService(id) { | ||
276 | const request = await sendAuthRequest(`${apiBase()}/service/${id}`, { | ||
277 | method: 'DELETE', | ||
278 | }); | ||
279 | if (!request.ok) { | ||
280 | throw request; | ||
281 | } | ||
282 | const data = await request.json(); | ||
283 | |||
284 | removeServicePartitionDirectory(id, true); | ||
285 | |||
286 | debug('ServerApi::deleteService resolves', data); | ||
287 | return data; | ||
288 | } | ||
289 | |||
290 | // Features | ||
291 | async getDefaultFeatures() { | ||
292 | const request = await sendAuthRequest(`${apiBase()}/features/default`); | ||
293 | if (!request.ok) { | ||
294 | throw request; | ||
295 | } | ||
296 | const data = await request.json(); | ||
297 | |||
298 | const features = data; | ||
299 | debug('ServerApi::getDefaultFeatures resolves', features); | ||
300 | return features; | ||
301 | } | ||
302 | |||
303 | async getFeatures() { | ||
304 | if (apiBase() === SERVER_NOT_LOADED) { | ||
305 | throw new Error('Server not loaded'); | ||
306 | } | ||
307 | |||
308 | const request = await sendAuthRequest(`${apiBase()}/features`); | ||
309 | if (!request.ok) { | ||
310 | throw request; | ||
311 | } | ||
312 | const data = await request.json(); | ||
313 | |||
314 | const features = data; | ||
315 | debug('ServerApi::getFeatures resolves', features); | ||
316 | return features; | ||
317 | } | ||
318 | |||
319 | // Recipes | ||
320 | async getInstalledRecipes() { | ||
321 | const recipesDirectory = getRecipeDirectory(); | ||
322 | const paths = readdirSync(recipesDirectory).filter( | ||
323 | file => | ||
324 | statSync(join(recipesDirectory, file)).isDirectory() && | ||
325 | file !== 'temp' && | ||
326 | file !== 'dev', | ||
327 | ); | ||
328 | |||
329 | this.recipes = paths | ||
330 | .map(id => { | ||
331 | // eslint-disable-next-line import/no-dynamic-require | ||
332 | const Recipe = require(id)(RecipeModel); | ||
333 | return new Recipe(loadRecipeConfig(id)); | ||
334 | }) | ||
335 | .filter(recipe => recipe.id); | ||
336 | |||
337 | // eslint-disable-next-line unicorn/prefer-spread | ||
338 | this.recipes = this.recipes.concat(this._getDevRecipes()); | ||
339 | |||
340 | debug('StubServerApi::getInstalledRecipes resolves', this.recipes); | ||
341 | return this.recipes; | ||
342 | } | ||
343 | |||
344 | async getRecipeUpdates(recipeVersions) { | ||
345 | const request = await sendAuthRequest(`${apiBase()}/recipes/update`, { | ||
346 | method: 'POST', | ||
347 | body: JSON.stringify(recipeVersions), | ||
348 | }); | ||
349 | if (!request.ok) { | ||
350 | throw request; | ||
351 | } | ||
352 | const recipes = await request.json(); | ||
353 | debug('ServerApi::getRecipeUpdates resolves', recipes); | ||
354 | return recipes; | ||
355 | } | ||
356 | |||
357 | // Recipes Previews | ||
358 | async getRecipePreviews() { | ||
359 | const request = await sendAuthRequest(`${apiBase()}/recipes`); | ||
360 | if (!request.ok) throw request; | ||
361 | const data = await request.json(); | ||
362 | const recipePreviews = this._mapRecipePreviewModel(data); | ||
363 | debug('ServerApi::getRecipes resolves', recipePreviews); | ||
364 | return recipePreviews; | ||
365 | } | ||
366 | |||
367 | async getFeaturedRecipePreviews() { | ||
368 | // TODO: If we are hitting the internal-server, we need to return an empty list, else we can hit the remote server and get the data | ||
369 | const request = await sendAuthRequest(`${apiBase()}/recipes/popular`); | ||
370 | if (!request.ok) throw request; | ||
371 | |||
372 | const data = await request.json(); | ||
373 | const recipePreviews = this._mapRecipePreviewModel(data); | ||
374 | debug('ServerApi::getFeaturedRecipes resolves', recipePreviews); | ||
375 | return recipePreviews; | ||
376 | } | ||
377 | |||
378 | async searchRecipePreviews(needle) { | ||
379 | const url = `${apiBase()}/recipes/search?needle=${needle}`; | ||
380 | const request = await sendAuthRequest(url); | ||
381 | if (!request.ok) throw request; | ||
382 | |||
383 | const data = await request.json(); | ||
384 | const recipePreviews = this._mapRecipePreviewModel(data); | ||
385 | debug('ServerApi::searchRecipePreviews resolves', recipePreviews); | ||
386 | return recipePreviews; | ||
387 | } | ||
388 | |||
389 | async getRecipePackage(recipeId) { | ||
390 | try { | ||
391 | const recipesDirectory = userDataRecipesPath(); | ||
392 | const recipeTempDirectory = join(recipesDirectory, 'temp', recipeId); | ||
393 | const tempArchivePath = join(recipeTempDirectory, 'recipe.tar.gz'); | ||
394 | |||
395 | const internalRecipeFile = asarRecipesPath(`${recipeId}.tar.gz`); | ||
396 | |||
397 | ensureDirSync(recipeTempDirectory); | ||
398 | |||
399 | let archivePath; | ||
400 | |||
401 | if (pathExistsSync(internalRecipeFile)) { | ||
402 | debug('[ServerApi::getRecipePackage] Using internal recipe file'); | ||
403 | archivePath = internalRecipeFile; | ||
404 | } else { | ||
405 | debug('[ServerApi::getRecipePackage] Downloading recipe from server'); | ||
406 | archivePath = tempArchivePath; | ||
407 | |||
408 | const packageUrl = `${apiBase()}/recipes/download/${recipeId}`; | ||
409 | |||
410 | const res = await fetch(packageUrl); | ||
411 | debug('Recipe downloaded', recipeId); | ||
412 | const buffer = await res.buffer(); | ||
413 | writeFileSync(archivePath, buffer); | ||
414 | } | ||
415 | debug(archivePath); | ||
416 | |||
417 | await sleep(10); | ||
418 | |||
419 | await tar.x({ | ||
420 | file: archivePath, | ||
421 | cwd: recipeTempDirectory, | ||
422 | preservePaths: true, | ||
423 | unlink: true, | ||
424 | preserveOwner: false, | ||
425 | onwarn: x => debug('warn', recipeId, x), | ||
426 | }); | ||
427 | |||
428 | await sleep(10); | ||
429 | |||
430 | const { id } = readJsonSync(join(recipeTempDirectory, 'package.json')); | ||
431 | const recipeDirectory = join(recipesDirectory, id); | ||
432 | copySync(recipeTempDirectory, recipeDirectory); | ||
433 | removeSync(recipeTempDirectory); | ||
434 | removeSync(join(recipesDirectory, recipeId, 'recipe.tar.gz')); | ||
435 | |||
436 | return id; | ||
437 | } catch (error) { | ||
438 | console.error(error); | ||
439 | |||
440 | return false; | ||
441 | } | ||
442 | } | ||
443 | |||
444 | // Health Check | ||
445 | async healthCheck() { | ||
446 | if (apiBase() === SERVER_NOT_LOADED) { | ||
447 | throw new Error('Server not loaded'); | ||
448 | } | ||
449 | |||
450 | const request = await sendAuthRequest( | ||
451 | `${apiBase(false)}/health`, | ||
452 | { | ||
453 | method: 'GET', | ||
454 | }, | ||
455 | false, | ||
456 | ); | ||
457 | if (!request.ok) { | ||
458 | throw request; | ||
459 | } | ||
460 | debug('ServerApi::healthCheck resolves'); | ||
461 | } | ||
462 | |||
463 | async getLegacyServices() { | ||
464 | const file = userDataPath('settings', 'services.json'); | ||
465 | |||
466 | try { | ||
467 | const config = readJsonSync(file); | ||
468 | |||
469 | if (Object.prototype.hasOwnProperty.call(config, 'services')) { | ||
470 | const services = await Promise.all( | ||
471 | config.services.map(async s => { | ||
472 | const service = s; | ||
473 | const request = await sendAuthRequest( | ||
474 | `${apiBase()}/recipes/${s.service}`, | ||
475 | ); | ||
476 | |||
477 | if (request.status === 200) { | ||
478 | const data = await request.json(); | ||
479 | service.recipe = new RecipePreviewModel(data); | ||
480 | } | ||
481 | |||
482 | return service; | ||
483 | }), | ||
484 | ); | ||
485 | |||
486 | debug('ServerApi::getLegacyServices resolves', services); | ||
487 | return services; | ||
488 | } | ||
489 | } catch { | ||
490 | console.error('ServerApi::getLegacyServices no config found'); | ||
491 | } | ||
492 | |||
493 | return []; | ||
494 | } | ||
495 | |||
496 | // Helper | ||
497 | async _mapServiceModels(services) { | ||
498 | const recipes = services.map(s => s.recipeId); | ||
499 | await this._bulkRecipeCheck(recipes); | ||
500 | /* eslint-disable no-return-await */ | ||
501 | return Promise.all( | ||
502 | services.map(async service => await this._prepareServiceModel(service)), | ||
503 | ); | ||
504 | /* eslint-enable no-return-await */ | ||
505 | } | ||
506 | |||
507 | async _prepareServiceModel(service) { | ||
508 | let recipe; | ||
509 | try { | ||
510 | recipe = this.recipes.find(r => r.id === service.recipeId); | ||
511 | |||
512 | if (!recipe) { | ||
513 | console.warn(`Recipe ${service.recipeId} not loaded`); | ||
514 | return null; | ||
515 | } | ||
516 | |||
517 | return new ServiceModel(service, recipe); | ||
518 | } catch (error) { | ||
519 | debug(error); | ||
520 | return null; | ||
521 | } | ||
522 | } | ||
523 | |||
524 | async _bulkRecipeCheck(unfilteredRecipes) { | ||
525 | // Filter recipe duplicates as we don't need to download 3 Slack recipes | ||
526 | const recipes = unfilteredRecipes.filter( | ||
527 | (elem, pos, arr) => arr.indexOf(elem) === pos, | ||
528 | ); | ||
529 | |||
530 | return Promise.all( | ||
531 | recipes.map(async recipeId => { | ||
532 | let recipe = this.recipes.find(r => r.id === recipeId); | ||
533 | |||
534 | if (!recipe) { | ||
535 | console.warn( | ||
536 | `Recipe '${recipeId}' not installed, trying to fetch from server`, | ||
537 | ); | ||
538 | |||
539 | await this.getRecipePackage(recipeId); | ||
540 | |||
541 | debug('Rerun ServerAPI::getInstalledRecipes'); | ||
542 | await this.getInstalledRecipes(); | ||
543 | |||
544 | recipe = this.recipes.find(r => r.id === recipeId); | ||
545 | |||
546 | if (!recipe) { | ||
547 | console.warn(`Could not load recipe ${recipeId}`); | ||
548 | return null; | ||
549 | } | ||
550 | } | ||
551 | |||
552 | return recipe; | ||
553 | }), | ||
554 | ).catch(error => console.error("Can't load recipe", error)); | ||
555 | } | ||
556 | |||
557 | _mapRecipePreviewModel(recipes) { | ||
558 | return recipes | ||
559 | .map(recipe => { | ||
560 | try { | ||
561 | return new RecipePreviewModel(recipe); | ||
562 | } catch (error) { | ||
563 | console.error(error); | ||
564 | return null; | ||
565 | } | ||
566 | }) | ||
567 | .filter(recipe => recipe !== null); | ||
568 | } | ||
569 | |||
570 | _getDevRecipes() { | ||
571 | const recipesDirectory = getDevRecipeDirectory(); | ||
572 | try { | ||
573 | const paths = readdirSync(recipesDirectory).filter( | ||
574 | file => | ||
575 | statSync(join(recipesDirectory, file)).isDirectory() && | ||
576 | file !== 'temp', | ||
577 | ); | ||
578 | |||
579 | const recipes = paths | ||
580 | .map(id => { | ||
581 | let Recipe; | ||
582 | try { | ||
583 | // eslint-disable-next-line import/no-dynamic-require | ||
584 | Recipe = require(id)(RecipeModel); | ||
585 | return new Recipe(loadRecipeConfig(id)); | ||
586 | } catch (error) { | ||
587 | console.error(error); | ||
588 | } | ||
589 | |||
590 | return false; | ||
591 | }) | ||
592 | .filter(recipe => recipe.id) | ||
593 | .map(data => { | ||
594 | const recipe = data; | ||
595 | |||
596 | recipe.icons = { | ||
597 | svg: `${recipe.path}/icon.svg`, | ||
598 | }; | ||
599 | recipe.local = true; | ||
600 | |||
601 | return data; | ||
602 | }); | ||
603 | |||
604 | return recipes; | ||
605 | } catch { | ||
606 | debug('Could not load dev recipes'); | ||
607 | return false; | ||
608 | } | ||
609 | } | ||
610 | } | ||