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