diff options
author | 2019-10-14 10:40:40 +0200 | |
---|---|---|
committer | 2019-10-14 10:40:40 +0200 | |
commit | 42084627687c6308a26dca12243ab9969a433bc2 (patch) | |
tree | f051b104026b3b6548c5c4c896ce8b50e22aa812 /src/server | |
parent | Add local server to allow serverlesss usage (diff) | |
download | ferdium-app-42084627687c6308a26dca12243ab9969a433bc2.tar.gz ferdium-app-42084627687c6308a26dca12243ab9969a433bc2.tar.zst ferdium-app-42084627687c6308a26dca12243ab9969a433bc2.zip |
Add local server
Diffstat (limited to 'src/server')
48 files changed, 2983 insertions, 0 deletions
diff --git a/src/server/.editorconfig b/src/server/.editorconfig new file mode 100644 index 000000000..914223976 --- /dev/null +++ b/src/server/.editorconfig | |||
@@ -0,0 +1,13 @@ | |||
1 | # editorconfig.org | ||
2 | root = true | ||
3 | |||
4 | [*] | ||
5 | indent_size = 2 | ||
6 | indent_style = space | ||
7 | end_of_line = lf | ||
8 | charset = utf-8 | ||
9 | trim_trailing_whitespace = true | ||
10 | insert_final_newline = true | ||
11 | |||
12 | [*.md] | ||
13 | trim_trailing_whitespace = false | ||
diff --git a/src/server/.eslintrc.js b/src/server/.eslintrc.js new file mode 100644 index 000000000..d02f4890d --- /dev/null +++ b/src/server/.eslintrc.js | |||
@@ -0,0 +1,22 @@ | |||
1 | module.exports = { | ||
2 | env: { | ||
3 | commonjs: true, | ||
4 | es6: true, | ||
5 | node: true, | ||
6 | }, | ||
7 | extends: [ | ||
8 | 'airbnb-base', | ||
9 | ], | ||
10 | globals: { | ||
11 | Atomics: 'readonly', | ||
12 | SharedArrayBuffer: 'readonly', | ||
13 | use: 'readonly' | ||
14 | }, | ||
15 | parserOptions: { | ||
16 | ecmaVersion: 2018, | ||
17 | }, | ||
18 | rules: { | ||
19 | "class-methods-use-this": 'off', | ||
20 | "no-restricted-syntax": 'off', | ||
21 | }, | ||
22 | }; | ||
diff --git a/src/server/.gitattributes b/src/server/.gitattributes new file mode 100644 index 000000000..dfe077042 --- /dev/null +++ b/src/server/.gitattributes | |||
@@ -0,0 +1,2 @@ | |||
1 | # Auto detect text files and perform LF normalization | ||
2 | * text=auto | ||
diff --git a/src/server/.gitignore b/src/server/.gitignore new file mode 100644 index 000000000..d84ffadd4 --- /dev/null +++ b/src/server/.gitignore | |||
@@ -0,0 +1,19 @@ | |||
1 | # Node modules | ||
2 | node_modules | ||
3 | |||
4 | # Adonis directory for storing tmp files | ||
5 | tmp | ||
6 | |||
7 | # Environment variables, never commit this file | ||
8 | .env | ||
9 | |||
10 | # The development sqlite file | ||
11 | database/development.sqlite | ||
12 | database/adonis.sqlite | ||
13 | |||
14 | # Uploaded recipes | ||
15 | recipes/ | ||
16 | |||
17 | .DS_Store | ||
18 | public/terms.html | ||
19 | public/privacy.html | ||
diff --git a/src/server/README.md b/src/server/README.md new file mode 100644 index 000000000..833d9643e --- /dev/null +++ b/src/server/README.md | |||
@@ -0,0 +1,50 @@ | |||
1 | <p align="center"> | ||
2 | <img src="./logo.png" alt="" width="300"/> | ||
3 | </p> | ||
4 | |||
5 | # ferdi-internal-server | ||
6 | Internal Ferdi Server used for storing settings without logging into an external server. | ||
7 | |||
8 | npm i @adonisjs/ace @adonisjs/auth @adonisjs/bodyparser @adonisjs/cors @adonisjs/drive @adonisjs/fold @adonisjs/framework @adonisjs/ignitor @adonisjs/lucid @adonisjs/session @adonisjs/shield @adonisjs/validator atob btoa fs-extra node-fetch sqlite3 uuid targz | ||
9 | |||
10 | ### Manual setup | ||
11 | 1. Clone this repository | ||
12 | 2. Install the [AdonisJS CLI](https://adonisjs.com/) | ||
13 | 3. Run the database migrations with | ||
14 | ```js | ||
15 | adonis migration:run | ||
16 | ``` | ||
17 | 4. Start the server with | ||
18 | ```js | ||
19 | adonis serve --dev | ||
20 | ``` | ||
21 | |||
22 | ## Configuration | ||
23 | franz-server's configuration is saved inside the `.env` file. Besides AdonisJS's settings, ferdi-server has the following custom settings: | ||
24 | - `IS_CREATION_ENABLED` (`true` or `false`, default: `true`): Whether to enable the [creation of custom recipes](#creating-and-using-custom-recipes) | ||
25 | - `CONNECT_WITH_FRANZ` (`true` or `false`, default: `true`): Whether to enable connections to the Franz server. By enabling this option, ferdi-server can: | ||
26 | - Show the full Franz recipe library instead of only custom recipes | ||
27 | - Import Franz accounts | ||
28 | |||
29 | ## Importing your Franz account | ||
30 | ferdi-server allows you to import your full Franz account, including all its settings. | ||
31 | |||
32 | To import your Franz account, open `http://localhost:45569/import` in your browser and login using your Franz account details. ferdi-server will create a new user with the same credentials and copy your Franz settings, services and workspaces. | ||
33 | |||
34 | ## Creating and using custom recipes | ||
35 | ferdi-server allows to extends the Franz recipe catalogue with custom Ferdi recipes. | ||
36 | |||
37 | For documentation on how to create a recipe, please visit [the official guide by Franz](https://github.com/meetfranz/plugins/blob/master/docs/integration.md). | ||
38 | |||
39 | To add your recipe to ferdi-server, open `http://localhost:45569/new` in your browser. You can now define the following settings: | ||
40 | - `Author`: Author who created the recipe | ||
41 | - `Name`: Name for your new service. Can contain spaces and unicode characters | ||
42 | - `Service ID`: Unique ID for this recipe. Does not contain spaces or special characters (e.g. `google-drive`) | ||
43 | - `Link to PNG/SVG image`: Direct link to a 1024x1024 PNG image and SVG that is used as a logo inside the store. Please use jsDelivr when using a file uploaded to GitHub as raw.githubusercontent files won't load | ||
44 | - `Recipe files`: Recipe files that you created using the [Franz recipe creation guide](https://github.com/meetfranz/plugins/blob/master/docs/integration.md). Please do *not* package your files beforehand - upload the raw files (you can drag and drop multiple files). ferdi-server will automatically package and store the recipe in the right format. Please also do not drag and drop or select the whole folder, select the individual files. | ||
45 | |||
46 | ### Listing custom recipes | ||
47 | Inside Ferdi, searching for `ferdi:custom` will list all your custom recipes. | ||
48 | |||
49 | ## License | ||
50 | ferdi-server is licensed under the MIT License | ||
diff --git a/src/server/ace b/src/server/ace new file mode 100644 index 000000000..42f8f10d1 --- /dev/null +++ b/src/server/ace | |||
@@ -0,0 +1,21 @@ | |||
1 | 'use strict' | ||
2 | |||
3 | /* | ||
4 | |-------------------------------------------------------------------------- | ||
5 | | Ace Commands | ||
6 | |-------------------------------------------------------------------------- | ||
7 | | | ||
8 | | The ace file is just a regular Javascript file but with no extension. You | ||
9 | | can call `node ace` followed by the command name and it just works. | ||
10 | | | ||
11 | | Also you can use `adonis` followed by the command name, since the adonis | ||
12 | | global proxies all the ace commands. | ||
13 | | | ||
14 | */ | ||
15 | |||
16 | const { Ignitor } = require('@adonisjs/ignitor') | ||
17 | |||
18 | new Ignitor(require('@adonisjs/fold')) | ||
19 | .appRoot(__dirname) | ||
20 | .fireAce() | ||
21 | .catch(console.error) | ||
diff --git a/src/server/app/Controllers/Http/DashboardController.js b/src/server/app/Controllers/Http/DashboardController.js new file mode 100644 index 000000000..69af16227 --- /dev/null +++ b/src/server/app/Controllers/Http/DashboardController.js | |||
@@ -0,0 +1,38 @@ | |||
1 | class DashboardController { | ||
2 | async data({ | ||
3 | auth, | ||
4 | view, | ||
5 | }) { | ||
6 | const general = auth.user; | ||
7 | const services = (await auth.user.services().fetch()).toJSON(); | ||
8 | const workspaces = (await auth.user.workspaces().fetch()).toJSON(); | ||
9 | |||
10 | return view.render('dashboard.data', { | ||
11 | username: general.username, | ||
12 | mail: general.email, | ||
13 | created: general.created_at, | ||
14 | updated: general.updated_at, | ||
15 | services, | ||
16 | workspaces, | ||
17 | }); | ||
18 | } | ||
19 | |||
20 | logout({ | ||
21 | auth, | ||
22 | response, | ||
23 | }) { | ||
24 | auth.authenticator('session').logout(); | ||
25 | return response.redirect('/user/login'); | ||
26 | } | ||
27 | |||
28 | delete({ | ||
29 | auth, | ||
30 | response, | ||
31 | }) { | ||
32 | auth.user.delete(); | ||
33 | auth.authenticator('session').logout(); | ||
34 | return response.redirect('/user/login'); | ||
35 | } | ||
36 | } | ||
37 | |||
38 | module.exports = DashboardController; | ||
diff --git a/src/server/app/Controllers/Http/RecipeController.js b/src/server/app/Controllers/Http/RecipeController.js new file mode 100644 index 000000000..5ed21122a --- /dev/null +++ b/src/server/app/Controllers/Http/RecipeController.js | |||
@@ -0,0 +1,207 @@ | |||
1 | |||
2 | const Recipe = use('App/Models/Recipe'); | ||
3 | const Helpers = use('Helpers'); | ||
4 | const Drive = use('Drive'); | ||
5 | const { | ||
6 | validateAll, | ||
7 | } = use('Validator'); | ||
8 | const Env = use('Env'); | ||
9 | |||
10 | const fetch = require('node-fetch'); | ||
11 | const targz = require('targz'); | ||
12 | const path = require('path'); | ||
13 | const fs = require('fs-extra'); | ||
14 | |||
15 | const compress = (src, dest) => new Promise((resolve, reject) => { | ||
16 | targz.compress({ | ||
17 | src, | ||
18 | dest, | ||
19 | }, (err) => { | ||
20 | if (err) { | ||
21 | reject(err); | ||
22 | } else { | ||
23 | resolve(dest); | ||
24 | } | ||
25 | }); | ||
26 | }); | ||
27 | |||
28 | class RecipeController { | ||
29 | // List official and custom recipes | ||
30 | async list({ | ||
31 | response, | ||
32 | }) { | ||
33 | const officialRecipes = JSON.parse(await (await fetch('https://api.franzinfra.com/v1/recipes')).text()); | ||
34 | const customRecipesArray = (await Recipe.all()).rows; | ||
35 | const customRecipes = customRecipesArray.map(recipe => ({ | ||
36 | id: recipe.recipeId, | ||
37 | name: recipe.name, | ||
38 | ...JSON.parse(recipe.data), | ||
39 | })); | ||
40 | |||
41 | const recipes = [ | ||
42 | ...officialRecipes, | ||
43 | ...customRecipes, | ||
44 | ]; | ||
45 | |||
46 | return response.send(recipes); | ||
47 | } | ||
48 | |||
49 | // Create a new recipe using the new.html page | ||
50 | async create({ | ||
51 | request, | ||
52 | response, | ||
53 | }) { | ||
54 | // Check if recipe creation is enabled | ||
55 | if (Env.get('IS_CREATION_ENABLED') == 'false') { // eslint-disable-line eqeqeq | ||
56 | return response.send('This server doesn\'t allow the creation of new recipes.'); | ||
57 | } | ||
58 | |||
59 | // Validate user input | ||
60 | const validation = await validateAll(request.all(), { | ||
61 | name: 'required|string', | ||
62 | id: 'required|unique:recipes,recipeId', | ||
63 | author: 'required|accepted', | ||
64 | png: 'required|url', | ||
65 | svg: 'required|url', | ||
66 | }); | ||
67 | if (validation.fails()) { | ||
68 | return response.status(401).send({ | ||
69 | message: 'Invalid POST arguments', | ||
70 | messages: validation.messages(), | ||
71 | status: 401, | ||
72 | }); | ||
73 | } | ||
74 | |||
75 | const data = request.all(); | ||
76 | |||
77 | if (!data.id) { | ||
78 | return response.send('Please provide an ID'); | ||
79 | } | ||
80 | |||
81 | // Check for invalid characters | ||
82 | if (/\.{1,}/.test(data.id) || /\/{1,}/.test(data.id)) { | ||
83 | return response.send('Invalid recipe name. Your recipe name may not contain "." or "/"'); | ||
84 | } | ||
85 | |||
86 | // Clear temporary recipe folder | ||
87 | await fs.emptyDir(Helpers.tmpPath('recipe')); | ||
88 | |||
89 | // Move uploaded files to temporary path | ||
90 | const files = request.file('files'); | ||
91 | await files.moveAll(Helpers.tmpPath('recipe')); | ||
92 | |||
93 | // Compress files to .tar.gz file | ||
94 | const source = Helpers.tmpPath('recipe'); | ||
95 | const destination = path.join(Helpers.appRoot(), `/recipes/${data.id}.tar.gz`); | ||
96 | |||
97 | compress( | ||
98 | source, | ||
99 | destination, | ||
100 | ); | ||
101 | |||
102 | // Create recipe in db | ||
103 | await Recipe.create({ | ||
104 | name: data.name, | ||
105 | recipeId: data.id, | ||
106 | data: JSON.stringify({ | ||
107 | author: data.author, | ||
108 | featured: false, | ||
109 | version: '1.0.0', | ||
110 | icons: { | ||
111 | png: data.png, | ||
112 | svg: data.svg, | ||
113 | }, | ||
114 | }), | ||
115 | }); | ||
116 | |||
117 | return response.send('Created new recipe'); | ||
118 | } | ||
119 | |||
120 | // Search official and custom recipes | ||
121 | async search({ | ||
122 | request, | ||
123 | response, | ||
124 | }) { | ||
125 | // Validate user input | ||
126 | const validation = await validateAll(request.all(), { | ||
127 | needle: 'required', | ||
128 | }); | ||
129 | if (validation.fails()) { | ||
130 | return response.status(401).send({ | ||
131 | message: 'Please provide a needle', | ||
132 | messages: validation.messages(), | ||
133 | status: 401, | ||
134 | }); | ||
135 | } | ||
136 | |||
137 | const needle = request.input('needle'); | ||
138 | |||
139 | // Get results | ||
140 | let results; | ||
141 | |||
142 | if (needle === 'ferdi:custom') { | ||
143 | const dbResults = (await Recipe.all()).toJSON(); | ||
144 | results = dbResults.map(recipe => ({ | ||
145 | id: recipe.recipeId, | ||
146 | name: recipe.name, | ||
147 | ...JSON.parse(recipe.data), | ||
148 | })); | ||
149 | } else { | ||
150 | let remoteResults = []; | ||
151 | if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq | ||
152 | remoteResults = JSON.parse(await (await fetch(`https://api.franzinfra.com/v1/recipes/search?needle=${encodeURIComponent(needle)}`)).text()); | ||
153 | } | ||
154 | const localResultsArray = (await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()).toJSON(); | ||
155 | const localResults = localResultsArray.map(recipe => ({ | ||
156 | id: recipe.recipeId, | ||
157 | name: recipe.name, | ||
158 | ...JSON.parse(recipe.data), | ||
159 | })); | ||
160 | |||
161 | results = [ | ||
162 | ...localResults, | ||
163 | ...remoteResults || [], | ||
164 | ]; | ||
165 | } | ||
166 | |||
167 | return response.send(results); | ||
168 | } | ||
169 | |||
170 | // Download a recipe | ||
171 | async download({ | ||
172 | response, | ||
173 | params, | ||
174 | }) { | ||
175 | // Validate user input | ||
176 | const validation = await validateAll(params, { | ||
177 | recipe: 'required|accepted', | ||
178 | }); | ||
179 | if (validation.fails()) { | ||
180 | return response.status(401).send({ | ||
181 | message: 'Please provide a recipe ID', | ||
182 | messages: validation.messages(), | ||
183 | status: 401, | ||
184 | }); | ||
185 | } | ||
186 | |||
187 | const service = params.recipe; | ||
188 | |||
189 | // Check for invalid characters | ||
190 | if (/\.{1,}/.test(service) || /\/{1,}/.test(service)) { | ||
191 | return response.send('Invalid recipe name'); | ||
192 | } | ||
193 | |||
194 | // Check if recipe exists in recipes folder | ||
195 | if (await Drive.exists(`${service}.tar.gz`)) { | ||
196 | return response.send(await Drive.get(`${service}.tar.gz`)); | ||
197 | } if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq | ||
198 | return response.redirect(`https://api.franzinfra.com/v1/recipes/download/${service}`); | ||
199 | } | ||
200 | return response.status(400).send({ | ||
201 | message: 'Recipe not found', | ||
202 | code: 'recipe-not-found', | ||
203 | }); | ||
204 | } | ||
205 | } | ||
206 | |||
207 | module.exports = RecipeController; | ||
diff --git a/src/server/app/Controllers/Http/ServiceController.js b/src/server/app/Controllers/Http/ServiceController.js new file mode 100644 index 000000000..ea7035ca1 --- /dev/null +++ b/src/server/app/Controllers/Http/ServiceController.js | |||
@@ -0,0 +1,211 @@ | |||
1 | const Service = use('App/Models/Service'); | ||
2 | const { | ||
3 | validateAll, | ||
4 | } = use('Validator'); | ||
5 | |||
6 | const uuid = require('uuid/v4'); | ||
7 | |||
8 | class ServiceController { | ||
9 | // Create a new service for user | ||
10 | async create({ | ||
11 | request, | ||
12 | response, | ||
13 | }) { | ||
14 | // Validate user input | ||
15 | const validation = await validateAll(request.all(), { | ||
16 | name: 'required|string', | ||
17 | recipeId: 'required', | ||
18 | }); | ||
19 | if (validation.fails()) { | ||
20 | return response.status(401).send({ | ||
21 | message: 'Invalid POST arguments', | ||
22 | messages: validation.messages(), | ||
23 | status: 401, | ||
24 | }); | ||
25 | } | ||
26 | |||
27 | const data = request.all(); | ||
28 | |||
29 | // Get new, unused uuid | ||
30 | let serviceId; | ||
31 | do { | ||
32 | serviceId = uuid(); | ||
33 | } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
34 | |||
35 | await Service.create({ | ||
36 | serviceId, | ||
37 | name: data.name, | ||
38 | recipeId: data.recipeId, | ||
39 | settings: JSON.stringify(data), | ||
40 | }); | ||
41 | |||
42 | return response.send({ | ||
43 | data: { | ||
44 | userId: 1, | ||
45 | id: serviceId, | ||
46 | isEnabled: true, | ||
47 | isNotificationEnabled: true, | ||
48 | isBadgeEnabled: true, | ||
49 | isMuted: false, | ||
50 | isDarkModeEnabled: '', | ||
51 | spellcheckerLanguage: '', | ||
52 | order: 1, | ||
53 | customRecipe: false, | ||
54 | hasCustomIcon: false, | ||
55 | workspaces: [], | ||
56 | iconUrl: null, | ||
57 | ...data, | ||
58 | }, | ||
59 | status: ['created'], | ||
60 | }); | ||
61 | } | ||
62 | |||
63 | // List all services a user has created | ||
64 | async list({ | ||
65 | response, | ||
66 | }) { | ||
67 | const services = (await Service.all()).rows; | ||
68 | // Convert to array with all data Franz wants | ||
69 | const servicesArray = services.map(service => ({ | ||
70 | customRecipe: false, | ||
71 | hasCustomIcon: false, | ||
72 | isBadgeEnabled: true, | ||
73 | isDarkModeEnabled: '', | ||
74 | isEnabled: true, | ||
75 | isMuted: false, | ||
76 | isNotificationEnabled: true, | ||
77 | order: 1, | ||
78 | spellcheckerLanguage: '', | ||
79 | workspaces: [], | ||
80 | iconUrl: null, | ||
81 | ...JSON.parse(service.settings), | ||
82 | id: service.serviceId, | ||
83 | name: service.name, | ||
84 | recipeId: service.recipeId, | ||
85 | userId: 1, | ||
86 | })); | ||
87 | |||
88 | return response.send(servicesArray); | ||
89 | } | ||
90 | |||
91 | async edit({ | ||
92 | request, | ||
93 | response, | ||
94 | params, | ||
95 | }) { | ||
96 | // Validate user input | ||
97 | const validation = await validateAll(request.all(), { | ||
98 | name: 'required', | ||
99 | }); | ||
100 | if (validation.fails()) { | ||
101 | return response.status(401).send({ | ||
102 | message: 'Invalid POST arguments', | ||
103 | messages: validation.messages(), | ||
104 | status: 401, | ||
105 | }); | ||
106 | } | ||
107 | |||
108 | const data = request.all(); | ||
109 | const { | ||
110 | id, | ||
111 | } = params; | ||
112 | |||
113 | // Get current settings from db | ||
114 | const serviceData = (await Service.query() | ||
115 | .where('serviceId', id).fetch()).rows[0]; | ||
116 | |||
117 | const settings = { | ||
118 | ...JSON.parse(serviceData.settings), | ||
119 | ...data, | ||
120 | }; | ||
121 | |||
122 | // Update data in database | ||
123 | await (Service.query() | ||
124 | .where('serviceId', id)).update({ | ||
125 | name: data.name, | ||
126 | settings: JSON.stringify(settings), | ||
127 | }); | ||
128 | |||
129 | // Get updated row | ||
130 | const service = (await Service.query() | ||
131 | .where('serviceId', id).fetch()).rows[0]; | ||
132 | |||
133 | return response.send({ | ||
134 | id: service.serviceId, | ||
135 | name: data.name, | ||
136 | ...settings, | ||
137 | userId: 1, | ||
138 | }); | ||
139 | } | ||
140 | |||
141 | async reorder({ | ||
142 | request, | ||
143 | response, | ||
144 | }) { | ||
145 | const data = request.all(); | ||
146 | |||
147 | for (const service of Object.keys(data)) { | ||
148 | // Get current settings from db | ||
149 | const serviceData = (await Service.query() // eslint-disable-line no-await-in-loop | ||
150 | .where('serviceId', service).fetch()).rows[0]; | ||
151 | |||
152 | const settings = { | ||
153 | ...JSON.parse(serviceData.settings), | ||
154 | order: data[service], | ||
155 | }; | ||
156 | |||
157 | // Update data in database | ||
158 | await (Service.query() // eslint-disable-line no-await-in-loop | ||
159 | .where('serviceId', service)) | ||
160 | .update({ | ||
161 | settings: JSON.stringify(settings), | ||
162 | }); | ||
163 | } | ||
164 | |||
165 | // Get new services | ||
166 | const services = (await Service.all()).rows; | ||
167 | // Convert to array with all data Franz wants | ||
168 | const servicesArray = services.map(service => ({ | ||
169 | customRecipe: false, | ||
170 | hasCustomIcon: false, | ||
171 | isBadgeEnabled: true, | ||
172 | isDarkModeEnabled: '', | ||
173 | isEnabled: true, | ||
174 | isMuted: false, | ||
175 | isNotificationEnabled: true, | ||
176 | order: 1, | ||
177 | spellcheckerLanguage: '', | ||
178 | workspaces: [], | ||
179 | iconUrl: null, | ||
180 | ...JSON.parse(service.settings), | ||
181 | id: service.serviceId, | ||
182 | name: service.name, | ||
183 | recipeId: service.recipeId, | ||
184 | userId: 1, | ||
185 | })); | ||
186 | |||
187 | return response.send(servicesArray); | ||
188 | } | ||
189 | |||
190 | update({ | ||
191 | response, | ||
192 | }) { | ||
193 | return response.send([]); | ||
194 | } | ||
195 | |||
196 | async delete({ | ||
197 | params, | ||
198 | response, | ||
199 | }) { | ||
200 | // Update data in database | ||
201 | await (Service.query() | ||
202 | .where('serviceId', params.id)).delete(); | ||
203 | |||
204 | return response.send({ | ||
205 | message: 'Sucessfully deleted service', | ||
206 | status: 200, | ||
207 | }); | ||
208 | } | ||
209 | } | ||
210 | |||
211 | module.exports = ServiceController; | ||
diff --git a/src/server/app/Controllers/Http/StaticController.js b/src/server/app/Controllers/Http/StaticController.js new file mode 100644 index 000000000..b16e6cb6d --- /dev/null +++ b/src/server/app/Controllers/Http/StaticController.js | |||
@@ -0,0 +1,224 @@ | |||
1 | |||
2 | /** | ||
3 | * Controller for routes with static responses | ||
4 | */ | ||
5 | |||
6 | class StaticController { | ||
7 | // Enable all features | ||
8 | features({ | ||
9 | response, | ||
10 | }) { | ||
11 | return response.send({ | ||
12 | needToWaitToProceed: false, | ||
13 | isSpellcheckerPremiumFeature: true, | ||
14 | isServiceProxyEnabled: true, | ||
15 | isServiceProxyPremiumFeature: true, | ||
16 | isWorkspacePremiumFeature: true, | ||
17 | isWorkspaceEnabled: true, | ||
18 | isAnnouncementsEnabled: true, | ||
19 | isSettingsWSEnabled: false, | ||
20 | isServiceLimitEnabled: false, | ||
21 | serviceLimitCount: 0, | ||
22 | isCommunityRecipesPremiumFeature: false, | ||
23 | }); | ||
24 | } | ||
25 | |||
26 | // Return an empty array | ||
27 | emptyArray({ | ||
28 | response, | ||
29 | }) { | ||
30 | return response.send([]); | ||
31 | } | ||
32 | |||
33 | // Payment plans availible | ||
34 | plans({ | ||
35 | response, | ||
36 | }) { | ||
37 | return response.send({ | ||
38 | month: { | ||
39 | id: 'franz-supporter-license', | ||
40 | price: 99, | ||
41 | }, | ||
42 | year: { | ||
43 | id: 'franz-supporter-license-year-2019', | ||
44 | price: 99, | ||
45 | }, | ||
46 | }); | ||
47 | } | ||
48 | |||
49 | // Return list of popular recipes (copy of the response Franz's API is returning) | ||
50 | popularRecipes({ | ||
51 | response, | ||
52 | }) { | ||
53 | return response.send([{ | ||
54 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
55 | featured: false, | ||
56 | id: 'slack', | ||
57 | name: 'Slack', | ||
58 | version: '1.0.4', | ||
59 | icons: { | ||
60 | png: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.png', | ||
61 | svg: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.svg', | ||
62 | }, | ||
63 | }, { | ||
64 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
65 | featured: false, | ||
66 | id: 'whatsapp', | ||
67 | name: 'WhatsApp', | ||
68 | version: '1.0.1', | ||
69 | icons: { | ||
70 | png: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.png', | ||
71 | svg: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.svg', | ||
72 | }, | ||
73 | }, { | ||
74 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
75 | featured: false, | ||
76 | id: 'messenger', | ||
77 | name: 'Messenger', | ||
78 | version: '1.0.6', | ||
79 | icons: { | ||
80 | png: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.png', | ||
81 | svg: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.svg', | ||
82 | }, | ||
83 | }, { | ||
84 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
85 | featured: false, | ||
86 | id: 'telegram', | ||
87 | name: 'Telegram', | ||
88 | version: '1.0.0', | ||
89 | icons: { | ||
90 | png: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.png', | ||
91 | svg: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.svg', | ||
92 | }, | ||
93 | }, { | ||
94 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
95 | featured: false, | ||
96 | id: 'gmail', | ||
97 | name: 'Gmail', | ||
98 | version: '1.0.0', | ||
99 | icons: { | ||
100 | png: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.png', | ||
101 | svg: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.svg', | ||
102 | }, | ||
103 | }, { | ||
104 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
105 | featured: false, | ||
106 | id: 'skype', | ||
107 | name: 'Skype', | ||
108 | version: '1.0.0', | ||
109 | icons: { | ||
110 | png: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.png', | ||
111 | svg: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.svg', | ||
112 | }, | ||
113 | }, { | ||
114 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
115 | featured: false, | ||
116 | id: 'hangouts', | ||
117 | name: 'Hangouts', | ||
118 | version: '1.0.0', | ||
119 | icons: { | ||
120 | png: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.png', | ||
121 | svg: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.svg', | ||
122 | }, | ||
123 | }, { | ||
124 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
125 | featured: false, | ||
126 | id: 'discord', | ||
127 | name: 'Discord', | ||
128 | version: '1.0.0', | ||
129 | icons: { | ||
130 | png: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.png', | ||
131 | svg: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.svg', | ||
132 | }, | ||
133 | }, { | ||
134 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
135 | featured: false, | ||
136 | id: 'tweetdeck', | ||
137 | name: 'Tweetdeck', | ||
138 | version: '1.0.1', | ||
139 | icons: { | ||
140 | png: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.png', | ||
141 | svg: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.svg', | ||
142 | }, | ||
143 | }, { | ||
144 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
145 | featured: false, | ||
146 | id: 'hipchat', | ||
147 | name: 'HipChat', | ||
148 | version: '1.0.1', | ||
149 | icons: { | ||
150 | png: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.png', | ||
151 | svg: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.svg', | ||
152 | }, | ||
153 | }, { | ||
154 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
155 | featured: false, | ||
156 | id: 'gmailinbox', | ||
157 | name: 'Inbox by Gmail', | ||
158 | version: '1.0.0', | ||
159 | icons: { | ||
160 | png: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.png', | ||
161 | svg: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.svg', | ||
162 | }, | ||
163 | }, { | ||
164 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
165 | featured: false, | ||
166 | id: 'rocketchat', | ||
167 | name: 'Rocket.Chat', | ||
168 | version: '1.0.1', | ||
169 | icons: { | ||
170 | png: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.png', | ||
171 | svg: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.svg', | ||
172 | }, | ||
173 | }, { | ||
174 | author: 'Brian Gilbert <brian@briangilbert.net>', | ||
175 | featured: false, | ||
176 | id: 'gitter', | ||
177 | name: 'Gitter', | ||
178 | version: '1.0.0', | ||
179 | icons: { | ||
180 | png: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.png', | ||
181 | svg: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.svg', | ||
182 | }, | ||
183 | }, { | ||
184 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
185 | featured: false, | ||
186 | id: 'mattermost', | ||
187 | name: 'Mattermost', | ||
188 | version: '1.0.0', | ||
189 | icons: { | ||
190 | png: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.png', | ||
191 | svg: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.svg', | ||
192 | }, | ||
193 | }, { | ||
194 | author: 'Franz <recipe@meetfranz.com>', | ||
195 | featured: false, | ||
196 | id: 'toggl', | ||
197 | name: 'toggl', | ||
198 | version: '1.0.0', | ||
199 | icons: { | ||
200 | png: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.png', | ||
201 | svg: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.svg', | ||
202 | }, | ||
203 | }, { | ||
204 | author: 'Stuart Clark <stuart@realityloop.com>', | ||
205 | featured: false, | ||
206 | id: 'twist', | ||
207 | name: 'twist', | ||
208 | version: '1.0.0', | ||
209 | icons: { | ||
210 | png: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.png', | ||
211 | svg: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.svg', | ||
212 | }, | ||
213 | }]); | ||
214 | } | ||
215 | |||
216 | // Show announcements | ||
217 | announcement({ | ||
218 | response, | ||
219 | }) { | ||
220 | return response.send('No announcement found.'); | ||
221 | } | ||
222 | } | ||
223 | |||
224 | module.exports = StaticController; | ||
diff --git a/src/server/app/Controllers/Http/UserController.js b/src/server/app/Controllers/Http/UserController.js new file mode 100644 index 000000000..758250ec9 --- /dev/null +++ b/src/server/app/Controllers/Http/UserController.js | |||
@@ -0,0 +1,231 @@ | |||
1 | const Service = use('App/Models/Service'); | ||
2 | const Workspace = use('App/Models/Workspace'); | ||
3 | const { | ||
4 | validateAll, | ||
5 | } = use('Validator'); | ||
6 | const Env = use('Env'); | ||
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 franzRequest = (route, method, auth) => new Promise((resolve, reject) => { | ||
14 | const base = 'https://api.franzinfra.com/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|unique:users,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 | return response.send({ | ||
81 | accountType: 'individual', | ||
82 | beta: false, | ||
83 | donor: {}, | ||
84 | email: '', | ||
85 | emailValidated: true, | ||
86 | features: {}, | ||
87 | firstname: 'Ferdi', | ||
88 | id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8', | ||
89 | isPremium: true, | ||
90 | isSubscriptionOwner: true, | ||
91 | lastname: 'Application', | ||
92 | locale: 'en-US', | ||
93 | }); | ||
94 | } | ||
95 | |||
96 | |||
97 | async import({ | ||
98 | request, | ||
99 | response, | ||
100 | }) { | ||
101 | // Validate user input | ||
102 | const validation = await validateAll(request.all(), { | ||
103 | email: 'required|email|unique:users,email', | ||
104 | password: 'required', | ||
105 | }); | ||
106 | if (validation.fails()) { | ||
107 | let errorMessage = 'There was an error while trying to import your account:\n'; | ||
108 | for (const message of validation.messages()) { | ||
109 | if (message.validation === 'required') { | ||
110 | errorMessage += `- Please make sure to supply your ${message.field}\n`; | ||
111 | } else if (message.validation === 'unique') { | ||
112 | errorMessage += '- There is already a user with this email.\n'; | ||
113 | } else { | ||
114 | errorMessage += `${message.message}\n`; | ||
115 | } | ||
116 | } | ||
117 | return response.status(401).send(errorMessage); | ||
118 | } | ||
119 | |||
120 | const { | ||
121 | email, | ||
122 | password, | ||
123 | } = request.all(); | ||
124 | |||
125 | const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); | ||
126 | |||
127 | if (Env.get('CONNECT_WITH_FRANZ') == 'false') { // eslint-disable-line eqeqeq | ||
128 | return response.send('Your account has been created but due to this server\'s configuration, we could not import your Franz account data.\n\nIf you are the server owner, please set CONNECT_WITH_FRANZ to true to enable account imports.'); | ||
129 | } | ||
130 | |||
131 | const base = 'https://api.franzinfra.com/v1/'; | ||
132 | 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'; | ||
133 | |||
134 | // Try to get an authentication token | ||
135 | let token; | ||
136 | try { | ||
137 | const basicToken = btoa(`${email}:${hashedPassword}`); | ||
138 | |||
139 | const rawResponse = await fetch(`${base}auth/login`, { | ||
140 | method: 'POST', | ||
141 | headers: { | ||
142 | Authorization: `Basic ${basicToken}`, | ||
143 | 'User-Agent': userAgent, | ||
144 | }, | ||
145 | }); | ||
146 | const content = await rawResponse.json(); | ||
147 | |||
148 | if (!content.message || content.message !== 'Successfully logged in') { | ||
149 | const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again'; | ||
150 | return response.status(401).send(errorMessage); | ||
151 | } | ||
152 | |||
153 | // eslint-disable-next-line prefer-destructuring | ||
154 | token = content.token; | ||
155 | } catch (e) { | ||
156 | return response.status(401).send({ | ||
157 | message: 'Cannot login to Franz', | ||
158 | error: e, | ||
159 | }); | ||
160 | } | ||
161 | |||
162 | // Get user information | ||
163 | let userInf = false; | ||
164 | try { | ||
165 | userInf = await franzRequest('me', 'GET', token); | ||
166 | } catch (e) { | ||
167 | const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`; | ||
168 | return response.status(401).send(errorMessage); | ||
169 | } | ||
170 | if (!userInf) { | ||
171 | const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later'; | ||
172 | return response.status(401).send(errorMessage); | ||
173 | } | ||
174 | |||
175 | const serviceIdTranslation = {}; | ||
176 | |||
177 | // Import services | ||
178 | try { | ||
179 | const services = await franzRequest('me/services', 'GET', token); | ||
180 | |||
181 | for (const service of services) { | ||
182 | // Get new, unused uuid | ||
183 | let serviceId; | ||
184 | do { | ||
185 | serviceId = uuid(); | ||
186 | } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
187 | |||
188 | await Service.create({ // eslint-disable-line no-await-in-loop | ||
189 | serviceId, | ||
190 | name: service.name, | ||
191 | recipeId: service.recipeId, | ||
192 | settings: JSON.stringify(service), | ||
193 | }); | ||
194 | |||
195 | serviceIdTranslation[service.id] = serviceId; | ||
196 | } | ||
197 | } catch (e) { | ||
198 | const errorMessage = `Could not import your services into our system.\nError: ${e}`; | ||
199 | return response.status(401).send(errorMessage); | ||
200 | } | ||
201 | |||
202 | // Import workspaces | ||
203 | try { | ||
204 | const workspaces = await franzRequest('workspace', 'GET', token); | ||
205 | |||
206 | for (const workspace of workspaces) { | ||
207 | let workspaceId; | ||
208 | do { | ||
209 | workspaceId = uuid(); | ||
210 | } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
211 | |||
212 | const services = workspace.services.map(service => serviceIdTranslation[service]); | ||
213 | |||
214 | await Workspace.create({ // eslint-disable-line no-await-in-loop | ||
215 | workspaceId, | ||
216 | name: workspace.name, | ||
217 | order: workspace.order, | ||
218 | services: JSON.stringify(services), | ||
219 | data: JSON.stringify({}), | ||
220 | }); | ||
221 | } | ||
222 | } catch (e) { | ||
223 | const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`; | ||
224 | return response.status(401).send(errorMessage); | ||
225 | } | ||
226 | |||
227 | return response.send('Your account has been imported. You can now use your Franz account in Ferdi.'); | ||
228 | } | ||
229 | } | ||
230 | |||
231 | module.exports = UserController; | ||
diff --git a/src/server/app/Controllers/Http/WorkspaceController.js b/src/server/app/Controllers/Http/WorkspaceController.js new file mode 100644 index 000000000..7990b8434 --- /dev/null +++ b/src/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|alpha', | ||
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|alpha', | ||
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 | request, | ||
98 | response, | ||
99 | params, | ||
100 | }) { | ||
101 | // Validate user input | ||
102 | const validation = await validateAll(request.all(), { | ||
103 | id: 'required', | ||
104 | }); | ||
105 | if (validation.fails()) { | ||
106 | return response.status(401).send({ | ||
107 | message: 'Invalid POST arguments', | ||
108 | messages: validation.messages(), | ||
109 | status: 401, | ||
110 | }); | ||
111 | } | ||
112 | |||
113 | const { | ||
114 | id, | ||
115 | } = params; | ||
116 | |||
117 | // Update data in database | ||
118 | await (Workspace.query() | ||
119 | .where('workspaceId', id)).delete(); | ||
120 | |||
121 | return response.send({ | ||
122 | message: 'Successfully deleted workspace', | ||
123 | }); | ||
124 | } | ||
125 | |||
126 | // List all workspaces a user has created | ||
127 | async list({ | ||
128 | response, | ||
129 | }) { | ||
130 | const workspaces = (await Workspace.all()).rows; | ||
131 | // Convert to array with all data Franz wants | ||
132 | let workspacesArray = []; | ||
133 | if (workspaces) { | ||
134 | workspacesArray = workspaces.map(workspace => ({ | ||
135 | id: workspace.workspaceId, | ||
136 | name: workspace.name, | ||
137 | order: workspace.order, | ||
138 | services: JSON.parse(workspace.services), | ||
139 | userId: 1, | ||
140 | })); | ||
141 | } | ||
142 | |||
143 | |||
144 | return response.send(workspacesArray); | ||
145 | } | ||
146 | } | ||
147 | |||
148 | module.exports = WorkspaceController; | ||
diff --git a/src/server/app/Exceptions/Handler.js b/src/server/app/Exceptions/Handler.js new file mode 100644 index 000000000..cb9e10bbe --- /dev/null +++ b/src/server/app/Exceptions/Handler.js | |||
@@ -0,0 +1,47 @@ | |||
1 | |||
2 | const BaseExceptionHandler = use('BaseExceptionHandler'); | ||
3 | |||
4 | /** | ||
5 | * This class handles all exceptions thrown during | ||
6 | * the HTTP request lifecycle. | ||
7 | * | ||
8 | * @class ExceptionHandler | ||
9 | */ | ||
10 | class ExceptionHandler extends BaseExceptionHandler { | ||
11 | /** | ||
12 | * Handle exception thrown during the HTTP lifecycle | ||
13 | * | ||
14 | * @method handle | ||
15 | * | ||
16 | * @param {Object} error | ||
17 | * @param {Object} options.request | ||
18 | * @param {Object} options.response | ||
19 | * | ||
20 | * @return {void} | ||
21 | */ | ||
22 | async handle(error, { response }) { | ||
23 | if (error.name === 'ValidationException') { | ||
24 | return response.status(400).send('Invalid arguments'); | ||
25 | } if (error.name === 'InvalidSessionException') { | ||
26 | return response.status(401).redirect('/user/login'); | ||
27 | } | ||
28 | |||
29 | return response.status(error.status).send(error.message); | ||
30 | } | ||
31 | |||
32 | /** | ||
33 | * Report exception for logging or debugging. | ||
34 | * | ||
35 | * @method report | ||
36 | * | ||
37 | * @param {Object} error | ||
38 | * @param {Object} options.request | ||
39 | * | ||
40 | * @return {void} | ||
41 | */ | ||
42 | async report() { | ||
43 | return true; | ||
44 | } | ||
45 | } | ||
46 | |||
47 | module.exports = ExceptionHandler; | ||
diff --git a/src/server/app/Middleware/ConvertEmptyStringsToNull.js b/src/server/app/Middleware/ConvertEmptyStringsToNull.js new file mode 100644 index 000000000..bc3079a7f --- /dev/null +++ b/src/server/app/Middleware/ConvertEmptyStringsToNull.js | |||
@@ -0,0 +1,16 @@ | |||
1 | |||
2 | class ConvertEmptyStringsToNull { | ||
3 | async handle({ request }, next) { | ||
4 | if (Object.keys(request.body).length) { | ||
5 | request.body = Object.assign( | ||
6 | ...Object.keys(request.body).map(key => ({ | ||
7 | [key]: request.body[key] !== '' ? request.body[key] : null, | ||
8 | })), | ||
9 | ); | ||
10 | } | ||
11 | |||
12 | await next(); | ||
13 | } | ||
14 | } | ||
15 | |||
16 | module.exports = ConvertEmptyStringsToNull; | ||
diff --git a/src/server/app/Models/Recipe.js b/src/server/app/Models/Recipe.js new file mode 100644 index 000000000..da3618bf7 --- /dev/null +++ b/src/server/app/Models/Recipe.js | |||
@@ -0,0 +1,8 @@ | |||
1 | |||
2 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
3 | const Model = use('Model'); | ||
4 | |||
5 | class Recipe extends Model { | ||
6 | } | ||
7 | |||
8 | module.exports = Recipe; | ||
diff --git a/src/server/app/Models/Service.js b/src/server/app/Models/Service.js new file mode 100644 index 000000000..20679feb1 --- /dev/null +++ b/src/server/app/Models/Service.js | |||
@@ -0,0 +1,8 @@ | |||
1 | |||
2 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
3 | const Model = use('Model'); | ||
4 | |||
5 | class Service extends Model { | ||
6 | } | ||
7 | |||
8 | module.exports = Service; | ||
diff --git a/src/server/app/Models/Token.js b/src/server/app/Models/Token.js new file mode 100644 index 000000000..f6bec0852 --- /dev/null +++ b/src/server/app/Models/Token.js | |||
@@ -0,0 +1,8 @@ | |||
1 | |||
2 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
3 | const Model = use('Model'); | ||
4 | |||
5 | class Token extends Model { | ||
6 | } | ||
7 | |||
8 | module.exports = Token; | ||
diff --git a/src/server/app/Models/Traits/NoTimestamp.js b/src/server/app/Models/Traits/NoTimestamp.js new file mode 100644 index 000000000..c647428b3 --- /dev/null +++ b/src/server/app/Models/Traits/NoTimestamp.js | |||
@@ -0,0 +1,15 @@ | |||
1 | |||
2 | class NoTimestamp { | ||
3 | register(Model) { | ||
4 | Object.defineProperties(Model, { | ||
5 | createdAtColumn: { | ||
6 | get: () => null, | ||
7 | }, | ||
8 | updatedAtColumn: { | ||
9 | get: () => null, | ||
10 | }, | ||
11 | }); | ||
12 | } | ||
13 | } | ||
14 | |||
15 | module.exports = NoTimestamp; | ||
diff --git a/src/server/app/Models/User.js b/src/server/app/Models/User.js new file mode 100644 index 000000000..9783cbe45 --- /dev/null +++ b/src/server/app/Models/User.js | |||
@@ -0,0 +1,8 @@ | |||
1 | |||
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/server/app/Models/Workspace.js b/src/server/app/Models/Workspace.js new file mode 100644 index 000000000..3b73cbf33 --- /dev/null +++ b/src/server/app/Models/Workspace.js | |||
@@ -0,0 +1,8 @@ | |||
1 | |||
2 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
3 | const Model = use('Model'); | ||
4 | |||
5 | class Workspace extends Model { | ||
6 | } | ||
7 | |||
8 | module.exports = Workspace; | ||
diff --git a/src/server/config/app.js b/src/server/config/app.js new file mode 100644 index 000000000..7938b81df --- /dev/null +++ b/src/server/config/app.js | |||
@@ -0,0 +1,242 @@ | |||
1 | |||
2 | /** @type {import('@adonisjs/framework/src/Env')} */ | ||
3 | const Env = use('Env'); | ||
4 | |||
5 | module.exports = { | ||
6 | |||
7 | /* | ||
8 | |-------------------------------------------------------------------------- | ||
9 | | Application Name | ||
10 | |-------------------------------------------------------------------------- | ||
11 | | | ||
12 | | This value is the name of your application and can used when you | ||
13 | | need to place the application's name in a email, view or | ||
14 | | other location. | ||
15 | | | ||
16 | */ | ||
17 | |||
18 | name: Env.get('APP_NAME', 'Ferdi Internal Server'), | ||
19 | |||
20 | /* | ||
21 | |-------------------------------------------------------------------------- | ||
22 | | App Key | ||
23 | |-------------------------------------------------------------------------- | ||
24 | | | ||
25 | | App key is a randomly generated 16 or 32 characters long string required | ||
26 | | to encrypt cookies, sessions and other sensitive data. | ||
27 | | | ||
28 | */ | ||
29 | appKey: Env.getOrFail('APP_KEY'), | ||
30 | |||
31 | http: { | ||
32 | /* | ||
33 | |-------------------------------------------------------------------------- | ||
34 | | Allow Method Spoofing | ||
35 | |-------------------------------------------------------------------------- | ||
36 | | | ||
37 | | Method spoofing allows to make requests by spoofing the http verb. | ||
38 | | Which means you can make a GET request but instruct the server to | ||
39 | | treat as a POST or PUT request. If you want this feature, set the | ||
40 | | below value to true. | ||
41 | | | ||
42 | */ | ||
43 | allowMethodSpoofing: true, | ||
44 | |||
45 | /* | ||
46 | |-------------------------------------------------------------------------- | ||
47 | | Trust Proxy | ||
48 | |-------------------------------------------------------------------------- | ||
49 | | | ||
50 | | Trust proxy defines whether X-Forwarded-* headers should be trusted or not. | ||
51 | | When your application is behind a proxy server like nginx, these values | ||
52 | | are set automatically and should be trusted. Apart from setting it | ||
53 | | to true or false Adonis supports handful or ways to allow proxy | ||
54 | | values. Read documentation for that. | ||
55 | | | ||
56 | */ | ||
57 | trustProxy: false, | ||
58 | |||
59 | /* | ||
60 | |-------------------------------------------------------------------------- | ||
61 | | Subdomains | ||
62 | |-------------------------------------------------------------------------- | ||
63 | | | ||
64 | | Offset to be used for returning subdomains for a given request.For | ||
65 | | majority of applications it will be 2, until you have nested | ||
66 | | sudomains. | ||
67 | | cheatsheet.adonisjs.com - offset - 2 | ||
68 | | virk.cheatsheet.adonisjs.com - offset - 3 | ||
69 | | | ||
70 | */ | ||
71 | subdomainOffset: 2, | ||
72 | |||
73 | /* | ||
74 | |-------------------------------------------------------------------------- | ||
75 | | JSONP Callback | ||
76 | |-------------------------------------------------------------------------- | ||
77 | | | ||
78 | | Default jsonp callback to be used when callback query string is missing | ||
79 | | in request url. | ||
80 | | | ||
81 | */ | ||
82 | jsonpCallback: 'callback', | ||
83 | |||
84 | |||
85 | /* | ||
86 | |-------------------------------------------------------------------------- | ||
87 | | Etag | ||
88 | |-------------------------------------------------------------------------- | ||
89 | | | ||
90 | | Set etag on all HTTP response. In order to disable for selected routes, | ||
91 | | you can call the `response.send` with an options object as follows. | ||
92 | | | ||
93 | | response.send('Hello', { ignoreEtag: true }) | ||
94 | | | ||
95 | */ | ||
96 | etag: false, | ||
97 | }, | ||
98 | |||
99 | views: { | ||
100 | /* | ||
101 | |-------------------------------------------------------------------------- | ||
102 | | Cache Views | ||
103 | |-------------------------------------------------------------------------- | ||
104 | | | ||
105 | | Define whether or not to cache the compiled view. Set it to true in | ||
106 | | production to optimize view loading time. | ||
107 | | | ||
108 | */ | ||
109 | cache: Env.get('CACHE_VIEWS', true), | ||
110 | }, | ||
111 | |||
112 | static: { | ||
113 | /* | ||
114 | |-------------------------------------------------------------------------- | ||
115 | | Dot Files | ||
116 | |-------------------------------------------------------------------------- | ||
117 | | | ||
118 | | Define how to treat dot files when trying to server static resources. | ||
119 | | By default it is set to ignore, which will pretend that dotfiles | ||
120 | | does not exists. | ||
121 | | | ||
122 | | Can be one of the following | ||
123 | | ignore, deny, allow | ||
124 | | | ||
125 | */ | ||
126 | dotfiles: 'ignore', | ||
127 | |||
128 | /* | ||
129 | |-------------------------------------------------------------------------- | ||
130 | | ETag | ||
131 | |-------------------------------------------------------------------------- | ||
132 | | | ||
133 | | Enable or disable etag generation | ||
134 | | | ||
135 | */ | ||
136 | etag: true, | ||
137 | |||
138 | /* | ||
139 | |-------------------------------------------------------------------------- | ||
140 | | Extensions | ||
141 | |-------------------------------------------------------------------------- | ||
142 | | | ||
143 | | Set file extension fallbacks. When set, if a file is not found, the given | ||
144 | | extensions will be added to the file name and search for. The first | ||
145 | | that exists will be served. Example: ['html', 'htm']. | ||
146 | | | ||
147 | */ | ||
148 | extensions: false, | ||
149 | }, | ||
150 | |||
151 | locales: { | ||
152 | /* | ||
153 | |-------------------------------------------------------------------------- | ||
154 | | Loader | ||
155 | |-------------------------------------------------------------------------- | ||
156 | | | ||
157 | | The loader to be used for fetching and updating locales. Below is the | ||
158 | | list of available options. | ||
159 | | | ||
160 | | file, database | ||
161 | | | ||
162 | */ | ||
163 | loader: 'file', | ||
164 | |||
165 | /* | ||
166 | |-------------------------------------------------------------------------- | ||
167 | | Default Locale | ||
168 | |-------------------------------------------------------------------------- | ||
169 | | | ||
170 | | Default locale to be used by Antl provider. You can always switch drivers | ||
171 | | in runtime or use the official Antl middleware to detect the driver | ||
172 | | based on HTTP headers/query string. | ||
173 | | | ||
174 | */ | ||
175 | locale: 'en', | ||
176 | }, | ||
177 | |||
178 | logger: { | ||
179 | /* | ||
180 | |-------------------------------------------------------------------------- | ||
181 | | Transport | ||
182 | |-------------------------------------------------------------------------- | ||
183 | | | ||
184 | | Transport to be used for logging messages. You can have multiple | ||
185 | | transports using same driver. | ||
186 | | | ||
187 | | Available drivers are: `file` and `console`. | ||
188 | | | ||
189 | */ | ||
190 | transport: 'console', | ||
191 | |||
192 | /* | ||
193 | |-------------------------------------------------------------------------- | ||
194 | | Console Transport | ||
195 | |-------------------------------------------------------------------------- | ||
196 | | | ||
197 | | Using `console` driver for logging. This driver writes to `stdout` | ||
198 | | and `stderr` | ||
199 | | | ||
200 | */ | ||
201 | console: { | ||
202 | driver: 'console', | ||
203 | name: 'adonis-app', | ||
204 | level: 'info', | ||
205 | }, | ||
206 | |||
207 | /* | ||
208 | |-------------------------------------------------------------------------- | ||
209 | | File Transport | ||
210 | |-------------------------------------------------------------------------- | ||
211 | | | ||
212 | | File transport uses file driver and writes log messages for a given | ||
213 | | file inside `tmp` directory for your app. | ||
214 | | | ||
215 | | For a different directory, set an absolute path for the filename. | ||
216 | | | ||
217 | */ | ||
218 | file: { | ||
219 | driver: 'file', | ||
220 | name: 'adonis-app', | ||
221 | filename: 'adonis.log', | ||
222 | level: 'info', | ||
223 | }, | ||
224 | }, | ||
225 | |||
226 | /* | ||
227 | |-------------------------------------------------------------------------- | ||
228 | | Generic Cookie Options | ||
229 | |-------------------------------------------------------------------------- | ||
230 | | | ||
231 | | The following cookie options are generic settings used by AdonisJs to create | ||
232 | | cookies. However, some parts of the application like `sessions` can have | ||
233 | | separate settings for cookies inside `config/session.js`. | ||
234 | | | ||
235 | */ | ||
236 | cookie: { | ||
237 | httpOnly: true, | ||
238 | sameSite: false, | ||
239 | path: '/', | ||
240 | maxAge: 7200, | ||
241 | }, | ||
242 | }; | ||
diff --git a/src/server/config/auth.js b/src/server/config/auth.js new file mode 100644 index 000000000..b831b06c6 --- /dev/null +++ b/src/server/config/auth.js | |||
@@ -0,0 +1,93 @@ | |||
1 | |||
2 | /** @type {import('@adonisjs/framework/src/Env')} */ | ||
3 | const Env = use('Env'); | ||
4 | |||
5 | module.exports = { | ||
6 | /* | ||
7 | |-------------------------------------------------------------------------- | ||
8 | | Authenticator | ||
9 | |-------------------------------------------------------------------------- | ||
10 | | | ||
11 | | Authentication is a combination of serializer and scheme with extra | ||
12 | | config to define on how to authenticate a user. | ||
13 | | | ||
14 | | Available Schemes - basic, session, jwt, api | ||
15 | | Available Serializers - lucid, database | ||
16 | | | ||
17 | */ | ||
18 | authenticator: 'jwt', | ||
19 | |||
20 | /* | ||
21 | |-------------------------------------------------------------------------- | ||
22 | | Session | ||
23 | |-------------------------------------------------------------------------- | ||
24 | | | ||
25 | | Session authenticator makes use of sessions to authenticate a user. | ||
26 | | Session authentication is always persistent. | ||
27 | | | ||
28 | */ | ||
29 | session: { | ||
30 | serializer: 'lucid', | ||
31 | model: 'App/Models/User', | ||
32 | scheme: 'session', | ||
33 | uid: 'email', | ||
34 | password: 'password', | ||
35 | }, | ||
36 | |||
37 | /* | ||
38 | |-------------------------------------------------------------------------- | ||
39 | | Basic Auth | ||
40 | |-------------------------------------------------------------------------- | ||
41 | | | ||
42 | | The basic auth authenticator uses basic auth header to authenticate a | ||
43 | | user. | ||
44 | | | ||
45 | | NOTE: | ||
46 | | This scheme is not persistent and users are supposed to pass | ||
47 | | login credentials on each request. | ||
48 | | | ||
49 | */ | ||
50 | basic: { | ||
51 | serializer: 'lucid', | ||
52 | model: 'App/Models/User', | ||
53 | scheme: 'basic', | ||
54 | uid: 'email', | ||
55 | password: 'password', | ||
56 | }, | ||
57 | |||
58 | /* | ||
59 | |-------------------------------------------------------------------------- | ||
60 | | Jwt | ||
61 | |-------------------------------------------------------------------------- | ||
62 | | | ||
63 | | The jwt authenticator works by passing a jwt token on each HTTP request | ||
64 | | via HTTP `Authorization` header. | ||
65 | | | ||
66 | */ | ||
67 | jwt: { | ||
68 | serializer: 'lucid', | ||
69 | model: 'App/Models/User', | ||
70 | scheme: 'jwt', | ||
71 | uid: 'email', | ||
72 | password: 'password', | ||
73 | options: { | ||
74 | secret: Env.get('APP_KEY'), | ||
75 | }, | ||
76 | }, | ||
77 | |||
78 | /* | ||
79 | |-------------------------------------------------------------------------- | ||
80 | | Api | ||
81 | |-------------------------------------------------------------------------- | ||
82 | | | ||
83 | | The Api scheme makes use of API personal tokens to authenticate a user. | ||
84 | | | ||
85 | */ | ||
86 | api: { | ||
87 | serializer: 'lucid', | ||
88 | model: 'App/Models/User', | ||
89 | scheme: 'api', | ||
90 | uid: 'email', | ||
91 | password: 'password', | ||
92 | }, | ||
93 | }; | ||
diff --git a/src/server/config/bodyParser.js b/src/server/config/bodyParser.js new file mode 100644 index 000000000..c336e67d2 --- /dev/null +++ b/src/server/config/bodyParser.js | |||
@@ -0,0 +1,156 @@ | |||
1 | |||
2 | module.exports = { | ||
3 | /* | ||
4 | |-------------------------------------------------------------------------- | ||
5 | | JSON Parser | ||
6 | |-------------------------------------------------------------------------- | ||
7 | | | ||
8 | | Below settings are applied when the request body contains a JSON payload. | ||
9 | | If you want body parser to ignore JSON payloads, then simply set `types` | ||
10 | | to an empty array. | ||
11 | */ | ||
12 | json: { | ||
13 | /* | ||
14 | |-------------------------------------------------------------------------- | ||
15 | | limit | ||
16 | |-------------------------------------------------------------------------- | ||
17 | | | ||
18 | | Defines the limit of JSON that can be sent by the client. If payload | ||
19 | | is over 1mb it will not be processed. | ||
20 | | | ||
21 | */ | ||
22 | limit: '50mb', | ||
23 | |||
24 | /* | ||
25 | |-------------------------------------------------------------------------- | ||
26 | | strict | ||
27 | |-------------------------------------------------------------------------- | ||
28 | | | ||
29 | | When `strict` is set to true, body parser will only parse Arrays and | ||
30 | | Object. Otherwise everything parseable by `JSON.parse` is parsed. | ||
31 | | | ||
32 | */ | ||
33 | strict: true, | ||
34 | |||
35 | /* | ||
36 | |-------------------------------------------------------------------------- | ||
37 | | types | ||
38 | |-------------------------------------------------------------------------- | ||
39 | | | ||
40 | | Which content types are processed as JSON payloads. You are free to | ||
41 | | add your own types here, but the request body should be parseable | ||
42 | | by `JSON.parse` method. | ||
43 | | | ||
44 | */ | ||
45 | types: [ | ||
46 | 'application/json', | ||
47 | 'application/json-patch+json', | ||
48 | 'application/vnd.api+json', | ||
49 | 'application/csp-report', | ||
50 | ], | ||
51 | }, | ||
52 | |||
53 | /* | ||
54 | |-------------------------------------------------------------------------- | ||
55 | | Raw Parser | ||
56 | |-------------------------------------------------------------------------- | ||
57 | | | ||
58 | | | ||
59 | | | ||
60 | */ | ||
61 | raw: { | ||
62 | types: [ | ||
63 | 'text/*', | ||
64 | ], | ||
65 | }, | ||
66 | |||
67 | /* | ||
68 | |-------------------------------------------------------------------------- | ||
69 | | Form Parser | ||
70 | |-------------------------------------------------------------------------- | ||
71 | | | ||
72 | | | ||
73 | | | ||
74 | */ | ||
75 | form: { | ||
76 | types: [ | ||
77 | 'application/x-www-form-urlencoded', | ||
78 | ], | ||
79 | }, | ||
80 | |||
81 | /* | ||
82 | |-------------------------------------------------------------------------- | ||
83 | | Files Parser | ||
84 | |-------------------------------------------------------------------------- | ||
85 | | | ||
86 | | | ||
87 | | | ||
88 | */ | ||
89 | files: { | ||
90 | types: [ | ||
91 | 'multipart/form-data', | ||
92 | ], | ||
93 | |||
94 | /* | ||
95 | |-------------------------------------------------------------------------- | ||
96 | | Max Size | ||
97 | |-------------------------------------------------------------------------- | ||
98 | | | ||
99 | | Below value is the max size of all the files uploaded to the server. It | ||
100 | | is validated even before files have been processed and hard exception | ||
101 | | is thrown. | ||
102 | | | ||
103 | | Consider setting a reasonable value here, otherwise people may upload GB's | ||
104 | | of files which will keep your server busy. | ||
105 | | | ||
106 | | Also this value is considered when `autoProcess` is set to true. | ||
107 | | | ||
108 | */ | ||
109 | maxSize: '20mb', | ||
110 | |||
111 | /* | ||
112 | |-------------------------------------------------------------------------- | ||
113 | | Auto Process | ||
114 | |-------------------------------------------------------------------------- | ||
115 | | | ||
116 | | Whether or not to auto-process files. Since HTTP servers handle files via | ||
117 | | couple of specific endpoints. It is better to set this value off and | ||
118 | | manually process the files when required. | ||
119 | | | ||
120 | | This value can contain a boolean or an array of route patterns | ||
121 | | to be autoprocessed. | ||
122 | */ | ||
123 | autoProcess: true, | ||
124 | |||
125 | /* | ||
126 | |-------------------------------------------------------------------------- | ||
127 | | Process Manually | ||
128 | |-------------------------------------------------------------------------- | ||
129 | | | ||
130 | | The list of routes that should not process files and instead rely on | ||
131 | | manual process. This list should only contain routes when autoProcess | ||
132 | | is to true. Otherwise everything is processed manually. | ||
133 | | | ||
134 | */ | ||
135 | processManually: [], | ||
136 | |||
137 | /* | ||
138 | |-------------------------------------------------------------------------- | ||
139 | | Temporary file name | ||
140 | |-------------------------------------------------------------------------- | ||
141 | | | ||
142 | | Define a function, which should return a string to be used as the | ||
143 | | tmp file name. | ||
144 | | | ||
145 | | If not defined, Bodyparser will use `uuid` as the tmp file name. | ||
146 | | | ||
147 | | To be defined as. If you are defining the function, then do make sure | ||
148 | | to return a value from it. | ||
149 | | | ||
150 | | tmpFileName () { | ||
151 | | return 'some-unique-value' | ||
152 | | } | ||
153 | | | ||
154 | */ | ||
155 | }, | ||
156 | }; | ||
diff --git a/src/server/config/cors.js b/src/server/config/cors.js new file mode 100644 index 000000000..7ebbe3ffa --- /dev/null +++ b/src/server/config/cors.js | |||
@@ -0,0 +1,86 @@ | |||
1 | |||
2 | module.exports = { | ||
3 | /* | ||
4 | |-------------------------------------------------------------------------- | ||
5 | | Origin | ||
6 | |-------------------------------------------------------------------------- | ||
7 | | | ||
8 | | Set a list of origins to be allowed. The value can be one of the following | ||
9 | | | ||
10 | | Boolean: true - Allow current request origin | ||
11 | | Boolean: false - Disallow all | ||
12 | | String - Comma separated list of allowed origins | ||
13 | | Array - An array of allowed origins | ||
14 | | String: * - A wildcard to allow current request origin | ||
15 | | Function - Receives the current origin and should return one of the above values. | ||
16 | | | ||
17 | */ | ||
18 | origin: false, | ||
19 | |||
20 | /* | ||
21 | |-------------------------------------------------------------------------- | ||
22 | | Methods | ||
23 | |-------------------------------------------------------------------------- | ||
24 | | | ||
25 | | HTTP methods to be allowed. The value can be one of the following | ||
26 | | | ||
27 | | String - Comma separated list of allowed methods | ||
28 | | Array - An array of allowed methods | ||
29 | | | ||
30 | */ | ||
31 | methods: ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'], | ||
32 | |||
33 | /* | ||
34 | |-------------------------------------------------------------------------- | ||
35 | | Headers | ||
36 | |-------------------------------------------------------------------------- | ||
37 | | | ||
38 | | List of headers to be allowed via Access-Control-Request-Headers header. | ||
39 | | The value can be one of the following. | ||
40 | | | ||
41 | | Boolean: true - Allow current request headers | ||
42 | | Boolean: false - Disallow all | ||
43 | | String - Comma separated list of allowed headers | ||
44 | | Array - An array of allowed headers | ||
45 | | String: * - A wildcard to allow current request headers | ||
46 | | Function - Receives the current header and should return one of the above values. | ||
47 | | | ||
48 | */ | ||
49 | headers: true, | ||
50 | |||
51 | /* | ||
52 | |-------------------------------------------------------------------------- | ||
53 | | Expose Headers | ||
54 | |-------------------------------------------------------------------------- | ||
55 | | | ||
56 | | A list of headers to be exposed via `Access-Control-Expose-Headers` | ||
57 | | header. The value can be one of the following. | ||
58 | | | ||
59 | | Boolean: false - Disallow all | ||
60 | | String: Comma separated list of allowed headers | ||
61 | | Array - An array of allowed headers | ||
62 | | | ||
63 | */ | ||
64 | exposeHeaders: false, | ||
65 | |||
66 | /* | ||
67 | |-------------------------------------------------------------------------- | ||
68 | | Credentials | ||
69 | |-------------------------------------------------------------------------- | ||
70 | | | ||
71 | | Define Access-Control-Allow-Credentials header. It should always be a | ||
72 | | boolean. | ||
73 | | | ||
74 | */ | ||
75 | credentials: false, | ||
76 | |||
77 | /* | ||
78 | |-------------------------------------------------------------------------- | ||
79 | | MaxAge | ||
80 | |-------------------------------------------------------------------------- | ||
81 | | | ||
82 | | Define Access-Control-Allow-Max-Age | ||
83 | | | ||
84 | */ | ||
85 | maxAge: 90, | ||
86 | }; | ||
diff --git a/src/server/config/database.js b/src/server/config/database.js new file mode 100644 index 000000000..86f18dac5 --- /dev/null +++ b/src/server/config/database.js | |||
@@ -0,0 +1,87 @@ | |||
1 | |||
2 | /** @type {import('@adonisjs/framework/src/Env')} */ | ||
3 | const Env = use('Env'); | ||
4 | |||
5 | // eslint-disable-next-line import/no-extraneous-dependencies | ||
6 | const { app } = require('electron'); | ||
7 | const path = require('path'); | ||
8 | |||
9 | const dbPath = path.join(app.getPath('userData'), 'server.sqlite'); | ||
10 | |||
11 | module.exports = { | ||
12 | /* | ||
13 | |-------------------------------------------------------------------------- | ||
14 | | Default Connection | ||
15 | |-------------------------------------------------------------------------- | ||
16 | | | ||
17 | | Connection defines the default connection settings to be used while | ||
18 | | interacting with SQL databases. | ||
19 | | | ||
20 | */ | ||
21 | connection: Env.get('DB_CONNECTION', 'sqlite'), | ||
22 | |||
23 | /* | ||
24 | |-------------------------------------------------------------------------- | ||
25 | | Sqlite | ||
26 | |-------------------------------------------------------------------------- | ||
27 | | | ||
28 | | Sqlite is a flat file database and can be a good choice for a development | ||
29 | | environment. | ||
30 | | | ||
31 | | npm i --save sqlite3 | ||
32 | | | ||
33 | */ | ||
34 | sqlite: { | ||
35 | client: 'sqlite3', | ||
36 | connection: { | ||
37 | // filename: Helpers.databasePath(`${Env.get('DB_DATABASE', 'development')}.sqlite`), | ||
38 | filename: dbPath, | ||
39 | }, | ||
40 | useNullAsDefault: true, | ||
41 | debug: Env.get('DB_DEBUG', false), | ||
42 | }, | ||
43 | |||
44 | /* | ||
45 | |-------------------------------------------------------------------------- | ||
46 | | MySQL | ||
47 | |-------------------------------------------------------------------------- | ||
48 | | | ||
49 | | Here we define connection settings for MySQL database. | ||
50 | | | ||
51 | | npm i --save mysql | ||
52 | | | ||
53 | */ | ||
54 | mysql: { | ||
55 | client: 'mysql', | ||
56 | connection: { | ||
57 | host: Env.get('DB_HOST', 'localhost'), | ||
58 | port: Env.get('DB_PORT', ''), | ||
59 | user: Env.get('DB_USER', 'root'), | ||
60 | password: Env.get('DB_PASSWORD', ''), | ||
61 | database: Env.get('DB_DATABASE', 'adonis'), | ||
62 | }, | ||
63 | debug: Env.get('DB_DEBUG', false), | ||
64 | }, | ||
65 | |||
66 | /* | ||
67 | |-------------------------------------------------------------------------- | ||
68 | | PostgreSQL | ||
69 | |-------------------------------------------------------------------------- | ||
70 | | | ||
71 | | Here we define connection settings for PostgreSQL database. | ||
72 | | | ||
73 | | npm i --save pg | ||
74 | | | ||
75 | */ | ||
76 | pg: { | ||
77 | client: 'pg', | ||
78 | connection: { | ||
79 | host: Env.get('DB_HOST', 'localhost'), | ||
80 | port: Env.get('DB_PORT', ''), | ||
81 | user: Env.get('DB_USER', 'root'), | ||
82 | password: Env.get('DB_PASSWORD', ''), | ||
83 | database: Env.get('DB_DATABASE', 'adonis'), | ||
84 | }, | ||
85 | debug: Env.get('DB_DEBUG', false), | ||
86 | }, | ||
87 | }; | ||
diff --git a/src/server/config/drive.js b/src/server/config/drive.js new file mode 100644 index 000000000..617ce470a --- /dev/null +++ b/src/server/config/drive.js | |||
@@ -0,0 +1,45 @@ | |||
1 | const Env = use('Env'); | ||
2 | |||
3 | module.exports = { | ||
4 | /* | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | Default disk | ||
7 | |-------------------------------------------------------------------------- | ||
8 | | | ||
9 | | The default disk is used when you interact with the file system without | ||
10 | | defining a disk name | ||
11 | | | ||
12 | */ | ||
13 | default: 'local', | ||
14 | |||
15 | disks: { | ||
16 | /* | ||
17 | |-------------------------------------------------------------------------- | ||
18 | | Local | ||
19 | |-------------------------------------------------------------------------- | ||
20 | | | ||
21 | | Local disk interacts with the a local folder inside your application | ||
22 | | | ||
23 | */ | ||
24 | local: { | ||
25 | root: `${__dirname}/../recipes`, | ||
26 | driver: 'local', | ||
27 | }, | ||
28 | |||
29 | /* | ||
30 | |-------------------------------------------------------------------------- | ||
31 | | S3 | ||
32 | |-------------------------------------------------------------------------- | ||
33 | | | ||
34 | | S3 disk interacts with a bucket on aws s3 | ||
35 | | | ||
36 | */ | ||
37 | s3: { | ||
38 | driver: 's3', | ||
39 | key: Env.get('S3_KEY'), | ||
40 | secret: Env.get('S3_SECRET'), | ||
41 | bucket: Env.get('S3_BUCKET'), | ||
42 | region: Env.get('S3_REGION'), | ||
43 | }, | ||
44 | }, | ||
45 | }; | ||
diff --git a/src/server/config/hash.js b/src/server/config/hash.js new file mode 100644 index 000000000..297c977fc --- /dev/null +++ b/src/server/config/hash.js | |||
@@ -0,0 +1,48 @@ | |||
1 | |||
2 | /** @type {import('@adonisjs/framework/src/Env')} */ | ||
3 | const Env = use('Env'); | ||
4 | |||
5 | module.exports = { | ||
6 | /* | ||
7 | |-------------------------------------------------------------------------- | ||
8 | | Driver | ||
9 | |-------------------------------------------------------------------------- | ||
10 | | | ||
11 | | Driver to be used for hashing values. The same driver is used by the | ||
12 | | auth module too. | ||
13 | | | ||
14 | */ | ||
15 | driver: Env.get('HASH_DRIVER', 'bcrypt'), | ||
16 | |||
17 | /* | ||
18 | |-------------------------------------------------------------------------- | ||
19 | | Bcrypt | ||
20 | |-------------------------------------------------------------------------- | ||
21 | | | ||
22 | | Config related to bcrypt hashing. https://www.npmjs.com/package/bcrypt | ||
23 | | package is used internally. | ||
24 | | | ||
25 | */ | ||
26 | bcrypt: { | ||
27 | rounds: 10, | ||
28 | }, | ||
29 | |||
30 | /* | ||
31 | |-------------------------------------------------------------------------- | ||
32 | | Argon | ||
33 | |-------------------------------------------------------------------------- | ||
34 | | | ||
35 | | Config related to argon. https://www.npmjs.com/package/argon2 package is | ||
36 | | used internally. | ||
37 | | | ||
38 | | Since argon is optional, you will have to install the dependency yourself | ||
39 | | | ||
40 | |============================================================================ | ||
41 | | npm i argon2 | ||
42 | |============================================================================ | ||
43 | | | ||
44 | */ | ||
45 | argon: { | ||
46 | type: 1, | ||
47 | }, | ||
48 | }; | ||
diff --git a/src/server/config/session.js b/src/server/config/session.js new file mode 100644 index 000000000..bce28bdd9 --- /dev/null +++ b/src/server/config/session.js | |||
@@ -0,0 +1,98 @@ | |||
1 | |||
2 | const Env = use('Env'); | ||
3 | |||
4 | module.exports = { | ||
5 | /* | ||
6 | |-------------------------------------------------------------------------- | ||
7 | | Session Driver | ||
8 | |-------------------------------------------------------------------------- | ||
9 | | | ||
10 | | The session driver to be used for storing session values. It can be | ||
11 | | cookie, file or redis. | ||
12 | | | ||
13 | | For `redis` driver, make sure to install and register `@adonisjs/redis` | ||
14 | | | ||
15 | */ | ||
16 | driver: Env.get('SESSION_DRIVER', 'cookie'), | ||
17 | |||
18 | /* | ||
19 | |-------------------------------------------------------------------------- | ||
20 | | Cookie Name | ||
21 | |-------------------------------------------------------------------------- | ||
22 | | | ||
23 | | The name of the cookie to be used for saving session id. Session ids | ||
24 | | are signed and encrypted. | ||
25 | | | ||
26 | */ | ||
27 | cookieName: 'adonis-session', | ||
28 | |||
29 | /* | ||
30 | |-------------------------------------------------------------------------- | ||
31 | | Clear session when browser closes | ||
32 | |-------------------------------------------------------------------------- | ||
33 | | | ||
34 | | If this value is true, the session cookie will be temporary and will be | ||
35 | | removed when browser closes. | ||
36 | | | ||
37 | */ | ||
38 | clearWithBrowser: true, | ||
39 | |||
40 | /* | ||
41 | |-------------------------------------------------------------------------- | ||
42 | | Session age | ||
43 | |-------------------------------------------------------------------------- | ||
44 | | | ||
45 | | This value is only used when `clearWithBrowser` is set to false. The | ||
46 | | age must be a valid https://npmjs.org/package/ms string or should | ||
47 | | be in milliseconds. | ||
48 | | | ||
49 | | Valid values are: | ||
50 | | '2h', '10d', '5y', '2.5 hrs' | ||
51 | | | ||
52 | */ | ||
53 | age: '2h', | ||
54 | |||
55 | /* | ||
56 | |-------------------------------------------------------------------------- | ||
57 | | Cookie options | ||
58 | |-------------------------------------------------------------------------- | ||
59 | | | ||
60 | | Cookie options defines the options to be used for setting up session | ||
61 | | cookie | ||
62 | | | ||
63 | */ | ||
64 | cookie: { | ||
65 | httpOnly: true, | ||
66 | path: '/', | ||
67 | sameSite: false, | ||
68 | }, | ||
69 | |||
70 | /* | ||
71 | |-------------------------------------------------------------------------- | ||
72 | | Sessions location | ||
73 | |-------------------------------------------------------------------------- | ||
74 | | | ||
75 | | If driver is set to file, we need to define the relative location from | ||
76 | | the temporary path or absolute url to any location. | ||
77 | | | ||
78 | */ | ||
79 | file: { | ||
80 | location: 'sessions', | ||
81 | }, | ||
82 | |||
83 | /* | ||
84 | |-------------------------------------------------------------------------- | ||
85 | | Redis config | ||
86 | |-------------------------------------------------------------------------- | ||
87 | | | ||
88 | | The configuration for the redis driver. | ||
89 | | | ||
90 | */ | ||
91 | redis: { | ||
92 | host: '127.0.0.1', | ||
93 | port: 6379, | ||
94 | password: null, | ||
95 | db: 0, | ||
96 | keyPrefix: '', | ||
97 | }, | ||
98 | }; | ||
diff --git a/src/server/config/shield.js b/src/server/config/shield.js new file mode 100644 index 000000000..5c1c5cd73 --- /dev/null +++ b/src/server/config/shield.js | |||
@@ -0,0 +1,144 @@ | |||
1 | |||
2 | module.exports = { | ||
3 | /* | ||
4 | |-------------------------------------------------------------------------- | ||
5 | | Content Security Policy | ||
6 | |-------------------------------------------------------------------------- | ||
7 | | | ||
8 | | Content security policy filters out the origins not allowed to execute | ||
9 | | and load resources like scripts, styles and fonts. There are wide | ||
10 | | variety of options to choose from. | ||
11 | */ | ||
12 | csp: { | ||
13 | /* | ||
14 | |-------------------------------------------------------------------------- | ||
15 | | Directives | ||
16 | |-------------------------------------------------------------------------- | ||
17 | | | ||
18 | | All directives are defined in camelCase and here is the list of | ||
19 | | available directives and their possible values. | ||
20 | | | ||
21 | | https://content-security-policy.com | ||
22 | | | ||
23 | | @example | ||
24 | | directives: { | ||
25 | | defaultSrc: ['self', '@nonce', 'cdnjs.cloudflare.com'] | ||
26 | | } | ||
27 | | | ||
28 | */ | ||
29 | directives: { | ||
30 | }, | ||
31 | /* | ||
32 | |-------------------------------------------------------------------------- | ||
33 | | Report only | ||
34 | |-------------------------------------------------------------------------- | ||
35 | | | ||
36 | | Setting `reportOnly=true` will not block the scripts from running and | ||
37 | | instead report them to a URL. | ||
38 | | | ||
39 | */ | ||
40 | reportOnly: false, | ||
41 | /* | ||
42 | |-------------------------------------------------------------------------- | ||
43 | | Set all headers | ||
44 | |-------------------------------------------------------------------------- | ||
45 | | | ||
46 | | Headers staring with `X` have been depreciated, since all major browsers | ||
47 | | supports the standard CSP header. So its better to disable deperciated | ||
48 | | headers, unless you want them to be set. | ||
49 | | | ||
50 | */ | ||
51 | setAllHeaders: false, | ||
52 | |||
53 | /* | ||
54 | |-------------------------------------------------------------------------- | ||
55 | | Disable on android | ||
56 | |-------------------------------------------------------------------------- | ||
57 | | | ||
58 | | Certain versions of android are buggy with CSP policy. So you can set | ||
59 | | this value to true, to disable it for Android versions with buggy | ||
60 | | behavior. | ||
61 | | | ||
62 | | Here is an issue reported on a different package, but helpful to read | ||
63 | | if you want to know the behavior. https://github.com/helmetjs/helmet/pull/82 | ||
64 | | | ||
65 | */ | ||
66 | disableAndroid: true, | ||
67 | }, | ||
68 | |||
69 | /* | ||
70 | |-------------------------------------------------------------------------- | ||
71 | | X-XSS-Protection | ||
72 | |-------------------------------------------------------------------------- | ||
73 | | | ||
74 | | X-XSS Protection saves from applications from XSS attacks. It is adopted | ||
75 | | by IE and later followed by some other browsers. | ||
76 | | | ||
77 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection | ||
78 | | | ||
79 | */ | ||
80 | xss: { | ||
81 | enabled: true, | ||
82 | enableOnOldIE: false, | ||
83 | }, | ||
84 | |||
85 | /* | ||
86 | |-------------------------------------------------------------------------- | ||
87 | | Iframe Options | ||
88 | |-------------------------------------------------------------------------- | ||
89 | | | ||
90 | | xframe defines whether or not your website can be embedded inside an | ||
91 | | iframe. Choose from one of the following options. | ||
92 | | @available options | ||
93 | | DENY, SAMEORIGIN, ALLOW-FROM http://example.com | ||
94 | | | ||
95 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options | ||
96 | */ | ||
97 | xframe: 'DENY', | ||
98 | |||
99 | /* | ||
100 | |-------------------------------------------------------------------------- | ||
101 | | No Sniff | ||
102 | |-------------------------------------------------------------------------- | ||
103 | | | ||
104 | | Browsers have a habit of sniffing content-type of a response. Which means | ||
105 | | files with .txt extension containing Javascript code will be executed as | ||
106 | | Javascript. You can disable this behavior by setting nosniff to false. | ||
107 | | | ||
108 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options | ||
109 | | | ||
110 | */ | ||
111 | nosniff: true, | ||
112 | |||
113 | /* | ||
114 | |-------------------------------------------------------------------------- | ||
115 | | No Open | ||
116 | |-------------------------------------------------------------------------- | ||
117 | | | ||
118 | | IE users can execute webpages in the context of your website, which is | ||
119 | | a serious security risk. Below option will manage this for you. | ||
120 | | | ||
121 | */ | ||
122 | noopen: true, | ||
123 | |||
124 | /* | ||
125 | |-------------------------------------------------------------------------- | ||
126 | | CSRF Protection | ||
127 | |-------------------------------------------------------------------------- | ||
128 | | | ||
129 | | CSRF Protection adds another layer of security by making sure, actionable | ||
130 | | routes does have a valid token to execute an action. | ||
131 | | | ||
132 | */ | ||
133 | csrf: { | ||
134 | enable: true, | ||
135 | methods: ['POST', 'PUT', 'DELETE'], | ||
136 | filterUris: [], | ||
137 | cookieOptions: { | ||
138 | httpOnly: false, | ||
139 | sameSite: true, | ||
140 | path: '/', | ||
141 | maxAge: 7200, | ||
142 | }, | ||
143 | }, | ||
144 | }; | ||
diff --git a/src/server/database/factory.js b/src/server/database/factory.js new file mode 100644 index 000000000..550c5e6ab --- /dev/null +++ b/src/server/database/factory.js | |||
@@ -0,0 +1,20 @@ | |||
1 | |||
2 | /* | ||
3 | |-------------------------------------------------------------------------- | ||
4 | | Factory | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | | ||
7 | | Factories are used to define blueprints for database tables or Lucid | ||
8 | | models. Later you can use these blueprints to seed your database | ||
9 | | with dummy data. | ||
10 | | | ||
11 | */ | ||
12 | |||
13 | /** @type {import('@adonisjs/lucid/src/Factory')} */ | ||
14 | // const Factory = use('Factory') | ||
15 | |||
16 | // Factory.blueprint('App/Models/User', (faker) => { | ||
17 | // return { | ||
18 | // username: faker.username() | ||
19 | // } | ||
20 | // }) | ||
diff --git a/src/server/database/ferdi.sqlite b/src/server/database/ferdi.sqlite new file mode 100644 index 000000000..db5425ee6 --- /dev/null +++ b/src/server/database/ferdi.sqlite | |||
Binary files differ | |||
diff --git a/src/server/database/migrations/1566385379883_service_schema.js b/src/server/database/migrations/1566385379883_service_schema.js new file mode 100644 index 000000000..1db95c19d --- /dev/null +++ b/src/server/database/migrations/1566385379883_service_schema.js | |||
@@ -0,0 +1,22 @@ | |||
1 | |||
2 | /** @type {import('@adonisjs/lucid/src/Schema')} */ | ||
3 | const Schema = use('Schema'); | ||
4 | |||
5 | class ServiceSchema extends Schema { | ||
6 | up() { | ||
7 | this.create('services', (table) => { | ||
8 | table.increments(); | ||
9 | table.string('serviceId', 80).notNullable(); | ||
10 | table.string('name', 80).notNullable(); | ||
11 | table.string('recipeId', 254).notNullable(); | ||
12 | table.json('settings'); | ||
13 | table.timestamps(); | ||
14 | }); | ||
15 | } | ||
16 | |||
17 | down() { | ||
18 | this.drop('services'); | ||
19 | } | ||
20 | } | ||
21 | |||
22 | module.exports = ServiceSchema; | ||
diff --git a/src/server/database/migrations/1566554231482_recipe_schema.js b/src/server/database/migrations/1566554231482_recipe_schema.js new file mode 100644 index 000000000..14fcb82e5 --- /dev/null +++ b/src/server/database/migrations/1566554231482_recipe_schema.js | |||
@@ -0,0 +1,21 @@ | |||
1 | |||
2 | /** @type {import('@adonisjs/lucid/src/Schema')} */ | ||
3 | const Schema = use('Schema'); | ||
4 | |||
5 | class RecipeSchema extends Schema { | ||
6 | up() { | ||
7 | this.create('recipes', (table) => { | ||
8 | table.increments(); | ||
9 | table.string('name', 80).notNullable(); | ||
10 | table.string('recipeId', 254).notNullable().unique(); | ||
11 | table.json('data'); | ||
12 | table.timestamps(); | ||
13 | }); | ||
14 | } | ||
15 | |||
16 | down() { | ||
17 | this.drop('recipes'); | ||
18 | } | ||
19 | } | ||
20 | |||
21 | module.exports = RecipeSchema; | ||
diff --git a/src/server/database/migrations/1566554359294_workspace_schema.js b/src/server/database/migrations/1566554359294_workspace_schema.js new file mode 100644 index 000000000..b53bbe656 --- /dev/null +++ b/src/server/database/migrations/1566554359294_workspace_schema.js | |||
@@ -0,0 +1,23 @@ | |||
1 | |||
2 | /** @type {import('@adonisjs/lucid/src/Schema')} */ | ||
3 | const Schema = use('Schema'); | ||
4 | |||
5 | class WorkspaceSchema extends Schema { | ||
6 | up() { | ||
7 | this.create('workspaces', (table) => { | ||
8 | table.increments(); | ||
9 | table.string('workspaceId', 80).notNullable().unique(); | ||
10 | table.string('name', 80).notNullable(); | ||
11 | table.integer('order'); | ||
12 | table.json('services'); | ||
13 | table.json('data'); | ||
14 | table.timestamps(); | ||
15 | }); | ||
16 | } | ||
17 | |||
18 | down() { | ||
19 | this.drop('workspaces'); | ||
20 | } | ||
21 | } | ||
22 | |||
23 | module.exports = WorkspaceSchema; | ||
diff --git a/src/server/database/template.sqlite b/src/server/database/template.sqlite new file mode 100644 index 000000000..db5425ee6 --- /dev/null +++ b/src/server/database/template.sqlite | |||
Binary files differ | |||
diff --git a/src/server/env.ini b/src/server/env.ini new file mode 100644 index 000000000..902e8e4c8 --- /dev/null +++ b/src/server/env.ini | |||
@@ -0,0 +1,16 @@ | |||
1 | HOST=127.0.0.1 | ||
2 | PORT=45569 | ||
3 | NODE_ENV=development | ||
4 | APP_NAME=Ferdi Internal Server | ||
5 | APP_URL=http://${HOST}:${PORT} | ||
6 | CACHE_VIEWS=false | ||
7 | APP_KEY=FERDIINTERNALSERVER | ||
8 | DB_CONNECTION=sqlite | ||
9 | DB_HOST=127.0.0.1 | ||
10 | DB_PORT=3306 | ||
11 | DB_USER=root | ||
12 | DB_PASSWORD= | ||
13 | DB_DATABASE=ferdi | ||
14 | HASH_DRIVER=bcrypt | ||
15 | IS_CREATION_ENABLED=true | ||
16 | CONNECT_WITH_FRANZ=true \ No newline at end of file | ||
diff --git a/src/server/logo.png b/src/server/logo.png new file mode 100644 index 000000000..587e0b86e --- /dev/null +++ b/src/server/logo.png | |||
Binary files differ | |||
diff --git a/src/server/public/css/main.css b/src/server/public/css/main.css new file mode 100644 index 000000000..a1c5653d7 --- /dev/null +++ b/src/server/public/css/main.css | |||
@@ -0,0 +1,69 @@ | |||
1 | input { | ||
2 | margin-bottom: 1rem; | ||
3 | width: 100%; | ||
4 | padding: 0.5rem; | ||
5 | } | ||
6 | |||
7 | button, .button { | ||
8 | display: flex; | ||
9 | overflow: hidden; | ||
10 | padding: 12px 12px; | ||
11 | cursor: pointer; | ||
12 | width: 100%; | ||
13 | -webkit-user-select: none; | ||
14 | -moz-user-select: none; | ||
15 | -ms-user-select: none; | ||
16 | user-select: none; | ||
17 | transition: all 150ms linear; | ||
18 | text-align: center; | ||
19 | white-space: nowrap; | ||
20 | text-decoration: none !important; | ||
21 | text-transform: none; | ||
22 | text-transform: capitalize; | ||
23 | color: #fff !important; | ||
24 | border: 0 none; | ||
25 | border-radius: 4px; | ||
26 | font-size: 13px; | ||
27 | font-weight: 500; | ||
28 | line-height: 1.3; | ||
29 | -webkit-appearance: none; | ||
30 | -moz-appearance: none; | ||
31 | appearance: none; | ||
32 | justify-content: center; | ||
33 | align-items: center; | ||
34 | flex: 0 0 160px; | ||
35 | box-shadow: 2px 5px 10px #e4e4e4; | ||
36 | color: #FFFFFF; | ||
37 | background: #161616; | ||
38 | } | ||
39 | |||
40 | #dropzone { | ||
41 | width: 100%; | ||
42 | height: 30vh; | ||
43 | background-color: #ebebeb; | ||
44 | |||
45 | display: flex; | ||
46 | align-items: center; | ||
47 | justify-content: center; | ||
48 | text-align: center; | ||
49 | |||
50 | cursor: pointer; | ||
51 | } | ||
52 | |||
53 | #dropzone p { | ||
54 | font-size: 0.85rem; | ||
55 | } | ||
56 | |||
57 | #files { | ||
58 | display: none; | ||
59 | } | ||
60 | |||
61 | .alert { | ||
62 | background-color: #e7a8a6; | ||
63 | padding: 0.8rem; | ||
64 | margin-bottom: 1rem; | ||
65 | } | ||
66 | |||
67 | td { | ||
68 | word-break: break-all; | ||
69 | } \ No newline at end of file | ||
diff --git a/src/server/public/css/vanilla.css b/src/server/public/css/vanilla.css new file mode 100644 index 000000000..37bc051a2 --- /dev/null +++ b/src/server/public/css/vanilla.css | |||
@@ -0,0 +1,138 @@ | |||
1 | /* Reset */ | ||
2 | html, body, div, span, applet, object, iframe, | ||
3 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, | ||
4 | a, abbr, acronym, address, big, cite, code, | ||
5 | del, dfn, em, img, ins, kbd, q, s, samp, | ||
6 | small, strike, strong, sub, sup, tt, var, | ||
7 | b, u, i, center, | ||
8 | dl, dt, dd, ol, ul, li, | ||
9 | fieldset, form, label, legend, | ||
10 | table, caption, tbody, tfoot, thead, tr, th, td, | ||
11 | article, aside, canvas, details, embed, | ||
12 | figure, figcaption, footer, header, hgroup, | ||
13 | menu, nav, output, ruby, section, summary, | ||
14 | time, mark, audio, video { | ||
15 | margin: 0; | ||
16 | padding: 0; | ||
17 | border: 0; | ||
18 | font-size: 100%; | ||
19 | font: inherit; | ||
20 | vertical-align: baseline; | ||
21 | } | ||
22 | * { | ||
23 | box-sizing: border-box; | ||
24 | } | ||
25 | |||
26 | |||
27 | |||
28 | /* Variables */ | ||
29 | :root { | ||
30 | --desktop-font-size: 1.3rem/1.5; | ||
31 | --mobile-font-size: 1.1rem/1.4; | ||
32 | --text-color: #2d2d2d; | ||
33 | --link-color: blue; | ||
34 | --primary-color: lightsteelblue; | ||
35 | --secondary-color: aliceblue; | ||
36 | --tertiary-color: whitesmoke; | ||
37 | } | ||
38 | |||
39 | |||
40 | |||
41 | |||
42 | /* Typography */ | ||
43 | body { | ||
44 | color: var(--text-color); | ||
45 | padding: 3rem; | ||
46 | font: var(--desktop-font-size) -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto, Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji", "Segoe UI Symbol"; | ||
47 | } | ||
48 | |||
49 | h1,h2,h3,h4,h5,h6,p,blockquote,dl,img,figure { | ||
50 | margin: 2rem 0; | ||
51 | } | ||
52 | |||
53 | h1,h2,h3,h4,h5,h6 { font-weight: bold; } | ||
54 | h1 { font-size: 200%; } | ||
55 | h2 { font-size: 150%; } | ||
56 | h3 { font-size: 120%; } | ||
57 | h4,h5,h6 { font-size: 100%; } | ||
58 | h5, h6 { text-transform: uppercase; } | ||
59 | |||
60 | header h1 { border-bottom: 1px solid; } | ||
61 | |||
62 | p { margin: 2rem 0; } | ||
63 | |||
64 | a,a:visited { color: var(--link-color); } | ||
65 | |||
66 | strong, time, b { font-weight: bold; } | ||
67 | em, dfn, i { font-style: italic; } | ||
68 | sub { font-size: 60%; vertical-align: bottom; } | ||
69 | small { font-size: 80%; } | ||
70 | |||
71 | blockquote, q { | ||
72 | background: var(--secondary-color); | ||
73 | border-left: 10px solid var(--primary-color); | ||
74 | font-family: "Georgia", serif; | ||
75 | padding: 1rem; | ||
76 | } | ||
77 | blockquote p:first-child { margin-top: 0; } | ||
78 | cite { | ||
79 | font-family: "Georgia", serif; | ||
80 | font-style: italic; | ||
81 | font-weight: bold; | ||
82 | } | ||
83 | |||
84 | kbd,code,samp,pre,var { font-family: monospace; font-weight: bold; } | ||
85 | code, pre { | ||
86 | background: var(--tertiary-color); | ||
87 | padding: 0.5rem 1rem; | ||
88 | } | ||
89 | code pre , pre code { padding: 0; } | ||
90 | |||
91 | |||
92 | |||
93 | /* Elements */ | ||
94 | hr { | ||
95 | background: var(--text-color); | ||
96 | border: 0; | ||
97 | height: 1px; | ||
98 | margin: 4rem 0; | ||
99 | } | ||
100 | |||
101 | img { max-width: 100%; } | ||
102 | |||
103 | figure { | ||
104 | border: 1px solid var(--primary-color); | ||
105 | display: inline-block; | ||
106 | padding: 1rem; | ||
107 | width: auto; | ||
108 | } | ||
109 | figure img { margin: 0; } | ||
110 | figure figcaption { font-size: 80%; } | ||
111 | |||
112 | ul, ol { margin: 2rem 0; padding: 0 0 0 4rem; } | ||
113 | |||
114 | dl dd { padding-left: 2rem; } | ||
115 | |||
116 | table { | ||
117 | border: 1px solid var(--primary-color); | ||
118 | border-collapse: collapse; | ||
119 | table-layout: fixed; | ||
120 | width: 100%; | ||
121 | } | ||
122 | table caption { margin: 2rem 0; } | ||
123 | table thead { text-align: center; } | ||
124 | table tbody { text-align: right; } | ||
125 | table tr { border-bottom: 1px solid var(--primary-color); } | ||
126 | table tbody tr:nth-child(even) { background: var(--tertiary-color); } | ||
127 | table th { background: var(--secondary-color); font-weight: bold; } | ||
128 | table th, table td { padding: 1rem; } | ||
129 | table th:not(last-of-type), table td:not(last-of-type) { border-right: 1px solid var(--primary-color); } | ||
130 | |||
131 | |||
132 | |||
133 | /* Mobile Styling */ | ||
134 | @media screen and (max-width: 50rem) { | ||
135 | body { | ||
136 | font: var(--mobile-font-size) -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto, Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji", "Segoe UI Symbol" | ||
137 | } | ||
138 | } \ No newline at end of file | ||
diff --git a/src/server/public/js/new.js b/src/server/public/js/new.js new file mode 100644 index 000000000..beaa36da6 --- /dev/null +++ b/src/server/public/js/new.js | |||
@@ -0,0 +1,24 @@ | |||
1 | /* eslint-env browser */ | ||
2 | const elDrop = document.getElementById('dropzone'); | ||
3 | const submitBtn = document.getElementById('submitbutton'); | ||
4 | const fileInput = document.getElementById('files'); | ||
5 | |||
6 | elDrop.addEventListener('dragover', (event) => { | ||
7 | event.preventDefault(); | ||
8 | }); | ||
9 | |||
10 | elDrop.addEventListener('drop', async (event) => { | ||
11 | event.preventDefault(); | ||
12 | |||
13 | submitBtn.disabled = true; | ||
14 | |||
15 | fileInput.files = event.dataTransfer.files; | ||
16 | |||
17 | elDrop.innerText = `✓ ${fileInput.files.length} files selected`; | ||
18 | elDrop.style.height = 'inherit'; | ||
19 | |||
20 | submitBtn.disabled = false; | ||
21 | }); | ||
22 | elDrop.addEventListener('click', () => { | ||
23 | fileInput.click(); | ||
24 | }); | ||
diff --git a/src/server/resources/views/layouts/main.edge b/src/server/resources/views/layouts/main.edge new file mode 100644 index 000000000..b6cc8ca23 --- /dev/null +++ b/src/server/resources/views/layouts/main.edge | |||
@@ -0,0 +1,18 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html lang="en"> | ||
3 | |||
4 | <head> | ||
5 | <meta charset="UTF-8"> | ||
6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
7 | <meta http-equiv="X-UA-Compatible" content="ie=edge"> | ||
8 | <title>ferdi-server</title> | ||
9 | |||
10 | {{ style('css/vanilla') }} | ||
11 | {{ style('css/main') }} | ||
12 | </head> | ||
13 | |||
14 | <body> | ||
15 | @!section('content') | ||
16 | </body> | ||
17 | |||
18 | </html> | ||
diff --git a/src/server/resources/views/others/import.edge b/src/server/resources/views/others/import.edge new file mode 100644 index 000000000..b890bbb2a --- /dev/null +++ b/src/server/resources/views/others/import.edge | |||
@@ -0,0 +1,19 @@ | |||
1 | @layout('layouts.main') | ||
2 | |||
3 | @section('content') | ||
4 | <h1>Import a Franz account</h1> | ||
5 | <p>Please login using your Franz account. We will create a new Ferdi account with the same credentials.</p> | ||
6 | <form action="import" method="post"> | ||
7 | <label for="email">E-Mail address</label><br /> | ||
8 | <input type="email" name="email" placeholder="joe@example.com" required><br /> | ||
9 | |||
10 | <label for="password">Password</label><br /> | ||
11 | <input type="password" name="password" placeholder="********" required><br /> | ||
12 | |||
13 | <button type="submit" id="submitbutton">Import Franz account</button> | ||
14 | <small> | ||
15 | By importing your Franz account, you accept the <a href="/terms">Terms of service</a> and <a href="/privacy">Privacy | ||
16 | policy</a> | ||
17 | </small> | ||
18 | </form> | ||
19 | @endsection | ||
diff --git a/src/server/resources/views/others/index.edge b/src/server/resources/views/others/index.edge new file mode 100644 index 000000000..c594d3142 --- /dev/null +++ b/src/server/resources/views/others/index.edge | |||
@@ -0,0 +1,38 @@ | |||
1 | @layout('layouts.main') | ||
2 | |||
3 | @section('content') | ||
4 | <style> | ||
5 | ol, | ||
6 | p { | ||
7 | margin: 0.5rem 0; | ||
8 | } | ||
9 | |||
10 | </style> | ||
11 | <h1>ferdi-server</h1> | ||
12 | <p>You are accessing a custom <a href="https://github.com/kytwb/ferdi">Ferdi</a> server.</p> | ||
13 | <p> | ||
14 | To use this server in your Ferdi client, <a href="ferdi://settings/app">open Ferdi's settings</a> and as the | ||
15 | <code>server</code>, enter <code id="server"></code> | ||
16 | </p> | ||
17 | <p> | ||
18 | Alternatively, you can manage your account in the <a href="/user/account">account dashboard</a>. | ||
19 | </p> | ||
20 | |||
21 | <br /> | ||
22 | <small> | ||
23 | <a href="https://github.com/vantezzen/ferdi-server">ferdi-server</a> is a project by <a | ||
24 | href="https://vantezzen.io">vantezzen</a>. | ||
25 | </small> | ||
26 | |||
27 | <script> | ||
28 | // Get server URL for current location | ||
29 | let server = location.href.replace('/index.html', ''); | ||
30 | if (server[server.length - 1] == '/') { | ||
31 | server = server.substr(0, server.length - 1) | ||
32 | } | ||
33 | |||
34 | // Show on page | ||
35 | document.getElementById('server').innerText = server; | ||
36 | |||
37 | </script> | ||
38 | @endsection | ||
diff --git a/src/server/resources/views/others/new.edge b/src/server/resources/views/others/new.edge new file mode 100644 index 000000000..1b54558fc --- /dev/null +++ b/src/server/resources/views/others/new.edge | |||
@@ -0,0 +1,40 @@ | |||
1 | @layout('layouts.main') | ||
2 | |||
3 | @section('content') | ||
4 | <h1>Create a new recipe</h1> | ||
5 | <p>Please create a recipe using <a href="https://github.com/meetfranz/plugins/blob/master/docs/integration.md">the | ||
6 | official Franz guide</a>, then publish it here.</p> | ||
7 | <form action="new" method="post" enctype="multipart/form-data"> | ||
8 | <label for="author">Author</label><br /> | ||
9 | <input type="text" name="author" placeholder="Jon Doe" required><br /> | ||
10 | |||
11 | <label for="name">Name</label><br /> | ||
12 | <input type="text" name="name" placeholder="Sample Service" required><br /> | ||
13 | |||
14 | <label for="id">Service ID</label><br /> | ||
15 | <input type="text" name="id" placeholder="sample-service" required><br /> | ||
16 | |||
17 | <label for="png">Link to PNG image*</label><br /> | ||
18 | <input type="text" name="png" placeholder="https://.../logo.png" required><br /> | ||
19 | |||
20 | <label for="svg">Link to SVG image*</label><br /> | ||
21 | <input type="text" name="svg" placeholder="https://.../logo.svg" required><br /> | ||
22 | *These images must be publicly availible and have CORS enabled in order to work.<br /><br /> | ||
23 | |||
24 | <label for="package">Recipe files</label><br /> | ||
25 | <div id="dropzone" effectAllowed="move"> | ||
26 | <div> | ||
27 | Drop recipe files here<br />or click here to select files | ||
28 | <p> | ||
29 | Drag and drop your recipe files into this area.<br /> | ||
30 | Please do not select the folder that contains the files but rather the files itself. | ||
31 | </p> | ||
32 | </div> | ||
33 | </div> | ||
34 | <input type="file" name="files[]" id="files" value="" multiple required><br /><br /> | ||
35 | |||
36 | <button type="submit" id="submitbutton">Create recipe</button> | ||
37 | </form> | ||
38 | |||
39 | <script src="js/new.js"></script> | ||
40 | @endsection | ||
diff --git a/src/server/start.js b/src/server/start.js new file mode 100644 index 000000000..8a8711a78 --- /dev/null +++ b/src/server/start.js | |||
@@ -0,0 +1,40 @@ | |||
1 | |||
2 | /* | ||
3 | |-------------------------------------------------------------------------- | ||
4 | | Http server | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | | ||
7 | | This file bootstraps Adonisjs to start the HTTP server. You are free to | ||
8 | | customize the process of booting the http server. | ||
9 | | | ||
10 | | """ Loading ace commands """ | ||
11 | | At times you may want to load ace commands when starting the HTTP server. | ||
12 | | Same can be done by chaining `loadCommands()` method after | ||
13 | | | ||
14 | | """ Preloading files """ | ||
15 | | Also you can preload files by calling `preLoad('path/to/file')` method. | ||
16 | | Make sure to pass a relative path from the project root. | ||
17 | */ | ||
18 | const path = require('path'); | ||
19 | const fs = require('fs-extra'); | ||
20 | // eslint-disable-next-line import/no-extraneous-dependencies | ||
21 | const { app } = require('electron'); | ||
22 | |||
23 | process.env.ENV_PATH = path.join(__dirname, 'env.ini'); | ||
24 | |||
25 | // Make sure local database exists | ||
26 | const dbPath = path.join(app.getPath('userData'), 'server.sqlite'); | ||
27 | if (!fs.existsSync(dbPath)) { | ||
28 | fs.copySync( | ||
29 | path.join(__dirname, 'database', 'template.sqlite'), | ||
30 | dbPath, | ||
31 | ); | ||
32 | } | ||
33 | |||
34 | const { Ignitor } = require('@adonisjs/ignitor'); | ||
35 | const fold = require('@adonisjs/fold'); | ||
36 | |||
37 | new Ignitor(fold) | ||
38 | .appRoot(__dirname) | ||
39 | .fireHttpServer() | ||
40 | .catch(console.error); // eslint-disable-line no-console | ||
diff --git a/src/server/start/app.js b/src/server/start/app.js new file mode 100644 index 000000000..a29ca6594 --- /dev/null +++ b/src/server/start/app.js | |||
@@ -0,0 +1,62 @@ | |||
1 | |||
2 | /* | ||
3 | |-------------------------------------------------------------------------- | ||
4 | | Providers | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | | ||
7 | | Providers are building blocks for your Adonis app. Anytime you install | ||
8 | | a new Adonis specific package, chances are you will register the | ||
9 | | provider here. | ||
10 | | | ||
11 | */ | ||
12 | const providers = [ | ||
13 | '@adonisjs/framework/providers/AppProvider', | ||
14 | '@adonisjs/bodyparser/providers/BodyParserProvider', | ||
15 | '@adonisjs/cors/providers/CorsProvider', | ||
16 | '@adonisjs/lucid/providers/LucidProvider', | ||
17 | '@adonisjs/drive/providers/DriveProvider', | ||
18 | '@adonisjs/validator/providers/ValidatorProvider', | ||
19 | '@adonisjs/framework/providers/ViewProvider', | ||
20 | '@adonisjs/shield/providers/ShieldProvider', | ||
21 | ]; | ||
22 | |||
23 | /* | ||
24 | |-------------------------------------------------------------------------- | ||
25 | | Ace Providers | ||
26 | |-------------------------------------------------------------------------- | ||
27 | | | ||
28 | | Ace providers are required only when running ace commands. For example | ||
29 | | Providers for migrations, tests etc. | ||
30 | | | ||
31 | */ | ||
32 | const aceProviders = [ | ||
33 | '@adonisjs/lucid/providers/MigrationsProvider', | ||
34 | ]; | ||
35 | |||
36 | /* | ||
37 | |-------------------------------------------------------------------------- | ||
38 | | Aliases | ||
39 | |-------------------------------------------------------------------------- | ||
40 | | | ||
41 | | Aliases are short unique names for IoC container bindings. You are free | ||
42 | | to create your own aliases. | ||
43 | | | ||
44 | | For example: | ||
45 | | { Route: 'Adonis/Src/Route' } | ||
46 | | | ||
47 | */ | ||
48 | const aliases = {}; | ||
49 | |||
50 | /* | ||
51 | |-------------------------------------------------------------------------- | ||
52 | | Commands | ||
53 | |-------------------------------------------------------------------------- | ||
54 | | | ||
55 | | Here you store ace commands for your package | ||
56 | | | ||
57 | */ | ||
58 | const commands = []; | ||
59 | |||
60 | module.exports = { | ||
61 | providers, aceProviders, aliases, commands, | ||
62 | }; | ||
diff --git a/src/server/start/kernel.js b/src/server/start/kernel.js new file mode 100644 index 000000000..54fe1f35d --- /dev/null +++ b/src/server/start/kernel.js | |||
@@ -0,0 +1,56 @@ | |||
1 | |||
2 | /** @type {import('@adonisjs/framework/src/Server')} */ | ||
3 | const Server = use('Server'); | ||
4 | |||
5 | /* | ||
6 | |-------------------------------------------------------------------------- | ||
7 | | Global Middleware | ||
8 | |-------------------------------------------------------------------------- | ||
9 | | | ||
10 | | Global middleware are executed on each http request only when the routes | ||
11 | | match. | ||
12 | | | ||
13 | */ | ||
14 | const globalMiddleware = [ | ||
15 | 'Adonis/Middleware/BodyParser', | ||
16 | 'App/Middleware/ConvertEmptyStringsToNull', | ||
17 | ]; | ||
18 | |||
19 | /* | ||
20 | |-------------------------------------------------------------------------- | ||
21 | | Named Middleware | ||
22 | |-------------------------------------------------------------------------- | ||
23 | | | ||
24 | | Named middleware is key/value object to conditionally add middleware on | ||
25 | | specific routes or group of routes. | ||
26 | | | ||
27 | | // define | ||
28 | | { | ||
29 | | auth: 'Adonis/Middleware/Auth' | ||
30 | | } | ||
31 | | | ||
32 | | // use | ||
33 | | Route.get().middleware('auth') | ||
34 | | | ||
35 | */ | ||
36 | const namedMiddleware = { | ||
37 | }; | ||
38 | |||
39 | /* | ||
40 | |-------------------------------------------------------------------------- | ||
41 | | Server Middleware | ||
42 | |-------------------------------------------------------------------------- | ||
43 | | | ||
44 | | Server level middleware are executed even when route for a given URL is | ||
45 | | not registered. Features like `static assets` and `cors` needs better | ||
46 | | control over request lifecycle. | ||
47 | | | ||
48 | */ | ||
49 | const serverMiddleware = [ | ||
50 | 'Adonis/Middleware/Static', | ||
51 | ]; | ||
52 | |||
53 | Server | ||
54 | .registerGlobal(globalMiddleware) | ||
55 | .registerNamed(namedMiddleware) | ||
56 | .use(serverMiddleware); | ||
diff --git a/src/server/start/routes.js b/src/server/start/routes.js new file mode 100644 index 000000000..f3896bfa3 --- /dev/null +++ b/src/server/start/routes.js | |||
@@ -0,0 +1,74 @@ | |||
1 | |||
2 | /* | ||
3 | |-------------------------------------------------------------------------- | ||
4 | | Routes | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | | ||
7 | */ | ||
8 | |||
9 | /** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ | ||
10 | const Route = use('Route'); | ||
11 | const Env = use('Env'); | ||
12 | |||
13 | // Health: Returning if all systems function correctly | ||
14 | Route.get('health', ({ | ||
15 | response, | ||
16 | }) => response.send({ | ||
17 | api: 'success', | ||
18 | db: 'success', | ||
19 | })); | ||
20 | |||
21 | // API is grouped under '/v1/' route | ||
22 | Route.group(() => { | ||
23 | // User authentification | ||
24 | Route.post('auth/signup', 'UserController.signup'); | ||
25 | Route.post('auth/login', 'UserController.login'); | ||
26 | |||
27 | // User info | ||
28 | Route.get('me', 'UserController.me'); | ||
29 | |||
30 | // Service info | ||
31 | Route.post('service', 'ServiceController.create'); | ||
32 | Route.put('service/:id', 'ServiceController.edit'); | ||
33 | Route.delete('service/:id', 'ServiceController.delete'); | ||
34 | Route.get('me/services', 'ServiceController.list'); | ||
35 | Route.put('service/reorder', 'ServiceController.reorder'); | ||
36 | Route.get('recipe', 'ServiceController.list'); | ||
37 | Route.post('recipes/update', 'ServiceController.update'); | ||
38 | |||
39 | // Recipe store | ||
40 | Route.get('recipes', 'RecipeController.list'); | ||
41 | Route.get('recipes/download/:recipe', 'RecipeController.download'); | ||
42 | Route.get('recipes/search', 'RecipeController.search'); | ||
43 | Route.get('recipes/popular', 'StaticController.popularRecipes'); | ||
44 | Route.get('recipes/update', 'StaticController.emptyArray'); | ||
45 | |||
46 | // Workspaces | ||
47 | Route.put('workspace/:id', 'WorkspaceController.edit'); | ||
48 | Route.delete('workspace/:id', 'WorkspaceController.delete'); | ||
49 | Route.post('workspace', 'WorkspaceController.create'); | ||
50 | Route.get('workspace', 'WorkspaceController.list'); | ||
51 | |||
52 | // Static responses | ||
53 | Route.get('features', 'StaticController.features'); | ||
54 | Route.get('services', 'StaticController.emptyArray'); | ||
55 | Route.get('news', 'StaticController.emptyArray'); | ||
56 | Route.get('payment/plans', 'StaticController.plans'); | ||
57 | Route.get('announcements/:version', 'StaticController.announcement'); | ||
58 | }).prefix('v1'); | ||
59 | |||
60 | // Recipe creation | ||
61 | Route.post('new', 'RecipeController.create'); | ||
62 | Route.get('new', ({ response, view }) => { | ||
63 | if (Env.get('IS_CREATION_ENABLED') == 'false') { // eslint-disable-line eqeqeq | ||
64 | return response.send('This server doesn\'t allow the creation of new recipes.\n\nIf you are the server owner, please set IS_CREATION_ENABLED to true to enable recipe creation.'); | ||
65 | } | ||
66 | return view.render('others.new'); | ||
67 | }); | ||
68 | |||
69 | // Franz account import | ||
70 | Route.post('import', 'UserController.import'); | ||
71 | Route.get('import', ({ view }) => view.render('others.import')); | ||
72 | |||
73 | // Index | ||
74 | Route.get('/', ({ view }) => view.render('others.index')); | ||