aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/server/ServerApi.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/server/ServerApi.js')
-rw-r--r--src/api/server/ServerApi.js358
1 files changed, 184 insertions, 174 deletions
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index c63aa7dda..bc0b0dff9 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -1,12 +1,11 @@
1import path from 'path'; 1import { join } from 'path';
2import tar from 'tar'; 2import tar from 'tar';
3import fs from 'fs-extra'; 3import { readdirSync, statSync, writeFileSync, copySync, ensureDirSync, pathExistsSync, readJsonSync, removeSync } from 'fs-extra';
4import { app, require as remoteRequire } from '@electron/remote'; 4import { app, require as remoteRequire } from '@electron/remote';
5 5
6import ServiceModel from '../../models/Service'; 6import ServiceModel from '../../models/Service';
7import RecipePreviewModel from '../../models/RecipePreview'; 7import RecipePreviewModel from '../../models/RecipePreview';
8import RecipeModel from '../../models/Recipe'; 8import RecipeModel from '../../models/Recipe';
9import PlanModel from '../../models/Plan';
10import NewsModel from '../../models/News'; 9import NewsModel from '../../models/News';
11import UserModel from '../../models/User'; 10import UserModel from '../../models/User';
12import OrderModel from '../../models/Order'; 11import OrderModel from '../../models/Order';
@@ -14,7 +13,7 @@ import OrderModel from '../../models/Order';
14import { sleep } from '../../helpers/async-helpers'; 13import { sleep } from '../../helpers/async-helpers';
15 14
16import { SERVER_NOT_LOADED } from '../../config'; 15import { SERVER_NOT_LOADED } from '../../config';
17import { osArch, osPlatform, RECIPES_PATH } from '../../environment'; 16import { osArch, osPlatform, asarRecipesPath, userDataRecipesPath, userDataPath } from '../../environment';
18import apiBase from '../apiBase'; 17import apiBase from '../apiBase';
19import { prepareAuthRequest, sendAuthRequest } from '../utils/auth'; 18import { prepareAuthRequest, sendAuthRequest } from '../utils/auth';
20 19
@@ -24,16 +23,11 @@ import {
24 loadRecipeConfig, 23 loadRecipeConfig,
25} from '../../helpers/recipe-helpers'; 24} from '../../helpers/recipe-helpers';
26 25
27import { 26import { removeServicePartitionDirectory } from '../../helpers/service-helpers';
28 removeServicePartitionDirectory,
29} from '../../helpers/service-helpers.js';
30 27
31const debug = require('debug')('Ferdi:ServerApi'); 28const debug = require('debug')('Ferdi:ServerApi');
32 29
33module.paths.unshift( 30module.paths.unshift(getDevRecipeDirectory(), getRecipeDirectory());
34 getDevRecipeDirectory(),
35 getRecipeDirectory(),
36);
37 31
38const { default: fetch } = remoteRequire('electron-fetch'); 32const { default: fetch } = remoteRequire('electron-fetch');
39 33
@@ -44,12 +38,16 @@ export default class ServerApi {
44 38
45 // User 39 // User
46 async login(email, passwordHash) { 40 async login(email, passwordHash) {
47 const request = await sendAuthRequest(`${apiBase()}/auth/login`, { 41 const request = await sendAuthRequest(
48 method: 'POST', 42 `${apiBase()}/auth/login`,
49 headers: { 43 {
50 Authorization: `Basic ${window.btoa(`${email}:${passwordHash}`)}`, 44 method: 'POST',
45 headers: {
46 Authorization: `Basic ${window.btoa(`${email}:${passwordHash}`)}`,
47 },
51 }, 48 },
52 }, false); 49 false,
50 );
53 if (!request.ok) { 51 if (!request.ok) {
54 throw request; 52 throw request;
55 } 53 }
@@ -60,10 +58,14 @@ export default class ServerApi {
60 } 58 }
61 59
62 async signup(data) { 60 async signup(data) {
63 const request = await sendAuthRequest(`${apiBase()}/auth/signup`, { 61 const request = await sendAuthRequest(
64 method: 'POST', 62 `${apiBase()}/auth/signup`,
65 body: JSON.stringify(data), 63 {
66 }, false); 64 method: 'POST',
65 body: JSON.stringify(data),
66 },
67 false,
68 );
67 if (!request.ok) { 69 if (!request.ok) {
68 throw request; 70 throw request;
69 } 71 }
@@ -73,20 +75,6 @@ export default class ServerApi {
73 return u.token; 75 return u.token;
74 } 76 }
75 77
76 async activateTrial(data) {
77 const request = await sendAuthRequest(`${apiBase()}/payment/trial`, {
78 method: 'POST',
79 body: JSON.stringify(data),
80 });
81 if (!request.ok) {
82 throw request;
83 }
84 const trial = await request.json();
85
86 debug('ServerApi::activateTrial resolves', trial);
87 return true;
88 }
89
90 async inviteUser(data) { 78 async inviteUser(data) {
91 const request = await sendAuthRequest(`${apiBase()}/invite`, { 79 const request = await sendAuthRequest(`${apiBase()}/invite`, {
92 method: 'POST', 80 method: 'POST',
@@ -101,12 +89,16 @@ export default class ServerApi {
101 } 89 }
102 90
103 async retrievePassword(email) { 91 async retrievePassword(email) {
104 const request = await sendAuthRequest(`${apiBase()}/auth/password`, { 92 const request = await sendAuthRequest(
105 method: 'POST', 93 `${apiBase()}/auth/password`,
106 body: JSON.stringify({ 94 {
107 email, 95 method: 'POST',
108 }), 96 body: JSON.stringify({
109 }, false); 97 email,
98 }),
99 },
100 false,
101 );
110 if (!request.ok) { 102 if (!request.ok) {
111 throw request; 103 throw request;
112 } 104 }
@@ -143,7 +135,9 @@ export default class ServerApi {
143 } 135 }
144 const updatedData = await request.json(); 136 const updatedData = await request.json();
145 137
146 const user = Object.assign(updatedData, { data: new UserModel(updatedData.data) }); 138 const user = Object.assign(updatedData, {
139 data: new UserModel(updatedData.data),
140 });
147 debug('ServerApi::updateUserInfo resolves', user); 141 debug('ServerApi::updateUserInfo resolves', user);
148 return user; 142 return user;
149 } 143 }
@@ -173,10 +167,10 @@ export default class ServerApi {
173 } 167 }
174 const data = await request.json(); 168 const data = await request.json();
175 169
176 let services = await this._mapServiceModels(data); 170 const services = await this._mapServiceModels(data);
177 services = services.filter(service => service !== null); 171 const filteredServices = services.filter(service => !!service);
178 debug('ServerApi::getServices resolves', services); 172 debug('ServerApi::getServices resolves', filteredServices);
179 return services; 173 return filteredServices;
180 } 174 }
181 175
182 async createService(recipeId, data) { 176 async createService(recipeId, data) {
@@ -190,12 +184,17 @@ export default class ServerApi {
190 const serviceData = await request.json(); 184 const serviceData = await request.json();
191 185
192 if (data.iconFile) { 186 if (data.iconFile) {
193 const iconData = await this.uploadServiceIcon(serviceData.data.id, data.iconFile); 187 const iconData = await this.uploadServiceIcon(
188 serviceData.data.id,
189 data.iconFile,
190 );
194 191
195 serviceData.data = iconData; 192 serviceData.data = iconData;
196 } 193 }
197 194
198 const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); 195 const service = Object.assign(serviceData, {
196 data: await this._prepareServiceModel(serviceData.data),
197 });
199 198
200 debug('ServerApi::createService resolves', service); 199 debug('ServerApi::createService resolves', service);
201 return service; 200 return service;
@@ -219,7 +218,9 @@ export default class ServerApi {
219 218
220 const serviceData = await request.json(); 219 const serviceData = await request.json();
221 220
222 const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); 221 const service = Object.assign(serviceData, {
222 data: await this._prepareServiceModel(serviceData.data),
223 });
223 224
224 debug('ServerApi::updateService resolves', service); 225 debug('ServerApi::updateService resolves', service);
225 return service; 226 return service;
@@ -236,7 +237,10 @@ export default class ServerApi {
236 237
237 delete requestData.headers['Content-Type']; 238 delete requestData.headers['Content-Type'];
238 239
239 const request = await window.fetch(`${apiBase()}/service/${serviceId}`, requestData); 240 const request = await window.fetch(
241 `${apiBase()}/service/${serviceId}`,
242 requestData,
243 );
240 244
241 if (!request.ok) { 245 if (!request.ok) {
242 throw request; 246 throw request;
@@ -307,18 +311,21 @@ export default class ServerApi {
307 // Recipes 311 // Recipes
308 async getInstalledRecipes() { 312 async getInstalledRecipes() {
309 const recipesDirectory = getRecipeDirectory(); 313 const recipesDirectory = getRecipeDirectory();
310 const paths = fs.readdirSync(recipesDirectory) 314 const paths = readdirSync(recipesDirectory)
311 .filter(file => ( 315 .filter(
312 fs.statSync(path.join(recipesDirectory, file)).isDirectory() 316 file =>
313 && file !== 'temp' 317 statSync(join(recipesDirectory, file)).isDirectory() &&
314 && file !== 'dev' 318 file !== 'temp' &&
315 )); 319 file !== 'dev',
316 320 );
317 this.recipes = paths.map((id) => { 321
318 // eslint-disable-next-line 322 this.recipes = paths
319 const Recipe = require(id)(RecipeModel); 323 .map(id => {
320 return new Recipe(loadRecipeConfig(id)); 324 // eslint-disable-next-line
321 }).filter(recipe => recipe.id); 325 const Recipe = require(id)(RecipeModel);
326 return new Recipe(loadRecipeConfig(id));
327 })
328 .filter(recipe => recipe.id);
322 329
323 this.recipes = this.recipes.concat(this._getDevRecipes()); 330 this.recipes = this.recipes.concat(this._getDevRecipes());
324 331
@@ -350,12 +357,11 @@ export default class ServerApi {
350 } 357 }
351 358
352 async getFeaturedRecipePreviews() { 359 async getFeaturedRecipePreviews() {
360 // 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
353 const request = await sendAuthRequest(`${apiBase()}/recipes/popular`); 361 const request = await sendAuthRequest(`${apiBase()}/recipes/popular`);
354 if (!request.ok) throw request; 362 if (!request.ok) throw request;
355 363
356 const data = await request.json(); 364 const data = await request.json();
357 // data = this._addLocalRecipesToPreviews(data);
358
359 const recipePreviews = this._mapRecipePreviewModel(data); 365 const recipePreviews = this._mapRecipePreviewModel(data);
360 debug('ServerApi::getFeaturedRecipes resolves', recipePreviews); 366 debug('ServerApi::getFeaturedRecipes resolves', recipePreviews);
361 return recipePreviews; 367 return recipePreviews;
@@ -374,21 +380,21 @@ export default class ServerApi {
374 380
375 async getRecipePackage(recipeId) { 381 async getRecipePackage(recipeId) {
376 try { 382 try {
377 const recipesDirectory = path.join(app.getPath('userData'), 'recipes'); 383 const recipesDirectory = userDataRecipesPath();
378 const recipeTempDirectory = path.join(recipesDirectory, 'temp', recipeId); 384 const recipeTempDirectory = join(recipesDirectory, 'temp', recipeId);
379 const tempArchivePath = path.join(recipeTempDirectory, 'recipe.tar.gz'); 385 const tempArchivePath = join(recipeTempDirectory, 'recipe.tar.gz');
380 386
381 const internalRecipeFile = path.join(RECIPES_PATH, `${recipeId}.tar.gz`); 387 const internalRecipeFile = asarRecipesPath(`${recipeId}.tar.gz`);
382 388
383 fs.ensureDirSync(recipeTempDirectory); 389 ensureDirSync(recipeTempDirectory);
384 390
385 let archivePath; 391 let archivePath;
386 392
387 if (await fs.exists(internalRecipeFile)) { 393 if (pathExistsSync(internalRecipeFile)) {
388 console.log('[ServerApi::getRecipePackage] Using internal recipe file'); 394 debug('[ServerApi::getRecipePackage] Using internal recipe file');
389 archivePath = internalRecipeFile; 395 archivePath = internalRecipeFile;
390 } else { 396 } else {
391 console.log('[ServerApi::getRecipePackage] Downloading recipe from server'); 397 debug('[ServerApi::getRecipePackage] Downloading recipe from server');
392 archivePath = tempArchivePath; 398 archivePath = tempArchivePath;
393 399
394 const packageUrl = `${apiBase()}/recipes/download/${recipeId}`; 400 const packageUrl = `${apiBase()}/recipes/download/${recipeId}`;
@@ -396,9 +402,9 @@ export default class ServerApi {
396 const res = await fetch(packageUrl); 402 const res = await fetch(packageUrl);
397 debug('Recipe downloaded', recipeId); 403 debug('Recipe downloaded', recipeId);
398 const buffer = await res.buffer(); 404 const buffer = await res.buffer();
399 fs.writeFileSync(archivePath, buffer); 405 writeFileSync(archivePath, buffer);
400 } 406 }
401 console.log(archivePath); 407 debug(archivePath);
402 408
403 await sleep(10); 409 await sleep(10);
404 410
@@ -408,16 +414,16 @@ export default class ServerApi {
408 preservePaths: true, 414 preservePaths: true,
409 unlink: true, 415 unlink: true,
410 preserveOwner: false, 416 preserveOwner: false,
411 onwarn: x => console.log('warn', recipeId, x), 417 onwarn: x => debug('warn', recipeId, x),
412 }); 418 });
413 419
414 await sleep(10); 420 await sleep(10);
415 421
416 const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json')); 422 const { id } = readJsonSync(join(recipeTempDirectory, 'package.json'));
417 const recipeDirectory = path.join(recipesDirectory, id); 423 const recipeDirectory = join(recipesDirectory, id);
418 fs.copySync(recipeTempDirectory, recipeDirectory); 424 copySync(recipeTempDirectory, recipeDirectory);
419 fs.remove(recipeTempDirectory); 425 removeSync(recipeTempDirectory);
420 fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz')); 426 removeSync(join(recipesDirectory, recipeId, 'recipe.tar.gz'));
421 427
422 return id; 428 return id;
423 } catch (err) { 429 } catch (err) {
@@ -427,35 +433,11 @@ export default class ServerApi {
427 } 433 }
428 } 434 }
429 435
430 // Payment
431 async getPlans() {
432 const request = await sendAuthRequest(`${apiBase()}/payment/plans`);
433 if (!request.ok) throw request;
434 const data = await request.json();
435 const plan = new PlanModel(data);
436 debug('ServerApi::getPlans resolves', plan);
437 return plan;
438 }
439
440 async getHostedPage(planId) {
441 const request = await sendAuthRequest(`${apiBase()}/payment/init`, {
442 method: 'POST',
443 body: JSON.stringify({
444 planId,
445 }),
446 });
447 if (!request.ok) {
448 throw request;
449 }
450 const data = await request.json();
451
452 debug('ServerApi::getHostedPage resolves', data);
453 return data;
454 }
455
456 // News 436 // News
457 async getLatestNews() { 437 async getLatestNews() {
458 const url = `${apiBase(true)}/news?platform=${osPlatform}&arch=${osArch}&version=${app.getVersion()}`; 438 const url = `${apiBase(
439 true,
440 )}/news?platform=${osPlatform}&arch=${osArch}&version=${app.getVersion()}`;
459 const request = await sendAuthRequest(url); 441 const request = await sendAuthRequest(url);
460 if (!request.ok) throw request; 442 if (!request.ok) throw request;
461 const data = await request.json(); 443 const data = await request.json();
@@ -465,7 +447,7 @@ export default class ServerApi {
465 } 447 }
466 448
467 async hideNews(id) { 449 async hideNews(id) {
468 const request = await sendAuthRequest(`${apiBase(true)}/news/${id}/read`); 450 const request = await sendAuthRequest(`${apiBase()}/news/${id}/read`);
469 if (!request.ok) throw request; 451 if (!request.ok) throw request;
470 debug('ServerApi::hideNews resolves', id); 452 debug('ServerApi::hideNews resolves', id);
471 } 453 }
@@ -476,9 +458,13 @@ export default class ServerApi {
476 throw new Error('Server not loaded'); 458 throw new Error('Server not loaded');
477 } 459 }
478 460
479 const request = await sendAuthRequest(`${apiBase(false)}/health`, { 461 const request = await sendAuthRequest(
480 method: 'GET', 462 `${apiBase(false)}/health`,
481 }, false); 463 {
464 method: 'GET',
465 },
466 false,
467 );
482 if (!request.ok) { 468 if (!request.ok) {
483 throw request; 469 throw request;
484 } 470 }
@@ -486,23 +472,27 @@ export default class ServerApi {
486 } 472 }
487 473
488 async getLegacyServices() { 474 async getLegacyServices() {
489 const file = path.join(app.getPath('userData'), 'settings', 'services.json'); 475 const file = userDataPath('settings', 'services.json');
490 476
491 try { 477 try {
492 const config = fs.readJsonSync(file); 478 const config = readJsonSync(file);
493 479
494 if (Object.prototype.hasOwnProperty.call(config, 'services')) { 480 if (Object.prototype.hasOwnProperty.call(config, 'services')) {
495 const services = await Promise.all(config.services.map(async (s) => { 481 const services = await Promise.all(
496 const service = s; 482 config.services.map(async s => {
497 const request = await sendAuthRequest(`${apiBase()}/recipes/${s.service}`); 483 const service = s;
498 484 const request = await sendAuthRequest(
499 if (request.status === 200) { 485 `${apiBase()}/recipes/${s.service}`,
500 const data = await request.json(); 486 );
501 service.recipe = new RecipePreviewModel(data); 487
502 } 488 if (request.status === 200) {
503 489 const data = await request.json();
504 return service; 490 service.recipe = new RecipePreviewModel(data);
505 })); 491 }
492
493 return service;
494 }),
495 );
506 496
507 debug('ServerApi::getLegacyServices resolves', services); 497 debug('ServerApi::getLegacyServices resolves', services);
508 return services; 498 return services;
@@ -519,7 +509,9 @@ export default class ServerApi {
519 const recipes = services.map(s => s.recipeId); 509 const recipes = services.map(s => s.recipeId);
520 await this._bulkRecipeCheck(recipes); 510 await this._bulkRecipeCheck(recipes);
521 /* eslint-disable no-return-await */ 511 /* eslint-disable no-return-await */
522 return Promise.all(services.map(async service => await this._prepareServiceModel(service))); 512 return Promise.all(
513 services.map(async service => await this._prepareServiceModel(service)),
514 );
523 /* eslint-enable no-return-await */ 515 /* eslint-enable no-return-await */
524 } 516 }
525 517
@@ -542,14 +534,18 @@ export default class ServerApi {
542 534
543 async _bulkRecipeCheck(unfilteredRecipes) { 535 async _bulkRecipeCheck(unfilteredRecipes) {
544 // Filter recipe duplicates as we don't need to download 3 Slack recipes 536 // Filter recipe duplicates as we don't need to download 3 Slack recipes
545 const recipes = unfilteredRecipes.filter((elem, pos, arr) => arr.indexOf(elem) === pos); 537 const recipes = unfilteredRecipes.filter(
538 (elem, pos, arr) => arr.indexOf(elem) === pos,
539 );
546 540
547 return Promise.all(recipes 541 return Promise.all(
548 .map(async (recipeId) => { 542 recipes.map(async recipeId => {
549 let recipe = this.recipes.find(r => r.id === recipeId); 543 let recipe = this.recipes.find(r => r.id === recipeId);
550 544
551 if (!recipe) { 545 if (!recipe) {
552 console.warn(`Recipe '${recipeId}' not installed, trying to fetch from server`); 546 console.warn(
547 `Recipe '${recipeId}' not installed, trying to fetch from server`,
548 );
553 549
554 await this.getRecipePackage(recipeId); 550 await this.getRecipePackage(recipeId);
555 551
@@ -565,69 +561,83 @@ export default class ServerApi {
565 } 561 }
566 562
567 return recipe; 563 return recipe;
568 })).catch(err => console.error('Can\'t load recipe', err)); 564 }),
565 ).catch(err => console.error("Can't load recipe", err));
569 } 566 }
570 567
571 _mapRecipePreviewModel(recipes) { 568 _mapRecipePreviewModel(recipes) {
572 return recipes.map((recipe) => { 569 return recipes
573 try { 570 .map(recipe => {
574 return new RecipePreviewModel(recipe); 571 try {
575 } catch (e) { 572 return new RecipePreviewModel(recipe);
576 console.error(e); 573 } catch (e) {
577 return null; 574 console.error(e);
578 } 575 return null;
579 }).filter(recipe => recipe !== null); 576 }
577 })
578 .filter(recipe => recipe !== null);
580 } 579 }
581 580
582 _mapNewsModels(news) { 581 _mapNewsModels(news) {
583 return news.map((newsItem) => { 582 return news
584 try { 583 .map(newsItem => {
585 return new NewsModel(newsItem); 584 try {
586 } catch (e) { 585 return new NewsModel(newsItem);
587 console.error(e); 586 } catch (e) {
588 return null; 587 console.error(e);
589 } 588 return null;
590 }).filter(newsItem => newsItem !== null); 589 }
590 })
591 .filter(newsItem => newsItem !== null);
591 } 592 }
592 593
593 _mapOrderModels(orders) { 594 _mapOrderModels(orders) {
594 return orders.map((orderItem) => { 595 return orders
595 try { 596 .map(orderItem => {
596 return new OrderModel(orderItem); 597 try {
597 } catch (e) { 598 return new OrderModel(orderItem);
598 console.error(e); 599 } catch (e) {
599 return null; 600 console.error(e);
600 } 601 return null;
601 }).filter(orderItem => orderItem !== null); 602 }
603 })
604 .filter(orderItem => orderItem !== null);
602 } 605 }
603 606
604 _getDevRecipes() { 607 _getDevRecipes() {
605 const recipesDirectory = getDevRecipeDirectory(); 608 const recipesDirectory = getDevRecipeDirectory();
606 try { 609 try {
607 const paths = fs.readdirSync(recipesDirectory) 610 const paths = readdirSync(recipesDirectory)
608 .filter(file => fs.statSync(path.join(recipesDirectory, file)).isDirectory() && file !== 'temp'); 611 .filter(
609 612 file =>
610 const recipes = paths.map((id) => { 613 statSync(join(recipesDirectory, file)).isDirectory() &&
611 let Recipe; 614 file !== 'temp',
612 try { 615 );
613 // eslint-disable-next-line 616
614 Recipe = require(id)(RecipeModel); 617 const recipes = paths
615 return new Recipe(loadRecipeConfig(id)); 618 .map(id => {
616 } catch (err) { 619 let Recipe;
617 console.error(err); 620 try {
618 } 621 // eslint-disable-next-line
622 Recipe = require(id)(RecipeModel);
623 return new Recipe(loadRecipeConfig(id));
624 } catch (err) {
625 console.error(err);
626 }
619 627
620 return false; 628 return false;
621 }).filter(recipe => recipe.id).map((data) => { 629 })
622 const recipe = data; 630 .filter(recipe => recipe.id)
631 .map(data => {
632 const recipe = data;
623 633
624 recipe.icons = { 634 recipe.icons = {
625 svg: `${recipe.path}/icon.svg`, 635 svg: `${recipe.path}/icon.svg`,
626 }; 636 };
627 recipe.local = true; 637 recipe.local = true;
628 638
629 return data; 639 return data;
630 }); 640 });
631 641
632 return recipes; 642 return recipes;
633 } catch (err) { 643 } catch (err) {