aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/server/ServerApi.js
diff options
context:
space:
mode:
authorLibravatar Markus Hatvan <markus_hatvan@aon.at>2021-11-18 17:37:45 +0100
committerLibravatar GitHub <noreply@github.com>2021-11-18 22:07:45 +0530
commitb37a6b07b39c8c7827052dc6fb97f490f1e0f514 (patch)
tree0276e7c51f5ebfa14c566def7aac39f014c2291d /src/api/server/ServerApi.js
parentUpdate github issues template [skip ci] (diff)
downloadferdium-app-b37a6b07b39c8c7827052dc6fb97f490f1e0f514.tar.gz
ferdium-app-b37a6b07b39c8c7827052dc6fb97f490f1e0f514.tar.zst
ferdium-app-b37a6b07b39c8c7827052dc6fb97f490f1e0f514.zip
chore: convert various files to TS (#2246)
* convert various files to TS * removed outdated docs/example-feature folder * turn off unicorn/no-empty-file * update eslint config
Diffstat (limited to 'src/api/server/ServerApi.js')
-rw-r--r--src/api/server/ServerApi.js610
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 */
3import { join } from 'path';
4import tar from 'tar';
5import {
6 readdirSync,
7 statSync,
8 writeFileSync,
9 copySync,
10 ensureDirSync,
11 pathExistsSync,
12 readJsonSync,
13 removeSync,
14} from 'fs-extra';
15import fetch from 'electron-fetch';
16
17import ServiceModel from '../../models/Service';
18import RecipePreviewModel from '../../models/RecipePreview';
19import RecipeModel from '../../models/Recipe';
20import UserModel from '../../models/User';
21
22import { sleep } from '../../helpers/async-helpers';
23
24import { SERVER_NOT_LOADED } from '../../config';
25import { userDataRecipesPath, userDataPath } from '../../environment-remote';
26import { asarRecipesPath } from '../../helpers/asar-helpers';
27import apiBase from '../apiBase';
28import { prepareAuthRequest, sendAuthRequest } from '../utils/auth';
29
30import {
31 getRecipeDirectory,
32 getDevRecipeDirectory,
33 loadRecipeConfig,
34} from '../../helpers/recipe-helpers';
35
36import { removeServicePartitionDirectory } from '../../helpers/service-helpers';
37
38const debug = require('debug')('Ferdi:ServerApi');
39
40module.paths.unshift(getDevRecipeDirectory(), getRecipeDirectory());
41
42export 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}