aboutsummaryrefslogtreecommitdiffstats
path: root/src/internal-server/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/internal-server/app')
-rw-r--r--src/internal-server/app/Controllers/Http/RecipeController.js120
-rw-r--r--src/internal-server/app/Controllers/Http/ServiceController.js292
-rw-r--r--src/internal-server/app/Controllers/Http/StaticController.js37
-rw-r--r--src/internal-server/app/Controllers/Http/UserController.js370
-rw-r--r--src/internal-server/app/Controllers/Http/WorkspaceController.js148
-rw-r--r--src/internal-server/app/Exceptions/Handler.js40
-rw-r--r--src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js15
-rw-r--r--src/internal-server/app/Models/Recipe.js7
-rw-r--r--src/internal-server/app/Models/Service.js7
-rw-r--r--src/internal-server/app/Models/Token.js7
-rw-r--r--src/internal-server/app/Models/Traits/NoTimestamp.js14
-rw-r--r--src/internal-server/app/Models/User.js8
-rw-r--r--src/internal-server/app/Models/Workspace.js7
13 files changed, 1072 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..c76a287f7
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/ServiceController.js
@@ -0,0 +1,292 @@
1const Service = use('App/Models/Service');
2const { validateAll } = use('Validator');
3const Env = use('Env');
4
5const uuid = require('uuid/v4');
6const path = require('path');
7const fs = require('fs-extra');
8const { LOCAL_HOSTNAME } = require('../../../../config');
9
10const hostname = LOCAL_HOSTNAME;
11const port = Env.get('PORT');
12
13class ServiceController {
14 // Create a new service for user
15 async create({ request, response }) {
16 // Validate user input
17 const validation = await validateAll(request.all(), {
18 name: 'required|string',
19 recipeId: 'required',
20 });
21 if (validation.fails()) {
22 return response.status(401).send({
23 message: 'Invalid POST arguments',
24 messages: validation.messages(),
25 status: 401,
26 });
27 }
28
29 const data = request.all();
30
31 // Get new, unused uuid
32 let serviceId;
33 do {
34 serviceId = uuid();
35 } while (
36 (await Service.query().where('serviceId', serviceId).fetch()).rows
37 .length > 0
38 ); // eslint-disable-line no-await-in-loop
39
40 await Service.create({
41 serviceId,
42 name: data.name,
43 recipeId: data.recipeId,
44 settings: JSON.stringify(data),
45 });
46
47 return response.send({
48 data: {
49 userId: 1,
50 id: serviceId,
51 isEnabled: true,
52 isNotificationEnabled: true,
53 isBadgeEnabled: true,
54 isMuted: false,
55 isDarkModeEnabled: '', // TODO: This should ideally be a boolean (false). But, changing it caused the sidebar toggle to not work.
56 spellcheckerLanguage: '',
57 order: 1,
58 customRecipe: false,
59 hasCustomIcon: false,
60 workspaces: [],
61 iconUrl: null,
62 ...data,
63 },
64 status: ['created'],
65 });
66 }
67
68 // List all services a user has created
69 async list({ response }) {
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 =
74 typeof service.settings === 'string'
75 ? JSON.parse(service.settings)
76 : service.settings;
77
78 return {
79 customRecipe: false,
80 hasCustomIcon: false,
81 isBadgeEnabled: true,
82 isDarkModeEnabled: '', // TODO: This should ideally be a boolean (false). But, changing it caused the sidebar toggle to not work.
83 isEnabled: true,
84 isMuted: false,
85 isNotificationEnabled: true,
86 order: 1,
87 spellcheckerLanguage: '',
88 workspaces: [],
89 ...JSON.parse(service.settings),
90 iconUrl: settings.iconId
91 ? `http://${hostname}:${port}/v1/icon/${settings.iconId}`
92 : null,
93 id: service.serviceId,
94 name: service.name,
95 recipeId: service.recipeId,
96 userId: 1,
97 };
98 });
99
100 return response.send(servicesArray);
101 }
102
103 async edit({ request, response, params }) {
104 if (request.file('icon')) {
105 // Upload custom service icon
106 await fs.ensureDir(path.join(Env.get('USER_PATH'), 'icons'));
107
108 const icon = request.file('icon', {
109 types: ['image'],
110 size: '2mb',
111 });
112 const { id } = params;
113 const service = (await Service.query().where('serviceId', id).fetch())
114 .rows[0];
115 const settings =
116 typeof service.settings === 'string'
117 ? JSON.parse(service.settings)
118 : service.settings;
119
120 // Generate new icon ID
121 let iconId;
122 do {
123 iconId = uuid() + uuid();
124 } while (fs.existsSync(path.join(Env.get('USER_PATH'), 'icons', iconId)));
125
126 await icon.move(path.join(Env.get('USER_PATH'), 'icons'), {
127 name: iconId,
128 overwrite: true,
129 });
130
131 if (!icon.moved()) {
132 return response.status(500).send(icon.error());
133 }
134
135 const newSettings = {
136 ...settings,
137 ...{
138 iconId,
139 customIconVersion:
140 settings && settings.customIconVersion
141 ? settings.customIconVersion + 1
142 : 1,
143 },
144 };
145
146 // Update data in database
147 await Service.query()
148 .where('serviceId', id)
149 .update({
150 name: service.name,
151 settings: JSON.stringify(newSettings),
152 });
153
154 return response.send({
155 data: {
156 id,
157 name: service.name,
158 ...newSettings,
159 iconUrl: `http://${hostname}:${port}/v1/icon/${
160 newSettings.iconId
161 }`,
162 userId: 1,
163 },
164 status: ['updated'],
165 });
166 }
167 // Update service info
168 const data = request.all();
169 const { id } = params;
170
171 // Get current settings from db
172 const serviceData = (await Service.query().where('serviceId', id).fetch())
173 .rows[0];
174
175 const settings = {
176 ...(typeof serviceData.settings === 'string'
177 ? JSON.parse(serviceData.settings)
178 : serviceData.settings),
179 ...data,
180 };
181
182 // Update data in database
183 await Service.query()
184 .where('serviceId', id)
185 .update({
186 name: data.name,
187 settings: JSON.stringify(settings),
188 });
189
190 // Get updated row
191 const service = (await Service.query().where('serviceId', id).fetch())
192 .rows[0];
193
194 return response.send({
195 data: {
196 id,
197 name: service.name,
198 ...settings,
199 iconUrl: `${Env.get('APP_URL')}/v1/icon/${settings.iconId}`,
200 userId: 1,
201 },
202 status: ['updated'],
203 });
204 }
205
206 async icon({ params, response }) {
207 const { id } = params;
208
209 const iconPath = path.join(Env.get('USER_PATH'), 'icons', id);
210 if (!fs.existsSync(iconPath)) {
211 return response.status(404).send({
212 status: "Icon doesn't exist",
213 });
214 }
215
216 return response.download(iconPath);
217 }
218
219 async reorder({ request, response }) {
220 const data = request.all();
221
222 for (const service of Object.keys(data)) {
223 // Get current settings from db
224 const serviceData = (
225 await Service.query() // eslint-disable-line no-await-in-loop
226 .where('serviceId', service)
227 .fetch()
228 ).rows[0];
229
230 const settings = {
231 ...JSON.parse(serviceData.settings),
232 order: data[service],
233 };
234
235 // Update data in database
236 await Service.query() // eslint-disable-line no-await-in-loop
237 .where('serviceId', service)
238 .update({
239 settings: JSON.stringify(settings),
240 });
241 }
242
243 // Get new services
244 const services = (await Service.all()).rows;
245 // Convert to array with all data Franz wants
246 const servicesArray = services.map(service => {
247 const settings =
248 typeof service.settings === 'string'
249 ? JSON.parse(service.settings)
250 : service.settings;
251
252 return {
253 customRecipe: false,
254 hasCustomIcon: false,
255 isBadgeEnabled: true,
256 isDarkModeEnabled: '', // TODO: This should ideally be a boolean (false). But, changing it caused the sidebar toggle to not work.
257 isEnabled: true,
258 isMuted: false,
259 isNotificationEnabled: true,
260 order: 1,
261 spellcheckerLanguage: '',
262 workspaces: [],
263 ...JSON.parse(service.settings),
264 iconUrl: settings.iconId
265 ? `http://${hostname}:${port}/v1/icon/${settings.iconId}`
266 : null,
267 id: service.serviceId,
268 name: service.name,
269 recipeId: service.recipeId,
270 userId: 1,
271 };
272 });
273
274 return response.send(servicesArray);
275 }
276
277 update({ response }) {
278 return response.send([]);
279 }
280
281 async delete({ params, response }) {
282 // Update data in database
283 await Service.query().where('serviceId', params.id).delete();
284
285 return response.send({
286 message: 'Sucessfully deleted service',
287 status: 200,
288 });
289 }
290}
291
292module.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..b9a145061
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/StaticController.js
@@ -0,0 +1,37 @@
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 // Show announcements
30 announcement({
31 response,
32 }) {
33 return response.send({});
34 }
35}
36
37module.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..a3ad736fa
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/UserController.js
@@ -0,0 +1,370 @@
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');
12const { DEFAULT_APP_SETTINGS } = require('../../../../environment');
13
14const apiRequest = (url, route, method, auth) => new Promise((resolve, reject) => {
15 const base = `${url}/v1/`;
16 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';
17
18 try {
19 fetch(base + route, {
20 method,
21 headers: {
22 Authorization: `Bearer ${auth}`,
23 'User-Agent': user,
24 },
25 })
26 .then(data => data.json())
27 .then(json => resolve(json));
28 } catch (e) {
29 reject();
30 }
31});
32
33class UserController {
34 // Register a new user
35 async signup({
36 request,
37 response,
38 }) {
39 // Validate user input
40 const validation = await validateAll(request.all(), {
41 firstname: 'required',
42 email: 'required|email',
43 password: 'required',
44 });
45 if (validation.fails()) {
46 return response.status(401).send({
47 message: 'Invalid POST arguments',
48 messages: validation.messages(),
49 status: 401,
50 });
51 }
52
53 return response.send({
54 message: 'Successfully created account',
55 token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M',
56 });
57 }
58
59 // Login using an existing user
60 async login({
61 request,
62 response,
63 }) {
64 if (!request.header('Authorization')) {
65 return response.status(401).send({
66 message: 'Please provide authorization',
67 status: 401,
68 });
69 }
70
71 return response.send({
72 message: 'Successfully logged in',
73 token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M',
74 });
75 }
76
77 // Return information about the current user
78 async me({
79 response,
80 }) {
81 const user = await User.find(1);
82
83 const settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings;
84
85 return response.send({
86 accountType: 'individual',
87 beta: false,
88 donor: {},
89 email: '',
90 emailValidated: true,
91 features: {},
92 firstname: 'Ferdi',
93 id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8',
94 isSubscriptionOwner: true,
95 lastname: 'Application',
96 locale: DEFAULT_APP_SETTINGS.fallbackLocale,
97 ...settings || {},
98 });
99 }
100
101 async updateMe({
102 request,
103 response,
104 }) {
105 const user = await User.find(1);
106
107 let settings = user.settings || {};
108 if (typeof settings === 'string') {
109 settings = JSON.parse(settings);
110 }
111
112 const newSettings = {
113 ...settings,
114 ...request.all(),
115 };
116
117 user.settings = JSON.stringify(newSettings);
118 await user.save();
119
120 return response.send({
121 data: {
122 accountType: 'individual',
123 beta: false,
124 donor: {},
125 email: '',
126 emailValidated: true,
127 features: {},
128 firstname: 'Ferdi',
129 id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8',
130 isSubscriptionOwner: true,
131 lastname: 'Application',
132 locale: DEFAULT_APP_SETTINGS.fallbackLocale,
133 ...newSettings,
134 },
135 status: [
136 'data-updated',
137 ],
138 });
139 }
140
141 async import({
142 request,
143 response,
144 }) {
145 // Validate user input
146 const validation = await validateAll(request.all(), {
147 email: 'required|email',
148 password: 'required',
149 server: 'required',
150 });
151 if (validation.fails()) {
152 let errorMessage = 'There was an error while trying to import your account:\n';
153 for (const message of validation.messages()) {
154 if (message.validation === 'required') {
155 errorMessage += `- Please make sure to supply your ${message.field}\n`;
156 } else if (message.validation === 'unique') {
157 errorMessage += '- There is already a user with this email.\n';
158 } else {
159 errorMessage += `${message.message}\n`;
160 }
161 }
162 return response.status(401).send(errorMessage);
163 }
164
165 const {
166 email,
167 password,
168 server,
169 } = request.all();
170
171 const hashedPassword = crypto.createHash('sha256').update(password).digest('base64');
172
173 const base = `${server}/v1/`;
174 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';
175
176 // Try to get an authentication token
177 let token;
178 try {
179 const basicToken = btoa(`${email}:${hashedPassword}`);
180
181 const rawResponse = await fetch(`${base}auth/login`, {
182 method: 'POST',
183 headers: {
184 Authorization: `Basic ${basicToken}`,
185 'User-Agent': userAgent,
186 },
187 });
188 const content = await rawResponse.json();
189
190 if (!content.message || content.message !== 'Successfully logged in') {
191 const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again';
192 return response.status(401).send(errorMessage);
193 }
194
195 // eslint-disable-next-line prefer-destructuring
196 token = content.token;
197 } catch (e) {
198 return response.status(401).send({
199 message: 'Cannot login to Franz',
200 error: e,
201 });
202 }
203
204 // Get user information
205 let userInf = false;
206 try {
207 userInf = await apiRequest(server, 'me', 'GET', token);
208 } catch (e) {
209 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`;
210 return response.status(401).send(errorMessage);
211 }
212 if (!userInf) {
213 const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later';
214 return response.status(401).send(errorMessage);
215 }
216
217 const serviceIdTranslation = {};
218
219 // Import services
220 try {
221 const services = await apiRequest(server, 'me/services', 'GET', token);
222
223 for (const service of services) {
224 // Get new, unused uuid
225 let serviceId;
226 do {
227 serviceId = uuid();
228 } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
229
230 await Service.create({ // eslint-disable-line no-await-in-loop
231 serviceId,
232 name: service.name,
233 recipeId: service.recipeId,
234 settings: JSON.stringify(service),
235 });
236
237 serviceIdTranslation[service.id] = serviceId;
238 }
239 } catch (e) {
240 const errorMessage = `Could not import your services into our system.\nError: ${e}`;
241 return response.status(401).send(errorMessage);
242 }
243
244 // Import workspaces
245 try {
246 const workspaces = await apiRequest(server, 'workspace', 'GET', token);
247
248 for (const workspace of workspaces) {
249 let workspaceId;
250 do {
251 workspaceId = uuid();
252 } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
253
254 const services = workspace.services.map(service => serviceIdTranslation[service]);
255
256 await Workspace.create({ // eslint-disable-line no-await-in-loop
257 workspaceId,
258 name: workspace.name,
259 order: workspace.order,
260 services: JSON.stringify(services),
261 data: JSON.stringify({}),
262 });
263 }
264 } catch (e) {
265 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`;
266 return response.status(401).send(errorMessage);
267 }
268
269 return response.send('Your account has been imported. You can now use your Franz account in Ferdi.');
270 }
271
272 // Account import/export
273 async export({
274 // eslint-disable-next-line no-unused-vars
275 auth,
276 response,
277 }) {
278 const services = (await Service.all()).toJSON();
279 const workspaces = (await Workspace.all()).toJSON();
280
281 const exportData = {
282 username: 'Ferdi',
283 mail: 'internal@getferdi.com',
284 services,
285 workspaces,
286 };
287
288 return response
289 .header('Content-Type', 'application/force-download')
290 .header('Content-disposition', 'attachment; filename=export.ferdi-data')
291 .send(exportData);
292 }
293
294 async importFerdi({
295 request,
296 response,
297 }) {
298 const validation = await validateAll(request.all(), {
299 file: 'required',
300 });
301 if (validation.fails()) {
302 return response.send(validation.messages());
303 }
304
305 let file;
306 try {
307 file = JSON.parse(request.input('file'));
308 } catch (e) {
309 return response.send('Could not import: Invalid file, could not read file');
310 }
311
312 if (!file || !file.services || !file.workspaces) {
313 return response.send('Could not import: Invalid file (2)');
314 }
315
316 const serviceIdTranslation = {};
317
318 // Import services
319 try {
320 for (const service of file.services) {
321 // Get new, unused uuid
322 let serviceId;
323 do {
324 serviceId = uuid();
325 } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
326
327 await Service.create({ // eslint-disable-line no-await-in-loop
328 serviceId,
329 name: service.name,
330 recipeId: service.recipeId,
331 settings: JSON.stringify(service.settings),
332 });
333
334 serviceIdTranslation[service.id] = serviceId;
335 }
336 } catch (e) {
337 const errorMessage = `Could not import your services into our system.\nError: ${e}`;
338 return response.send(errorMessage);
339 }
340
341 // Import workspaces
342 try {
343 for (const workspace of file.workspaces) {
344 let workspaceId;
345 do {
346 workspaceId = uuid();
347 } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
348
349 const services = (workspace.services && typeof (workspace.services) === 'object') ?
350 workspace.services.map((service) => serviceIdTranslation[service]) :
351 [];
352
353 await Workspace.create({ // eslint-disable-line no-await-in-loop
354 workspaceId,
355 name: workspace.name,
356 order: workspace.order,
357 services: JSON.stringify(services),
358 data: JSON.stringify(workspace.data),
359 });
360 }
361 } catch (e) {
362 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`;
363 return response.status(401).send(errorMessage);
364 }
365
366 return response.send('Your account has been imported.');
367 }
368}
369
370module.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;
diff --git a/src/internal-server/app/Exceptions/Handler.js b/src/internal-server/app/Exceptions/Handler.js
new file mode 100644
index 000000000..ab323fd38
--- /dev/null
+++ b/src/internal-server/app/Exceptions/Handler.js
@@ -0,0 +1,40 @@
1const BaseExceptionHandler = use('BaseExceptionHandler');
2
3/**
4 * This class handles all exceptions thrown during
5 * the HTTP request lifecycle.
6 *
7 * @class ExceptionHandler
8 */
9class ExceptionHandler extends BaseExceptionHandler {
10 /**
11 * Handle exception thrown during the HTTP lifecycle
12 *
13 * @method handle
14 *
15 * @param {Object} error
16 * @param {object} options.response
17 *
18 * @return {Promise<void>}
19 */
20 async handle(error, { response }) {
21 if (error.name === 'ValidationException') {
22 return response.status(400).send('Invalid arguments');
23 }
24
25 return response.status(error.status).send(error.message);
26 }
27
28 /**
29 * Report exception for logging or debugging.
30 *
31 * @method report
32 *
33 * @return {Promise<boolean>}
34 */
35 async report() {
36 return true;
37 }
38}
39
40module.exports = ExceptionHandler;
diff --git a/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js b/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js
new file mode 100644
index 000000000..87f1f6c25
--- /dev/null
+++ b/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js
@@ -0,0 +1,15 @@
1class ConvertEmptyStringsToNull {
2 async handle({ request }, next) {
3 if (Object.keys(request.body).length) {
4 request.body = Object.assign(
5 ...Object.keys(request.body).map(key => ({
6 [key]: request.body[key] !== '' ? request.body[key] : null,
7 })),
8 );
9 }
10
11 await next();
12 }
13}
14
15module.exports = ConvertEmptyStringsToNull;
diff --git a/src/internal-server/app/Models/Recipe.js b/src/internal-server/app/Models/Recipe.js
new file mode 100644
index 000000000..bd9741114
--- /dev/null
+++ b/src/internal-server/app/Models/Recipe.js
@@ -0,0 +1,7 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model');
3
4class Recipe extends Model {
5}
6
7module.exports = Recipe;
diff --git a/src/internal-server/app/Models/Service.js b/src/internal-server/app/Models/Service.js
new file mode 100644
index 000000000..a2e5c981e
--- /dev/null
+++ b/src/internal-server/app/Models/Service.js
@@ -0,0 +1,7 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model');
3
4class Service extends Model {
5}
6
7module.exports = Service;
diff --git a/src/internal-server/app/Models/Token.js b/src/internal-server/app/Models/Token.js
new file mode 100644
index 000000000..83e989117
--- /dev/null
+++ b/src/internal-server/app/Models/Token.js
@@ -0,0 +1,7 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model');
3
4class Token extends Model {
5}
6
7module.exports = Token;
diff --git a/src/internal-server/app/Models/Traits/NoTimestamp.js b/src/internal-server/app/Models/Traits/NoTimestamp.js
new file mode 100644
index 000000000..914f542f0
--- /dev/null
+++ b/src/internal-server/app/Models/Traits/NoTimestamp.js
@@ -0,0 +1,14 @@
1class NoTimestamp {
2 register(Model) {
3 Object.defineProperties(Model, {
4 createdAtColumn: {
5 get: () => null,
6 },
7 updatedAtColumn: {
8 get: () => null,
9 },
10 });
11 }
12}
13
14module.exports = NoTimestamp;
diff --git a/src/internal-server/app/Models/User.js b/src/internal-server/app/Models/User.js
new file mode 100644
index 000000000..907710d8d
--- /dev/null
+++ b/src/internal-server/app/Models/User.js
@@ -0,0 +1,8 @@
1// File is required by AdonisJS but not used by the server
2/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
3const Model = use('Model');
4
5class User extends Model {
6}
7
8module.exports = User;
diff --git a/src/internal-server/app/Models/Workspace.js b/src/internal-server/app/Models/Workspace.js
new file mode 100644
index 000000000..dcf39ac75
--- /dev/null
+++ b/src/internal-server/app/Models/Workspace.js
@@ -0,0 +1,7 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model');
3
4class Workspace extends Model {
5}
6
7module.exports = Workspace;