diff options
Diffstat (limited to 'src/internal-server/app')
-rw-r--r-- | src/internal-server/app/Controllers/Http/RecipeController.js | 120 | ||||
-rw-r--r-- | src/internal-server/app/Controllers/Http/ServiceController.js | 290 | ||||
-rw-r--r-- | src/internal-server/app/Controllers/Http/StaticController.js | 205 | ||||
-rw-r--r-- | src/internal-server/app/Controllers/Http/UserController.js | 367 | ||||
-rw-r--r-- | src/internal-server/app/Controllers/Http/WorkspaceController.js | 148 | ||||
-rw-r--r-- | src/internal-server/app/Exceptions/Handler.js | 44 | ||||
-rw-r--r-- | src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js | 15 | ||||
-rw-r--r-- | src/internal-server/app/Models/Recipe.js | 7 | ||||
-rw-r--r-- | src/internal-server/app/Models/Service.js | 7 | ||||
-rw-r--r-- | src/internal-server/app/Models/Token.js | 7 | ||||
-rw-r--r-- | src/internal-server/app/Models/Traits/NoTimestamp.js | 14 | ||||
-rw-r--r-- | src/internal-server/app/Models/User.js | 8 | ||||
-rw-r--r-- | src/internal-server/app/Models/Workspace.js | 7 |
13 files changed, 1239 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 @@ | |||
1 | const Recipe = use('App/Models/Recipe'); | ||
2 | const Drive = use('Drive'); | ||
3 | const { | ||
4 | validateAll, | ||
5 | } = use('Validator'); | ||
6 | const Env = use('Env'); | ||
7 | |||
8 | const fetch = require('node-fetch'); | ||
9 | |||
10 | const RECIPES_URL = 'https://api.getferdi.com/v1/recipes'; | ||
11 | |||
12 | class 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 | |||
120 | module.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 @@ | |||
1 | const Service = use('App/Models/Service'); | ||
2 | const { | ||
3 | validateAll, | ||
4 | } = use('Validator'); | ||
5 | const Env = use('Env'); | ||
6 | |||
7 | const uuid = require('uuid/v4'); | ||
8 | const path = require('path'); | ||
9 | const fs = require('fs-extra'); | ||
10 | |||
11 | class 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 | |||
290 | module.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 | |||
5 | class 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 | |||
205 | module.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 @@ | |||
1 | const User = use('App/Models/User'); | ||
2 | const Service = use('App/Models/Service'); | ||
3 | const Workspace = use('App/Models/Workspace'); | ||
4 | const { | ||
5 | validateAll, | ||
6 | } = use('Validator'); | ||
7 | |||
8 | const btoa = require('btoa'); | ||
9 | const fetch = require('node-fetch'); | ||
10 | const uuid = require('uuid/v4'); | ||
11 | const crypto = require('crypto'); | ||
12 | |||
13 | const 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 | |||
32 | class 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 | |||
367 | module.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 @@ | |||
1 | const Workspace = use('App/Models/Workspace'); | ||
2 | const { | ||
3 | validateAll, | ||
4 | } = use('Validator'); | ||
5 | |||
6 | const uuid = require('uuid/v4'); | ||
7 | |||
8 | class 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 | |||
148 | module.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..111ef4e0e --- /dev/null +++ b/src/internal-server/app/Exceptions/Handler.js | |||
@@ -0,0 +1,44 @@ | |||
1 | const BaseExceptionHandler = use('BaseExceptionHandler'); | ||
2 | |||
3 | /** | ||
4 | * This class handles all exceptions thrown during | ||
5 | * the HTTP request lifecycle. | ||
6 | * | ||
7 | * @class ExceptionHandler | ||
8 | */ | ||
9 | class 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.request | ||
17 | * @param {Object} options.response | ||
18 | * | ||
19 | * @return {void} | ||
20 | */ | ||
21 | async handle(error, { response }) { | ||
22 | if (error.name === 'ValidationException') { | ||
23 | return response.status(400).send('Invalid arguments'); | ||
24 | } | ||
25 | |||
26 | return response.status(error.status).send(error.message); | ||
27 | } | ||
28 | |||
29 | /** | ||
30 | * Report exception for logging or debugging. | ||
31 | * | ||
32 | * @method report | ||
33 | * | ||
34 | * @param {Object} error | ||
35 | * @param {Object} options.request | ||
36 | * | ||
37 | * @return {void} | ||
38 | */ | ||
39 | async report() { | ||
40 | return true; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | module.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 @@ | |||
1 | class 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 | |||
15 | module.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')} */ | ||
2 | const Model = use('Model'); | ||
3 | |||
4 | class Recipe extends Model { | ||
5 | } | ||
6 | |||
7 | module.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')} */ | ||
2 | const Model = use('Model'); | ||
3 | |||
4 | class Service extends Model { | ||
5 | } | ||
6 | |||
7 | module.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')} */ | ||
2 | const Model = use('Model'); | ||
3 | |||
4 | class Token extends Model { | ||
5 | } | ||
6 | |||
7 | module.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 @@ | |||
1 | class NoTimestamp { | ||
2 | register(Model) { | ||
3 | Object.defineProperties(Model, { | ||
4 | createdAtColumn: { | ||
5 | get: () => null, | ||
6 | }, | ||
7 | updatedAtColumn: { | ||
8 | get: () => null, | ||
9 | }, | ||
10 | }); | ||
11 | } | ||
12 | } | ||
13 | |||
14 | module.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')} */ | ||
3 | const Model = use('Model'); | ||
4 | |||
5 | class User extends Model { | ||
6 | } | ||
7 | |||
8 | module.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')} */ | ||
2 | const Model = use('Model'); | ||
3 | |||
4 | class Workspace extends Model { | ||
5 | } | ||
6 | |||
7 | module.exports = Workspace; | ||