aboutsummaryrefslogtreecommitdiffstats
path: root/src/internal-server/app/Controllers/Http
diff options
context:
space:
mode:
Diffstat (limited to 'src/internal-server/app/Controllers/Http')
-rw-r--r--src/internal-server/app/Controllers/Http/RecipeController.js120
-rw-r--r--src/internal-server/app/Controllers/Http/ServiceController.js290
-rw-r--r--src/internal-server/app/Controllers/Http/StaticController.js205
-rw-r--r--src/internal-server/app/Controllers/Http/UserController.js367
-rw-r--r--src/internal-server/app/Controllers/Http/WorkspaceController.js148
5 files changed, 1130 insertions, 0 deletions
diff --git a/src/internal-server/app/Controllers/Http/RecipeController.js b/src/internal-server/app/Controllers/Http/RecipeController.js
new file mode 100644
index 000000000..8a6b4f684
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/RecipeController.js
@@ -0,0 +1,120 @@
1const Recipe = use('App/Models/Recipe');
2const Drive = use('Drive');
3const {
4 validateAll,
5} = use('Validator');
6const Env = use('Env');
7
8const fetch = require('node-fetch');
9
10const RECIPES_URL = 'https://api.getferdi.com/v1/recipes';
11
12class RecipeController {
13 // List official and custom recipes
14 async list({
15 response,
16 }) {
17 const officialRecipes = JSON.parse(await (await fetch(RECIPES_URL)).text());
18 const customRecipesArray = (await Recipe.all()).rows;
19 const customRecipes = customRecipesArray.map(recipe => ({
20 id: recipe.recipeId,
21 name: recipe.name,
22 ...JSON.parse(recipe.data),
23 }));
24
25 const recipes = [
26 ...officialRecipes,
27 ...customRecipes,
28 ];
29
30 return response.send(recipes);
31 }
32
33 // Search official and custom recipes
34 async search({
35 request,
36 response,
37 }) {
38 // Validate user input
39 const validation = await validateAll(request.all(), {
40 needle: 'required',
41 });
42 if (validation.fails()) {
43 return response.status(401).send({
44 message: 'Please provide a needle',
45 messages: validation.messages(),
46 status: 401,
47 });
48 }
49
50 const needle = request.input('needle');
51
52 // Get results
53 let results;
54
55 if (needle === 'ferdi:custom') {
56 const dbResults = (await Recipe.all()).toJSON();
57 results = dbResults.map(recipe => ({
58 id: recipe.recipeId,
59 name: recipe.name,
60 ...JSON.parse(recipe.data),
61 }));
62 } else {
63 let remoteResults = [];
64 if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq
65 remoteResults = JSON.parse(await (await fetch(`${RECIPES_URL}/search?needle=${encodeURIComponent(needle)}`)).text());
66 }
67 const localResultsArray = (await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()).toJSON();
68 const localResults = localResultsArray.map(recipe => ({
69 id: recipe.recipeId,
70 name: recipe.name,
71 ...JSON.parse(recipe.data),
72 }));
73
74 results = [
75 ...localResults,
76 ...remoteResults || [],
77 ];
78 }
79
80 return response.send(results);
81 }
82
83 // Download a recipe
84 async download({
85 response,
86 params,
87 }) {
88 // Validate user input
89 const validation = await validateAll(params, {
90 recipe: 'required|accepted',
91 });
92 if (validation.fails()) {
93 return response.status(401).send({
94 message: 'Please provide a recipe ID',
95 messages: validation.messages(),
96 status: 401,
97 });
98 }
99
100 const service = params.recipe;
101
102 // Check for invalid characters
103 if (/\.{1,}/.test(service) || /\/{1,}/.test(service)) {
104 return response.send('Invalid recipe name');
105 }
106
107 // Check if recipe exists in recipes folder
108 if (await Drive.exists(`${service}.tar.gz`)) {
109 return response.send(await Drive.get(`${service}.tar.gz`));
110 } if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq
111 return response.redirect(`${RECIPES_URL}/download/${service}`);
112 }
113 return response.status(400).send({
114 message: 'Recipe not found',
115 code: 'recipe-not-found',
116 });
117 }
118}
119
120module.exports = RecipeController;
diff --git a/src/internal-server/app/Controllers/Http/ServiceController.js b/src/internal-server/app/Controllers/Http/ServiceController.js
new file mode 100644
index 000000000..36d20c70c
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/ServiceController.js
@@ -0,0 +1,290 @@
1const Service = use('App/Models/Service');
2const {
3 validateAll,
4} = use('Validator');
5const Env = use('Env');
6
7const uuid = require('uuid/v4');
8const path = require('path');
9const fs = require('fs-extra');
10
11class ServiceController {
12 // Create a new service for user
13 async create({
14 request,
15 response,
16 }) {
17 // Validate user input
18 const validation = await validateAll(request.all(), {
19 name: 'required|string',
20 recipeId: 'required',
21 });
22 if (validation.fails()) {
23 return response.status(401).send({
24 message: 'Invalid POST arguments',
25 messages: validation.messages(),
26 status: 401,
27 });
28 }
29
30 const data = request.all();
31
32 // Get new, unused uuid
33 let serviceId;
34 do {
35 serviceId = uuid();
36 } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
37
38 await Service.create({
39 serviceId,
40 name: data.name,
41 recipeId: data.recipeId,
42 settings: JSON.stringify(data),
43 });
44
45 return response.send({
46 data: {
47 userId: 1,
48 id: serviceId,
49 isEnabled: true,
50 isNotificationEnabled: true,
51 isBadgeEnabled: true,
52 isMuted: false,
53 isDarkModeEnabled: '',
54 spellcheckerLanguage: '',
55 order: 1,
56 customRecipe: false,
57 hasCustomIcon: false,
58 workspaces: [],
59 iconUrl: null,
60 ...data,
61 },
62 status: ['created'],
63 });
64 }
65
66 // List all services a user has created
67 async list({
68 response,
69 }) {
70 const services = (await Service.all()).rows;
71 // Convert to array with all data Franz wants
72 const servicesArray = services.map((service) => {
73 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
74
75 return {
76 customRecipe: false,
77 hasCustomIcon: false,
78 isBadgeEnabled: true,
79 isDarkModeEnabled: '',
80 isEnabled: true,
81 isMuted: false,
82 isNotificationEnabled: true,
83 order: 1,
84 spellcheckerLanguage: '',
85 workspaces: [],
86 ...JSON.parse(service.settings),
87 iconUrl: settings.iconId ? `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${settings.iconId}` : null,
88 id: service.serviceId,
89 name: service.name,
90 recipeId: service.recipeId,
91 userId: 1,
92 };
93 });
94
95 return response.send(servicesArray);
96 }
97
98 async edit({
99 request,
100 response,
101 params,
102 }) {
103 if (request.file('icon')) {
104 // Upload custom service icon
105 await fs.ensureDir(path.join(Env.get('USER_PATH'), 'icons'));
106
107 const icon = request.file('icon', {
108 types: ['image'],
109 size: '2mb',
110 });
111 const {
112 id,
113 } = params;
114 const service = (await Service.query()
115 .where('serviceId', id).fetch()).rows[0];
116 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
117
118 // Generate new icon ID
119 let iconId;
120 do {
121 iconId = uuid() + uuid();
122 // eslint-disable-next-line no-await-in-loop
123 } while (await fs.exists(path.join(Env.get('USER_PATH'), 'icons', iconId)));
124
125 await icon.move(path.join(Env.get('USER_PATH'), 'icons'), {
126 name: iconId,
127 overwrite: true,
128 });
129
130 if (!icon.moved()) {
131 return response.status(500).send(icon.error());
132 }
133
134 const newSettings = {
135 ...settings,
136 ...{
137 iconId,
138 customIconVersion: settings && settings.customIconVersion ? settings.customIconVersion + 1 : 1,
139 },
140 };
141
142 // Update data in database
143 await (Service.query()
144 .where('serviceId', id)).update({
145 name: service.name,
146 settings: JSON.stringify(newSettings),
147 });
148
149 return response.send({
150 data: {
151 id,
152 name: service.name,
153 ...newSettings,
154 iconUrl: `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${newSettings.iconId}`,
155 userId: 1,
156 },
157 status: ['updated'],
158 });
159 }
160 // Update service info
161 const data = request.all();
162 const {
163 id,
164 } = params;
165
166 // Get current settings from db
167 const serviceData = (await Service.query()
168 .where('serviceId', id).fetch()).rows[0];
169
170 const settings = {
171 ...typeof serviceData.settings === 'string' ? JSON.parse(serviceData.settings) : serviceData.settings,
172 ...data,
173 };
174
175 // Update data in database
176 await (Service.query()
177 .where('serviceId', id)).update({
178 name: data.name,
179 settings: JSON.stringify(settings),
180 });
181
182 // Get updated row
183 const service = (await Service.query()
184 .where('serviceId', id).fetch()).rows[0];
185
186 return response.send({
187 data: {
188 id,
189 name: service.name,
190 ...settings,
191 iconUrl: `${Env.get('APP_URL')}/v1/icon/${settings.iconId}`,
192 userId: 1,
193 },
194 status: ['updated'],
195 });
196 }
197
198 async icon({
199 params,
200 response,
201 }) {
202 const {
203 id,
204 } = params;
205
206 const iconPath = path.join(Env.get('USER_PATH'), 'icons', id);
207 if (!await fs.exists(iconPath)) {
208 return response.status(404).send({
209 status: 'Icon doesn\'t exist',
210 });
211 }
212
213 return response.download(iconPath);
214 }
215
216 async reorder({
217 request,
218 response,
219 }) {
220 const data = request.all();
221
222 for (const service of Object.keys(data)) {
223 // Get current settings from db
224 const serviceData = (await Service.query() // eslint-disable-line no-await-in-loop
225 .where('serviceId', service).fetch()).rows[0];
226
227 const settings = {
228 ...JSON.parse(serviceData.settings),
229 order: data[service],
230 };
231
232 // Update data in database
233 await (Service.query() // eslint-disable-line no-await-in-loop
234 .where('serviceId', service))
235 .update({
236 settings: JSON.stringify(settings),
237 });
238 }
239
240 // Get new services
241 const services = (await Service.all()).rows;
242 // Convert to array with all data Franz wants
243 const servicesArray = services.map((service) => {
244 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
245
246 return {
247 customRecipe: false,
248 hasCustomIcon: false,
249 isBadgeEnabled: true,
250 isDarkModeEnabled: '',
251 isEnabled: true,
252 isMuted: false,
253 isNotificationEnabled: true,
254 order: 1,
255 spellcheckerLanguage: '',
256 workspaces: [],
257 ...JSON.parse(service.settings),
258 iconUrl: settings.iconId ? `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${settings.iconId}` : null,
259 id: service.serviceId,
260 name: service.name,
261 recipeId: service.recipeId,
262 userId: 1,
263 };
264 });
265
266 return response.send(servicesArray);
267 }
268
269 update({
270 response,
271 }) {
272 return response.send([]);
273 }
274
275 async delete({
276 params,
277 response,
278 }) {
279 // Update data in database
280 await (Service.query()
281 .where('serviceId', params.id)).delete();
282
283 return response.send({
284 message: 'Sucessfully deleted service',
285 status: 200,
286 });
287 }
288}
289
290module.exports = ServiceController;
diff --git a/src/internal-server/app/Controllers/Http/StaticController.js b/src/internal-server/app/Controllers/Http/StaticController.js
new file mode 100644
index 000000000..69dfee0a3
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/StaticController.js
@@ -0,0 +1,205 @@
1/**
2 * Controller for routes with static responses
3 */
4
5class StaticController {
6 // Enable all features
7 features({
8 response,
9 }) {
10 return response.send({
11 isServiceProxyEnabled: true,
12 isWorkspaceEnabled: true,
13 isAnnouncementsEnabled: true,
14 isSettingsWSEnabled: false,
15 isMagicBarEnabled: true,
16 isTodosEnabled: true,
17 subscribeURL: 'https://getferdi.com',
18 hasInlineCheckout: true,
19 });
20 }
21
22 // Return an empty array
23 emptyArray({
24 response,
25 }) {
26 return response.send([]);
27 }
28
29 // Return list of popular recipes (copy of the response Franz's API is returning)
30 popularRecipes({
31 response,
32 }) {
33 return response.send([{
34 // TODO: Why is this list hardcoded?
35 author: 'Stefan Malzner <stefan@adlk.io>',
36 featured: false,
37 id: 'slack',
38 name: 'Slack',
39 version: '1.0.4',
40 icons: {
41 png: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.png',
42 svg: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.svg',
43 },
44 }, {
45 author: 'Stefan Malzner <stefan@adlk.io>',
46 featured: false,
47 id: 'whatsapp',
48 name: 'WhatsApp',
49 version: '1.0.1',
50 icons: {
51 png: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.png',
52 svg: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.svg',
53 },
54 }, {
55 author: 'Stefan Malzner <stefan@adlk.io>',
56 featured: false,
57 id: 'messenger',
58 name: 'Messenger',
59 version: '1.0.6',
60 icons: {
61 png: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.png',
62 svg: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.svg',
63 },
64 }, {
65 author: 'Stefan Malzner <stefan@adlk.io>',
66 featured: false,
67 id: 'telegram',
68 name: 'Telegram',
69 version: '1.0.0',
70 icons: {
71 png: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.png',
72 svg: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.svg',
73 },
74 }, {
75 author: 'Stefan Malzner <stefan@adlk.io>',
76 featured: false,
77 id: 'gmail',
78 name: 'Gmail',
79 version: '1.0.0',
80 icons: {
81 png: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.png',
82 svg: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.svg',
83 },
84 }, {
85 author: 'Stefan Malzner <stefan@adlk.io>',
86 featured: false,
87 id: 'skype',
88 name: 'Skype',
89 version: '1.0.0',
90 icons: {
91 png: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.png',
92 svg: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.svg',
93 },
94 }, {
95 author: 'Stefan Malzner <stefan@adlk.io>',
96 featured: false,
97 id: 'hangouts',
98 name: 'Hangouts',
99 version: '1.0.0',
100 icons: {
101 png: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.png',
102 svg: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.svg',
103 },
104 }, {
105 author: 'Stefan Malzner <stefan@adlk.io>',
106 featured: false,
107 id: 'discord',
108 name: 'Discord',
109 version: '1.0.0',
110 icons: {
111 png: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.png',
112 svg: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.svg',
113 },
114 }, {
115 author: 'Stefan Malzner <stefan@adlk.io>',
116 featured: false,
117 id: 'tweetdeck',
118 name: 'Tweetdeck',
119 version: '1.0.1',
120 icons: {
121 png: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.png',
122 svg: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.svg',
123 },
124 }, {
125 author: 'Stefan Malzner <stefan@adlk.io>',
126 featured: false,
127 id: 'hipchat',
128 name: 'HipChat',
129 version: '1.0.1',
130 icons: {
131 png: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.png',
132 svg: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.svg',
133 },
134 }, {
135 author: 'Stefan Malzner <stefan@adlk.io>',
136 featured: false,
137 id: 'gmailinbox',
138 name: 'Inbox by Gmail',
139 version: '1.0.0',
140 icons: {
141 png: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.png',
142 svg: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.svg',
143 },
144 }, {
145 author: 'Stefan Malzner <stefan@adlk.io>',
146 featured: false,
147 id: 'rocketchat',
148 name: 'Rocket.Chat',
149 version: '1.0.1',
150 icons: {
151 png: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.png',
152 svg: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.svg',
153 },
154 }, {
155 author: 'Brian Gilbert <brian@briangilbert.net>',
156 featured: false,
157 id: 'gitter',
158 name: 'Gitter',
159 version: '1.0.0',
160 icons: {
161 png: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.png',
162 svg: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.svg',
163 },
164 }, {
165 author: 'Stefan Malzner <stefan@adlk.io>',
166 featured: false,
167 id: 'mattermost',
168 name: 'Mattermost',
169 version: '1.0.0',
170 icons: {
171 png: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.png',
172 svg: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.svg',
173 },
174 }, {
175 author: 'Franz <recipe@meetfranz.com>',
176 featured: false,
177 id: 'toggl',
178 name: 'toggl',
179 version: '1.0.0',
180 icons: {
181 png: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.png',
182 svg: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.svg',
183 },
184 }, {
185 author: 'Stuart Clark <stuart@realityloop.com>',
186 featured: false,
187 id: 'twist',
188 name: 'twist',
189 version: '1.0.0',
190 icons: {
191 png: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.png',
192 svg: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.svg',
193 },
194 }]);
195 }
196
197 // Show announcements
198 announcement({
199 response,
200 }) {
201 return response.send({});
202 }
203}
204
205module.exports = StaticController;
diff --git a/src/internal-server/app/Controllers/Http/UserController.js b/src/internal-server/app/Controllers/Http/UserController.js
new file mode 100644
index 000000000..f7cdfc9c9
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/UserController.js
@@ -0,0 +1,367 @@
1const User = use('App/Models/User');
2const Service = use('App/Models/Service');
3const Workspace = use('App/Models/Workspace');
4const {
5 validateAll,
6} = use('Validator');
7
8const btoa = require('btoa');
9const fetch = require('node-fetch');
10const uuid = require('uuid/v4');
11const crypto = require('crypto');
12
13const apiRequest = (url, route, method, auth) => new Promise((resolve, reject) => {
14 const base = `${url}/v1/`;
15 const user = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36';
16
17 try {
18 fetch(base + route, {
19 method,
20 headers: {
21 Authorization: `Bearer ${auth}`,
22 'User-Agent': user,
23 },
24 })
25 .then(data => data.json())
26 .then(json => resolve(json));
27 } catch (e) {
28 reject();
29 }
30});
31
32class UserController {
33 // Register a new user
34 async signup({
35 request,
36 response,
37 }) {
38 // Validate user input
39 const validation = await validateAll(request.all(), {
40 firstname: 'required',
41 email: 'required|email',
42 password: 'required',
43 });
44 if (validation.fails()) {
45 return response.status(401).send({
46 message: 'Invalid POST arguments',
47 messages: validation.messages(),
48 status: 401,
49 });
50 }
51
52 return response.send({
53 message: 'Successfully created account',
54 token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M',
55 });
56 }
57
58 // Login using an existing user
59 async login({
60 request,
61 response,
62 }) {
63 if (!request.header('Authorization')) {
64 return response.status(401).send({
65 message: 'Please provide authorization',
66 status: 401,
67 });
68 }
69
70 return response.send({
71 message: 'Successfully logged in',
72 token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M',
73 });
74 }
75
76 // Return information about the current user
77 async me({
78 response,
79 }) {
80 const user = await User.find(1);
81
82 const settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings;
83
84 return response.send({
85 accountType: 'individual',
86 beta: false,
87 donor: {},
88 email: '',
89 emailValidated: true,
90 features: {},
91 firstname: 'Ferdi',
92 id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8',
93 isSubscriptionOwner: true,
94 lastname: 'Application',
95 locale: 'en-US',
96 ...settings || {},
97 });
98 }
99
100 async updateMe({
101 request,
102 response,
103 }) {
104 const user = await User.find(1);
105
106 let settings = user.settings || {};
107 if (typeof settings === 'string') {
108 settings = JSON.parse(settings);
109 }
110
111 const newSettings = {
112 ...settings,
113 ...request.all(),
114 };
115
116 user.settings = JSON.stringify(newSettings);
117 await user.save();
118
119 return response.send({
120 data: {
121 accountType: 'individual',
122 beta: false,
123 donor: {},
124 email: '',
125 emailValidated: true,
126 features: {},
127 firstname: 'Ferdi',
128 id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8',
129 isSubscriptionOwner: true,
130 lastname: 'Application',
131 locale: 'en-US',
132 ...newSettings,
133 },
134 status: [
135 'data-updated',
136 ],
137 });
138 }
139
140 async import({
141 request,
142 response,
143 }) {
144 // Validate user input
145 const validation = await validateAll(request.all(), {
146 email: 'required|email',
147 password: 'required',
148 server: 'required',
149 });
150 if (validation.fails()) {
151 let errorMessage = 'There was an error while trying to import your account:\n';
152 for (const message of validation.messages()) {
153 if (message.validation === 'required') {
154 errorMessage += `- Please make sure to supply your ${message.field}\n`;
155 } else if (message.validation === 'unique') {
156 errorMessage += '- There is already a user with this email.\n';
157 } else {
158 errorMessage += `${message.message}\n`;
159 }
160 }
161 return response.status(401).send(errorMessage);
162 }
163
164 const {
165 email,
166 password,
167 server,
168 } = request.all();
169
170 const hashedPassword = crypto.createHash('sha256').update(password).digest('base64');
171
172 const base = `${server}/v1/`;
173 const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36';
174
175 // Try to get an authentication token
176 let token;
177 try {
178 const basicToken = btoa(`${email}:${hashedPassword}`);
179
180 const rawResponse = await fetch(`${base}auth/login`, {
181 method: 'POST',
182 headers: {
183 Authorization: `Basic ${basicToken}`,
184 'User-Agent': userAgent,
185 },
186 });
187 const content = await rawResponse.json();
188
189 if (!content.message || content.message !== 'Successfully logged in') {
190 const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again';
191 return response.status(401).send(errorMessage);
192 }
193
194 // eslint-disable-next-line prefer-destructuring
195 token = content.token;
196 } catch (e) {
197 return response.status(401).send({
198 message: 'Cannot login to Franz',
199 error: e,
200 });
201 }
202
203 // Get user information
204 let userInf = false;
205 try {
206 userInf = await apiRequest(server, 'me', 'GET', token);
207 } catch (e) {
208 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`;
209 return response.status(401).send(errorMessage);
210 }
211 if (!userInf) {
212 const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later';
213 return response.status(401).send(errorMessage);
214 }
215
216 const serviceIdTranslation = {};
217
218 // Import services
219 try {
220 const services = await apiRequest(server, 'me/services', 'GET', token);
221
222 for (const service of services) {
223 // Get new, unused uuid
224 let serviceId;
225 do {
226 serviceId = uuid();
227 } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
228
229 await Service.create({ // eslint-disable-line no-await-in-loop
230 serviceId,
231 name: service.name,
232 recipeId: service.recipeId,
233 settings: JSON.stringify(service),
234 });
235
236 serviceIdTranslation[service.id] = serviceId;
237 }
238 } catch (e) {
239 const errorMessage = `Could not import your services into our system.\nError: ${e}`;
240 return response.status(401).send(errorMessage);
241 }
242
243 // Import workspaces
244 try {
245 const workspaces = await apiRequest(server, 'workspace', 'GET', token);
246
247 for (const workspace of workspaces) {
248 let workspaceId;
249 do {
250 workspaceId = uuid();
251 } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
252
253 const services = workspace.services.map(service => serviceIdTranslation[service]);
254
255 await Workspace.create({ // eslint-disable-line no-await-in-loop
256 workspaceId,
257 name: workspace.name,
258 order: workspace.order,
259 services: JSON.stringify(services),
260 data: JSON.stringify({}),
261 });
262 }
263 } catch (e) {
264 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`;
265 return response.status(401).send(errorMessage);
266 }
267
268 return response.send('Your account has been imported. You can now use your Franz account in Ferdi.');
269 }
270
271 // Account import/export
272 async export({
273 // eslint-disable-next-line no-unused-vars
274 auth,
275 response,
276 }) {
277 const services = (await Service.all()).toJSON();
278 const workspaces = (await Workspace.all()).toJSON();
279
280 const exportData = {
281 username: 'Ferdi',
282 mail: 'internal@getferdi.com',
283 services,
284 workspaces,
285 };
286
287 return response
288 .header('Content-Type', 'application/force-download')
289 .header('Content-disposition', 'attachment; filename=export.ferdi-data')
290 .send(exportData);
291 }
292
293 async importFerdi({
294 request,
295 response,
296 }) {
297 const validation = await validateAll(request.all(), {
298 file: 'required',
299 });
300 if (validation.fails()) {
301 return response.send(validation.messages());
302 }
303
304 let file;
305 try {
306 file = JSON.parse(request.input('file'));
307 } catch (e) {
308 return response.send('Could not import: Invalid file, could not read file');
309 }
310
311 if (!file || !file.services || !file.workspaces) {
312 return response.send('Could not import: Invalid file (2)');
313 }
314
315 const serviceIdTranslation = {};
316
317 // Import services
318 try {
319 for (const service of file.services) {
320 // Get new, unused uuid
321 let serviceId;
322 do {
323 serviceId = uuid();
324 } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
325
326 await Service.create({ // eslint-disable-line no-await-in-loop
327 serviceId,
328 name: service.name,
329 recipeId: service.recipeId,
330 settings: JSON.stringify(service.settings),
331 });
332
333 serviceIdTranslation[service.id] = serviceId;
334 }
335 } catch (e) {
336 const errorMessage = `Could not import your services into our system.\nError: ${e}`;
337 return response.send(errorMessage);
338 }
339
340 // Import workspaces
341 try {
342 for (const workspace of file.workspaces) {
343 let workspaceId;
344 do {
345 workspaceId = uuid();
346 } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
347
348 const services = workspace.services.map((service) => serviceIdTranslation[service]);
349
350 await Workspace.create({ // eslint-disable-line no-await-in-loop
351 workspaceId,
352 name: workspace.name,
353 order: workspace.order,
354 services: JSON.stringify(services),
355 data: JSON.stringify(workspace.data),
356 });
357 }
358 } catch (e) {
359 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`;
360 return response.status(401).send(errorMessage);
361 }
362
363 return response.send('Your account has been imported.');
364 }
365}
366
367module.exports = UserController;
diff --git a/src/internal-server/app/Controllers/Http/WorkspaceController.js b/src/internal-server/app/Controllers/Http/WorkspaceController.js
new file mode 100644
index 000000000..4189fbcdd
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/WorkspaceController.js
@@ -0,0 +1,148 @@
1const Workspace = use('App/Models/Workspace');
2const {
3 validateAll,
4} = use('Validator');
5
6const uuid = require('uuid/v4');
7
8class WorkspaceController {
9 // Create a new workspace for user
10 async create({
11 request,
12 response,
13 }) {
14 // Validate user input
15 const validation = await validateAll(request.all(), {
16 name: 'required',
17 });
18 if (validation.fails()) {
19 return response.status(401).send({
20 message: 'Invalid POST arguments',
21 messages: validation.messages(),
22 status: 401,
23 });
24 }
25
26 const data = request.all();
27
28 // Get new, unused uuid
29 let workspaceId;
30 do {
31 workspaceId = uuid();
32 } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
33
34 const order = (await Workspace.all()).rows.length;
35
36 await Workspace.create({
37 workspaceId,
38 name: data.name,
39 order,
40 services: JSON.stringify([]),
41 data: JSON.stringify(data),
42 });
43
44 return response.send({
45 userId: 1,
46 name: data.name,
47 id: workspaceId,
48 order,
49 workspaces: [],
50 });
51 }
52
53 async edit({
54 request,
55 response,
56 params,
57 }) {
58 // Validate user input
59 const validation = await validateAll(request.all(), {
60 name: 'required',
61 services: 'required|array',
62 });
63 if (validation.fails()) {
64 return response.status(401).send({
65 message: 'Invalid POST arguments',
66 messages: validation.messages(),
67 status: 401,
68 });
69 }
70
71 const data = request.all();
72 const {
73 id,
74 } = params;
75
76 // Update data in database
77 await (Workspace.query()
78 .where('workspaceId', id)).update({
79 name: data.name,
80 services: JSON.stringify(data.services),
81 });
82
83 // Get updated row
84 const workspace = (await Workspace.query()
85 .where('workspaceId', id).fetch()).rows[0];
86
87 return response.send({
88 id: workspace.workspaceId,
89 name: data.name,
90 order: workspace.order,
91 services: data.services,
92 userId: 1,
93 });
94 }
95
96 async delete({
97 // eslint-disable-next-line no-unused-vars
98 request,
99 response,
100 params,
101 }) {
102 // Validate user input
103 const validation = await validateAll(params, {
104 id: 'required',
105 });
106 if (validation.fails()) {
107 return response.status(401).send({
108 message: 'Invalid arguments',
109 messages: validation.messages(),
110 status: 401,
111 });
112 }
113
114 const {
115 id,
116 } = params;
117
118 // Update data in database
119 await (Workspace.query()
120 .where('workspaceId', id)).delete();
121
122 return response.send({
123 message: 'Successfully deleted workspace',
124 });
125 }
126
127 // List all workspaces a user has created
128 async list({
129 response,
130 }) {
131 const workspaces = (await Workspace.all()).rows;
132 // Convert to array with all data Franz wants
133 let workspacesArray = [];
134 if (workspaces) {
135 workspacesArray = workspaces.map(workspace => ({
136 id: workspace.workspaceId,
137 name: workspace.name,
138 order: workspace.order,
139 services: typeof workspace.services === 'string' ? JSON.parse(workspace.services) : workspace.services,
140 userId: 1,
141 }));
142 }
143
144 return response.send(workspacesArray);
145 }
146}
147
148module.exports = WorkspaceController;