aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.dockerignore8
-rw-r--r--.eslintignore1
-rw-r--r--.eslintrc3
-rw-r--r--.gitattributes2
-rw-r--r--.gitignore20
-rw-r--r--.gitmodules5
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--INTERNAL_SERVER.md38
-rw-r--r--jest.config.js3
-rw-r--r--package-lock.json14
-rw-r--r--package.json10
-rwxr-xr-xscripts/minify-images.sh2
m---------src/internal-server0
-rw-r--r--src/internal-server/ace21
-rw-r--r--src/internal-server/app/Controllers/Http/RecipeController.js120
-rw-r--r--src/internal-server/app/Controllers/Http/ServiceController.js290
-rw-r--r--src/internal-server/app/Controllers/Http/StaticController.js205
-rw-r--r--src/internal-server/app/Controllers/Http/UserController.js367
-rw-r--r--src/internal-server/app/Controllers/Http/WorkspaceController.js148
-rw-r--r--src/internal-server/app/Exceptions/Handler.js44
-rw-r--r--src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js15
-rw-r--r--src/internal-server/app/Models/Recipe.js7
-rw-r--r--src/internal-server/app/Models/Service.js7
-rw-r--r--src/internal-server/app/Models/Token.js7
-rw-r--r--src/internal-server/app/Models/Traits/NoTimestamp.js14
-rw-r--r--src/internal-server/app/Models/User.js8
-rw-r--r--src/internal-server/app/Models/Workspace.js7
-rw-r--r--src/internal-server/config/app.js240
-rw-r--r--src/internal-server/config/auth.js92
-rw-r--r--src/internal-server/config/bodyParser.js155
-rw-r--r--src/internal-server/config/cors.js85
-rw-r--r--src/internal-server/config/database.js82
-rw-r--r--src/internal-server/config/drive.js45
-rw-r--r--src/internal-server/config/hash.js47
-rw-r--r--src/internal-server/config/session.js97
-rw-r--r--src/internal-server/config/shield.js143
-rw-r--r--src/internal-server/database/factory.js19
-rw-r--r--src/internal-server/database/migrations/1503250034279_user.js18
-rw-r--r--src/internal-server/database/migrations/1566385379883_service_schema.js21
-rw-r--r--src/internal-server/database/migrations/1566554231482_recipe_schema.js20
-rw-r--r--src/internal-server/database/migrations/1566554359294_workspace_schema.js22
-rw-r--r--src/internal-server/database/template.sqlitebin0 -> 40960 bytes
-rw-r--r--src/internal-server/env.ini16
-rw-r--r--src/internal-server/package.json3
-rw-r--r--src/internal-server/public/css/main.css69
-rw-r--r--src/internal-server/public/css/vanilla.css138
-rw-r--r--src/internal-server/public/images/logo.pngbin0 -> 298671 bytes
-rw-r--r--src/internal-server/public/js/transfer.js14
-rw-r--r--src/internal-server/resources/views/import.edge18
-rw-r--r--src/internal-server/resources/views/index.edge19
-rw-r--r--src/internal-server/resources/views/layouts/main.edge19
-rw-r--r--src/internal-server/resources/views/transfer.edge32
-rw-r--r--src/internal-server/start.js53
-rw-r--r--src/internal-server/start/app.js61
-rw-r--r--src/internal-server/start/kernel.js55
-rw-r--r--src/internal-server/start/migrate.js44
-rw-r--r--src/internal-server/start/routes.js87
-rw-r--r--src/internal-server/test.js9
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
25tmp-out/ 25tmp-out/
26uidev/lib 26uidev/lib
27docs/ 27docs/
28src/internal-server/database/tmp/
29src/internal-server/database/development.sqlite
30src/internal-server/database/adonis.sqlite
31src/internal-server/recipes/
32src/internal-server/public/terms.html
33src/internal-server/public/privacy.html
34src/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/
2out/ 2out/
3packages/*/lib 3packages/*/lib
4packages/**/*.test.* 4packages/**/*.test.*
5src/internal-server
6recipes/ 5recipes/
diff --git a/.eslintrc b/.eslintrc
index 6b8fe42de..640e29aa6 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -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
13uidev/lib 13uidev/lib
14*.tsbuildinfo 14*.tsbuildinfo
15server*.log 15server*.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
113git submodule update --init --recursive 113git submodule update --init --recursive
114``` 114```
115 115
116It is important you execute the last command to get the required submodules (recipes, src/internal-server). 116It 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
6Internal 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
17franz-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
25To 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
29You can locally develop `ferdi-internal-server` outside of Ferdi.
30
311. Start the local server via
32 ```bash
33 npm run start:server
34 ```
352. Change Ferdi's server to `http://localhost:45568` to start using the local test server.
36
37## Note For previous contributors
38For 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
5FILES=`find . -name "*.jpg" -o -name "*.jpeg" -o -name "*.bmp" -o -name "*.png" -type f | GREP_OPTIONS= egrep -v "node_modules|internal-server|recipes"` 5FILES=`find . -name "*.jpg" -o -name "*.jpeg" -o -name "*.bmp" -o -name "*.png" -type f | GREP_OPTIONS= egrep -v "node_modules|recipes"`
6for file in $FILES; do 6for 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
16const { Ignitor } = require('@adonisjs/ignitor')
17
18new 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 @@
1const Recipe = use('App/Models/Recipe');
2const Drive = use('Drive');
3const {
4 validateAll,
5} = use('Validator');
6const Env = use('Env');
7
8const fetch = require('node-fetch');
9
10const RECIPES_URL = 'https://api.getferdi.com/v1/recipes';
11
12class RecipeController {
13 // List official and custom recipes
14 async list({
15 response,
16 }) {
17 const officialRecipes = JSON.parse(await (await fetch(RECIPES_URL)).text());
18 const customRecipesArray = (await Recipe.all()).rows;
19 const customRecipes = customRecipesArray.map(recipe => ({
20 id: recipe.recipeId,
21 name: recipe.name,
22 ...JSON.parse(recipe.data),
23 }));
24
25 const recipes = [
26 ...officialRecipes,
27 ...customRecipes,
28 ];
29
30 return response.send(recipes);
31 }
32
33 // Search official and custom recipes
34 async search({
35 request,
36 response,
37 }) {
38 // Validate user input
39 const validation = await validateAll(request.all(), {
40 needle: 'required',
41 });
42 if (validation.fails()) {
43 return response.status(401).send({
44 message: 'Please provide a needle',
45 messages: validation.messages(),
46 status: 401,
47 });
48 }
49
50 const needle = request.input('needle');
51
52 // Get results
53 let results;
54
55 if (needle === 'ferdi:custom') {
56 const dbResults = (await Recipe.all()).toJSON();
57 results = dbResults.map(recipe => ({
58 id: recipe.recipeId,
59 name: recipe.name,
60 ...JSON.parse(recipe.data),
61 }));
62 } else {
63 let remoteResults = [];
64 if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq
65 remoteResults = JSON.parse(await (await fetch(`${RECIPES_URL}/search?needle=${encodeURIComponent(needle)}`)).text());
66 }
67 const localResultsArray = (await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()).toJSON();
68 const localResults = localResultsArray.map(recipe => ({
69 id: recipe.recipeId,
70 name: recipe.name,
71 ...JSON.parse(recipe.data),
72 }));
73
74 results = [
75 ...localResults,
76 ...remoteResults || [],
77 ];
78 }
79
80 return response.send(results);
81 }
82
83 // Download a recipe
84 async download({
85 response,
86 params,
87 }) {
88 // Validate user input
89 const validation = await validateAll(params, {
90 recipe: 'required|accepted',
91 });
92 if (validation.fails()) {
93 return response.status(401).send({
94 message: 'Please provide a recipe ID',
95 messages: validation.messages(),
96 status: 401,
97 });
98 }
99
100 const service = params.recipe;
101
102 // Check for invalid characters
103 if (/\.{1,}/.test(service) || /\/{1,}/.test(service)) {
104 return response.send('Invalid recipe name');
105 }
106
107 // Check if recipe exists in recipes folder
108 if (await Drive.exists(`${service}.tar.gz`)) {
109 return response.send(await Drive.get(`${service}.tar.gz`));
110 } if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq
111 return response.redirect(`${RECIPES_URL}/download/${service}`);
112 }
113 return response.status(400).send({
114 message: 'Recipe not found',
115 code: 'recipe-not-found',
116 });
117 }
118}
119
120module.exports = RecipeController;
diff --git a/src/internal-server/app/Controllers/Http/ServiceController.js b/src/internal-server/app/Controllers/Http/ServiceController.js
new file mode 100644
index 000000000..36d20c70c
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/ServiceController.js
@@ -0,0 +1,290 @@
1const Service = use('App/Models/Service');
2const {
3 validateAll,
4} = use('Validator');
5const Env = use('Env');
6
7const uuid = require('uuid/v4');
8const path = require('path');
9const fs = require('fs-extra');
10
11class ServiceController {
12 // Create a new service for user
13 async create({
14 request,
15 response,
16 }) {
17 // Validate user input
18 const validation = await validateAll(request.all(), {
19 name: 'required|string',
20 recipeId: 'required',
21 });
22 if (validation.fails()) {
23 return response.status(401).send({
24 message: 'Invalid POST arguments',
25 messages: validation.messages(),
26 status: 401,
27 });
28 }
29
30 const data = request.all();
31
32 // Get new, unused uuid
33 let serviceId;
34 do {
35 serviceId = uuid();
36 } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
37
38 await Service.create({
39 serviceId,
40 name: data.name,
41 recipeId: data.recipeId,
42 settings: JSON.stringify(data),
43 });
44
45 return response.send({
46 data: {
47 userId: 1,
48 id: serviceId,
49 isEnabled: true,
50 isNotificationEnabled: true,
51 isBadgeEnabled: true,
52 isMuted: false,
53 isDarkModeEnabled: '',
54 spellcheckerLanguage: '',
55 order: 1,
56 customRecipe: false,
57 hasCustomIcon: false,
58 workspaces: [],
59 iconUrl: null,
60 ...data,
61 },
62 status: ['created'],
63 });
64 }
65
66 // List all services a user has created
67 async list({
68 response,
69 }) {
70 const services = (await Service.all()).rows;
71 // Convert to array with all data Franz wants
72 const servicesArray = services.map((service) => {
73 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
74
75 return {
76 customRecipe: false,
77 hasCustomIcon: false,
78 isBadgeEnabled: true,
79 isDarkModeEnabled: '',
80 isEnabled: true,
81 isMuted: false,
82 isNotificationEnabled: true,
83 order: 1,
84 spellcheckerLanguage: '',
85 workspaces: [],
86 ...JSON.parse(service.settings),
87 iconUrl: settings.iconId ? `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${settings.iconId}` : null,
88 id: service.serviceId,
89 name: service.name,
90 recipeId: service.recipeId,
91 userId: 1,
92 };
93 });
94
95 return response.send(servicesArray);
96 }
97
98 async edit({
99 request,
100 response,
101 params,
102 }) {
103 if (request.file('icon')) {
104 // Upload custom service icon
105 await fs.ensureDir(path.join(Env.get('USER_PATH'), 'icons'));
106
107 const icon = request.file('icon', {
108 types: ['image'],
109 size: '2mb',
110 });
111 const {
112 id,
113 } = params;
114 const service = (await Service.query()
115 .where('serviceId', id).fetch()).rows[0];
116 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
117
118 // Generate new icon ID
119 let iconId;
120 do {
121 iconId = uuid() + uuid();
122 // eslint-disable-next-line no-await-in-loop
123 } while (await fs.exists(path.join(Env.get('USER_PATH'), 'icons', iconId)));
124
125 await icon.move(path.join(Env.get('USER_PATH'), 'icons'), {
126 name: iconId,
127 overwrite: true,
128 });
129
130 if (!icon.moved()) {
131 return response.status(500).send(icon.error());
132 }
133
134 const newSettings = {
135 ...settings,
136 ...{
137 iconId,
138 customIconVersion: settings && settings.customIconVersion ? settings.customIconVersion + 1 : 1,
139 },
140 };
141
142 // Update data in database
143 await (Service.query()
144 .where('serviceId', id)).update({
145 name: service.name,
146 settings: JSON.stringify(newSettings),
147 });
148
149 return response.send({
150 data: {
151 id,
152 name: service.name,
153 ...newSettings,
154 iconUrl: `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${newSettings.iconId}`,
155 userId: 1,
156 },
157 status: ['updated'],
158 });
159 }
160 // Update service info
161 const data = request.all();
162 const {
163 id,
164 } = params;
165
166 // Get current settings from db
167 const serviceData = (await Service.query()
168 .where('serviceId', id).fetch()).rows[0];
169
170 const settings = {
171 ...typeof serviceData.settings === 'string' ? JSON.parse(serviceData.settings) : serviceData.settings,
172 ...data,
173 };
174
175 // Update data in database
176 await (Service.query()
177 .where('serviceId', id)).update({
178 name: data.name,
179 settings: JSON.stringify(settings),
180 });
181
182 // Get updated row
183 const service = (await Service.query()
184 .where('serviceId', id).fetch()).rows[0];
185
186 return response.send({
187 data: {
188 id,
189 name: service.name,
190 ...settings,
191 iconUrl: `${Env.get('APP_URL')}/v1/icon/${settings.iconId}`,
192 userId: 1,
193 },
194 status: ['updated'],
195 });
196 }
197
198 async icon({
199 params,
200 response,
201 }) {
202 const {
203 id,
204 } = params;
205
206 const iconPath = path.join(Env.get('USER_PATH'), 'icons', id);
207 if (!await fs.exists(iconPath)) {
208 return response.status(404).send({
209 status: 'Icon doesn\'t exist',
210 });
211 }
212
213 return response.download(iconPath);
214 }
215
216 async reorder({
217 request,
218 response,
219 }) {
220 const data = request.all();
221
222 for (const service of Object.keys(data)) {
223 // Get current settings from db
224 const serviceData = (await Service.query() // eslint-disable-line no-await-in-loop
225 .where('serviceId', service).fetch()).rows[0];
226
227 const settings = {
228 ...JSON.parse(serviceData.settings),
229 order: data[service],
230 };
231
232 // Update data in database
233 await (Service.query() // eslint-disable-line no-await-in-loop
234 .where('serviceId', service))
235 .update({
236 settings: JSON.stringify(settings),
237 });
238 }
239
240 // Get new services
241 const services = (await Service.all()).rows;
242 // Convert to array with all data Franz wants
243 const servicesArray = services.map((service) => {
244 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
245
246 return {
247 customRecipe: false,
248 hasCustomIcon: false,
249 isBadgeEnabled: true,
250 isDarkModeEnabled: '',
251 isEnabled: true,
252 isMuted: false,
253 isNotificationEnabled: true,
254 order: 1,
255 spellcheckerLanguage: '',
256 workspaces: [],
257 ...JSON.parse(service.settings),
258 iconUrl: settings.iconId ? `http://127.0.0.1:${Env.get('PORT')}/v1/icon/${settings.iconId}` : null,
259 id: service.serviceId,
260 name: service.name,
261 recipeId: service.recipeId,
262 userId: 1,
263 };
264 });
265
266 return response.send(servicesArray);
267 }
268
269 update({
270 response,
271 }) {
272 return response.send([]);
273 }
274
275 async delete({
276 params,
277 response,
278 }) {
279 // Update data in database
280 await (Service.query()
281 .where('serviceId', params.id)).delete();
282
283 return response.send({
284 message: 'Sucessfully deleted service',
285 status: 200,
286 });
287 }
288}
289
290module.exports = ServiceController;
diff --git a/src/internal-server/app/Controllers/Http/StaticController.js b/src/internal-server/app/Controllers/Http/StaticController.js
new file mode 100644
index 000000000..69dfee0a3
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/StaticController.js
@@ -0,0 +1,205 @@
1/**
2 * Controller for routes with static responses
3 */
4
5class StaticController {
6 // Enable all features
7 features({
8 response,
9 }) {
10 return response.send({
11 isServiceProxyEnabled: true,
12 isWorkspaceEnabled: true,
13 isAnnouncementsEnabled: true,
14 isSettingsWSEnabled: false,
15 isMagicBarEnabled: true,
16 isTodosEnabled: true,
17 subscribeURL: 'https://getferdi.com',
18 hasInlineCheckout: true,
19 });
20 }
21
22 // Return an empty array
23 emptyArray({
24 response,
25 }) {
26 return response.send([]);
27 }
28
29 // Return list of popular recipes (copy of the response Franz's API is returning)
30 popularRecipes({
31 response,
32 }) {
33 return response.send([{
34 // TODO: Why is this list hardcoded?
35 author: 'Stefan Malzner <stefan@adlk.io>',
36 featured: false,
37 id: 'slack',
38 name: 'Slack',
39 version: '1.0.4',
40 icons: {
41 png: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.png',
42 svg: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.svg',
43 },
44 }, {
45 author: 'Stefan Malzner <stefan@adlk.io>',
46 featured: false,
47 id: 'whatsapp',
48 name: 'WhatsApp',
49 version: '1.0.1',
50 icons: {
51 png: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.png',
52 svg: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.svg',
53 },
54 }, {
55 author: 'Stefan Malzner <stefan@adlk.io>',
56 featured: false,
57 id: 'messenger',
58 name: 'Messenger',
59 version: '1.0.6',
60 icons: {
61 png: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.png',
62 svg: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.svg',
63 },
64 }, {
65 author: 'Stefan Malzner <stefan@adlk.io>',
66 featured: false,
67 id: 'telegram',
68 name: 'Telegram',
69 version: '1.0.0',
70 icons: {
71 png: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.png',
72 svg: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.svg',
73 },
74 }, {
75 author: 'Stefan Malzner <stefan@adlk.io>',
76 featured: false,
77 id: 'gmail',
78 name: 'Gmail',
79 version: '1.0.0',
80 icons: {
81 png: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.png',
82 svg: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.svg',
83 },
84 }, {
85 author: 'Stefan Malzner <stefan@adlk.io>',
86 featured: false,
87 id: 'skype',
88 name: 'Skype',
89 version: '1.0.0',
90 icons: {
91 png: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.png',
92 svg: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.svg',
93 },
94 }, {
95 author: 'Stefan Malzner <stefan@adlk.io>',
96 featured: false,
97 id: 'hangouts',
98 name: 'Hangouts',
99 version: '1.0.0',
100 icons: {
101 png: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.png',
102 svg: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.svg',
103 },
104 }, {
105 author: 'Stefan Malzner <stefan@adlk.io>',
106 featured: false,
107 id: 'discord',
108 name: 'Discord',
109 version: '1.0.0',
110 icons: {
111 png: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.png',
112 svg: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.svg',
113 },
114 }, {
115 author: 'Stefan Malzner <stefan@adlk.io>',
116 featured: false,
117 id: 'tweetdeck',
118 name: 'Tweetdeck',
119 version: '1.0.1',
120 icons: {
121 png: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.png',
122 svg: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.svg',
123 },
124 }, {
125 author: 'Stefan Malzner <stefan@adlk.io>',
126 featured: false,
127 id: 'hipchat',
128 name: 'HipChat',
129 version: '1.0.1',
130 icons: {
131 png: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.png',
132 svg: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.svg',
133 },
134 }, {
135 author: 'Stefan Malzner <stefan@adlk.io>',
136 featured: false,
137 id: 'gmailinbox',
138 name: 'Inbox by Gmail',
139 version: '1.0.0',
140 icons: {
141 png: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.png',
142 svg: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.svg',
143 },
144 }, {
145 author: 'Stefan Malzner <stefan@adlk.io>',
146 featured: false,
147 id: 'rocketchat',
148 name: 'Rocket.Chat',
149 version: '1.0.1',
150 icons: {
151 png: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.png',
152 svg: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.svg',
153 },
154 }, {
155 author: 'Brian Gilbert <brian@briangilbert.net>',
156 featured: false,
157 id: 'gitter',
158 name: 'Gitter',
159 version: '1.0.0',
160 icons: {
161 png: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.png',
162 svg: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.svg',
163 },
164 }, {
165 author: 'Stefan Malzner <stefan@adlk.io>',
166 featured: false,
167 id: 'mattermost',
168 name: 'Mattermost',
169 version: '1.0.0',
170 icons: {
171 png: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.png',
172 svg: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.svg',
173 },
174 }, {
175 author: 'Franz <recipe@meetfranz.com>',
176 featured: false,
177 id: 'toggl',
178 name: 'toggl',
179 version: '1.0.0',
180 icons: {
181 png: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.png',
182 svg: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.svg',
183 },
184 }, {
185 author: 'Stuart Clark <stuart@realityloop.com>',
186 featured: false,
187 id: 'twist',
188 name: 'twist',
189 version: '1.0.0',
190 icons: {
191 png: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.png',
192 svg: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.svg',
193 },
194 }]);
195 }
196
197 // Show announcements
198 announcement({
199 response,
200 }) {
201 return response.send({});
202 }
203}
204
205module.exports = StaticController;
diff --git a/src/internal-server/app/Controllers/Http/UserController.js b/src/internal-server/app/Controllers/Http/UserController.js
new file mode 100644
index 000000000..f7cdfc9c9
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/UserController.js
@@ -0,0 +1,367 @@
1const User = use('App/Models/User');
2const Service = use('App/Models/Service');
3const Workspace = use('App/Models/Workspace');
4const {
5 validateAll,
6} = use('Validator');
7
8const btoa = require('btoa');
9const fetch = require('node-fetch');
10const uuid = require('uuid/v4');
11const crypto = require('crypto');
12
13const apiRequest = (url, route, method, auth) => new Promise((resolve, reject) => {
14 const base = `${url}/v1/`;
15 const user = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36';
16
17 try {
18 fetch(base + route, {
19 method,
20 headers: {
21 Authorization: `Bearer ${auth}`,
22 'User-Agent': user,
23 },
24 })
25 .then(data => data.json())
26 .then(json => resolve(json));
27 } catch (e) {
28 reject();
29 }
30});
31
32class UserController {
33 // Register a new user
34 async signup({
35 request,
36 response,
37 }) {
38 // Validate user input
39 const validation = await validateAll(request.all(), {
40 firstname: 'required',
41 email: 'required|email',
42 password: 'required',
43 });
44 if (validation.fails()) {
45 return response.status(401).send({
46 message: 'Invalid POST arguments',
47 messages: validation.messages(),
48 status: 401,
49 });
50 }
51
52 return response.send({
53 message: 'Successfully created account',
54 token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M',
55 });
56 }
57
58 // Login using an existing user
59 async login({
60 request,
61 response,
62 }) {
63 if (!request.header('Authorization')) {
64 return response.status(401).send({
65 message: 'Please provide authorization',
66 status: 401,
67 });
68 }
69
70 return response.send({
71 message: 'Successfully logged in',
72 token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M',
73 });
74 }
75
76 // Return information about the current user
77 async me({
78 response,
79 }) {
80 const user = await User.find(1);
81
82 const settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings;
83
84 return response.send({
85 accountType: 'individual',
86 beta: false,
87 donor: {},
88 email: '',
89 emailValidated: true,
90 features: {},
91 firstname: 'Ferdi',
92 id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8',
93 isSubscriptionOwner: true,
94 lastname: 'Application',
95 locale: 'en-US',
96 ...settings || {},
97 });
98 }
99
100 async updateMe({
101 request,
102 response,
103 }) {
104 const user = await User.find(1);
105
106 let settings = user.settings || {};
107 if (typeof settings === 'string') {
108 settings = JSON.parse(settings);
109 }
110
111 const newSettings = {
112 ...settings,
113 ...request.all(),
114 };
115
116 user.settings = JSON.stringify(newSettings);
117 await user.save();
118
119 return response.send({
120 data: {
121 accountType: 'individual',
122 beta: false,
123 donor: {},
124 email: '',
125 emailValidated: true,
126 features: {},
127 firstname: 'Ferdi',
128 id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8',
129 isSubscriptionOwner: true,
130 lastname: 'Application',
131 locale: 'en-US',
132 ...newSettings,
133 },
134 status: [
135 'data-updated',
136 ],
137 });
138 }
139
140 async import({
141 request,
142 response,
143 }) {
144 // Validate user input
145 const validation = await validateAll(request.all(), {
146 email: 'required|email',
147 password: 'required',
148 server: 'required',
149 });
150 if (validation.fails()) {
151 let errorMessage = 'There was an error while trying to import your account:\n';
152 for (const message of validation.messages()) {
153 if (message.validation === 'required') {
154 errorMessage += `- Please make sure to supply your ${message.field}\n`;
155 } else if (message.validation === 'unique') {
156 errorMessage += '- There is already a user with this email.\n';
157 } else {
158 errorMessage += `${message.message}\n`;
159 }
160 }
161 return response.status(401).send(errorMessage);
162 }
163
164 const {
165 email,
166 password,
167 server,
168 } = request.all();
169
170 const hashedPassword = crypto.createHash('sha256').update(password).digest('base64');
171
172 const base = `${server}/v1/`;
173 const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36';
174
175 // Try to get an authentication token
176 let token;
177 try {
178 const basicToken = btoa(`${email}:${hashedPassword}`);
179
180 const rawResponse = await fetch(`${base}auth/login`, {
181 method: 'POST',
182 headers: {
183 Authorization: `Basic ${basicToken}`,
184 'User-Agent': userAgent,
185 },
186 });
187 const content = await rawResponse.json();
188
189 if (!content.message || content.message !== 'Successfully logged in') {
190 const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again';
191 return response.status(401).send(errorMessage);
192 }
193
194 // eslint-disable-next-line prefer-destructuring
195 token = content.token;
196 } catch (e) {
197 return response.status(401).send({
198 message: 'Cannot login to Franz',
199 error: e,
200 });
201 }
202
203 // Get user information
204 let userInf = false;
205 try {
206 userInf = await apiRequest(server, 'me', 'GET', token);
207 } catch (e) {
208 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`;
209 return response.status(401).send(errorMessage);
210 }
211 if (!userInf) {
212 const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later';
213 return response.status(401).send(errorMessage);
214 }
215
216 const serviceIdTranslation = {};
217
218 // Import services
219 try {
220 const services = await apiRequest(server, 'me/services', 'GET', token);
221
222 for (const service of services) {
223 // Get new, unused uuid
224 let serviceId;
225 do {
226 serviceId = uuid();
227 } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
228
229 await Service.create({ // eslint-disable-line no-await-in-loop
230 serviceId,
231 name: service.name,
232 recipeId: service.recipeId,
233 settings: JSON.stringify(service),
234 });
235
236 serviceIdTranslation[service.id] = serviceId;
237 }
238 } catch (e) {
239 const errorMessage = `Could not import your services into our system.\nError: ${e}`;
240 return response.status(401).send(errorMessage);
241 }
242
243 // Import workspaces
244 try {
245 const workspaces = await apiRequest(server, 'workspace', 'GET', token);
246
247 for (const workspace of workspaces) {
248 let workspaceId;
249 do {
250 workspaceId = uuid();
251 } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
252
253 const services = workspace.services.map(service => serviceIdTranslation[service]);
254
255 await Workspace.create({ // eslint-disable-line no-await-in-loop
256 workspaceId,
257 name: workspace.name,
258 order: workspace.order,
259 services: JSON.stringify(services),
260 data: JSON.stringify({}),
261 });
262 }
263 } catch (e) {
264 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`;
265 return response.status(401).send(errorMessage);
266 }
267
268 return response.send('Your account has been imported. You can now use your Franz account in Ferdi.');
269 }
270
271 // Account import/export
272 async export({
273 // eslint-disable-next-line no-unused-vars
274 auth,
275 response,
276 }) {
277 const services = (await Service.all()).toJSON();
278 const workspaces = (await Workspace.all()).toJSON();
279
280 const exportData = {
281 username: 'Ferdi',
282 mail: 'internal@getferdi.com',
283 services,
284 workspaces,
285 };
286
287 return response
288 .header('Content-Type', 'application/force-download')
289 .header('Content-disposition', 'attachment; filename=export.ferdi-data')
290 .send(exportData);
291 }
292
293 async importFerdi({
294 request,
295 response,
296 }) {
297 const validation = await validateAll(request.all(), {
298 file: 'required',
299 });
300 if (validation.fails()) {
301 return response.send(validation.messages());
302 }
303
304 let file;
305 try {
306 file = JSON.parse(request.input('file'));
307 } catch (e) {
308 return response.send('Could not import: Invalid file, could not read file');
309 }
310
311 if (!file || !file.services || !file.workspaces) {
312 return response.send('Could not import: Invalid file (2)');
313 }
314
315 const serviceIdTranslation = {};
316
317 // Import services
318 try {
319 for (const service of file.services) {
320 // Get new, unused uuid
321 let serviceId;
322 do {
323 serviceId = uuid();
324 } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
325
326 await Service.create({ // eslint-disable-line no-await-in-loop
327 serviceId,
328 name: service.name,
329 recipeId: service.recipeId,
330 settings: JSON.stringify(service.settings),
331 });
332
333 serviceIdTranslation[service.id] = serviceId;
334 }
335 } catch (e) {
336 const errorMessage = `Could not import your services into our system.\nError: ${e}`;
337 return response.send(errorMessage);
338 }
339
340 // Import workspaces
341 try {
342 for (const workspace of file.workspaces) {
343 let workspaceId;
344 do {
345 workspaceId = uuid();
346 } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
347
348 const services = workspace.services.map((service) => serviceIdTranslation[service]);
349
350 await Workspace.create({ // eslint-disable-line no-await-in-loop
351 workspaceId,
352 name: workspace.name,
353 order: workspace.order,
354 services: JSON.stringify(services),
355 data: JSON.stringify(workspace.data),
356 });
357 }
358 } catch (e) {
359 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`;
360 return response.status(401).send(errorMessage);
361 }
362
363 return response.send('Your account has been imported.');
364 }
365}
366
367module.exports = UserController;
diff --git a/src/internal-server/app/Controllers/Http/WorkspaceController.js b/src/internal-server/app/Controllers/Http/WorkspaceController.js
new file mode 100644
index 000000000..4189fbcdd
--- /dev/null
+++ b/src/internal-server/app/Controllers/Http/WorkspaceController.js
@@ -0,0 +1,148 @@
1const Workspace = use('App/Models/Workspace');
2const {
3 validateAll,
4} = use('Validator');
5
6const uuid = require('uuid/v4');
7
8class WorkspaceController {
9 // Create a new workspace for user
10 async create({
11 request,
12 response,
13 }) {
14 // Validate user input
15 const validation = await validateAll(request.all(), {
16 name: 'required',
17 });
18 if (validation.fails()) {
19 return response.status(401).send({
20 message: 'Invalid POST arguments',
21 messages: validation.messages(),
22 status: 401,
23 });
24 }
25
26 const data = request.all();
27
28 // Get new, unused uuid
29 let workspaceId;
30 do {
31 workspaceId = uuid();
32 } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop
33
34 const order = (await Workspace.all()).rows.length;
35
36 await Workspace.create({
37 workspaceId,
38 name: data.name,
39 order,
40 services: JSON.stringify([]),
41 data: JSON.stringify(data),
42 });
43
44 return response.send({
45 userId: 1,
46 name: data.name,
47 id: workspaceId,
48 order,
49 workspaces: [],
50 });
51 }
52
53 async edit({
54 request,
55 response,
56 params,
57 }) {
58 // Validate user input
59 const validation = await validateAll(request.all(), {
60 name: 'required',
61 services: 'required|array',
62 });
63 if (validation.fails()) {
64 return response.status(401).send({
65 message: 'Invalid POST arguments',
66 messages: validation.messages(),
67 status: 401,
68 });
69 }
70
71 const data = request.all();
72 const {
73 id,
74 } = params;
75
76 // Update data in database
77 await (Workspace.query()
78 .where('workspaceId', id)).update({
79 name: data.name,
80 services: JSON.stringify(data.services),
81 });
82
83 // Get updated row
84 const workspace = (await Workspace.query()
85 .where('workspaceId', id).fetch()).rows[0];
86
87 return response.send({
88 id: workspace.workspaceId,
89 name: data.name,
90 order: workspace.order,
91 services: data.services,
92 userId: 1,
93 });
94 }
95
96 async delete({
97 // eslint-disable-next-line no-unused-vars
98 request,
99 response,
100 params,
101 }) {
102 // Validate user input
103 const validation = await validateAll(params, {
104 id: 'required',
105 });
106 if (validation.fails()) {
107 return response.status(401).send({
108 message: 'Invalid arguments',
109 messages: validation.messages(),
110 status: 401,
111 });
112 }
113
114 const {
115 id,
116 } = params;
117
118 // Update data in database
119 await (Workspace.query()
120 .where('workspaceId', id)).delete();
121
122 return response.send({
123 message: 'Successfully deleted workspace',
124 });
125 }
126
127 // List all workspaces a user has created
128 async list({
129 response,
130 }) {
131 const workspaces = (await Workspace.all()).rows;
132 // Convert to array with all data Franz wants
133 let workspacesArray = [];
134 if (workspaces) {
135 workspacesArray = workspaces.map(workspace => ({
136 id: workspace.workspaceId,
137 name: workspace.name,
138 order: workspace.order,
139 services: typeof workspace.services === 'string' ? JSON.parse(workspace.services) : workspace.services,
140 userId: 1,
141 }));
142 }
143
144 return response.send(workspacesArray);
145 }
146}
147
148module.exports = WorkspaceController;
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 @@
1const BaseExceptionHandler = use('BaseExceptionHandler');
2
3/**
4 * This class handles all exceptions thrown during
5 * the HTTP request lifecycle.
6 *
7 * @class ExceptionHandler
8 */
9class ExceptionHandler extends BaseExceptionHandler {
10 /**
11 * Handle exception thrown during the HTTP lifecycle
12 *
13 * @method handle
14 *
15 * @param {Object} error
16 * @param {Object} options.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
44module.exports = ExceptionHandler;
diff --git a/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js b/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js
new file mode 100644
index 000000000..87f1f6c25
--- /dev/null
+++ b/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js
@@ -0,0 +1,15 @@
1class ConvertEmptyStringsToNull {
2 async handle({ request }, next) {
3 if (Object.keys(request.body).length) {
4 request.body = Object.assign(
5 ...Object.keys(request.body).map(key => ({
6 [key]: request.body[key] !== '' ? request.body[key] : null,
7 })),
8 );
9 }
10
11 await next();
12 }
13}
14
15module.exports = ConvertEmptyStringsToNull;
diff --git a/src/internal-server/app/Models/Recipe.js b/src/internal-server/app/Models/Recipe.js
new file mode 100644
index 000000000..bd9741114
--- /dev/null
+++ b/src/internal-server/app/Models/Recipe.js
@@ -0,0 +1,7 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model');
3
4class Recipe extends Model {
5}
6
7module.exports = Recipe;
diff --git a/src/internal-server/app/Models/Service.js b/src/internal-server/app/Models/Service.js
new file mode 100644
index 000000000..a2e5c981e
--- /dev/null
+++ b/src/internal-server/app/Models/Service.js
@@ -0,0 +1,7 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model');
3
4class Service extends Model {
5}
6
7module.exports = Service;
diff --git a/src/internal-server/app/Models/Token.js b/src/internal-server/app/Models/Token.js
new file mode 100644
index 000000000..83e989117
--- /dev/null
+++ b/src/internal-server/app/Models/Token.js
@@ -0,0 +1,7 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model');
3
4class Token extends Model {
5}
6
7module.exports = Token;
diff --git a/src/internal-server/app/Models/Traits/NoTimestamp.js b/src/internal-server/app/Models/Traits/NoTimestamp.js
new file mode 100644
index 000000000..914f542f0
--- /dev/null
+++ b/src/internal-server/app/Models/Traits/NoTimestamp.js
@@ -0,0 +1,14 @@
1class NoTimestamp {
2 register(Model) {
3 Object.defineProperties(Model, {
4 createdAtColumn: {
5 get: () => null,
6 },
7 updatedAtColumn: {
8 get: () => null,
9 },
10 });
11 }
12}
13
14module.exports = NoTimestamp;
diff --git a/src/internal-server/app/Models/User.js b/src/internal-server/app/Models/User.js
new file mode 100644
index 000000000..907710d8d
--- /dev/null
+++ b/src/internal-server/app/Models/User.js
@@ -0,0 +1,8 @@
1// File is required by AdonisJS but not used by the server
2/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
3const Model = use('Model');
4
5class User extends Model {
6}
7
8module.exports = User;
diff --git a/src/internal-server/app/Models/Workspace.js b/src/internal-server/app/Models/Workspace.js
new file mode 100644
index 000000000..dcf39ac75
--- /dev/null
+++ b/src/internal-server/app/Models/Workspace.js
@@ -0,0 +1,7 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model');
3
4class Workspace extends Model {
5}
6
7module.exports = Workspace;
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')} */
2const Env = use('Env');
3
4module.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')} */
2const Env = use('Env');
3
4module.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 @@
1module.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 @@
1module.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')} */
2const Env = use('Env');
3
4const dbPath = process.env.DB_PATH;
5
6module.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 @@
1const Env = use('Env');
2
3module.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')} */
2const Env = use('Env');
3
4module.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 @@
1const Env = use('Env');
2
3module.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 @@
1module.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')} */
2const Schema = use('Schema');
3
4class 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
18module.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')} */
2const Schema = use('Schema');
3
4class 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
21module.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')} */
2const Schema = use('Schema');
3
4class 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
20module.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')} */
2const Schema = use('Schema');
3
4class 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
22module.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 @@
1HOST=127.0.0.1
2PORT=45569
3NODE_ENV=development
4APP_NAME=Ferdi Internal Server
5APP_URL=http://${HOST}:${PORT}
6CACHE_VIEWS=false
7APP_KEY=FERDIINTERNALSERVER
8DB_CONNECTION=sqlite
9DB_HOST=127.0.0.1
10DB_PORT=3306
11DB_USER=root
12DB_PASSWORD=
13DB_DATABASE=ferdi
14HASH_DRIVER=bcrypt
15IS_CREATION_ENABLED=true
16CONNECT_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 @@
1input {
2 margin-bottom: 1rem;
3 width: 100%;
4 padding: 0.5rem;
5}
6
7button, .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
67td {
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 */
2html, body, div, span, applet, object, iframe,
3h1, h2, h3, h4, h5, h6, p, blockquote, pre,
4a, abbr, acronym, address, big, cite, code,
5del, dfn, em, img, ins, kbd, q, s, samp,
6small, strike, strong, sub, sup, tt, var,
7b, u, i, center,
8dl, dt, dd, ol, ul, li,
9fieldset, form, label, legend,
10table, caption, tbody, tfoot, thead, tr, th, td,
11article, aside, canvas, details, embed,
12figure, figcaption, footer, header, hgroup,
13menu, nav, output, ruby, section, summary,
14time, 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 */
43body {
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
49h1,h2,h3,h4,h5,h6,p,blockquote,dl,img,figure {
50 margin: 2rem 0;
51}
52
53h1,h2,h3,h4,h5,h6 { font-weight: bold; }
54h1 { font-size: 200%; }
55h2 { font-size: 150%; }
56h3 { font-size: 120%; }
57h4,h5,h6 { font-size: 100%; }
58h5, h6 { text-transform: uppercase; }
59
60header h1 { border-bottom: 1px solid; }
61
62p { margin: 2rem 0; }
63
64a,a:visited { color: var(--link-color); }
65
66strong, time, b { font-weight: bold; }
67em, dfn, i { font-style: italic; }
68sub { font-size: 60%; vertical-align: bottom; }
69small { font-size: 80%; }
70
71blockquote, q {
72 background: var(--secondary-color);
73 border-left: 10px solid var(--primary-color);
74 font-family: "Georgia", serif;
75 padding: 1rem;
76}
77blockquote p:first-child { margin-top: 0; }
78cite {
79 font-family: "Georgia", serif;
80 font-style: italic;
81 font-weight: bold;
82}
83
84kbd,code,samp,pre,var { font-family: monospace; font-weight: bold; }
85code, pre {
86 background: var(--tertiary-color);
87 padding: 0.5rem 1rem;
88}
89code pre , pre code { padding: 0; }
90
91
92
93/* Elements */
94hr {
95 background: var(--text-color);
96 border: 0;
97 height: 1px;
98 margin: 4rem 0;
99}
100
101img { max-width: 100%; }
102
103figure {
104 border: 1px solid var(--primary-color);
105 display: inline-block;
106 padding: 1rem;
107 width: auto;
108}
109figure img { margin: 0; }
110figure figcaption { font-size: 80%; }
111
112ul, ol { margin: 2rem 0; padding: 0 0 0 4rem; }
113
114dl dd { padding-left: 2rem; }
115
116table {
117 border: 1px solid var(--primary-color);
118 border-collapse: collapse;
119 table-layout: fixed;
120 width: 100%;
121}
122table caption { margin: 2rem 0; }
123table thead { text-align: center; }
124table tbody { text-align: right; }
125table tr { border-bottom: 1px solid var(--primary-color); }
126table tbody tr:nth-child(even) { background: var(--tertiary-color); }
127table th { background: var(--secondary-color); font-weight: bold; }
128table th, table td { padding: 1rem; }
129table 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 */
2const submitBtn = document.getElementById('submit');
3const fileInput = document.getElementById('file');
4const fileOutput = document.getElementById('fileoutput');
5
6fileInput.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*/
17process.env.FERDI_VERSION = '5.4.0-beta.5';
18
19const path = require('path');
20const fs = require('fs-extra');
21const os = require('os');
22
23process.env.ENV_PATH = path.join(__dirname, 'env.ini');
24
25const { Ignitor } = require('@adonisjs/ignitor');
26const fold = require('@adonisjs/fold');
27
28module.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*/
11const 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*/
31const 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*/
47const aliases = {};
48
49/*
50|--------------------------------------------------------------------------
51| Commands
52|--------------------------------------------------------------------------
53|
54| Here you store ace commands for your package
55|
56*/
57const commands = [];
58
59module.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')} */
2const 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*/
13const 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*/
35const 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*/
48const serverMiddleware = [
49 'Adonis/Middleware/Static',
50];
51
52Server
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 */
4const Database = use('Database');
5const User = use('App/Models/User');
6
7const migrateLog = (text) => {
8 console.log('\x1b[36m%s\x1b[0m', 'Ferdi Migration:', '\x1b[0m', text);
9};
10
11module.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')} */
9const Route = use('Route');
10
11// Run latest database migration
12const migrate = require('./migrate');
13
14migrate();
15
16const 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
27Route.get('health', ({
28 response,
29}) => response.send({
30 api: 'success',
31 db: 'success',
32})).middleware(OnlyAllowFerdi);
33
34// API is grouped under '/v1/' route
35Route.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
73Route.group(() => {
74 Route.get('icon/:id', 'ServiceController.icon');
75}).prefix('v1');
76
77// Franz account import
78Route.post('import', 'UserController.import');
79Route.get('import', ({ view }) => view.render('import'));
80
81// Account transfer
82Route.get('export', 'UserController.export');
83Route.post('transfer', 'UserController.importFerdi');
84Route.get('transfer', ({ view }) => view.render('transfer'));
85
86// Index
87Route.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 @@
1const path = require('path');
2const fs = require('fs-extra');
3const server = require('./start');
4
5const dummyUserFolder = path.join(__dirname, 'user_data');
6
7fs.ensureDirSync(dummyUserFolder);
8
9server(dummyUserFolder, 45568);