diff options
Diffstat (limited to 'src/api/server/ServerApi.js')
-rw-r--r-- | src/api/server/ServerApi.js | 574 |
1 files changed, 574 insertions, 0 deletions
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 @@ | |||
1 | import os from 'os'; | ||
2 | import path from 'path'; | ||
3 | import targz from 'tar.gz'; | ||
4 | import fs from 'fs-extra'; | ||
5 | import { remote } from 'electron'; | ||
6 | |||
7 | import ServiceModel from '../../models/Service'; | ||
8 | import RecipePreviewModel from '../../models/RecipePreview'; | ||
9 | import RecipeModel from '../../models/Recipe'; | ||
10 | import PlanModel from '../../models/Plan'; | ||
11 | import NewsModel from '../../models/News'; | ||
12 | import UserModel from '../../models/User'; | ||
13 | import OrderModel from '../../models/Order'; | ||
14 | |||
15 | import { API } from '../../environment'; | ||
16 | |||
17 | import { | ||
18 | getRecipeDirectory, | ||
19 | getDevRecipeDirectory, | ||
20 | loadRecipeConfig, | ||
21 | } from '../../helpers/recipe-helpers'; | ||
22 | |||
23 | module.paths.unshift( | ||
24 | getDevRecipeDirectory(), | ||
25 | getRecipeDirectory(), | ||
26 | ); | ||
27 | |||
28 | const { app } = remote; | ||
29 | const fetch = remote.require('electron-fetch'); | ||
30 | |||
31 | const SERVER_URL = API; | ||
32 | const API_VERSION = 'v1'; | ||
33 | |||
34 | export 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 | } | ||