aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/server
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2017-10-13 12:29:40 +0200
committerLibravatar Stefan Malzner <stefan@adlk.io>2017-10-13 12:29:40 +0200
commit58cda9cc7fb79ca9df6746de7f9662bc08dc156a (patch)
tree1211600c2a5d3b5f81c435c6896618111a611720 /src/api/server
downloadferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.gz
ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.zst
ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.zip
initial commit
Diffstat (limited to 'src/api/server')
-rw-r--r--src/api/server/LocalApi.js33
-rw-r--r--src/api/server/ServerApi.js574
2 files changed, 607 insertions, 0 deletions
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js
new file mode 100644
index 000000000..79ac6e12f
--- /dev/null
+++ b/src/api/server/LocalApi.js
@@ -0,0 +1,33 @@
1export default class LocalApi {
2 // App
3 async updateAppSettings(data) {
4 const currentSettings = await this.getAppSettings();
5 const settings = Object.assign(currentSettings, data);
6
7 localStorage.setItem('app', JSON.stringify(settings));
8 console.debug('LocalApi::updateAppSettings resolves', settings);
9
10 return settings;
11 }
12
13 async getAppSettings() {
14 const settingsString = localStorage.getItem('app');
15 try {
16 const settings = JSON.parse(settingsString) || {};
17 console.debug('LocalApi::getAppSettings resolves', settings);
18
19 return settings;
20 } catch (err) {
21 return {};
22 }
23 }
24
25 async removeKey(key) {
26 const settings = await this.getAppSettings();
27
28 if (Object.hasOwnProperty.call(settings, key)) {
29 delete settings[key];
30 localStorage.setItem('app', JSON.stringify(settings));
31 }
32 }
33}
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
new file mode 100644
index 000000000..b369796e8
--- /dev/null
+++ b/src/api/server/ServerApi.js
@@ -0,0 +1,574 @@
1import os from 'os';
2import path from 'path';
3import targz from 'tar.gz';
4import fs from 'fs-extra';
5import { remote } from 'electron';
6
7import ServiceModel from '../../models/Service';
8import RecipePreviewModel from '../../models/RecipePreview';
9import RecipeModel from '../../models/Recipe';
10import PlanModel from '../../models/Plan';
11import NewsModel from '../../models/News';
12import UserModel from '../../models/User';
13import OrderModel from '../../models/Order';
14
15import { API } from '../../environment';
16
17import {
18 getRecipeDirectory,
19 getDevRecipeDirectory,
20 loadRecipeConfig,
21} from '../../helpers/recipe-helpers';
22
23module.paths.unshift(
24 getDevRecipeDirectory(),
25 getRecipeDirectory(),
26);
27
28const { app } = remote;
29const fetch = remote.require('electron-fetch');
30
31const SERVER_URL = API;
32const API_VERSION = 'v1';
33
34export default class ServerApi {
35 recipePreviews = [];
36 recipes = [];
37
38 // User
39 async login(email, passwordHash) {
40 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/auth/login`, this._prepareAuthRequest({
41 method: 'POST',
42 headers: {
43 Authorization: `Basic ${window.btoa(`${email}:${passwordHash}`)}`,
44 },
45 }, false));
46 if (!request.ok) {
47 throw request;
48 }
49 const u = await request.json();
50
51 console.debug('ServerApi::login resolves', u);
52 return u.token;
53 }
54
55 async signup(data) {
56 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/auth/signup`, this._prepareAuthRequest({
57 method: 'POST',
58 body: JSON.stringify(data),
59 }, false));
60 if (!request.ok) {
61 throw request;
62 }
63 const u = await request.json();
64
65 console.debug('ServerApi::signup resolves', u);
66 return u.token;
67 }
68
69 async inviteUser(data) {
70 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/invite`, this._prepareAuthRequest({
71 method: 'POST',
72 body: JSON.stringify(data),
73 }));
74 if (!request.ok) {
75 throw request;
76 }
77
78 console.debug('ServerApi::inviteUser');
79 return true;
80 }
81
82 async retrievePassword(email) {
83 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/auth/password`, this._prepareAuthRequest({
84 method: 'POST',
85 body: JSON.stringify({
86 email,
87 }),
88 }, false));
89 if (!request.ok) {
90 throw request;
91 }
92 const r = await request.json();
93
94 console.debug('ServerApi::retrievePassword');
95 return r;
96 }
97
98 async userInfo() {
99 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me`, this._prepareAuthRequest({
100 method: 'GET',
101 }));
102 if (!request.ok) {
103 throw request;
104 }
105 const data = await request.json();
106
107 const user = new UserModel(data);
108 console.debug('ServerApi::userInfo resolves', user);
109
110 return user;
111 }
112
113 async updateUserInfo(data) {
114 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me`, this._prepareAuthRequest({
115 method: 'PUT',
116 body: JSON.stringify(data),
117 }));
118 if (!request.ok) {
119 throw request;
120 }
121 const updatedData = await request.json();
122
123 const user = Object.assign(updatedData, { data: new UserModel(updatedData.data) });
124 console.debug('ServerApi::updateUserInfo resolves', user);
125 return user;
126 }
127
128 // Services
129 async getServices() {
130 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/services`, this._prepareAuthRequest({
131 method: 'GET',
132 }));
133 if (!request.ok) {
134 throw request;
135 }
136 const data = await request.json();
137
138 let services = await this._mapServiceModels(data);
139 services = services.filter(service => service !== null);
140 console.debug('ServerApi::getServices resolves', services);
141 return services;
142 }
143
144 async createService(recipeId, data) {
145 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service`, this._prepareAuthRequest({
146 method: 'POST',
147 body: JSON.stringify(Object.assign({
148 recipeId,
149 }, data)),
150 }));
151 if (!request.ok) {
152 throw request;
153 }
154 const serviceData = await request.json();
155 const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) });
156
157 console.debug('ServerApi::createService resolves', service);
158 return service;
159 }
160
161 async updateService(recipeId, data) {
162 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/${recipeId}`, this._prepareAuthRequest({
163 method: 'PUT',
164 body: JSON.stringify(data),
165 }));
166 if (!request.ok) {
167 throw request;
168 }
169 const serviceData = await request.json();
170 const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) });
171
172 console.debug('ServerApi::updateService resolves', service);
173 return service;
174 }
175
176 async reorderService(data) {
177 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/reorder`, this._prepareAuthRequest({
178 method: 'PUT',
179 body: JSON.stringify(data),
180 }));
181 if (!request.ok) {
182 throw request;
183 }
184 const serviceData = await request.json();
185 console.debug('ServerApi::reorderService resolves', serviceData);
186 return serviceData;
187 }
188
189 async deleteService(id) {
190 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/${id}`, this._prepareAuthRequest({
191 method: 'DELETE',
192 }));
193 if (!request.ok) {
194 throw request;
195 }
196 const data = await request.json();
197
198 console.debug('ServerApi::deleteService resolves', data);
199 return data;
200 }
201
202 // Recipes
203 async getInstalledRecipes() {
204 const recipesDirectory = getRecipeDirectory();
205 const paths = fs.readdirSync(recipesDirectory)
206 .filter(file => (
207 fs.statSync(path.join(recipesDirectory, file)).isDirectory()
208 && file !== 'temp'
209 && file !== 'dev'
210 ));
211
212 this.recipes = paths.map((id) => {
213 // eslint-disable-next-line
214 const Recipe = require(id)(RecipeModel);
215 return new Recipe(loadRecipeConfig(id));
216 }).filter(recipe => recipe.id);
217
218 this.recipes = this.recipes.concat(this._getDevRecipes());
219
220 console.debug('StubServerApi::getInstalledRecipes resolves', this.recipes);
221 return this.recipes;
222 }
223
224 async getRecipeUpdates(recipeVersions) {
225 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes/update`, this._prepareAuthRequest({
226 method: 'POST',
227 body: JSON.stringify(recipeVersions),
228 }));
229 if (!request.ok) {
230 throw request;
231 }
232 const recipes = await request.json();
233 console.debug('ServerApi::getRecipeUpdates resolves', recipes);
234 return recipes;
235 }
236
237 // Recipes Previews
238 async getRecipePreviews() {
239 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes`, this._prepareAuthRequest({
240 method: 'GET',
241 }));
242 if (!request.ok) {
243 throw request;
244 }
245 const data = await request.json();
246
247 const recipePreviews = this._mapRecipePreviewModel(data);
248 console.debug('ServerApi::getRecipes resolves', recipePreviews);
249
250 return recipePreviews;
251 }
252
253 async getFeaturedRecipePreviews() {
254 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes/popular`, this._prepareAuthRequest({
255 method: 'GET',
256 }));
257 if (!request.ok) {
258 throw request;
259 }
260 const data = await request.json();
261
262 // data = this._addLocalRecipesToPreviews(data);
263
264 const recipePreviews = this._mapRecipePreviewModel(data);
265 console.debug('ServerApi::getFeaturedRecipes resolves', recipePreviews);
266 return recipePreviews;
267 }
268
269 async searchRecipePreviews(needle) {
270 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes/search?needle=${needle}`, this._prepareAuthRequest({
271 method: 'GET',
272 }));
273 if (!request.ok) {
274 throw request;
275 }
276 const data = await request.json();
277
278 const recipePreviews = this._mapRecipePreviewModel(data);
279 console.debug('ServerApi::searchRecipePreviews resolves', recipePreviews);
280 return recipePreviews;
281 }
282
283 async getRecipePackage(recipeId) {
284 try {
285 const recipesDirectory = path.join(app.getPath('userData'), 'recipes');
286
287 const recipeTempDirectory = path.join(recipesDirectory, 'temp', recipeId);
288 const archivePath = path.join(recipeTempDirectory, 'recipe.tar.gz');
289 const packageUrl = `${SERVER_URL}/${API_VERSION}/recipes/download/${recipeId}`;
290
291 fs.ensureDirSync(recipeTempDirectory);
292 const res = await fetch(packageUrl);
293 const buffer = await res.buffer();
294 fs.writeFileSync(archivePath, buffer);
295
296 await targz().extract(archivePath, recipeTempDirectory);
297
298 const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json'));
299 const recipeDirectory = path.join(recipesDirectory, id);
300
301 fs.copySync(recipeTempDirectory, recipeDirectory);
302 fs.remove(recipeTempDirectory);
303 fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz'));
304
305 return id;
306 } catch (err) {
307 console.error(err);
308
309 return false;
310 }
311 }
312
313 // Payment
314 async getPlans() {
315 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/payment/plans`, this._prepareAuthRequest({
316 method: 'GET',
317 }));
318 if (!request.ok) {
319 throw request;
320 }
321 const data = await request.json();
322
323 const plan = new PlanModel(data);
324 console.debug('ServerApi::getPlans resolves', plan);
325 return plan;
326 }
327
328 async getHostedPage(planId) {
329 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/payment/init`, this._prepareAuthRequest({
330 method: 'POST',
331 body: JSON.stringify({
332 planId,
333 }),
334 }));
335 if (!request.ok) {
336 throw request;
337 }
338 const data = await request.json();
339
340 console.debug('ServerApi::getHostedPage resolves', data);
341 return data;
342 }
343
344 async getPaymentDashboardUrl() {
345 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/billing`, this._prepareAuthRequest({
346 method: 'GET',
347 }));
348 if (!request.ok) {
349 throw request;
350 }
351 const data = await request.json();
352
353 console.debug('ServerApi::getPaymentDashboardUrl resolves', data);
354 return data;
355 }
356
357 async getSubscriptionOrders() {
358 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/subscription`, this._prepareAuthRequest({
359 method: 'GET',
360 }));
361 if (!request.ok) {
362 throw request;
363 }
364 const data = await request.json();
365 const orders = this._mapOrderModels(data);
366 console.debug('ServerApi::getSubscriptionOrders resolves', orders);
367 return orders;
368 }
369
370 // News
371 async getLatestNews() {
372 // eslint-disable-next-line
373 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/news?platform=${os.platform()}&arch=${os.arch()}version=${app.getVersion()}`,
374 this._prepareAuthRequest({
375 method: 'GET',
376 }));
377
378 if (!request.ok) {
379 throw request;
380 }
381 const data = await request.json();
382 const news = this._mapNewsModels(data);
383 console.debug('ServerApi::getLatestNews resolves', news);
384 return news;
385 }
386
387 async hideNews(id) {
388 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/news/${id}/read`,
389 this._prepareAuthRequest({
390 method: 'GET',
391 }));
392
393 if (!request.ok) {
394 throw request;
395 }
396
397 console.debug('ServerApi::hideNews resolves', id);
398 }
399
400 // Health Check
401 async healthCheck() {
402 const request = await window.fetch(`${SERVER_URL}/health`, this._prepareAuthRequest({
403 method: 'GET',
404 }, false));
405 if (!request.ok) {
406 throw request;
407 }
408 console.debug('ServerApi::healthCheck resolves');
409 }
410
411 async getLegacyServices() {
412 const file = path.join(app.getPath('userData'), 'settings', 'services.json');
413
414 try {
415 const config = fs.readJsonSync(file);
416
417 if (Object.prototype.hasOwnProperty.call(config, 'services')) {
418 const services = await Promise.all(config.services.map(async (s) => {
419 const service = s;
420 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/recipes/${s.service}`,
421 this._prepareAuthRequest({
422 method: 'GET',
423 }),
424 );
425
426 if (request.status === 200) {
427 const data = await request.json();
428 service.recipe = new RecipePreviewModel(data);
429 }
430
431 return service;
432 }));
433
434 console.debug('ServerApi::getLegacyServices resolves', services);
435 return services;
436 }
437 } catch (err) {
438 throw (new Error('ServerApi::getLegacyServices no config found'));
439 }
440
441 return [];
442 }
443
444 // Helper
445 async _mapServiceModels(services) {
446 return Promise.all(services
447 .map(async service => await this._prepareServiceModel(service)) // eslint-disable-line
448 );
449 }
450
451 async _prepareServiceModel(service) {
452 let recipe;
453 try {
454 recipe = this.recipes.find(r => r.id === service.recipeId);
455
456 if (!recipe) {
457 console.warn(`Recipe '${service.recipeId}' not installed, trying to fetch from server`);
458
459 await this.getRecipePackage(service.recipeId);
460
461 console.debug('Rerun ServerAPI::getInstalledRecipes');
462 await this.getInstalledRecipes();
463
464 recipe = this.recipes.find(r => r.id === service.recipeId);
465
466 if (!recipe) {
467 console.warn(`Could not load recipe ${service.recipeId}`);
468 return null;
469 }
470 }
471
472 return new ServiceModel(service, recipe);
473 } catch (e) {
474 console.debug(e);
475 return null;
476 }
477 }
478
479 _mapRecipePreviewModel(recipes) {
480 return recipes.map((recipe) => {
481 try {
482 return new RecipePreviewModel(recipe);
483 } catch (e) {
484 console.error(e);
485 return null;
486 }
487 }).filter(recipe => recipe !== null);
488 }
489
490 _mapNewsModels(news) {
491 return news.map((newsItem) => {
492 try {
493 return new NewsModel(newsItem);
494 } catch (e) {
495 console.error(e);
496 return null;
497 }
498 }).filter(newsItem => newsItem !== null);
499 }
500
501 _mapOrderModels(orders) {
502 return orders.map((orderItem) => {
503 try {
504 return new OrderModel(orderItem);
505 } catch (e) {
506 console.error(e);
507 return null;
508 }
509 }).filter(orderItem => orderItem !== null);
510 }
511
512 _prepareAuthRequest(options, auth = true) {
513 const request = Object.assign(options, {
514 mode: 'cors',
515 headers: {
516 'Content-Type': 'application/json',
517 'X-Franz-Source': 'desktop',
518 'X-Franz-Version': app.getVersion(),
519 'X-Franz-platform': process.platform,
520 'X-Franz-Timezone-Offset': new Date().getTimezoneOffset(),
521 'X-Franz-System-Locale': app.getLocale(),
522 },
523 });
524
525 // const headers = new window.Headers();
526 // headers.append('foo', 'bar');
527 // console.log(headers, request.headers);
528 //
529 //
530 // // request.headers.map((value, header) => headers.append(header, value));
531 // Object.keys(request.headers).map((key) => {
532 // console.log(key);
533 // return headers.append(key, request.headers[key]);
534 // });
535 // request.headers = headers;
536
537 // console.log(request);
538
539 if (auth) {
540 request.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`;
541 }
542
543 return request;
544 }
545
546 _getDevRecipes() {
547 const recipesDirectory = getDevRecipeDirectory();
548 try {
549 const paths = fs.readdirSync(recipesDirectory)
550 .filter(file => fs.statSync(path.join(recipesDirectory, file)).isDirectory() && file !== 'temp');
551
552 const recipes = paths.map((id) => {
553 // eslint-disable-next-line
554 const Recipe = require(id)(RecipeModel);
555 return new Recipe(loadRecipeConfig(id));
556 }).filter(recipe => recipe.id).map((data) => {
557 const recipe = data;
558
559 recipe.icons = {
560 svg: `${recipe.path}/icon.svg`,
561 png: `${recipe.path}/icon.png`,
562 };
563 recipe.local = true;
564
565 return data;
566 });
567
568 return recipes;
569 } catch (err) {
570 console.debug('Folder `recipe/dev` does not exist');
571 return false;
572 }
573 }
574}