diff options
author | Vijay Raghavan Aravamudhan <vraravam@users.noreply.github.com> | 2021-08-01 11:07:57 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-01 16:37:57 +0530 |
commit | 419933f6505caf4c5e685f8436b1ff735185e55a (patch) | |
tree | 152dcb9d2b35d29f862cc57a605b9ae2a0f7c300 | |
parent | Removed duplicated contributors badge. (diff) | |
download | ferdium-app-419933f6505caf4c5e685f8436b1ff735185e55a.tar.gz ferdium-app-419933f6505caf4c5e685f8436b1ff735185e55a.tar.zst ferdium-app-419933f6505caf4c5e685f8436b1ff735185e55a.zip |
Moved 'internal-server' into a sub-folder as opposed to a git submodule. (#1715)
* Ignored tests in 'internal-server' folder since there are none.
* Linter fixes
58 files changed, 3071 insertions, 20 deletions
diff --git a/.dockerignore b/.dockerignore index 4b3cd60be..d1ed260c9 100644 --- a/.dockerignore +++ b/.dockerignore | |||
@@ -25,3 +25,11 @@ recipes/archives | |||
25 | tmp-out/ | 25 | tmp-out/ |
26 | uidev/lib | 26 | uidev/lib |
27 | docs/ | 27 | docs/ |
28 | src/internal-server/database/tmp/ | ||
29 | src/internal-server/database/development.sqlite | ||
30 | src/internal-server/database/adonis.sqlite | ||
31 | src/internal-server/recipes/ | ||
32 | src/internal-server/public/terms.html | ||
33 | src/internal-server/public/privacy.html | ||
34 | src/internal-server/user_data/ | ||
35 | **/.env | ||
diff --git a/.eslintignore b/.eslintignore index 5385bbb5f..9913d7272 100644 --- a/.eslintignore +++ b/.eslintignore | |||
@@ -2,5 +2,4 @@ build/ | |||
2 | out/ | 2 | out/ |
3 | packages/*/lib | 3 | packages/*/lib |
4 | packages/**/*.test.* | 4 | packages/**/*.test.* |
5 | src/internal-server | ||
6 | recipes/ | 5 | recipes/ |
@@ -88,7 +88,8 @@ | |||
88 | "FormData": true, | 88 | "FormData": true, |
89 | "localStorage": true, | 89 | "localStorage": true, |
90 | "navigator": true, | 90 | "navigator": true, |
91 | "Element": true | 91 | "Element": true, |
92 | "use": true | ||
92 | }, | 93 | }, |
93 | "env": { | 94 | "env": { |
94 | "jest/globals": true | 95 | "jest/globals": true |
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..dfe077042 --- /dev/null +++ b/.gitattributes | |||
@@ -0,0 +1,2 @@ | |||
1 | # Auto detect text files and perform LF normalization | ||
2 | * text=auto | ||
diff --git a/.gitignore b/.gitignore index 7be0f2a33..f05e9220c 100644 --- a/.gitignore +++ b/.gitignore | |||
@@ -13,3 +13,23 @@ lerna-debug.log | |||
13 | uidev/lib | 13 | uidev/lib |
14 | *.tsbuildinfo | 14 | *.tsbuildinfo |
15 | server*.log | 15 | server*.log |
16 | |||
17 | # These entries have been merged from the git submodule repo 'internal-server' | ||
18 | |||
19 | # Adonis directory for storing tmp files | ||
20 | /src/internal-server/tmp | ||
21 | |||
22 | # Environment variables, never commit this file | ||
23 | /src/internal-server/.env | ||
24 | |||
25 | # The development sqlite file | ||
26 | /src/internal-server/database/development.sqlite | ||
27 | /src/internal-server/database/adonis.sqlite | ||
28 | |||
29 | # Uploaded recipes | ||
30 | /src/internal-server/recipes/ | ||
31 | |||
32 | /src/internal-server/public/terms.html | ||
33 | /src/internal-server/public/privacy.html | ||
34 | |||
35 | /src/internal-server/user_data/ | ||
diff --git a/.gitmodules b/.gitmodules index 81b6b6246..02804b22f 100644 --- a/.gitmodules +++ b/.gitmodules | |||
@@ -3,8 +3,3 @@ | |||
3 | url = https://github.com/getferdi/recipes.git | 3 | url = https://github.com/getferdi/recipes.git |
4 | ignore = all | 4 | ignore = all |
5 | branch = master | 5 | branch = master |
6 | [submodule "src/internal-server"] | ||
7 | path = src/internal-server | ||
8 | url = https://github.com/getferdi/internal-server.git | ||
9 | ignore = all | ||
10 | branch = master | ||
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a95dbbc8f..1c3fb977f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md | |||
@@ -113,7 +113,7 @@ cd ferdi | |||
113 | git submodule update --init --recursive | 113 | git submodule update --init --recursive |
114 | ``` | 114 | ``` |
115 | 115 | ||
116 | It is important you execute the last command to get the required submodules (recipes, src/internal-server). | 116 | It is important you execute the last command to get the required submodules (recipes). |
117 | 117 | ||
118 | ### Local caching of dependencies | 118 | ### Local caching of dependencies |
119 | 119 | ||
diff --git a/INTERNAL_SERVER.md b/INTERNAL_SERVER.md new file mode 100644 index 000000000..66bb3fcb6 --- /dev/null +++ b/INTERNAL_SERVER.md | |||
@@ -0,0 +1,38 @@ | |||
1 | <p align="center"> | ||
2 | <img src="./src/internal-server/public/images/logo.png" alt="" width="300"/> | ||
3 | </p> | ||
4 | |||
5 | # ferdi-internal-server | ||
6 | Internal Ferdi Server used for storing settings/preferences without logging into an external server. | ||
7 | |||
8 | ## Differences to ferdi-server | ||
9 | - Doesn't contain user management (only one user) | ||
10 | - Doesn't require logging in | ||
11 | - No recipe creation | ||
12 | - Contains `start.js` script to allow starting the server via script | ||
13 | - Uses `env.ini` instead of `.env` to stay compatible with Ferdi's build script | ||
14 | - Only allows Ferdi clients to connect to the API | ||
15 | |||
16 | ## Configuration | ||
17 | franz-server's configuration is saved inside the `env.ini` file. Besides AdonisJS's settings, `ferdi-internal-server` has the following custom settings: | ||
18 | - `CONNECT_WITH_FRANZ` (`true` or `false`, default: `true`): Whether to enable connections to the Franz server. By enabling this option, ferdi-internal-server can: | ||
19 | - Show the full Franz recipe library instead of only custom recipes | ||
20 | - Import Franz accounts | ||
21 | |||
22 | ## Importing your Franz account | ||
23 | `ferdi-internal-server` allows you to import your full Franz account, including all its settings. | ||
24 | |||
25 | To import your Franz account, open `http://localhost:45569/import` in your browser and login using your Franz account details. `ferdi-internal-server` will create a new user with the same credentials and copy your Franz settings, services and workspaces. | ||
26 | |||
27 | ## Development | ||
28 | |||
29 | You can locally develop `ferdi-internal-server` outside of Ferdi. | ||
30 | |||
31 | 1. Start the local server via | ||
32 | ```bash | ||
33 | npm run start:server | ||
34 | ``` | ||
35 | 2. Change Ferdi's server to `http://localhost:45568` to start using the local test server. | ||
36 | |||
37 | ## Note For previous contributors | ||
38 | For anyone who has *previously* setup Ferdi for development, you will need to unregister the `src/internal-server` from being a git submodule. You can do this by following the steps outlined [here](https://www.w3docs.com/snippets/git/how-to-remove-a-git-submodule.html) | ||
diff --git a/jest.config.js b/jest.config.js index cca24440f..406c9c6d1 100644 --- a/jest.config.js +++ b/jest.config.js | |||
@@ -3,6 +3,7 @@ module.exports = { | |||
3 | testPathIgnorePatterns: [ | 3 | testPathIgnorePatterns: [ |
4 | 'node_modules/', | 4 | 'node_modules/', |
5 | 'recipes/', | 5 | 'recipes/', |
6 | // TODO: Need to unignore tests | ||
6 | 'src/internal-server', | 7 | 'src/internal-server', |
7 | ] | 8 | ], |
8 | }; | 9 | }; |
diff --git a/package-lock.json b/package-lock.json index 3c7da1de1..ff64c1610 100644 --- a/package-lock.json +++ b/package-lock.json | |||
@@ -27076,19 +27076,19 @@ | |||
27076 | "dev": true | 27076 | "dev": true |
27077 | }, | 27077 | }, |
27078 | "sqlite3": { | 27078 | "sqlite3": { |
27079 | "version": "5.0.2", | 27079 | "version": "5.0.0", |
27080 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz", | 27080 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.0.tgz", |
27081 | "integrity": "sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==", | 27081 | "integrity": "sha512-rjvqHFUaSGnzxDy2AHCwhHy6Zp6MNJzCPGYju4kD8yi6bze4d1/zMTg6C7JI49b7/EM7jKMTvyfN/4ylBKdwfw==", |
27082 | "requires": { | 27082 | "requires": { |
27083 | "node-addon-api": "^3.0.0", | 27083 | "node-addon-api": "2.0.0", |
27084 | "node-gyp": "3.x", | 27084 | "node-gyp": "3.x", |
27085 | "node-pre-gyp": "^0.11.0" | 27085 | "node-pre-gyp": "^0.11.0" |
27086 | }, | 27086 | }, |
27087 | "dependencies": { | 27087 | "dependencies": { |
27088 | "node-addon-api": { | 27088 | "node-addon-api": { |
27089 | "version": "3.2.1", | 27089 | "version": "2.0.0", |
27090 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", | 27090 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz", |
27091 | "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" | 27091 | "integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==" |
27092 | } | 27092 | } |
27093 | } | 27093 | } |
27094 | }, | 27094 | }, |
diff --git a/package.json b/package.json index e03a2a4c6..6a012d250 100644 --- a/package.json +++ b/package.json | |||
@@ -42,7 +42,8 @@ | |||
42 | "prepare-code": "npm run lint && npm run reformat-files && npm run manage-translations && npm run apply-branding", | 42 | "prepare-code": "npm run lint && npm run reformat-files && npm run manage-translations && npm run apply-branding", |
43 | "build-theme-info": "node src/scripts/build-theme-info.js", | 43 | "build-theme-info": "node src/scripts/build-theme-info.js", |
44 | "link-readme": "node src/scripts/link-readme.js", | 44 | "link-readme": "node src/scripts/link-readme.js", |
45 | "minify-images": "./scripts/minify-images.sh" | 45 | "minify-images": "./scripts/minify-images.sh", |
46 | "start:server": "node src/internal-server/test.js" | ||
46 | }, | 47 | }, |
47 | "keywords": [], | 48 | "keywords": [], |
48 | "author": "Amine Mouafik <amine@mouafik.fr>", | 49 | "author": "Amine Mouafik <amine@mouafik.fr>", |
@@ -129,7 +130,7 @@ | |||
129 | "route-parser": "0.0.5", | 130 | "route-parser": "0.0.5", |
130 | "semver": "7.3.5", | 131 | "semver": "7.3.5", |
131 | "smoothscroll-polyfill": "0.4.4", | 132 | "smoothscroll-polyfill": "0.4.4", |
132 | "sqlite3": "5.0.2", | 133 | "sqlite3": "5.0.0", |
133 | "tar": "4.4.13", | 134 | "tar": "4.4.13", |
134 | "targz": "1.0.1", | 135 | "targz": "1.0.1", |
135 | "terser": "4.4.0", | 136 | "terser": "4.4.0", |
@@ -214,5 +215,8 @@ | |||
214 | }, | 215 | }, |
215 | "browserslist": [ | 216 | "browserslist": [ |
216 | "last 2 Chrome versions" | 217 | "last 2 Chrome versions" |
217 | ] | 218 | ], |
219 | "autoload": { | ||
220 | "App": "./src/internal-server/app" | ||
221 | } | ||
218 | } | 222 | } |
diff --git a/scripts/minify-images.sh b/scripts/minify-images.sh index 25a761f0b..a6ec71045 100755 --- a/scripts/minify-images.sh +++ b/scripts/minify-images.sh | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | # Note: This script is needed due to this bug: https://github.com/imagemin/imagemin/issues/348 | 3 | # Note: This script is needed due to this bug: https://github.com/imagemin/imagemin/issues/348 |
4 | # once the above is fixed, we should simply be able to specify the input directory where all image files are to be processed recursively | 4 | # once the above is fixed, we should simply be able to specify the input directory where all image files are to be processed recursively |
5 | FILES=`find . -name "*.jpg" -o -name "*.jpeg" -o -name "*.bmp" -o -name "*.png" -type f | GREP_OPTIONS= egrep -v "node_modules|internal-server|recipes"` | 5 | FILES=`find . -name "*.jpg" -o -name "*.jpeg" -o -name "*.bmp" -o -name "*.png" -type f | GREP_OPTIONS= egrep -v "node_modules|recipes"` |
6 | for file in $FILES; do | 6 | for file in $FILES; do |
7 | echo "Minifying file: $file" | 7 | echo "Minifying file: $file" |
8 | size_before=`/usr/bin/du $file | cut -f1` | 8 | size_before=`/usr/bin/du $file | cut -f1` |
diff --git a/src/internal-server b/src/internal-server deleted file mode 160000 | |||
Subproject 1177bc275609804ff2fb9818c6404792e936f23 | |||
diff --git a/src/internal-server/ace b/src/internal-server/ace new file mode 100644 index 000000000..42f8f10d1 --- /dev/null +++ b/src/internal-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/internal-server/app/Controllers/Http/RecipeController.js b/src/internal-server/app/Controllers/Http/RecipeController.js new file mode 100644 index 000000000..8a6b4f684 --- /dev/null +++ b/src/internal-server/app/Controllers/Http/RecipeController.js | |||
@@ -0,0 +1,120 @@ | |||
1 | const Recipe = use('App/Models/Recipe'); | ||
2 | const Drive = use('Drive'); | ||
3 | const { | ||
4 | validateAll, | ||
5 | } = use('Validator'); | ||
6 | const Env = use('Env'); | ||
7 | |||
8 | const fetch = require('node-fetch'); | ||
9 | |||
10 | const RECIPES_URL = 'https://api.getferdi.com/v1/recipes'; | ||
11 | |||
12 | class RecipeController { | ||
13 | // List official and custom recipes | ||
14 | async list({ | ||
15 | response, | ||
16 | }) { | ||
17 | const officialRecipes = JSON.parse(await (await fetch(RECIPES_URL)).text()); | ||
18 | const customRecipesArray = (await Recipe.all()).rows; | ||
19 | const customRecipes = customRecipesArray.map(recipe => ({ | ||
20 | id: recipe.recipeId, | ||
21 | name: recipe.name, | ||
22 | ...JSON.parse(recipe.data), | ||
23 | })); | ||
24 | |||
25 | const recipes = [ | ||
26 | ...officialRecipes, | ||
27 | ...customRecipes, | ||
28 | ]; | ||
29 | |||
30 | return response.send(recipes); | ||
31 | } | ||
32 | |||
33 | // Search official and custom recipes | ||
34 | async search({ | ||
35 | request, | ||
36 | response, | ||
37 | }) { | ||
38 | // Validate user input | ||
39 | const validation = await validateAll(request.all(), { | ||
40 | needle: 'required', | ||
41 | }); | ||
42 | if (validation.fails()) { | ||
43 | return response.status(401).send({ | ||
44 | message: 'Please provide a needle', | ||
45 | messages: validation.messages(), | ||
46 | status: 401, | ||
47 | }); | ||
48 | } | ||
49 | |||
50 | const needle = request.input('needle'); | ||
51 | |||
52 | // Get results | ||
53 | let results; | ||
54 | |||
55 | if (needle === 'ferdi:custom') { | ||
56 | const dbResults = (await Recipe.all()).toJSON(); | ||
57 | results = dbResults.map(recipe => ({ | ||
58 | id: recipe.recipeId, | ||
59 | name: recipe.name, | ||
60 | ...JSON.parse(recipe.data), | ||
61 | })); | ||
62 | } else { | ||
63 | let remoteResults = []; | ||
64 | if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq | ||
65 | remoteResults = JSON.parse(await (await fetch(`${RECIPES_URL}/search?needle=${encodeURIComponent(needle)}`)).text()); | ||
66 | } | ||
67 | const localResultsArray = (await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()).toJSON(); | ||
68 | const localResults = localResultsArray.map(recipe => ({ | ||
69 | id: recipe.recipeId, | ||
70 | name: recipe.name, | ||
71 | ...JSON.parse(recipe.data), | ||
72 | })); | ||
73 | |||
74 | results = [ | ||
75 | ...localResults, | ||
76 | ...remoteResults || [], | ||
77 | ]; | ||
78 | } | ||
79 | |||
80 | return response.send(results); | ||
81 | } | ||
82 | |||
83 | // Download a recipe | ||
84 | async download({ | ||
85 | response, | ||
86 | params, | ||
87 | }) { | ||
88 | // Validate user input | ||
89 | const validation = await validateAll(params, { | ||
90 | recipe: 'required|accepted', | ||
91 | }); | ||
92 | if (validation.fails()) { | ||
93 | return response.status(401).send({ | ||
94 | message: 'Please provide a recipe ID', | ||
95 | messages: validation.messages(), | ||
96 | status: 401, | ||
97 | }); | ||
98 | } | ||
99 | |||
100 | const service = params.recipe; | ||
101 | |||
102 | // Check for invalid characters | ||
103 | if (/\.{1,}/.test(service) || /\/{1,}/.test(service)) { | ||
104 | return response.send('Invalid recipe name'); | ||
105 | } | ||
106 | |||
107 | // Check if recipe exists in recipes folder | ||
108 | if (await Drive.exists(`${service}.tar.gz`)) { | ||
109 | return response.send(await Drive.get(`${service}.tar.gz`)); | ||
110 | } if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq | ||
111 | return response.redirect(`${RECIPES_URL}/download/${service}`); | ||
112 | } | ||
113 | return response.status(400).send({ | ||
114 | message: 'Recipe not found', | ||
115 | code: 'recipe-not-found', | ||
116 | }); | ||
117 | } | ||
118 | } | ||
119 | |||
120 | module.exports = RecipeController; | ||
diff --git a/src/internal-server/app/Controllers/Http/ServiceController.js b/src/internal-server/app/Controllers/Http/ServiceController.js new file mode 100644 index 000000000..36d20c70c --- /dev/null +++ b/src/internal-server/app/Controllers/Http/ServiceController.js | |||
@@ -0,0 +1,290 @@ | |||
1 | const Service = use('App/Models/Service'); | ||
2 | const { | ||
3 | validateAll, | ||
4 | } = use('Validator'); | ||
5 | const Env = use('Env'); | ||
6 | |||
7 | const uuid = require('uuid/v4'); | ||
8 | const path = require('path'); | ||
9 | const fs = require('fs-extra'); | ||
10 | |||
11 | class ServiceController { | ||
12 | // Create a new service for user | ||
13 | async create({ | ||
14 | request, | ||
15 | response, | ||
16 | }) { | ||
17 | // Validate user input | ||
18 | const validation = await validateAll(request.all(), { | ||
19 | name: 'required|string', | ||
20 | recipeId: 'required', | ||
21 | }); | ||
22 | if (validation.fails()) { | ||
23 | return response.status(401).send({ | ||
24 | message: 'Invalid POST arguments', | ||
25 | messages: validation.messages(), | ||
26 | status: 401, | ||
27 | }); | ||
28 | } | ||
29 | |||
30 | const data = request.all(); | ||
31 | |||
32 | // Get new, unused uuid | ||
33 | let serviceId; | ||
34 | do { | ||
35 | serviceId = uuid(); | ||
36 | } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
37 | |||
38 | await Service.create({ | ||
39 | serviceId, | ||
40 | name: data.name, | ||
41 | recipeId: data.recipeId, | ||
42 | settings: JSON.stringify(data), | ||
43 | }); | ||
44 | |||
45 | return response.send({ | ||
46 | data: { | ||
47 | userId: 1, | ||
48 | id: serviceId, | ||
49 | isEnabled: true, | ||
50 | isNotificationEnabled: true, | ||
51 | isBadgeEnabled: true, | ||
52 | isMuted: false, | ||
53 | isDarkModeEnabled: '', | ||
54 | spellcheckerLanguage: '', | ||
55 | order: 1, | ||
56 | customRecipe: false, | ||
57 | hasCustomIcon: false, | ||
58 | workspaces: [], | ||
59 | iconUrl: null, | ||
60 | ...data, | ||
61 | }, | ||
62 | status: ['created'], | ||
63 | }); | ||
64 | } | ||
65 | |||
66 | // List all services a user has created | ||
67 | async list({ | ||
68 | response, | ||
69 | }) { | ||
70 | const services = (await Service.all()).rows; | ||
71 | // Convert to array with all data Franz wants | ||
72 | const servicesArray = services.map((service) => { | ||
73 | const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings; | ||
74 | |||
75 | return { | ||
76 | customRecipe: false, | ||
77 | hasCustomIcon: false, | ||
78 | isBadgeEnabled: true, | ||
79 | isDarkModeEnabled: '', | ||
80 | isEnabled: true, | ||
81 | isMuted: false, | ||
82 | isNotificationEnabled: true, | ||
83 | order: 1, | ||
84 | spellcheckerLanguage: '', | ||
85 | workspaces: [], | ||
86 | ...JSON.parse(service.settings), | ||
87 | iconUrl: settings.iconId ? `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${settings.iconId}` : null, | ||
88 | id: service.serviceId, | ||
89 | name: service.name, | ||
90 | recipeId: service.recipeId, | ||
91 | userId: 1, | ||
92 | }; | ||
93 | }); | ||
94 | |||
95 | return response.send(servicesArray); | ||
96 | } | ||
97 | |||
98 | async edit({ | ||
99 | request, | ||
100 | response, | ||
101 | params, | ||
102 | }) { | ||
103 | if (request.file('icon')) { | ||
104 | // Upload custom service icon | ||
105 | await fs.ensureDir(path.join(Env.get('USER_PATH'), 'icons')); | ||
106 | |||
107 | const icon = request.file('icon', { | ||
108 | types: ['image'], | ||
109 | size: '2mb', | ||
110 | }); | ||
111 | const { | ||
112 | id, | ||
113 | } = params; | ||
114 | const service = (await Service.query() | ||
115 | .where('serviceId', id).fetch()).rows[0]; | ||
116 | const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings; | ||
117 | |||
118 | // Generate new icon ID | ||
119 | let iconId; | ||
120 | do { | ||
121 | iconId = uuid() + uuid(); | ||
122 | // eslint-disable-next-line no-await-in-loop | ||
123 | } while (await fs.exists(path.join(Env.get('USER_PATH'), 'icons', iconId))); | ||
124 | |||
125 | await icon.move(path.join(Env.get('USER_PATH'), 'icons'), { | ||
126 | name: iconId, | ||
127 | overwrite: true, | ||
128 | }); | ||
129 | |||
130 | if (!icon.moved()) { | ||
131 | return response.status(500).send(icon.error()); | ||
132 | } | ||
133 | |||
134 | const newSettings = { | ||
135 | ...settings, | ||
136 | ...{ | ||
137 | iconId, | ||
138 | customIconVersion: settings && settings.customIconVersion ? settings.customIconVersion + 1 : 1, | ||
139 | }, | ||
140 | }; | ||
141 | |||
142 | // Update data in database | ||
143 | await (Service.query() | ||
144 | .where('serviceId', id)).update({ | ||
145 | name: service.name, | ||
146 | settings: JSON.stringify(newSettings), | ||
147 | }); | ||
148 | |||
149 | return response.send({ | ||
150 | data: { | ||
151 | id, | ||
152 | name: service.name, | ||
153 | ...newSettings, | ||
154 | iconUrl: `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${newSettings.iconId}`, | ||
155 | userId: 1, | ||
156 | }, | ||
157 | status: ['updated'], | ||
158 | }); | ||
159 | } | ||
160 | // Update service info | ||
161 | const data = request.all(); | ||
162 | const { | ||
163 | id, | ||
164 | } = params; | ||
165 | |||
166 | // Get current settings from db | ||
167 | const serviceData = (await Service.query() | ||
168 | .where('serviceId', id).fetch()).rows[0]; | ||
169 | |||
170 | const settings = { | ||
171 | ...typeof serviceData.settings === 'string' ? JSON.parse(serviceData.settings) : serviceData.settings, | ||
172 | ...data, | ||
173 | }; | ||
174 | |||
175 | // Update data in database | ||
176 | await (Service.query() | ||
177 | .where('serviceId', id)).update({ | ||
178 | name: data.name, | ||
179 | settings: JSON.stringify(settings), | ||
180 | }); | ||
181 | |||
182 | // Get updated row | ||
183 | const service = (await Service.query() | ||
184 | .where('serviceId', id).fetch()).rows[0]; | ||
185 | |||
186 | return response.send({ | ||
187 | data: { | ||
188 | id, | ||
189 | name: service.name, | ||
190 | ...settings, | ||
191 | iconUrl: `${Env.get('APP_URL')}/v1/icon/${settings.iconId}`, | ||
192 | userId: 1, | ||
193 | }, | ||
194 | status: ['updated'], | ||
195 | }); | ||
196 | } | ||
197 | |||
198 | async icon({ | ||
199 | params, | ||
200 | response, | ||
201 | }) { | ||
202 | const { | ||
203 | id, | ||
204 | } = params; | ||
205 | |||
206 | const iconPath = path.join(Env.get('USER_PATH'), 'icons', id); | ||
207 | if (!await fs.exists(iconPath)) { | ||
208 | return response.status(404).send({ | ||
209 | status: 'Icon doesn\'t exist', | ||
210 | }); | ||
211 | } | ||
212 | |||
213 | return response.download(iconPath); | ||
214 | } | ||
215 | |||
216 | async reorder({ | ||
217 | request, | ||
218 | response, | ||
219 | }) { | ||
220 | const data = request.all(); | ||
221 | |||
222 | for (const service of Object.keys(data)) { | ||
223 | // Get current settings from db | ||
224 | const serviceData = (await Service.query() // eslint-disable-line no-await-in-loop | ||
225 | .where('serviceId', service).fetch()).rows[0]; | ||
226 | |||
227 | const settings = { | ||
228 | ...JSON.parse(serviceData.settings), | ||
229 | order: data[service], | ||
230 | }; | ||
231 | |||
232 | // Update data in database | ||
233 | await (Service.query() // eslint-disable-line no-await-in-loop | ||
234 | .where('serviceId', service)) | ||
235 | .update({ | ||
236 | settings: JSON.stringify(settings), | ||
237 | }); | ||
238 | } | ||
239 | |||
240 | // Get new services | ||
241 | const services = (await Service.all()).rows; | ||
242 | // Convert to array with all data Franz wants | ||
243 | const servicesArray = services.map((service) => { | ||
244 | const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings; | ||
245 | |||
246 | return { | ||
247 | customRecipe: false, | ||
248 | hasCustomIcon: false, | ||
249 | isBadgeEnabled: true, | ||
250 | isDarkModeEnabled: '', | ||
251 | isEnabled: true, | ||
252 | isMuted: false, | ||
253 | isNotificationEnabled: true, | ||
254 | order: 1, | ||
255 | spellcheckerLanguage: '', | ||
256 | workspaces: [], | ||
257 | ...JSON.parse(service.settings), | ||
258 | iconUrl: settings.iconId ? `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${settings.iconId}` : null, | ||
259 | id: service.serviceId, | ||
260 | name: service.name, | ||
261 | recipeId: service.recipeId, | ||
262 | userId: 1, | ||
263 | }; | ||
264 | }); | ||
265 | |||
266 | return response.send(servicesArray); | ||
267 | } | ||
268 | |||
269 | update({ | ||
270 | response, | ||
271 | }) { | ||
272 | return response.send([]); | ||
273 | } | ||
274 | |||
275 | async delete({ | ||
276 | params, | ||
277 | response, | ||
278 | }) { | ||
279 | // Update data in database | ||
280 | await (Service.query() | ||
281 | .where('serviceId', params.id)).delete(); | ||
282 | |||
283 | return response.send({ | ||
284 | message: 'Sucessfully deleted service', | ||
285 | status: 200, | ||
286 | }); | ||
287 | } | ||
288 | } | ||
289 | |||
290 | module.exports = ServiceController; | ||
diff --git a/src/internal-server/app/Controllers/Http/StaticController.js b/src/internal-server/app/Controllers/Http/StaticController.js new file mode 100644 index 000000000..69dfee0a3 --- /dev/null +++ b/src/internal-server/app/Controllers/Http/StaticController.js | |||
@@ -0,0 +1,205 @@ | |||
1 | /** | ||
2 | * Controller for routes with static responses | ||
3 | */ | ||
4 | |||
5 | class StaticController { | ||
6 | // Enable all features | ||
7 | features({ | ||
8 | response, | ||
9 | }) { | ||
10 | return response.send({ | ||
11 | isServiceProxyEnabled: true, | ||
12 | isWorkspaceEnabled: true, | ||
13 | isAnnouncementsEnabled: true, | ||
14 | isSettingsWSEnabled: false, | ||
15 | isMagicBarEnabled: true, | ||
16 | isTodosEnabled: true, | ||
17 | subscribeURL: 'https://getferdi.com', | ||
18 | hasInlineCheckout: true, | ||
19 | }); | ||
20 | } | ||
21 | |||
22 | // Return an empty array | ||
23 | emptyArray({ | ||
24 | response, | ||
25 | }) { | ||
26 | return response.send([]); | ||
27 | } | ||
28 | |||
29 | // Return list of popular recipes (copy of the response Franz's API is returning) | ||
30 | popularRecipes({ | ||
31 | response, | ||
32 | }) { | ||
33 | return response.send([{ | ||
34 | // TODO: Why is this list hardcoded? | ||
35 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
36 | featured: false, | ||
37 | id: 'slack', | ||
38 | name: 'Slack', | ||
39 | version: '1.0.4', | ||
40 | icons: { | ||
41 | png: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.png', | ||
42 | svg: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.svg', | ||
43 | }, | ||
44 | }, { | ||
45 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
46 | featured: false, | ||
47 | id: 'whatsapp', | ||
48 | name: 'WhatsApp', | ||
49 | version: '1.0.1', | ||
50 | icons: { | ||
51 | png: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.png', | ||
52 | svg: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.svg', | ||
53 | }, | ||
54 | }, { | ||
55 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
56 | featured: false, | ||
57 | id: 'messenger', | ||
58 | name: 'Messenger', | ||
59 | version: '1.0.6', | ||
60 | icons: { | ||
61 | png: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.png', | ||
62 | svg: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.svg', | ||
63 | }, | ||
64 | }, { | ||
65 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
66 | featured: false, | ||
67 | id: 'telegram', | ||
68 | name: 'Telegram', | ||
69 | version: '1.0.0', | ||
70 | icons: { | ||
71 | png: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.png', | ||
72 | svg: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.svg', | ||
73 | }, | ||
74 | }, { | ||
75 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
76 | featured: false, | ||
77 | id: 'gmail', | ||
78 | name: 'Gmail', | ||
79 | version: '1.0.0', | ||
80 | icons: { | ||
81 | png: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.png', | ||
82 | svg: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.svg', | ||
83 | }, | ||
84 | }, { | ||
85 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
86 | featured: false, | ||
87 | id: 'skype', | ||
88 | name: 'Skype', | ||
89 | version: '1.0.0', | ||
90 | icons: { | ||
91 | png: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.png', | ||
92 | svg: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.svg', | ||
93 | }, | ||
94 | }, { | ||
95 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
96 | featured: false, | ||
97 | id: 'hangouts', | ||
98 | name: 'Hangouts', | ||
99 | version: '1.0.0', | ||
100 | icons: { | ||
101 | png: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.png', | ||
102 | svg: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.svg', | ||
103 | }, | ||
104 | }, { | ||
105 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
106 | featured: false, | ||
107 | id: 'discord', | ||
108 | name: 'Discord', | ||
109 | version: '1.0.0', | ||
110 | icons: { | ||
111 | png: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.png', | ||
112 | svg: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.svg', | ||
113 | }, | ||
114 | }, { | ||
115 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
116 | featured: false, | ||
117 | id: 'tweetdeck', | ||
118 | name: 'Tweetdeck', | ||
119 | version: '1.0.1', | ||
120 | icons: { | ||
121 | png: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.png', | ||
122 | svg: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.svg', | ||
123 | }, | ||
124 | }, { | ||
125 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
126 | featured: false, | ||
127 | id: 'hipchat', | ||
128 | name: 'HipChat', | ||
129 | version: '1.0.1', | ||
130 | icons: { | ||
131 | png: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.png', | ||
132 | svg: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.svg', | ||
133 | }, | ||
134 | }, { | ||
135 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
136 | featured: false, | ||
137 | id: 'gmailinbox', | ||
138 | name: 'Inbox by Gmail', | ||
139 | version: '1.0.0', | ||
140 | icons: { | ||
141 | png: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.png', | ||
142 | svg: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.svg', | ||
143 | }, | ||
144 | }, { | ||
145 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
146 | featured: false, | ||
147 | id: 'rocketchat', | ||
148 | name: 'Rocket.Chat', | ||
149 | version: '1.0.1', | ||
150 | icons: { | ||
151 | png: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.png', | ||
152 | svg: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.svg', | ||
153 | }, | ||
154 | }, { | ||
155 | author: 'Brian Gilbert <brian@briangilbert.net>', | ||
156 | featured: false, | ||
157 | id: 'gitter', | ||
158 | name: 'Gitter', | ||
159 | version: '1.0.0', | ||
160 | icons: { | ||
161 | png: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.png', | ||
162 | svg: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.svg', | ||
163 | }, | ||
164 | }, { | ||
165 | author: 'Stefan Malzner <stefan@adlk.io>', | ||
166 | featured: false, | ||
167 | id: 'mattermost', | ||
168 | name: 'Mattermost', | ||
169 | version: '1.0.0', | ||
170 | icons: { | ||
171 | png: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.png', | ||
172 | svg: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.svg', | ||
173 | }, | ||
174 | }, { | ||
175 | author: 'Franz <recipe@meetfranz.com>', | ||
176 | featured: false, | ||
177 | id: 'toggl', | ||
178 | name: 'toggl', | ||
179 | version: '1.0.0', | ||
180 | icons: { | ||
181 | png: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.png', | ||
182 | svg: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.svg', | ||
183 | }, | ||
184 | }, { | ||
185 | author: 'Stuart Clark <stuart@realityloop.com>', | ||
186 | featured: false, | ||
187 | id: 'twist', | ||
188 | name: 'twist', | ||
189 | version: '1.0.0', | ||
190 | icons: { | ||
191 | png: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.png', | ||
192 | svg: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.svg', | ||
193 | }, | ||
194 | }]); | ||
195 | } | ||
196 | |||
197 | // Show announcements | ||
198 | announcement({ | ||
199 | response, | ||
200 | }) { | ||
201 | return response.send({}); | ||
202 | } | ||
203 | } | ||
204 | |||
205 | module.exports = StaticController; | ||
diff --git a/src/internal-server/app/Controllers/Http/UserController.js b/src/internal-server/app/Controllers/Http/UserController.js new file mode 100644 index 000000000..f7cdfc9c9 --- /dev/null +++ b/src/internal-server/app/Controllers/Http/UserController.js | |||
@@ -0,0 +1,367 @@ | |||
1 | const User = use('App/Models/User'); | ||
2 | const Service = use('App/Models/Service'); | ||
3 | const Workspace = use('App/Models/Workspace'); | ||
4 | const { | ||
5 | validateAll, | ||
6 | } = use('Validator'); | ||
7 | |||
8 | const btoa = require('btoa'); | ||
9 | const fetch = require('node-fetch'); | ||
10 | const uuid = require('uuid/v4'); | ||
11 | const crypto = require('crypto'); | ||
12 | |||
13 | const apiRequest = (url, route, method, auth) => new Promise((resolve, reject) => { | ||
14 | const base = `${url}/v1/`; | ||
15 | const user = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; | ||
16 | |||
17 | try { | ||
18 | fetch(base + route, { | ||
19 | method, | ||
20 | headers: { | ||
21 | Authorization: `Bearer ${auth}`, | ||
22 | 'User-Agent': user, | ||
23 | }, | ||
24 | }) | ||
25 | .then(data => data.json()) | ||
26 | .then(json => resolve(json)); | ||
27 | } catch (e) { | ||
28 | reject(); | ||
29 | } | ||
30 | }); | ||
31 | |||
32 | class UserController { | ||
33 | // Register a new user | ||
34 | async signup({ | ||
35 | request, | ||
36 | response, | ||
37 | }) { | ||
38 | // Validate user input | ||
39 | const validation = await validateAll(request.all(), { | ||
40 | firstname: 'required', | ||
41 | email: 'required|email', | ||
42 | password: 'required', | ||
43 | }); | ||
44 | if (validation.fails()) { | ||
45 | return response.status(401).send({ | ||
46 | message: 'Invalid POST arguments', | ||
47 | messages: validation.messages(), | ||
48 | status: 401, | ||
49 | }); | ||
50 | } | ||
51 | |||
52 | return response.send({ | ||
53 | message: 'Successfully created account', | ||
54 | token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M', | ||
55 | }); | ||
56 | } | ||
57 | |||
58 | // Login using an existing user | ||
59 | async login({ | ||
60 | request, | ||
61 | response, | ||
62 | }) { | ||
63 | if (!request.header('Authorization')) { | ||
64 | return response.status(401).send({ | ||
65 | message: 'Please provide authorization', | ||
66 | status: 401, | ||
67 | }); | ||
68 | } | ||
69 | |||
70 | return response.send({ | ||
71 | message: 'Successfully logged in', | ||
72 | token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M', | ||
73 | }); | ||
74 | } | ||
75 | |||
76 | // Return information about the current user | ||
77 | async me({ | ||
78 | response, | ||
79 | }) { | ||
80 | const user = await User.find(1); | ||
81 | |||
82 | const settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings; | ||
83 | |||
84 | return response.send({ | ||
85 | accountType: 'individual', | ||
86 | beta: false, | ||
87 | donor: {}, | ||
88 | email: '', | ||
89 | emailValidated: true, | ||
90 | features: {}, | ||
91 | firstname: 'Ferdi', | ||
92 | id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8', | ||
93 | isSubscriptionOwner: true, | ||
94 | lastname: 'Application', | ||
95 | locale: 'en-US', | ||
96 | ...settings || {}, | ||
97 | }); | ||
98 | } | ||
99 | |||
100 | async updateMe({ | ||
101 | request, | ||
102 | response, | ||
103 | }) { | ||
104 | const user = await User.find(1); | ||
105 | |||
106 | let settings = user.settings || {}; | ||
107 | if (typeof settings === 'string') { | ||
108 | settings = JSON.parse(settings); | ||
109 | } | ||
110 | |||
111 | const newSettings = { | ||
112 | ...settings, | ||
113 | ...request.all(), | ||
114 | }; | ||
115 | |||
116 | user.settings = JSON.stringify(newSettings); | ||
117 | await user.save(); | ||
118 | |||
119 | return response.send({ | ||
120 | data: { | ||
121 | accountType: 'individual', | ||
122 | beta: false, | ||
123 | donor: {}, | ||
124 | email: '', | ||
125 | emailValidated: true, | ||
126 | features: {}, | ||
127 | firstname: 'Ferdi', | ||
128 | id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8', | ||
129 | isSubscriptionOwner: true, | ||
130 | lastname: 'Application', | ||
131 | locale: 'en-US', | ||
132 | ...newSettings, | ||
133 | }, | ||
134 | status: [ | ||
135 | 'data-updated', | ||
136 | ], | ||
137 | }); | ||
138 | } | ||
139 | |||
140 | async import({ | ||
141 | request, | ||
142 | response, | ||
143 | }) { | ||
144 | // Validate user input | ||
145 | const validation = await validateAll(request.all(), { | ||
146 | email: 'required|email', | ||
147 | password: 'required', | ||
148 | server: 'required', | ||
149 | }); | ||
150 | if (validation.fails()) { | ||
151 | let errorMessage = 'There was an error while trying to import your account:\n'; | ||
152 | for (const message of validation.messages()) { | ||
153 | if (message.validation === 'required') { | ||
154 | errorMessage += `- Please make sure to supply your ${message.field}\n`; | ||
155 | } else if (message.validation === 'unique') { | ||
156 | errorMessage += '- There is already a user with this email.\n'; | ||
157 | } else { | ||
158 | errorMessage += `${message.message}\n`; | ||
159 | } | ||
160 | } | ||
161 | return response.status(401).send(errorMessage); | ||
162 | } | ||
163 | |||
164 | const { | ||
165 | email, | ||
166 | password, | ||
167 | server, | ||
168 | } = request.all(); | ||
169 | |||
170 | const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); | ||
171 | |||
172 | const base = `${server}/v1/`; | ||
173 | const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; | ||
174 | |||
175 | // Try to get an authentication token | ||
176 | let token; | ||
177 | try { | ||
178 | const basicToken = btoa(`${email}:${hashedPassword}`); | ||
179 | |||
180 | const rawResponse = await fetch(`${base}auth/login`, { | ||
181 | method: 'POST', | ||
182 | headers: { | ||
183 | Authorization: `Basic ${basicToken}`, | ||
184 | 'User-Agent': userAgent, | ||
185 | }, | ||
186 | }); | ||
187 | const content = await rawResponse.json(); | ||
188 | |||
189 | if (!content.message || content.message !== 'Successfully logged in') { | ||
190 | const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again'; | ||
191 | return response.status(401).send(errorMessage); | ||
192 | } | ||
193 | |||
194 | // eslint-disable-next-line prefer-destructuring | ||
195 | token = content.token; | ||
196 | } catch (e) { | ||
197 | return response.status(401).send({ | ||
198 | message: 'Cannot login to Franz', | ||
199 | error: e, | ||
200 | }); | ||
201 | } | ||
202 | |||
203 | // Get user information | ||
204 | let userInf = false; | ||
205 | try { | ||
206 | userInf = await apiRequest(server, 'me', 'GET', token); | ||
207 | } catch (e) { | ||
208 | const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`; | ||
209 | return response.status(401).send(errorMessage); | ||
210 | } | ||
211 | if (!userInf) { | ||
212 | const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later'; | ||
213 | return response.status(401).send(errorMessage); | ||
214 | } | ||
215 | |||
216 | const serviceIdTranslation = {}; | ||
217 | |||
218 | // Import services | ||
219 | try { | ||
220 | const services = await apiRequest(server, 'me/services', 'GET', token); | ||
221 | |||
222 | for (const service of services) { | ||
223 | // Get new, unused uuid | ||
224 | let serviceId; | ||
225 | do { | ||
226 | serviceId = uuid(); | ||
227 | } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
228 | |||
229 | await Service.create({ // eslint-disable-line no-await-in-loop | ||
230 | serviceId, | ||
231 | name: service.name, | ||
232 | recipeId: service.recipeId, | ||
233 | settings: JSON.stringify(service), | ||
234 | }); | ||
235 | |||
236 | serviceIdTranslation[service.id] = serviceId; | ||
237 | } | ||
238 | } catch (e) { | ||
239 | const errorMessage = `Could not import your services into our system.\nError: ${e}`; | ||
240 | return response.status(401).send(errorMessage); | ||
241 | } | ||
242 | |||
243 | // Import workspaces | ||
244 | try { | ||
245 | const workspaces = await apiRequest(server, 'workspace', 'GET', token); | ||
246 | |||
247 | for (const workspace of workspaces) { | ||
248 | let workspaceId; | ||
249 | do { | ||
250 | workspaceId = uuid(); | ||
251 | } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
252 | |||
253 | const services = workspace.services.map(service => serviceIdTranslation[service]); | ||
254 | |||
255 | await Workspace.create({ // eslint-disable-line no-await-in-loop | ||
256 | workspaceId, | ||
257 | name: workspace.name, | ||
258 | order: workspace.order, | ||
259 | services: JSON.stringify(services), | ||
260 | data: JSON.stringify({}), | ||
261 | }); | ||
262 | } | ||
263 | } catch (e) { | ||
264 | const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`; | ||
265 | return response.status(401).send(errorMessage); | ||
266 | } | ||
267 | |||
268 | return response.send('Your account has been imported. You can now use your Franz account in Ferdi.'); | ||
269 | } | ||
270 | |||
271 | // Account import/export | ||
272 | async export({ | ||
273 | // eslint-disable-next-line no-unused-vars | ||
274 | auth, | ||
275 | response, | ||
276 | }) { | ||
277 | const services = (await Service.all()).toJSON(); | ||
278 | const workspaces = (await Workspace.all()).toJSON(); | ||
279 | |||
280 | const exportData = { | ||
281 | username: 'Ferdi', | ||
282 | mail: 'internal@getferdi.com', | ||
283 | services, | ||
284 | workspaces, | ||
285 | }; | ||
286 | |||
287 | return response | ||
288 | .header('Content-Type', 'application/force-download') | ||
289 | .header('Content-disposition', 'attachment; filename=export.ferdi-data') | ||
290 | .send(exportData); | ||
291 | } | ||
292 | |||
293 | async importFerdi({ | ||
294 | request, | ||
295 | response, | ||
296 | }) { | ||
297 | const validation = await validateAll(request.all(), { | ||
298 | file: 'required', | ||
299 | }); | ||
300 | if (validation.fails()) { | ||
301 | return response.send(validation.messages()); | ||
302 | } | ||
303 | |||
304 | let file; | ||
305 | try { | ||
306 | file = JSON.parse(request.input('file')); | ||
307 | } catch (e) { | ||
308 | return response.send('Could not import: Invalid file, could not read file'); | ||
309 | } | ||
310 | |||
311 | if (!file || !file.services || !file.workspaces) { | ||
312 | return response.send('Could not import: Invalid file (2)'); | ||
313 | } | ||
314 | |||
315 | const serviceIdTranslation = {}; | ||
316 | |||
317 | // Import services | ||
318 | try { | ||
319 | for (const service of file.services) { | ||
320 | // Get new, unused uuid | ||
321 | let serviceId; | ||
322 | do { | ||
323 | serviceId = uuid(); | ||
324 | } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
325 | |||
326 | await Service.create({ // eslint-disable-line no-await-in-loop | ||
327 | serviceId, | ||
328 | name: service.name, | ||
329 | recipeId: service.recipeId, | ||
330 | settings: JSON.stringify(service.settings), | ||
331 | }); | ||
332 | |||
333 | serviceIdTranslation[service.id] = serviceId; | ||
334 | } | ||
335 | } catch (e) { | ||
336 | const errorMessage = `Could not import your services into our system.\nError: ${e}`; | ||
337 | return response.send(errorMessage); | ||
338 | } | ||
339 | |||
340 | // Import workspaces | ||
341 | try { | ||
342 | for (const workspace of file.workspaces) { | ||
343 | let workspaceId; | ||
344 | do { | ||
345 | workspaceId = uuid(); | ||
346 | } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
347 | |||
348 | const services = workspace.services.map((service) => serviceIdTranslation[service]); | ||
349 | |||
350 | await Workspace.create({ // eslint-disable-line no-await-in-loop | ||
351 | workspaceId, | ||
352 | name: workspace.name, | ||
353 | order: workspace.order, | ||
354 | services: JSON.stringify(services), | ||
355 | data: JSON.stringify(workspace.data), | ||
356 | }); | ||
357 | } | ||
358 | } catch (e) { | ||
359 | const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`; | ||
360 | return response.status(401).send(errorMessage); | ||
361 | } | ||
362 | |||
363 | return response.send('Your account has been imported.'); | ||
364 | } | ||
365 | } | ||
366 | |||
367 | module.exports = UserController; | ||
diff --git a/src/internal-server/app/Controllers/Http/WorkspaceController.js b/src/internal-server/app/Controllers/Http/WorkspaceController.js new file mode 100644 index 000000000..4189fbcdd --- /dev/null +++ b/src/internal-server/app/Controllers/Http/WorkspaceController.js | |||
@@ -0,0 +1,148 @@ | |||
1 | const Workspace = use('App/Models/Workspace'); | ||
2 | const { | ||
3 | validateAll, | ||
4 | } = use('Validator'); | ||
5 | |||
6 | const uuid = require('uuid/v4'); | ||
7 | |||
8 | class WorkspaceController { | ||
9 | // Create a new workspace for user | ||
10 | async create({ | ||
11 | request, | ||
12 | response, | ||
13 | }) { | ||
14 | // Validate user input | ||
15 | const validation = await validateAll(request.all(), { | ||
16 | name: 'required', | ||
17 | }); | ||
18 | if (validation.fails()) { | ||
19 | return response.status(401).send({ | ||
20 | message: 'Invalid POST arguments', | ||
21 | messages: validation.messages(), | ||
22 | status: 401, | ||
23 | }); | ||
24 | } | ||
25 | |||
26 | const data = request.all(); | ||
27 | |||
28 | // Get new, unused uuid | ||
29 | let workspaceId; | ||
30 | do { | ||
31 | workspaceId = uuid(); | ||
32 | } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop | ||
33 | |||
34 | const order = (await Workspace.all()).rows.length; | ||
35 | |||
36 | await Workspace.create({ | ||
37 | workspaceId, | ||
38 | name: data.name, | ||
39 | order, | ||
40 | services: JSON.stringify([]), | ||
41 | data: JSON.stringify(data), | ||
42 | }); | ||
43 | |||
44 | return response.send({ | ||
45 | userId: 1, | ||
46 | name: data.name, | ||
47 | id: workspaceId, | ||
48 | order, | ||
49 | workspaces: [], | ||
50 | }); | ||
51 | } | ||
52 | |||
53 | async edit({ | ||
54 | request, | ||
55 | response, | ||
56 | params, | ||
57 | }) { | ||
58 | // Validate user input | ||
59 | const validation = await validateAll(request.all(), { | ||
60 | name: 'required', | ||
61 | services: 'required|array', | ||
62 | }); | ||
63 | if (validation.fails()) { | ||
64 | return response.status(401).send({ | ||
65 | message: 'Invalid POST arguments', | ||
66 | messages: validation.messages(), | ||
67 | status: 401, | ||
68 | }); | ||
69 | } | ||
70 | |||
71 | const data = request.all(); | ||
72 | const { | ||
73 | id, | ||
74 | } = params; | ||
75 | |||
76 | // Update data in database | ||
77 | await (Workspace.query() | ||
78 | .where('workspaceId', id)).update({ | ||
79 | name: data.name, | ||
80 | services: JSON.stringify(data.services), | ||
81 | }); | ||
82 | |||
83 | // Get updated row | ||
84 | const workspace = (await Workspace.query() | ||
85 | .where('workspaceId', id).fetch()).rows[0]; | ||
86 | |||
87 | return response.send({ | ||
88 | id: workspace.workspaceId, | ||
89 | name: data.name, | ||
90 | order: workspace.order, | ||
91 | services: data.services, | ||
92 | userId: 1, | ||
93 | }); | ||
94 | } | ||
95 | |||
96 | async delete({ | ||
97 | // eslint-disable-next-line no-unused-vars | ||
98 | request, | ||
99 | response, | ||
100 | params, | ||
101 | }) { | ||
102 | // Validate user input | ||
103 | const validation = await validateAll(params, { | ||
104 | id: 'required', | ||
105 | }); | ||
106 | if (validation.fails()) { | ||
107 | return response.status(401).send({ | ||
108 | message: 'Invalid arguments', | ||
109 | messages: validation.messages(), | ||
110 | status: 401, | ||
111 | }); | ||
112 | } | ||
113 | |||
114 | const { | ||
115 | id, | ||
116 | } = params; | ||
117 | |||
118 | // Update data in database | ||
119 | await (Workspace.query() | ||
120 | .where('workspaceId', id)).delete(); | ||
121 | |||
122 | return response.send({ | ||
123 | message: 'Successfully deleted workspace', | ||
124 | }); | ||
125 | } | ||
126 | |||
127 | // List all workspaces a user has created | ||
128 | async list({ | ||
129 | response, | ||
130 | }) { | ||
131 | const workspaces = (await Workspace.all()).rows; | ||
132 | // Convert to array with all data Franz wants | ||
133 | let workspacesArray = []; | ||
134 | if (workspaces) { | ||
135 | workspacesArray = workspaces.map(workspace => ({ | ||
136 | id: workspace.workspaceId, | ||
137 | name: workspace.name, | ||
138 | order: workspace.order, | ||
139 | services: typeof workspace.services === 'string' ? JSON.parse(workspace.services) : workspace.services, | ||
140 | userId: 1, | ||
141 | })); | ||
142 | } | ||
143 | |||
144 | return response.send(workspacesArray); | ||
145 | } | ||
146 | } | ||
147 | |||
148 | module.exports = WorkspaceController; | ||
diff --git a/src/internal-server/app/Exceptions/Handler.js b/src/internal-server/app/Exceptions/Handler.js new file mode 100644 index 000000000..111ef4e0e --- /dev/null +++ b/src/internal-server/app/Exceptions/Handler.js | |||
@@ -0,0 +1,44 @@ | |||
1 | const BaseExceptionHandler = use('BaseExceptionHandler'); | ||
2 | |||
3 | /** | ||
4 | * This class handles all exceptions thrown during | ||
5 | * the HTTP request lifecycle. | ||
6 | * | ||
7 | * @class ExceptionHandler | ||
8 | */ | ||
9 | class ExceptionHandler extends BaseExceptionHandler { | ||
10 | /** | ||
11 | * Handle exception thrown during the HTTP lifecycle | ||
12 | * | ||
13 | * @method handle | ||
14 | * | ||
15 | * @param {Object} error | ||
16 | * @param {Object} options.request | ||
17 | * @param {Object} options.response | ||
18 | * | ||
19 | * @return {void} | ||
20 | */ | ||
21 | async handle(error, { response }) { | ||
22 | if (error.name === 'ValidationException') { | ||
23 | return response.status(400).send('Invalid arguments'); | ||
24 | } | ||
25 | |||
26 | return response.status(error.status).send(error.message); | ||
27 | } | ||
28 | |||
29 | /** | ||
30 | * Report exception for logging or debugging. | ||
31 | * | ||
32 | * @method report | ||
33 | * | ||
34 | * @param {Object} error | ||
35 | * @param {Object} options.request | ||
36 | * | ||
37 | * @return {void} | ||
38 | */ | ||
39 | async report() { | ||
40 | return true; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | module.exports = ExceptionHandler; | ||
diff --git a/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js b/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js new file mode 100644 index 000000000..87f1f6c25 --- /dev/null +++ b/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js | |||
@@ -0,0 +1,15 @@ | |||
1 | class ConvertEmptyStringsToNull { | ||
2 | async handle({ request }, next) { | ||
3 | if (Object.keys(request.body).length) { | ||
4 | request.body = Object.assign( | ||
5 | ...Object.keys(request.body).map(key => ({ | ||
6 | [key]: request.body[key] !== '' ? request.body[key] : null, | ||
7 | })), | ||
8 | ); | ||
9 | } | ||
10 | |||
11 | await next(); | ||
12 | } | ||
13 | } | ||
14 | |||
15 | module.exports = ConvertEmptyStringsToNull; | ||
diff --git a/src/internal-server/app/Models/Recipe.js b/src/internal-server/app/Models/Recipe.js new file mode 100644 index 000000000..bd9741114 --- /dev/null +++ b/src/internal-server/app/Models/Recipe.js | |||
@@ -0,0 +1,7 @@ | |||
1 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
2 | const Model = use('Model'); | ||
3 | |||
4 | class Recipe extends Model { | ||
5 | } | ||
6 | |||
7 | module.exports = Recipe; | ||
diff --git a/src/internal-server/app/Models/Service.js b/src/internal-server/app/Models/Service.js new file mode 100644 index 000000000..a2e5c981e --- /dev/null +++ b/src/internal-server/app/Models/Service.js | |||
@@ -0,0 +1,7 @@ | |||
1 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
2 | const Model = use('Model'); | ||
3 | |||
4 | class Service extends Model { | ||
5 | } | ||
6 | |||
7 | module.exports = Service; | ||
diff --git a/src/internal-server/app/Models/Token.js b/src/internal-server/app/Models/Token.js new file mode 100644 index 000000000..83e989117 --- /dev/null +++ b/src/internal-server/app/Models/Token.js | |||
@@ -0,0 +1,7 @@ | |||
1 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
2 | const Model = use('Model'); | ||
3 | |||
4 | class Token extends Model { | ||
5 | } | ||
6 | |||
7 | module.exports = Token; | ||
diff --git a/src/internal-server/app/Models/Traits/NoTimestamp.js b/src/internal-server/app/Models/Traits/NoTimestamp.js new file mode 100644 index 000000000..914f542f0 --- /dev/null +++ b/src/internal-server/app/Models/Traits/NoTimestamp.js | |||
@@ -0,0 +1,14 @@ | |||
1 | class NoTimestamp { | ||
2 | register(Model) { | ||
3 | Object.defineProperties(Model, { | ||
4 | createdAtColumn: { | ||
5 | get: () => null, | ||
6 | }, | ||
7 | updatedAtColumn: { | ||
8 | get: () => null, | ||
9 | }, | ||
10 | }); | ||
11 | } | ||
12 | } | ||
13 | |||
14 | module.exports = NoTimestamp; | ||
diff --git a/src/internal-server/app/Models/User.js b/src/internal-server/app/Models/User.js new file mode 100644 index 000000000..907710d8d --- /dev/null +++ b/src/internal-server/app/Models/User.js | |||
@@ -0,0 +1,8 @@ | |||
1 | // File is required by AdonisJS but not used by the server | ||
2 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
3 | const Model = use('Model'); | ||
4 | |||
5 | class User extends Model { | ||
6 | } | ||
7 | |||
8 | module.exports = User; | ||
diff --git a/src/internal-server/app/Models/Workspace.js b/src/internal-server/app/Models/Workspace.js new file mode 100644 index 000000000..dcf39ac75 --- /dev/null +++ b/src/internal-server/app/Models/Workspace.js | |||
@@ -0,0 +1,7 @@ | |||
1 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ | ||
2 | const Model = use('Model'); | ||
3 | |||
4 | class Workspace extends Model { | ||
5 | } | ||
6 | |||
7 | module.exports = Workspace; | ||
diff --git a/src/internal-server/config/app.js b/src/internal-server/config/app.js new file mode 100644 index 000000000..0a1644932 --- /dev/null +++ b/src/internal-server/config/app.js | |||
@@ -0,0 +1,240 @@ | |||
1 | /** @type {import('@adonisjs/framework/src/Env')} */ | ||
2 | const Env = use('Env'); | ||
3 | |||
4 | module.exports = { | ||
5 | |||
6 | /* | ||
7 | |-------------------------------------------------------------------------- | ||
8 | | Application Name | ||
9 | |-------------------------------------------------------------------------- | ||
10 | | | ||
11 | | This value is the name of your application and can used when you | ||
12 | | need to place the application's name in a email, view or | ||
13 | | other location. | ||
14 | | | ||
15 | */ | ||
16 | |||
17 | name: Env.get('APP_NAME', 'Ferdi Internal Server'), | ||
18 | |||
19 | /* | ||
20 | |-------------------------------------------------------------------------- | ||
21 | | App Key | ||
22 | |-------------------------------------------------------------------------- | ||
23 | | | ||
24 | | App key is a randomly generated 16 or 32 characters long string required | ||
25 | | to encrypt cookies, sessions and other sensitive data. | ||
26 | | | ||
27 | */ | ||
28 | appKey: Env.getOrFail('APP_KEY'), | ||
29 | |||
30 | http: { | ||
31 | /* | ||
32 | |-------------------------------------------------------------------------- | ||
33 | | Allow Method Spoofing | ||
34 | |-------------------------------------------------------------------------- | ||
35 | | | ||
36 | | Method spoofing allows to make requests by spoofing the http verb. | ||
37 | | Which means you can make a GET request but instruct the server to | ||
38 | | treat as a POST or PUT request. If you want this feature, set the | ||
39 | | below value to true. | ||
40 | | | ||
41 | */ | ||
42 | allowMethodSpoofing: true, | ||
43 | |||
44 | /* | ||
45 | |-------------------------------------------------------------------------- | ||
46 | | Trust Proxy | ||
47 | |-------------------------------------------------------------------------- | ||
48 | | | ||
49 | | Trust proxy defines whether X-Forwarded-* headers should be trusted or not. | ||
50 | | When your application is behind a proxy server like nginx, these values | ||
51 | | are set automatically and should be trusted. Apart from setting it | ||
52 | | to true or false Adonis supports handful or ways to allow proxy | ||
53 | | values. Read documentation for that. | ||
54 | | | ||
55 | */ | ||
56 | trustProxy: false, | ||
57 | |||
58 | /* | ||
59 | |-------------------------------------------------------------------------- | ||
60 | | Subdomains | ||
61 | |-------------------------------------------------------------------------- | ||
62 | | | ||
63 | | Offset to be used for returning subdomains for a given request.For | ||
64 | | majority of applications it will be 2, until you have nested | ||
65 | | sudomains. | ||
66 | | cheatsheet.adonisjs.com - offset - 2 | ||
67 | | virk.cheatsheet.adonisjs.com - offset - 3 | ||
68 | | | ||
69 | */ | ||
70 | subdomainOffset: 2, | ||
71 | |||
72 | /* | ||
73 | |-------------------------------------------------------------------------- | ||
74 | | JSONP Callback | ||
75 | |-------------------------------------------------------------------------- | ||
76 | | | ||
77 | | Default jsonp callback to be used when callback query string is missing | ||
78 | | in request url. | ||
79 | | | ||
80 | */ | ||
81 | jsonpCallback: 'callback', | ||
82 | |||
83 | /* | ||
84 | |-------------------------------------------------------------------------- | ||
85 | | Etag | ||
86 | |-------------------------------------------------------------------------- | ||
87 | | | ||
88 | | Set etag on all HTTP response. In order to disable for selected routes, | ||
89 | | you can call the `response.send` with an options object as follows. | ||
90 | | | ||
91 | | response.send('Hello', { ignoreEtag: true }) | ||
92 | | | ||
93 | */ | ||
94 | etag: false, | ||
95 | }, | ||
96 | |||
97 | views: { | ||
98 | /* | ||
99 | |-------------------------------------------------------------------------- | ||
100 | | Cache Views | ||
101 | |-------------------------------------------------------------------------- | ||
102 | | | ||
103 | | Define whether or not to cache the compiled view. Set it to true in | ||
104 | | production to optimize view loading time. | ||
105 | | | ||
106 | */ | ||
107 | cache: Env.get('CACHE_VIEWS', true), | ||
108 | }, | ||
109 | |||
110 | static: { | ||
111 | /* | ||
112 | |-------------------------------------------------------------------------- | ||
113 | | Dot Files | ||
114 | |-------------------------------------------------------------------------- | ||
115 | | | ||
116 | | Define how to treat dot files when trying to server static resources. | ||
117 | | By default it is set to ignore, which will pretend that dotfiles | ||
118 | | does not exists. | ||
119 | | | ||
120 | | Can be one of the following | ||
121 | | ignore, deny, allow | ||
122 | | | ||
123 | */ | ||
124 | dotfiles: 'ignore', | ||
125 | |||
126 | /* | ||
127 | |-------------------------------------------------------------------------- | ||
128 | | ETag | ||
129 | |-------------------------------------------------------------------------- | ||
130 | | | ||
131 | | Enable or disable etag generation | ||
132 | | | ||
133 | */ | ||
134 | etag: true, | ||
135 | |||
136 | /* | ||
137 | |-------------------------------------------------------------------------- | ||
138 | | Extensions | ||
139 | |-------------------------------------------------------------------------- | ||
140 | | | ||
141 | | Set file extension fallbacks. When set, if a file is not found, the given | ||
142 | | extensions will be added to the file name and search for. The first | ||
143 | | that exists will be served. Example: ['html', 'htm']. | ||
144 | | | ||
145 | */ | ||
146 | extensions: false, | ||
147 | }, | ||
148 | |||
149 | locales: { | ||
150 | /* | ||
151 | |-------------------------------------------------------------------------- | ||
152 | | Loader | ||
153 | |-------------------------------------------------------------------------- | ||
154 | | | ||
155 | | The loader to be used for fetching and updating locales. Below is the | ||
156 | | list of available options. | ||
157 | | | ||
158 | | file, database | ||
159 | | | ||
160 | */ | ||
161 | loader: 'file', | ||
162 | |||
163 | /* | ||
164 | |-------------------------------------------------------------------------- | ||
165 | | Default Locale | ||
166 | |-------------------------------------------------------------------------- | ||
167 | | | ||
168 | | Default locale to be used by Antl provider. You can always switch drivers | ||
169 | | in runtime or use the official Antl middleware to detect the driver | ||
170 | | based on HTTP headers/query string. | ||
171 | | | ||
172 | */ | ||
173 | locale: 'en', | ||
174 | }, | ||
175 | |||
176 | logger: { | ||
177 | /* | ||
178 | |-------------------------------------------------------------------------- | ||
179 | | Transport | ||
180 | |-------------------------------------------------------------------------- | ||
181 | | | ||
182 | | Transport to be used for logging messages. You can have multiple | ||
183 | | transports using same driver. | ||
184 | | | ||
185 | | Available drivers are: `file` and `console`. | ||
186 | | | ||
187 | */ | ||
188 | transport: 'console', | ||
189 | |||
190 | /* | ||
191 | |-------------------------------------------------------------------------- | ||
192 | | Console Transport | ||
193 | |-------------------------------------------------------------------------- | ||
194 | | | ||
195 | | Using `console` driver for logging. This driver writes to `stdout` | ||
196 | | and `stderr` | ||
197 | | | ||
198 | */ | ||
199 | console: { | ||
200 | driver: 'console', | ||
201 | name: 'adonis-app', | ||
202 | level: 'info', | ||
203 | }, | ||
204 | |||
205 | /* | ||
206 | |-------------------------------------------------------------------------- | ||
207 | | File Transport | ||
208 | |-------------------------------------------------------------------------- | ||
209 | | | ||
210 | | File transport uses file driver and writes log messages for a given | ||
211 | | file inside `tmp` directory for your app. | ||
212 | | | ||
213 | | For a different directory, set an absolute path for the filename. | ||
214 | | | ||
215 | */ | ||
216 | file: { | ||
217 | driver: 'file', | ||
218 | name: 'adonis-app', | ||
219 | filename: 'adonis.log', | ||
220 | level: 'info', | ||
221 | }, | ||
222 | }, | ||
223 | |||
224 | /* | ||
225 | |-------------------------------------------------------------------------- | ||
226 | | Generic Cookie Options | ||
227 | |-------------------------------------------------------------------------- | ||
228 | | | ||
229 | | The following cookie options are generic settings used by AdonisJs to create | ||
230 | | cookies. However, some parts of the application like `sessions` can have | ||
231 | | separate settings for cookies inside `config/session.js`. | ||
232 | | | ||
233 | */ | ||
234 | cookie: { | ||
235 | httpOnly: true, | ||
236 | sameSite: false, | ||
237 | path: '/', | ||
238 | maxAge: 7200, | ||
239 | }, | ||
240 | }; | ||
diff --git a/src/internal-server/config/auth.js b/src/internal-server/config/auth.js new file mode 100644 index 000000000..adb38126a --- /dev/null +++ b/src/internal-server/config/auth.js | |||
@@ -0,0 +1,92 @@ | |||
1 | /** @type {import('@adonisjs/framework/src/Env')} */ | ||
2 | const Env = use('Env'); | ||
3 | |||
4 | module.exports = { | ||
5 | /* | ||
6 | |-------------------------------------------------------------------------- | ||
7 | | Authenticator | ||
8 | |-------------------------------------------------------------------------- | ||
9 | | | ||
10 | | Authentication is a combination of serializer and scheme with extra | ||
11 | | config to define on how to authenticate a user. | ||
12 | | | ||
13 | | Available Schemes - basic, session, jwt, api | ||
14 | | Available Serializers - lucid, database | ||
15 | | | ||
16 | */ | ||
17 | authenticator: 'jwt', | ||
18 | |||
19 | /* | ||
20 | |-------------------------------------------------------------------------- | ||
21 | | Session | ||
22 | |-------------------------------------------------------------------------- | ||
23 | | | ||
24 | | Session authenticator makes use of sessions to authenticate a user. | ||
25 | | Session authentication is always persistent. | ||
26 | | | ||
27 | */ | ||
28 | session: { | ||
29 | serializer: 'lucid', | ||
30 | model: 'App/Models/User', | ||
31 | scheme: 'session', | ||
32 | uid: 'email', | ||
33 | password: 'password', | ||
34 | }, | ||
35 | |||
36 | /* | ||
37 | |-------------------------------------------------------------------------- | ||
38 | | Basic Auth | ||
39 | |-------------------------------------------------------------------------- | ||
40 | | | ||
41 | | The basic auth authenticator uses basic auth header to authenticate a | ||
42 | | user. | ||
43 | | | ||
44 | | NOTE: | ||
45 | | This scheme is not persistent and users are supposed to pass | ||
46 | | login credentials on each request. | ||
47 | | | ||
48 | */ | ||
49 | basic: { | ||
50 | serializer: 'lucid', | ||
51 | model: 'App/Models/User', | ||
52 | scheme: 'basic', | ||
53 | uid: 'email', | ||
54 | password: 'password', | ||
55 | }, | ||
56 | |||
57 | /* | ||
58 | |-------------------------------------------------------------------------- | ||
59 | | Jwt | ||
60 | |-------------------------------------------------------------------------- | ||
61 | | | ||
62 | | The jwt authenticator works by passing a jwt token on each HTTP request | ||
63 | | via HTTP `Authorization` header. | ||
64 | | | ||
65 | */ | ||
66 | jwt: { | ||
67 | serializer: 'lucid', | ||
68 | model: 'App/Models/User', | ||
69 | scheme: 'jwt', | ||
70 | uid: 'email', | ||
71 | password: 'password', | ||
72 | options: { | ||
73 | secret: Env.get('APP_KEY'), | ||
74 | }, | ||
75 | }, | ||
76 | |||
77 | /* | ||
78 | |-------------------------------------------------------------------------- | ||
79 | | Api | ||
80 | |-------------------------------------------------------------------------- | ||
81 | | | ||
82 | | The Api scheme makes use of API personal tokens to authenticate a user. | ||
83 | | | ||
84 | */ | ||
85 | api: { | ||
86 | serializer: 'lucid', | ||
87 | model: 'App/Models/User', | ||
88 | scheme: 'api', | ||
89 | uid: 'email', | ||
90 | password: 'password', | ||
91 | }, | ||
92 | }; | ||
diff --git a/src/internal-server/config/bodyParser.js b/src/internal-server/config/bodyParser.js new file mode 100644 index 000000000..8a5406f9e --- /dev/null +++ b/src/internal-server/config/bodyParser.js | |||
@@ -0,0 +1,155 @@ | |||
1 | module.exports = { | ||
2 | /* | ||
3 | |-------------------------------------------------------------------------- | ||
4 | | JSON Parser | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | | ||
7 | | Below settings are applied when the request body contains a JSON payload. | ||
8 | | If you want body parser to ignore JSON payloads, then simply set `types` | ||
9 | | to an empty array. | ||
10 | */ | ||
11 | json: { | ||
12 | /* | ||
13 | |-------------------------------------------------------------------------- | ||
14 | | limit | ||
15 | |-------------------------------------------------------------------------- | ||
16 | | | ||
17 | | Defines the limit of JSON that can be sent by the client. If payload | ||
18 | | is over 1mb it will not be processed. | ||
19 | | | ||
20 | */ | ||
21 | limit: '50mb', | ||
22 | |||
23 | /* | ||
24 | |-------------------------------------------------------------------------- | ||
25 | | strict | ||
26 | |-------------------------------------------------------------------------- | ||
27 | | | ||
28 | | When `strict` is set to true, body parser will only parse Arrays and | ||
29 | | Object. Otherwise everything parseable by `JSON.parse` is parsed. | ||
30 | | | ||
31 | */ | ||
32 | strict: true, | ||
33 | |||
34 | /* | ||
35 | |-------------------------------------------------------------------------- | ||
36 | | types | ||
37 | |-------------------------------------------------------------------------- | ||
38 | | | ||
39 | | Which content types are processed as JSON payloads. You are free to | ||
40 | | add your own types here, but the request body should be parseable | ||
41 | | by `JSON.parse` method. | ||
42 | | | ||
43 | */ | ||
44 | types: [ | ||
45 | 'application/json', | ||
46 | 'application/json-patch+json', | ||
47 | 'application/vnd.api+json', | ||
48 | 'application/csp-report', | ||
49 | ], | ||
50 | }, | ||
51 | |||
52 | /* | ||
53 | |-------------------------------------------------------------------------- | ||
54 | | Raw Parser | ||
55 | |-------------------------------------------------------------------------- | ||
56 | | | ||
57 | | | ||
58 | | | ||
59 | */ | ||
60 | raw: { | ||
61 | types: [ | ||
62 | 'text/*', | ||
63 | ], | ||
64 | }, | ||
65 | |||
66 | /* | ||
67 | |-------------------------------------------------------------------------- | ||
68 | | Form Parser | ||
69 | |-------------------------------------------------------------------------- | ||
70 | | | ||
71 | | | ||
72 | | | ||
73 | */ | ||
74 | form: { | ||
75 | types: [ | ||
76 | 'application/x-www-form-urlencoded', | ||
77 | ], | ||
78 | }, | ||
79 | |||
80 | /* | ||
81 | |-------------------------------------------------------------------------- | ||
82 | | Files Parser | ||
83 | |-------------------------------------------------------------------------- | ||
84 | | | ||
85 | | | ||
86 | | | ||
87 | */ | ||
88 | files: { | ||
89 | types: [ | ||
90 | 'multipart/form-data', | ||
91 | ], | ||
92 | |||
93 | /* | ||
94 | |-------------------------------------------------------------------------- | ||
95 | | Max Size | ||
96 | |-------------------------------------------------------------------------- | ||
97 | | | ||
98 | | Below value is the max size of all the files uploaded to the server. It | ||
99 | | is validated even before files have been processed and hard exception | ||
100 | | is thrown. | ||
101 | | | ||
102 | | Consider setting a reasonable value here, otherwise people may upload GB's | ||
103 | | of files which will keep your server busy. | ||
104 | | | ||
105 | | Also this value is considered when `autoProcess` is set to true. | ||
106 | | | ||
107 | */ | ||
108 | maxSize: '20mb', | ||
109 | |||
110 | /* | ||
111 | |-------------------------------------------------------------------------- | ||
112 | | Auto Process | ||
113 | |-------------------------------------------------------------------------- | ||
114 | | | ||
115 | | Whether or not to auto-process files. Since HTTP servers handle files via | ||
116 | | couple of specific endpoints. It is better to set this value off and | ||
117 | | manually process the files when required. | ||
118 | | | ||
119 | | This value can contain a boolean or an array of route patterns | ||
120 | | to be autoprocessed. | ||
121 | */ | ||
122 | autoProcess: true, | ||
123 | |||
124 | /* | ||
125 | |-------------------------------------------------------------------------- | ||
126 | | Process Manually | ||
127 | |-------------------------------------------------------------------------- | ||
128 | | | ||
129 | | The list of routes that should not process files and instead rely on | ||
130 | | manual process. This list should only contain routes when autoProcess | ||
131 | | is to true. Otherwise everything is processed manually. | ||
132 | | | ||
133 | */ | ||
134 | processManually: [], | ||
135 | |||
136 | /* | ||
137 | |-------------------------------------------------------------------------- | ||
138 | | Temporary file name | ||
139 | |-------------------------------------------------------------------------- | ||
140 | | | ||
141 | | Define a function, which should return a string to be used as the | ||
142 | | tmp file name. | ||
143 | | | ||
144 | | If not defined, Bodyparser will use `uuid` as the tmp file name. | ||
145 | | | ||
146 | | To be defined as. If you are defining the function, then do make sure | ||
147 | | to return a value from it. | ||
148 | | | ||
149 | | tmpFileName () { | ||
150 | | return 'some-unique-value' | ||
151 | | } | ||
152 | | | ||
153 | */ | ||
154 | }, | ||
155 | }; | ||
diff --git a/src/internal-server/config/cors.js b/src/internal-server/config/cors.js new file mode 100644 index 000000000..ca57dff0d --- /dev/null +++ b/src/internal-server/config/cors.js | |||
@@ -0,0 +1,85 @@ | |||
1 | module.exports = { | ||
2 | /* | ||
3 | |-------------------------------------------------------------------------- | ||
4 | | Origin | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | | ||
7 | | Set a list of origins to be allowed. The value can be one of the following | ||
8 | | | ||
9 | | Boolean: true - Allow current request origin | ||
10 | | Boolean: false - Disallow all | ||
11 | | String - Comma separated list of allowed origins | ||
12 | | Array - An array of allowed origins | ||
13 | | String: * - A wildcard to allow current request origin | ||
14 | | Function - Receives the current origin and should return one of the above values. | ||
15 | | | ||
16 | */ | ||
17 | origin: false, | ||
18 | |||
19 | /* | ||
20 | |-------------------------------------------------------------------------- | ||
21 | | Methods | ||
22 | |-------------------------------------------------------------------------- | ||
23 | | | ||
24 | | HTTP methods to be allowed. The value can be one of the following | ||
25 | | | ||
26 | | String - Comma separated list of allowed methods | ||
27 | | Array - An array of allowed methods | ||
28 | | | ||
29 | */ | ||
30 | methods: ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'], | ||
31 | |||
32 | /* | ||
33 | |-------------------------------------------------------------------------- | ||
34 | | Headers | ||
35 | |-------------------------------------------------------------------------- | ||
36 | | | ||
37 | | List of headers to be allowed via Access-Control-Request-Headers header. | ||
38 | | The value can be one of the following. | ||
39 | | | ||
40 | | Boolean: true - Allow current request headers | ||
41 | | Boolean: false - Disallow all | ||
42 | | String - Comma separated list of allowed headers | ||
43 | | Array - An array of allowed headers | ||
44 | | String: * - A wildcard to allow current request headers | ||
45 | | Function - Receives the current header and should return one of the above values. | ||
46 | | | ||
47 | */ | ||
48 | headers: true, | ||
49 | |||
50 | /* | ||
51 | |-------------------------------------------------------------------------- | ||
52 | | Expose Headers | ||
53 | |-------------------------------------------------------------------------- | ||
54 | | | ||
55 | | A list of headers to be exposed via `Access-Control-Expose-Headers` | ||
56 | | header. The value can be one of the following. | ||
57 | | | ||
58 | | Boolean: false - Disallow all | ||
59 | | String: Comma separated list of allowed headers | ||
60 | | Array - An array of allowed headers | ||
61 | | | ||
62 | */ | ||
63 | exposeHeaders: false, | ||
64 | |||
65 | /* | ||
66 | |-------------------------------------------------------------------------- | ||
67 | | Credentials | ||
68 | |-------------------------------------------------------------------------- | ||
69 | | | ||
70 | | Define Access-Control-Allow-Credentials header. It should always be a | ||
71 | | boolean. | ||
72 | | | ||
73 | */ | ||
74 | credentials: false, | ||
75 | |||
76 | /* | ||
77 | |-------------------------------------------------------------------------- | ||
78 | | MaxAge | ||
79 | |-------------------------------------------------------------------------- | ||
80 | | | ||
81 | | Define Access-Control-Allow-Max-Age | ||
82 | | | ||
83 | */ | ||
84 | maxAge: 90, | ||
85 | }; | ||
diff --git a/src/internal-server/config/database.js b/src/internal-server/config/database.js new file mode 100644 index 000000000..1b5974359 --- /dev/null +++ b/src/internal-server/config/database.js | |||
@@ -0,0 +1,82 @@ | |||
1 | /** @type {import('@adonisjs/framework/src/Env')} */ | ||
2 | const Env = use('Env'); | ||
3 | |||
4 | const dbPath = process.env.DB_PATH; | ||
5 | |||
6 | module.exports = { | ||
7 | /* | ||
8 | |-------------------------------------------------------------------------- | ||
9 | | Default Connection | ||
10 | |-------------------------------------------------------------------------- | ||
11 | | | ||
12 | | Connection defines the default connection settings to be used while | ||
13 | | interacting with SQL databases. | ||
14 | | | ||
15 | */ | ||
16 | connection: Env.get('DB_CONNECTION', 'sqlite'), | ||
17 | |||
18 | /* | ||
19 | |-------------------------------------------------------------------------- | ||
20 | | Sqlite | ||
21 | |-------------------------------------------------------------------------- | ||
22 | | | ||
23 | | Sqlite is a flat file database and can be a good choice for a development | ||
24 | | environment. | ||
25 | | | ||
26 | | npm i --save sqlite3 | ||
27 | | | ||
28 | */ | ||
29 | sqlite: { | ||
30 | client: 'sqlite3', | ||
31 | connection: { | ||
32 | // filename: Helpers.databasePath(`${Env.get('DB_DATABASE', 'development')}.sqlite`), | ||
33 | filename: dbPath, | ||
34 | }, | ||
35 | useNullAsDefault: true, | ||
36 | debug: Env.get('DB_DEBUG', false), | ||
37 | }, | ||
38 | |||
39 | /* | ||
40 | |-------------------------------------------------------------------------- | ||
41 | | MySQL | ||
42 | |-------------------------------------------------------------------------- | ||
43 | | | ||
44 | | Here we define connection settings for MySQL database. | ||
45 | | | ||
46 | | npm i --save mysql | ||
47 | | | ||
48 | */ | ||
49 | mysql: { | ||
50 | client: 'mysql', | ||
51 | connection: { | ||
52 | host: Env.get('DB_HOST', 'localhost'), | ||
53 | port: Env.get('DB_PORT', ''), | ||
54 | user: Env.get('DB_USER', 'root'), | ||
55 | password: Env.get('DB_PASSWORD', ''), | ||
56 | database: Env.get('DB_DATABASE', 'adonis'), | ||
57 | }, | ||
58 | debug: Env.get('DB_DEBUG', false), | ||
59 | }, | ||
60 | |||
61 | /* | ||
62 | |-------------------------------------------------------------------------- | ||
63 | | PostgreSQL | ||
64 | |-------------------------------------------------------------------------- | ||
65 | | | ||
66 | | Here we define connection settings for PostgreSQL database. | ||
67 | | | ||
68 | | npm i --save pg | ||
69 | | | ||
70 | */ | ||
71 | pg: { | ||
72 | client: 'pg', | ||
73 | connection: { | ||
74 | host: Env.get('DB_HOST', 'localhost'), | ||
75 | port: Env.get('DB_PORT', ''), | ||
76 | user: Env.get('DB_USER', 'root'), | ||
77 | password: Env.get('DB_PASSWORD', ''), | ||
78 | database: Env.get('DB_DATABASE', 'adonis'), | ||
79 | }, | ||
80 | debug: Env.get('DB_DEBUG', false), | ||
81 | }, | ||
82 | }; | ||
diff --git a/src/internal-server/config/drive.js b/src/internal-server/config/drive.js new file mode 100644 index 000000000..617ce470a --- /dev/null +++ b/src/internal-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/internal-server/config/hash.js b/src/internal-server/config/hash.js new file mode 100644 index 000000000..bbf32f691 --- /dev/null +++ b/src/internal-server/config/hash.js | |||
@@ -0,0 +1,47 @@ | |||
1 | /** @type {import('@adonisjs/framework/src/Env')} */ | ||
2 | const Env = use('Env'); | ||
3 | |||
4 | module.exports = { | ||
5 | /* | ||
6 | |-------------------------------------------------------------------------- | ||
7 | | Driver | ||
8 | |-------------------------------------------------------------------------- | ||
9 | | | ||
10 | | Driver to be used for hashing values. The same driver is used by the | ||
11 | | auth module too. | ||
12 | | | ||
13 | */ | ||
14 | driver: Env.get('HASH_DRIVER', 'bcrypt'), | ||
15 | |||
16 | /* | ||
17 | |-------------------------------------------------------------------------- | ||
18 | | Bcrypt | ||
19 | |-------------------------------------------------------------------------- | ||
20 | | | ||
21 | | Config related to bcrypt hashing. https://www.npmjs.com/package/bcrypt | ||
22 | | package is used internally. | ||
23 | | | ||
24 | */ | ||
25 | bcrypt: { | ||
26 | rounds: 10, | ||
27 | }, | ||
28 | |||
29 | /* | ||
30 | |-------------------------------------------------------------------------- | ||
31 | | Argon | ||
32 | |-------------------------------------------------------------------------- | ||
33 | | | ||
34 | | Config related to argon. https://www.npmjs.com/package/argon2 package is | ||
35 | | used internally. | ||
36 | | | ||
37 | | Since argon is optional, you will have to install the dependency yourself | ||
38 | | | ||
39 | |============================================================================ | ||
40 | | npm i argon2 | ||
41 | |============================================================================ | ||
42 | | | ||
43 | */ | ||
44 | argon: { | ||
45 | type: 1, | ||
46 | }, | ||
47 | }; | ||
diff --git a/src/internal-server/config/session.js b/src/internal-server/config/session.js new file mode 100644 index 000000000..62c4f9cc8 --- /dev/null +++ b/src/internal-server/config/session.js | |||
@@ -0,0 +1,97 @@ | |||
1 | const Env = use('Env'); | ||
2 | |||
3 | module.exports = { | ||
4 | /* | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | Session Driver | ||
7 | |-------------------------------------------------------------------------- | ||
8 | | | ||
9 | | The session driver to be used for storing session values. It can be | ||
10 | | cookie, file or redis. | ||
11 | | | ||
12 | | For `redis` driver, make sure to install and register `@adonisjs/redis` | ||
13 | | | ||
14 | */ | ||
15 | driver: Env.get('SESSION_DRIVER', 'cookie'), | ||
16 | |||
17 | /* | ||
18 | |-------------------------------------------------------------------------- | ||
19 | | Cookie Name | ||
20 | |-------------------------------------------------------------------------- | ||
21 | | | ||
22 | | The name of the cookie to be used for saving session id. Session ids | ||
23 | | are signed and encrypted. | ||
24 | | | ||
25 | */ | ||
26 | cookieName: 'adonis-session', | ||
27 | |||
28 | /* | ||
29 | |-------------------------------------------------------------------------- | ||
30 | | Clear session when browser closes | ||
31 | |-------------------------------------------------------------------------- | ||
32 | | | ||
33 | | If this value is true, the session cookie will be temporary and will be | ||
34 | | removed when browser closes. | ||
35 | | | ||
36 | */ | ||
37 | clearWithBrowser: true, | ||
38 | |||
39 | /* | ||
40 | |-------------------------------------------------------------------------- | ||
41 | | Session age | ||
42 | |-------------------------------------------------------------------------- | ||
43 | | | ||
44 | | This value is only used when `clearWithBrowser` is set to false. The | ||
45 | | age must be a valid https://npmjs.org/package/ms string or should | ||
46 | | be in milliseconds. | ||
47 | | | ||
48 | | Valid values are: | ||
49 | | '2h', '10d', '5y', '2.5 hrs' | ||
50 | | | ||
51 | */ | ||
52 | age: '2h', | ||
53 | |||
54 | /* | ||
55 | |-------------------------------------------------------------------------- | ||
56 | | Cookie options | ||
57 | |-------------------------------------------------------------------------- | ||
58 | | | ||
59 | | Cookie options defines the options to be used for setting up session | ||
60 | | cookie | ||
61 | | | ||
62 | */ | ||
63 | cookie: { | ||
64 | httpOnly: true, | ||
65 | path: '/', | ||
66 | sameSite: false, | ||
67 | }, | ||
68 | |||
69 | /* | ||
70 | |-------------------------------------------------------------------------- | ||
71 | | Sessions location | ||
72 | |-------------------------------------------------------------------------- | ||
73 | | | ||
74 | | If driver is set to file, we need to define the relative location from | ||
75 | | the temporary path or absolute url to any location. | ||
76 | | | ||
77 | */ | ||
78 | file: { | ||
79 | location: 'sessions', | ||
80 | }, | ||
81 | |||
82 | /* | ||
83 | |-------------------------------------------------------------------------- | ||
84 | | Redis config | ||
85 | |-------------------------------------------------------------------------- | ||
86 | | | ||
87 | | The configuration for the redis driver. | ||
88 | | | ||
89 | */ | ||
90 | redis: { | ||
91 | host: '127.0.0.1', | ||
92 | port: 6379, | ||
93 | password: null, | ||
94 | db: 0, | ||
95 | keyPrefix: '', | ||
96 | }, | ||
97 | }; | ||
diff --git a/src/internal-server/config/shield.js b/src/internal-server/config/shield.js new file mode 100644 index 000000000..76f430e91 --- /dev/null +++ b/src/internal-server/config/shield.js | |||
@@ -0,0 +1,143 @@ | |||
1 | module.exports = { | ||
2 | /* | ||
3 | |-------------------------------------------------------------------------- | ||
4 | | Content Security Policy | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | | ||
7 | | Content security policy filters out the origins not allowed to execute | ||
8 | | and load resources like scripts, styles and fonts. There are wide | ||
9 | | variety of options to choose from. | ||
10 | */ | ||
11 | csp: { | ||
12 | /* | ||
13 | |-------------------------------------------------------------------------- | ||
14 | | Directives | ||
15 | |-------------------------------------------------------------------------- | ||
16 | | | ||
17 | | All directives are defined in camelCase and here is the list of | ||
18 | | available directives and their possible values. | ||
19 | | | ||
20 | | https://content-security-policy.com | ||
21 | | | ||
22 | | @example | ||
23 | | directives: { | ||
24 | | defaultSrc: ['self', '@nonce', 'cdnjs.cloudflare.com'] | ||
25 | | } | ||
26 | | | ||
27 | */ | ||
28 | directives: { | ||
29 | }, | ||
30 | /* | ||
31 | |-------------------------------------------------------------------------- | ||
32 | | Report only | ||
33 | |-------------------------------------------------------------------------- | ||
34 | | | ||
35 | | Setting `reportOnly=true` will not block the scripts from running and | ||
36 | | instead report them to a URL. | ||
37 | | | ||
38 | */ | ||
39 | reportOnly: false, | ||
40 | /* | ||
41 | |-------------------------------------------------------------------------- | ||
42 | | Set all headers | ||
43 | |-------------------------------------------------------------------------- | ||
44 | | | ||
45 | | Headers staring with `X` have been depreciated, since all major browsers | ||
46 | | supports the standard CSP header. So its better to disable deperciated | ||
47 | | headers, unless you want them to be set. | ||
48 | | | ||
49 | */ | ||
50 | setAllHeaders: false, | ||
51 | |||
52 | /* | ||
53 | |-------------------------------------------------------------------------- | ||
54 | | Disable on android | ||
55 | |-------------------------------------------------------------------------- | ||
56 | | | ||
57 | | Certain versions of android are buggy with CSP policy. So you can set | ||
58 | | this value to true, to disable it for Android versions with buggy | ||
59 | | behavior. | ||
60 | | | ||
61 | | Here is an issue reported on a different package, but helpful to read | ||
62 | | if you want to know the behavior. https://github.com/helmetjs/helmet/pull/82 | ||
63 | | | ||
64 | */ | ||
65 | disableAndroid: true, | ||
66 | }, | ||
67 | |||
68 | /* | ||
69 | |-------------------------------------------------------------------------- | ||
70 | | X-XSS-Protection | ||
71 | |-------------------------------------------------------------------------- | ||
72 | | | ||
73 | | X-XSS Protection saves from applications from XSS attacks. It is adopted | ||
74 | | by IE and later followed by some other browsers. | ||
75 | | | ||
76 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection | ||
77 | | | ||
78 | */ | ||
79 | xss: { | ||
80 | enabled: true, | ||
81 | enableOnOldIE: false, | ||
82 | }, | ||
83 | |||
84 | /* | ||
85 | |-------------------------------------------------------------------------- | ||
86 | | Iframe Options | ||
87 | |-------------------------------------------------------------------------- | ||
88 | | | ||
89 | | xframe defines whether or not your website can be embedded inside an | ||
90 | | iframe. Choose from one of the following options. | ||
91 | | @available options | ||
92 | | DENY, SAMEORIGIN, ALLOW-FROM http://example.com | ||
93 | | | ||
94 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options | ||
95 | */ | ||
96 | xframe: 'DENY', | ||
97 | |||
98 | /* | ||
99 | |-------------------------------------------------------------------------- | ||
100 | | No Sniff | ||
101 | |-------------------------------------------------------------------------- | ||
102 | | | ||
103 | | Browsers have a habit of sniffing content-type of a response. Which means | ||
104 | | files with .txt extension containing Javascript code will be executed as | ||
105 | | Javascript. You can disable this behavior by setting nosniff to false. | ||
106 | | | ||
107 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options | ||
108 | | | ||
109 | */ | ||
110 | nosniff: true, | ||
111 | |||
112 | /* | ||
113 | |-------------------------------------------------------------------------- | ||
114 | | No Open | ||
115 | |-------------------------------------------------------------------------- | ||
116 | | | ||
117 | | IE users can execute webpages in the context of your website, which is | ||
118 | | a serious security risk. Below option will manage this for you. | ||
119 | | | ||
120 | */ | ||
121 | noopen: true, | ||
122 | |||
123 | /* | ||
124 | |-------------------------------------------------------------------------- | ||
125 | | CSRF Protection | ||
126 | |-------------------------------------------------------------------------- | ||
127 | | | ||
128 | | CSRF Protection adds another layer of security by making sure, actionable | ||
129 | | routes does have a valid token to execute an action. | ||
130 | | | ||
131 | */ | ||
132 | csrf: { | ||
133 | enable: true, | ||
134 | methods: ['POST', 'PUT', 'DELETE'], | ||
135 | filterUris: [], | ||
136 | cookieOptions: { | ||
137 | httpOnly: false, | ||
138 | sameSite: true, | ||
139 | path: '/', | ||
140 | maxAge: 7200, | ||
141 | }, | ||
142 | }, | ||
143 | }; | ||
diff --git a/src/internal-server/database/factory.js b/src/internal-server/database/factory.js new file mode 100644 index 000000000..8534fc20a --- /dev/null +++ b/src/internal-server/database/factory.js | |||
@@ -0,0 +1,19 @@ | |||
1 | /* | ||
2 | |-------------------------------------------------------------------------- | ||
3 | | Factory | ||
4 | |-------------------------------------------------------------------------- | ||
5 | | | ||
6 | | Factories are used to define blueprints for database tables or Lucid | ||
7 | | models. Later you can use these blueprints to seed your database | ||
8 | | with dummy data. | ||
9 | | | ||
10 | */ | ||
11 | |||
12 | /** @type {import('@adonisjs/lucid/src/Factory')} */ | ||
13 | // const Factory = use('Factory') | ||
14 | |||
15 | // Factory.blueprint('App/Models/User', (faker) => { | ||
16 | // return { | ||
17 | // username: faker.username() | ||
18 | // } | ||
19 | // }) | ||
diff --git a/src/internal-server/database/migrations/1503250034279_user.js b/src/internal-server/database/migrations/1503250034279_user.js new file mode 100644 index 000000000..80b49020a --- /dev/null +++ b/src/internal-server/database/migrations/1503250034279_user.js | |||
@@ -0,0 +1,18 @@ | |||
1 | /** @type {import('@adonisjs/lucid/src/Schema')} */ | ||
2 | const Schema = use('Schema'); | ||
3 | |||
4 | class UserSchema extends Schema { | ||
5 | up() { | ||
6 | this.create('users', (table) => { | ||
7 | table.increments(); | ||
8 | table.json('settings'); | ||
9 | table.timestamps(); | ||
10 | }); | ||
11 | } | ||
12 | |||
13 | down() { | ||
14 | this.drop('users'); | ||
15 | } | ||
16 | } | ||
17 | |||
18 | module.exports = UserSchema; | ||
diff --git a/src/internal-server/database/migrations/1566385379883_service_schema.js b/src/internal-server/database/migrations/1566385379883_service_schema.js new file mode 100644 index 000000000..d887ef193 --- /dev/null +++ b/src/internal-server/database/migrations/1566385379883_service_schema.js | |||
@@ -0,0 +1,21 @@ | |||
1 | /** @type {import('@adonisjs/lucid/src/Schema')} */ | ||
2 | const Schema = use('Schema'); | ||
3 | |||
4 | class ServiceSchema extends Schema { | ||
5 | up() { | ||
6 | this.create('services', (table) => { | ||
7 | table.increments(); | ||
8 | table.string('serviceId', 80).notNullable(); | ||
9 | table.string('name', 80).notNullable(); | ||
10 | table.string('recipeId', 254).notNullable(); | ||
11 | table.json('settings'); | ||
12 | table.timestamps(); | ||
13 | }); | ||
14 | } | ||
15 | |||
16 | down() { | ||
17 | this.drop('services'); | ||
18 | } | ||
19 | } | ||
20 | |||
21 | module.exports = ServiceSchema; | ||
diff --git a/src/internal-server/database/migrations/1566554231482_recipe_schema.js b/src/internal-server/database/migrations/1566554231482_recipe_schema.js new file mode 100644 index 000000000..514d57600 --- /dev/null +++ b/src/internal-server/database/migrations/1566554231482_recipe_schema.js | |||
@@ -0,0 +1,20 @@ | |||
1 | /** @type {import('@adonisjs/lucid/src/Schema')} */ | ||
2 | const Schema = use('Schema'); | ||
3 | |||
4 | class RecipeSchema extends Schema { | ||
5 | up() { | ||
6 | this.create('recipes', (table) => { | ||
7 | table.increments(); | ||
8 | table.string('name', 80).notNullable(); | ||
9 | table.string('recipeId', 254).notNullable().unique(); | ||
10 | table.json('data'); | ||
11 | table.timestamps(); | ||
12 | }); | ||
13 | } | ||
14 | |||
15 | down() { | ||
16 | this.drop('recipes'); | ||
17 | } | ||
18 | } | ||
19 | |||
20 | module.exports = RecipeSchema; | ||
diff --git a/src/internal-server/database/migrations/1566554359294_workspace_schema.js b/src/internal-server/database/migrations/1566554359294_workspace_schema.js new file mode 100644 index 000000000..421a406b5 --- /dev/null +++ b/src/internal-server/database/migrations/1566554359294_workspace_schema.js | |||
@@ -0,0 +1,22 @@ | |||
1 | /** @type {import('@adonisjs/lucid/src/Schema')} */ | ||
2 | const Schema = use('Schema'); | ||
3 | |||
4 | class WorkspaceSchema extends Schema { | ||
5 | up() { | ||
6 | this.create('workspaces', (table) => { | ||
7 | table.increments(); | ||
8 | table.string('workspaceId', 80).notNullable().unique(); | ||
9 | table.string('name', 80).notNullable(); | ||
10 | table.integer('order'); | ||
11 | table.json('services'); | ||
12 | table.json('data'); | ||
13 | table.timestamps(); | ||
14 | }); | ||
15 | } | ||
16 | |||
17 | down() { | ||
18 | this.drop('workspaces'); | ||
19 | } | ||
20 | } | ||
21 | |||
22 | module.exports = WorkspaceSchema; | ||
diff --git a/src/internal-server/database/template.sqlite b/src/internal-server/database/template.sqlite new file mode 100644 index 000000000..3750f92c7 --- /dev/null +++ b/src/internal-server/database/template.sqlite | |||
Binary files differ | |||
diff --git a/src/internal-server/env.ini b/src/internal-server/env.ini new file mode 100644 index 000000000..902e8e4c8 --- /dev/null +++ b/src/internal-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/internal-server/package.json b/src/internal-server/package.json new file mode 100644 index 000000000..14ab704d8 --- /dev/null +++ b/src/internal-server/package.json | |||
@@ -0,0 +1,3 @@ | |||
1 | { | ||
2 | "main": "index.js" | ||
3 | } | ||
diff --git a/src/internal-server/public/css/main.css b/src/internal-server/public/css/main.css new file mode 100644 index 000000000..a1c5653d7 --- /dev/null +++ b/src/internal-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/internal-server/public/css/vanilla.css b/src/internal-server/public/css/vanilla.css new file mode 100644 index 000000000..37bc051a2 --- /dev/null +++ b/src/internal-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/internal-server/public/images/logo.png b/src/internal-server/public/images/logo.png new file mode 100644 index 000000000..4145a077a --- /dev/null +++ b/src/internal-server/public/images/logo.png | |||
Binary files differ | |||
diff --git a/src/internal-server/public/js/transfer.js b/src/internal-server/public/js/transfer.js new file mode 100644 index 000000000..c04a6d3b1 --- /dev/null +++ b/src/internal-server/public/js/transfer.js | |||
@@ -0,0 +1,14 @@ | |||
1 | /* eslint-env browser */ | ||
2 | const submitBtn = document.getElementById('submit'); | ||
3 | const fileInput = document.getElementById('file'); | ||
4 | const fileOutput = document.getElementById('fileoutput'); | ||
5 | |||
6 | fileInput.addEventListener('change', () => { | ||
7 | const reader = new FileReader(); | ||
8 | reader.onload = function () { | ||
9 | const text = reader.result; | ||
10 | fileOutput.value = text; | ||
11 | submitBtn.disabled = false; | ||
12 | }; | ||
13 | reader.readAsText(fileInput.files[0]); | ||
14 | }); | ||
diff --git a/src/internal-server/resources/views/import.edge b/src/internal-server/resources/views/import.edge new file mode 100644 index 000000000..561021a0c --- /dev/null +++ b/src/internal-server/resources/views/import.edge | |||
@@ -0,0 +1,18 @@ | |||
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 import your services and workspaces.</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 | <label for="server">API Server to import from</label><br /> | ||
14 | <input type="text" name="server" value="https://api.franzinfra.com" required><br /> | ||
15 | |||
16 | <button type="submit" id="submitbutton">Import Franz account</button> | ||
17 | </form> | ||
18 | @endsection | ||
diff --git a/src/internal-server/resources/views/index.edge b/src/internal-server/resources/views/index.edge new file mode 100644 index 000000000..b01bd7569 --- /dev/null +++ b/src/internal-server/resources/views/index.edge | |||
@@ -0,0 +1,19 @@ | |||
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>Internal Ferdi Server</h1> | ||
12 | <p>You are accessing the local server instance of your Ferdi application. This server is used to enable Ferdi's "Use without an Account" feature.</p> | ||
13 | <p> | ||
14 | To use Ferdi without an account, log out of your current account (if you are already logged in) and choose "Use Ferdi without an Account". | ||
15 | </p> | ||
16 | <p> | ||
17 | Alternatively, you can <a href="/import">import your account from a remote server</a> or <a href="/transfer">import your data from a ".ferdi-data" file</a> or <a href="/transfer">export your data to a ".ferdi-data" file</a>. | ||
18 | </p> | ||
19 | @endsection | ||
diff --git a/src/internal-server/resources/views/layouts/main.edge b/src/internal-server/resources/views/layouts/main.edge new file mode 100644 index 000000000..8856b5d1e --- /dev/null +++ b/src/internal-server/resources/views/layouts/main.edge | |||
@@ -0,0 +1,19 @@ | |||
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-internal-server</title> | ||
9 | |||
10 | {{ style('css/vanilla') }} | ||
11 | {{ style('css/main') }} | ||
12 | </head> | ||
13 | |||
14 | <body> | ||
15 | <img src='images/logo.png' width='300' /> | ||
16 | @!section('content') | ||
17 | </body> | ||
18 | |||
19 | </html> | ||
diff --git a/src/internal-server/resources/views/transfer.edge b/src/internal-server/resources/views/transfer.edge new file mode 100644 index 000000000..58febb24b --- /dev/null +++ b/src/internal-server/resources/views/transfer.edge | |||
@@ -0,0 +1,32 @@ | |||
1 | @layout('layouts.main') | ||
2 | |||
3 | @section('content') | ||
4 | <h2>Import/Export data from another Ferdi server</h2> | ||
5 | @if(success === true) | ||
6 | <div class="alert" style="background-color:#28C76F;"> | ||
7 | Sucessfully imported your account data | ||
8 | </div> | ||
9 | @endif | ||
10 | |||
11 | <h3>Import data</h3> | ||
12 | <div> | ||
13 | <label>Account data</label> | ||
14 | <div> | ||
15 | <input type="file" name="file" id="file" value="" accept=".json,.ferdi-data" required> | ||
16 | </div> | ||
17 | </div> | ||
18 | |||
19 | <form action="/transfer" method="POST"> | ||
20 | <input type="hidden" name="file" id="fileoutput" value=""> | ||
21 | <div> | ||
22 | <button style="background-color:#28C76F;margin-bottom:1rem;" id="submit" disabled>Import data</button> | ||
23 | </div> | ||
24 | </form> | ||
25 | |||
26 | <h3>Export data</h3> | ||
27 | <a class="button" style="background-color:#28C76F;margin-bottom:1rem;" href="/export">Export data</a> | ||
28 | |||
29 | </div> | ||
30 | <script src="/js/transfer.js"></script> | ||
31 | |||
32 | @endsection \ No newline at end of file | ||
diff --git a/src/internal-server/start.js b/src/internal-server/start.js new file mode 100644 index 000000000..adcac0bec --- /dev/null +++ b/src/internal-server/start.js | |||
@@ -0,0 +1,53 @@ | |||
1 | /* | ||
2 | |-------------------------------------------------------------------------- | ||
3 | | Http server | ||
4 | |-------------------------------------------------------------------------- | ||
5 | | | ||
6 | | This file bootstraps Adonisjs to start the HTTP server. You are free to | ||
7 | | customize the process of booting the http server. | ||
8 | | | ||
9 | | """ Loading ace commands """ | ||
10 | | At times you may want to load ace commands when starting the HTTP server. | ||
11 | | Same can be done by chaining `loadCommands()` method after | ||
12 | | | ||
13 | | """ Preloading files """ | ||
14 | | Also you can preload files by calling `preLoad('path/to/file')` method. | ||
15 | | Make sure to pass a relative path from the project root. | ||
16 | */ | ||
17 | process.env.FERDI_VERSION = '5.4.0-beta.5'; | ||
18 | |||
19 | const path = require('path'); | ||
20 | const fs = require('fs-extra'); | ||
21 | const os = require('os'); | ||
22 | |||
23 | process.env.ENV_PATH = path.join(__dirname, 'env.ini'); | ||
24 | |||
25 | const { Ignitor } = require('@adonisjs/ignitor'); | ||
26 | const fold = require('@adonisjs/fold'); | ||
27 | |||
28 | module.exports = async (userPath, port) => { | ||
29 | const dbPath = path.join(userPath, 'server.sqlite'); | ||
30 | const dbTemplatePath = path.join(__dirname, 'database', 'template.sqlite'); | ||
31 | |||
32 | if (!await fs.exists(dbPath)) { | ||
33 | // Manually copy file | ||
34 | // We can't use copyFile here as it will cause the file to be readonly on Windows | ||
35 | const dbTemplate = await fs.readFile(dbTemplatePath); | ||
36 | await fs.writeFile(dbPath, dbTemplate); | ||
37 | |||
38 | // Change permissions to ensure to file is not read-only | ||
39 | if (os.platform() === 'win32') { | ||
40 | // eslint-disable-next-line no-bitwise | ||
41 | fs.chmodSync(dbPath, fs.statSync(dbPath).mode | 146); | ||
42 | } | ||
43 | } | ||
44 | |||
45 | process.env.DB_PATH = dbPath; | ||
46 | process.env.USER_PATH = userPath; | ||
47 | process.env.PORT = port; | ||
48 | |||
49 | new Ignitor(fold) | ||
50 | .appRoot(__dirname) | ||
51 | .fireHttpServer() | ||
52 | .catch(console.error); // eslint-disable-line no-console | ||
53 | }; | ||
diff --git a/src/internal-server/start/app.js b/src/internal-server/start/app.js new file mode 100644 index 000000000..8b1a49f57 --- /dev/null +++ b/src/internal-server/start/app.js | |||
@@ -0,0 +1,61 @@ | |||
1 | /* | ||
2 | |-------------------------------------------------------------------------- | ||
3 | | Providers | ||
4 | |-------------------------------------------------------------------------- | ||
5 | | | ||
6 | | Providers are building blocks for your Adonis app. Anytime you install | ||
7 | | a new Adonis specific package, chances are you will register the | ||
8 | | provider here. | ||
9 | | | ||
10 | */ | ||
11 | const providers = [ | ||
12 | '@adonisjs/framework/providers/AppProvider', | ||
13 | '@adonisjs/bodyparser/providers/BodyParserProvider', | ||
14 | '@adonisjs/cors/providers/CorsProvider', | ||
15 | '@adonisjs/lucid/providers/LucidProvider', | ||
16 | '@adonisjs/drive/providers/DriveProvider', | ||
17 | '@adonisjs/validator/providers/ValidatorProvider', | ||
18 | '@adonisjs/framework/providers/ViewProvider', | ||
19 | '@adonisjs/shield/providers/ShieldProvider', | ||
20 | ]; | ||
21 | |||
22 | /* | ||
23 | |-------------------------------------------------------------------------- | ||
24 | | Ace Providers | ||
25 | |-------------------------------------------------------------------------- | ||
26 | | | ||
27 | | Ace providers are required only when running ace commands. For example | ||
28 | | Providers for migrations, tests etc. | ||
29 | | | ||
30 | */ | ||
31 | const aceProviders = [ | ||
32 | '@adonisjs/lucid/providers/MigrationsProvider', | ||
33 | ]; | ||
34 | |||
35 | /* | ||
36 | |-------------------------------------------------------------------------- | ||
37 | | Aliases | ||
38 | |-------------------------------------------------------------------------- | ||
39 | | | ||
40 | | Aliases are short unique names for IoC container bindings. You are free | ||
41 | | to create your own aliases. | ||
42 | | | ||
43 | | For example: | ||
44 | | { Route: 'Adonis/Src/Route' } | ||
45 | | | ||
46 | */ | ||
47 | const aliases = {}; | ||
48 | |||
49 | /* | ||
50 | |-------------------------------------------------------------------------- | ||
51 | | Commands | ||
52 | |-------------------------------------------------------------------------- | ||
53 | | | ||
54 | | Here you store ace commands for your package | ||
55 | | | ||
56 | */ | ||
57 | const commands = []; | ||
58 | |||
59 | module.exports = { | ||
60 | providers, aceProviders, aliases, commands, | ||
61 | }; | ||
diff --git a/src/internal-server/start/kernel.js b/src/internal-server/start/kernel.js new file mode 100644 index 000000000..7b540f829 --- /dev/null +++ b/src/internal-server/start/kernel.js | |||
@@ -0,0 +1,55 @@ | |||
1 | /** @type {import('@adonisjs/framework/src/Server')} */ | ||
2 | const Server = use('Server'); | ||
3 | |||
4 | /* | ||
5 | |-------------------------------------------------------------------------- | ||
6 | | Global Middleware | ||
7 | |-------------------------------------------------------------------------- | ||
8 | | | ||
9 | | Global middleware are executed on each http request only when the routes | ||
10 | | match. | ||
11 | | | ||
12 | */ | ||
13 | const globalMiddleware = [ | ||
14 | 'Adonis/Middleware/BodyParser', | ||
15 | 'App/Middleware/ConvertEmptyStringsToNull', | ||
16 | ]; | ||
17 | |||
18 | /* | ||
19 | |-------------------------------------------------------------------------- | ||
20 | | Named Middleware | ||
21 | |-------------------------------------------------------------------------- | ||
22 | | | ||
23 | | Named middleware is key/value object to conditionally add middleware on | ||
24 | | specific routes or group of routes. | ||
25 | | | ||
26 | | // define | ||
27 | | { | ||
28 | | auth: 'Adonis/Middleware/Auth' | ||
29 | | } | ||
30 | | | ||
31 | | // use | ||
32 | | Route.get().middleware('auth') | ||
33 | | | ||
34 | */ | ||
35 | const namedMiddleware = { | ||
36 | }; | ||
37 | |||
38 | /* | ||
39 | |-------------------------------------------------------------------------- | ||
40 | | Server Middleware | ||
41 | |-------------------------------------------------------------------------- | ||
42 | | | ||
43 | | Server level middleware are executed even when route for a given URL is | ||
44 | | not registered. Features like `static assets` and `cors` needs better | ||
45 | | control over request lifecycle. | ||
46 | | | ||
47 | */ | ||
48 | const serverMiddleware = [ | ||
49 | 'Adonis/Middleware/Static', | ||
50 | ]; | ||
51 | |||
52 | Server | ||
53 | .registerGlobal(globalMiddleware) | ||
54 | .registerNamed(namedMiddleware) | ||
55 | .use(serverMiddleware); | ||
diff --git a/src/internal-server/start/migrate.js b/src/internal-server/start/migrate.js new file mode 100644 index 000000000..6846beef6 --- /dev/null +++ b/src/internal-server/start/migrate.js | |||
@@ -0,0 +1,44 @@ | |||
1 | /** | ||
2 | * Migrate server database to work with current Ferdi version | ||
3 | */ | ||
4 | const Database = use('Database'); | ||
5 | const User = use('App/Models/User'); | ||
6 | |||
7 | const migrateLog = (text) => { | ||
8 | console.log('\x1b[36m%s\x1b[0m', 'Ferdi Migration:', '\x1b[0m', text); | ||
9 | }; | ||
10 | |||
11 | module.exports = async () => { | ||
12 | migrateLog('🧙 Running database migration wizard'); | ||
13 | |||
14 | // Make sure user table exists | ||
15 | await Database.raw('CREATE TABLE IF NOT EXISTS `users` (`id` integer not null primary key autoincrement, `settings` text, `created_at` datetime, `updated_at` datetime);'); | ||
16 | |||
17 | const user = await User.find(1); | ||
18 | let settings; | ||
19 | if (!user) { | ||
20 | migrateLog('🎩 Migrating from old Ferdi version as user doesn\'t exist'); | ||
21 | |||
22 | // Create new user | ||
23 | await Database.raw('INSERT INTO "users" ("id") VALUES (\'1\');'); | ||
24 | } else { | ||
25 | settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings; | ||
26 | } | ||
27 | |||
28 | if (!settings || !settings.db_version || settings.db_version !== process.env.FERDI_VERSION) { | ||
29 | const srcVersion = settings && settings.db_version ? settings.db_version : '5.4.0-beta.2'; | ||
30 | migrateLog(`🔮 Migrating table from ${srcVersion} to ${process.env.FERDI_VERSION}`); | ||
31 | |||
32 | // Migrate database to current Ferdi version | ||
33 | // Currently no migrations | ||
34 | |||
35 | // Update version number in database | ||
36 | if (!settings) settings = {}; | ||
37 | settings.db_version = process.env.FERDI_VERSION; | ||
38 | const newUser = await User.find(1); // Fetch user again as we might have only just created it | ||
39 | newUser.settings = JSON.stringify(settings); | ||
40 | await newUser.save(); | ||
41 | } else { | ||
42 | migrateLog('🔧 Nothing to migrate, already on the newest version'); | ||
43 | } | ||
44 | }; | ||
diff --git a/src/internal-server/start/routes.js b/src/internal-server/start/routes.js new file mode 100644 index 000000000..63ac42c47 --- /dev/null +++ b/src/internal-server/start/routes.js | |||
@@ -0,0 +1,87 @@ | |||
1 | /* | ||
2 | |-------------------------------------------------------------------------- | ||
3 | | Routes | ||
4 | |-------------------------------------------------------------------------- | ||
5 | | | ||
6 | */ | ||
7 | |||
8 | /** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ | ||
9 | const Route = use('Route'); | ||
10 | |||
11 | // Run latest database migration | ||
12 | const migrate = require('./migrate'); | ||
13 | |||
14 | migrate(); | ||
15 | |||
16 | const OnlyAllowFerdi = async ({ request, response }, next) => { | ||
17 | const version = request.header('X-Franz-Version'); | ||
18 | if (!version) { | ||
19 | return response.status(403).redirect('/'); | ||
20 | } | ||
21 | |||
22 | await next(); | ||
23 | return true; | ||
24 | }; | ||
25 | |||
26 | // Health: Returning if all systems function correctly | ||
27 | Route.get('health', ({ | ||
28 | response, | ||
29 | }) => response.send({ | ||
30 | api: 'success', | ||
31 | db: 'success', | ||
32 | })).middleware(OnlyAllowFerdi); | ||
33 | |||
34 | // API is grouped under '/v1/' route | ||
35 | Route.group(() => { | ||
36 | // User authentification | ||
37 | Route.post('auth/signup', 'UserController.signup'); | ||
38 | Route.post('auth/login', 'UserController.login'); | ||
39 | |||
40 | // User info | ||
41 | Route.get('me', 'UserController.me'); | ||
42 | Route.put('me', 'UserController.updateMe'); | ||
43 | |||
44 | // Service info | ||
45 | Route.post('service', 'ServiceController.create'); | ||
46 | Route.put('service/reorder', 'ServiceController.reorder'); | ||
47 | Route.put('service/:id', 'ServiceController.edit'); | ||
48 | Route.delete('service/:id', 'ServiceController.delete'); | ||
49 | Route.get('me/services', 'ServiceController.list'); | ||
50 | |||
51 | // Recipe store | ||
52 | Route.get('recipe', 'ServiceController.list'); | ||
53 | Route.post('recipes/update', 'ServiceController.update'); | ||
54 | Route.get('recipes', 'RecipeController.list'); | ||
55 | Route.get('recipes/download/:recipe', 'RecipeController.download'); | ||
56 | Route.get('recipes/search', 'RecipeController.search'); | ||
57 | Route.get('recipes/popular', 'StaticController.popularRecipes'); | ||
58 | Route.get('recipes/update', 'StaticController.emptyArray'); | ||
59 | |||
60 | // Workspaces | ||
61 | Route.put('workspace/:id', 'WorkspaceController.edit'); | ||
62 | Route.delete('workspace/:id', 'WorkspaceController.delete'); | ||
63 | Route.post('workspace', 'WorkspaceController.create'); | ||
64 | Route.get('workspace', 'WorkspaceController.list'); | ||
65 | |||
66 | // Static responses | ||
67 | Route.get('features/:mode?', 'StaticController.features'); | ||
68 | Route.get('services', 'StaticController.emptyArray'); | ||
69 | Route.get('news', 'StaticController.emptyArray'); | ||
70 | Route.get('announcements/:version', 'StaticController.announcement'); | ||
71 | }).prefix('v1').middleware(OnlyAllowFerdi); | ||
72 | |||
73 | Route.group(() => { | ||
74 | Route.get('icon/:id', 'ServiceController.icon'); | ||
75 | }).prefix('v1'); | ||
76 | |||
77 | // Franz account import | ||
78 | Route.post('import', 'UserController.import'); | ||
79 | Route.get('import', ({ view }) => view.render('import')); | ||
80 | |||
81 | // Account transfer | ||
82 | Route.get('export', 'UserController.export'); | ||
83 | Route.post('transfer', 'UserController.importFerdi'); | ||
84 | Route.get('transfer', ({ view }) => view.render('transfer')); | ||
85 | |||
86 | // Index | ||
87 | Route.get('/', ({ view }) => view.render('index')); | ||
diff --git a/src/internal-server/test.js b/src/internal-server/test.js new file mode 100644 index 000000000..8d4807d06 --- /dev/null +++ b/src/internal-server/test.js | |||
@@ -0,0 +1,9 @@ | |||
1 | const path = require('path'); | ||
2 | const fs = require('fs-extra'); | ||
3 | const server = require('./start'); | ||
4 | |||
5 | const dummyUserFolder = path.join(__dirname, 'user_data'); | ||
6 | |||
7 | fs.ensureDirSync(dummyUserFolder); | ||
8 | |||
9 | server(dummyUserFolder, 45568); | ||