aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar vantezzen <hello@vantezzen.io>2020-04-02 14:22:55 +0200
committerLibravatar vantezzen <hello@vantezzen.io>2020-04-02 14:22:55 +0200
commit2ff924e0cc3494489abf0853080f316cc009e18a (patch)
treea6bb51f77c126f6174a595dda0dbfbfd03b006c8
parentAdd new home page (diff)
parentMerge pull request #22 from getferdi/dependabot/npm_and_yarn/acorn-7.1.1 (diff)
downloadferdium-server-2ff924e0cc3494489abf0853080f316cc009e18a.tar.gz
ferdium-server-2ff924e0cc3494489abf0853080f316cc009e18a.tar.zst
ferdium-server-2ff924e0cc3494489abf0853080f316cc009e18a.zip
Merge branch 'master' of https://github.com/getferdi/ferdi-server
-rw-r--r--.env.example2
-rw-r--r--.github/workflows/main.yml18
-rw-r--r--.gitignore3
-rw-r--r--README.md42
-rw-r--r--app/Controllers/Http/DashboardController.js11
-rw-r--r--app/Controllers/Http/RecipeController.js9
-rw-r--r--app/Controllers/Http/ServiceController.js103
-rw-r--r--app/Controllers/Http/StaticController.js36
-rw-r--r--app/Controllers/Http/UserController.js22
-rw-r--r--app/Controllers/Http/WorkspaceController.js2
-rw-r--r--app/Exceptions/Handler.js4
-rw-r--r--app/Middleware/HandleDoubleSlash.js24
-rw-r--r--package-lock.json8
-rw-r--r--package.json2
-rw-r--r--public/js/transfer.js2
-rw-r--r--resources/views/others/index.edge16
-rw-r--r--server.js1
-rw-r--r--start/kernel.js1
-rw-r--r--start/routes.js44
19 files changed, 222 insertions, 128 deletions
diff --git a/.env.example b/.env.example
index c175cd1..bcc4c7c 100644
--- a/.env.example
+++ b/.env.example
@@ -19,4 +19,6 @@ DB_DATABASE=adonis
19HASH_DRIVER=bcrypt 19HASH_DRIVER=bcrypt
20 20
21IS_CREATION_ENABLED=true 21IS_CREATION_ENABLED=true
22IS_DASHBOARD_ENABLED=true
23IS_REGISTRATION_ENABLED=true
22CONNECT_WITH_FRANZ=true \ No newline at end of file 24CONNECT_WITH_FRANZ=true \ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..e21943b
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,18 @@
1name: Trigger Docker Hub build
2
3on:
4 release:
5 types: [published]
6
7jobs:
8 build:
9
10 runs-on: ubuntu-latest
11
12 steps:
13 - name: Install HTTPie
14 run: sudo apt-get install httpie
15
16 - name: Send request to Docker Hub to trigger a build
17 run: >
18 http post https://hub.docker.com/api/build/v1/source/83564f19-c21a-4dae-9690-971aee3b2a3b/trigger/${{ env.HUB_TRIGGER_ID }}/call/'
diff --git a/.gitignore b/.gitignore
index f0a5147..12fc024 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,5 @@ public/terms.html
19public/privacy.html 19public/privacy.html
20 20
21resources/announcements/*.json 21resources/announcements/*.json
22!resources/announcements/version.json \ No newline at end of file 22!resources/announcements/version.json
23npm-debug.log
diff --git a/README.md b/README.md
index 6aa2d02..b0424c2 100644
--- a/README.md
+++ b/README.md
@@ -3,16 +3,25 @@
3</p> 3</p>
4 4
5# ferdi-server 5# ferdi-server
6Unofficial Franz server replacement for use with the Ferdi Client. 6Official Server software for the [Ferdi Messaging Browser](https://getferdi.com)
7 7
8## Looking for a smaller alternative? 8- [ferdi-server](#ferdi-server)
9[ferdi-slim-server](https://github.com/vantezzen/ferdi-slim-server) is a slim alternative to this project. Opposed to ferdi-server, ferdi-slim-server is only a wrapper around the Franz API that allows you to add custom recipes while still using the original Franz API. 9 - [Why use a custom Ferdi server?](#why-use-a-custom-ferdi-server)
10 10 - [Features](#features)
11## Why use a custom ferdi-server? 11 - [Setup](#setup)
12A custom ferdi-server allows you to experience the full potential of the Ferdi client. It allows you to use all Premium features (e.g. Workspaces and custom URL recipes) and [adding your own recipes](#creating-and-using-custom-recipes). 12 - [with Docker](#with-docker)
13 13 - [Manual setup](#manual-setup)
14## Demo 14 - [Configuration](#configuration)
15You can find Ferdi's official API running this software at <https://api.getferdi.com> 15 - [Importing your Franz account](#importing-your-franz-account)
16 - [Transferring user data](#transferring-user-data)
17 - [Creating and using custom recipes](#creating-and-using-custom-recipes)
18 - [Listing custom recipes](#listing-custom-recipes)
19 - [License](#license)
20
21## Why use a custom Ferdi server?
22A custom server allows you to manage the data of all registered users yourself and add your own recipes to the repository.
23
24If you are not interested in doing this you can use our official instance of Ferdi server at <https://api.getferdi.com>.
16 25
17## Features 26## Features
18- [x] User registration and login 27- [x] User registration and login
@@ -20,13 +29,13 @@ You can find Ferdi's official API running this software at <https://api.getferdi
20- [x] Workspace support 29- [x] Workspace support
21- [x] Functioning service store 30- [x] Functioning service store
22- [x] User dashboard 31- [x] User dashboard
32- [x] Export/import data to other ferdi-servers
23- [ ] Password recovery 33- [ ] Password recovery
24- [ ] Export/import data to other ferdi-servers
25- [ ] Recipe update 34- [ ] Recipe update
26 35
27## Setup 36## Setup
28### with Docker 37### with Docker
29The easiest way to set up ferdi-server on your server is with Docker. 38The easiest way to set up Ferdi server on your server is with Docker.
30 39
31The Docker image can be run as is, with the default sqlite database or you can modifying your ENV variables to use an external database (e.g. MySQL, MariaDB, Postgres, etc). 40The Docker image can be run as is, with the default sqlite database or you can modifying your ENV variables to use an external database (e.g. MySQL, MariaDB, Postgres, etc).
32After setting up the docker container we recommend you to set up an NGINX reverse proxy to access ferdi-server outside of your home network and protect it with an SSL certificate. 41After setting up the docker container we recommend you to set up an NGINX reverse proxy to access ferdi-server outside of your home network and protect it with an SSL certificate.
@@ -76,7 +85,9 @@ After setting up the docker container we recommend you to set up an NGINX revers
76 - DB_PASSWORD=<yourdbpass> 85 - DB_PASSWORD=<yourdbpass>
77 - DB_DATABASE=<yourdbdatabase> 86 - DB_DATABASE=<yourdbdatabase>
78 - IS_CREATION_ENABLED=true/false 87 - IS_CREATION_ENABLED=true/false
79 - CONNECT_WITH_FRANZ=true/flase 88 - CONNECT_WITH_FRANZ=true/false
89 - IS_REGISTRATION_ENABLED=true/false
90 - IS_DASHBOARD_ENABLED=true/false
80 volumes: 91 volumes:
81 - <path to data>:/config 92 - <path to data>:/config
82 - <path to database>:/usr/src/app/database 93 - <path to database>:/usr/src/app/database
@@ -106,6 +117,8 @@ For more information on configuring the Docker image, visit the Docker image rep
106## Configuration 117## Configuration
107franz-server's configuration is saved inside the `.env` file. Besides AdonisJS's settings, ferdi-server has the following custom settings: 118franz-server's configuration is saved inside the `.env` file. Besides AdonisJS's settings, ferdi-server has the following custom settings:
108- `IS_CREATION_ENABLED` (`true` or `false`, default: `true`): Whether to enable the [creation of custom recipes](#creating-and-using-custom-recipes) 119- `IS_CREATION_ENABLED` (`true` or `false`, default: `true`): Whether to enable the [creation of custom recipes](#creating-and-using-custom-recipes)
120- `IS_REGISTRATION_ENABLED` (`true` or `false`, default: `true`): Whether to enable the creation of new user accounts
121- `IS_DASHBOARD_ENABLED` (`true` or `false`, default: `true`): Whether to enable the user dashboard
109- `CONNECT_WITH_FRANZ` (`true` or `false`, default: `true`): Whether to enable connections to the Franz server. By enabling this option, ferdi-server can: 122- `CONNECT_WITH_FRANZ` (`true` or `false`, default: `true`): Whether to enable connections to the Franz server. By enabling this option, ferdi-server can:
110 - Show the full Franz recipe library instead of only custom recipes 123 - Show the full Franz recipe library instead of only custom recipes
111 - Import Franz accounts 124 - Import Franz accounts
@@ -115,6 +128,9 @@ ferdi-server allows you to import your full Franz account, including all its set
115 128
116To import your Franz account, open `http://[YOUR FERDI-SERVER]/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. 129To import your Franz account, open `http://[YOUR FERDI-SERVER]/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.
117 130
131## Transferring user data
132Please refer to <https://github.com/getferdi/ferdi/wiki/Transferring-data-between-servers>
133
118## Creating and using custom recipes 134## Creating and using custom recipes
119ferdi-server allows to extends the Franz recipe catalogue with custom Ferdi recipes. 135ferdi-server allows to extends the Franz recipe catalogue with custom Ferdi recipes.
120 136
diff --git a/app/Controllers/Http/DashboardController.js b/app/Controllers/Http/DashboardController.js
index fe179c9..86cfa74 100644
--- a/app/Controllers/Http/DashboardController.js
+++ b/app/Controllers/Http/DashboardController.js
@@ -166,7 +166,7 @@ class DashboardController {
166 session, 166 session,
167 response, 167 response,
168 }) { 168 }) {
169 let validation = await validateAll(request.all(), { 169 const validation = await validateAll(request.all(), {
170 file: 'required', 170 file: 'required',
171 }); 171 });
172 if (validation.fails()) { 172 if (validation.fails()) {
@@ -177,14 +177,13 @@ class DashboardController {
177 let file; 177 let file;
178 try { 178 try {
179 file = JSON.parse(request.input('file')); 179 file = JSON.parse(request.input('file'));
180 } catch(e) { 180 } catch (e) {
181 session.flash({ type: 'danger', message: 'Invalid Ferdi account file' }) 181 session.flash({ type: 'danger', message: 'Invalid Ferdi account file' });
182 return response.redirect('back'); 182 return response.redirect('back');
183 } 183 }
184 console.log(file);
185 184
186 if(!file || !file.services || !file.workspaces) { 185 if (!file || !file.services || !file.workspaces) {
187 session.flash({ type: 'danger', message: 'Invalid Ferdi account file (2)' }) 186 session.flash({ type: 'danger', message: 'Invalid Ferdi account file (2)' });
188 return response.redirect('back'); 187 return response.redirect('back');
189 } 188 }
190 189
diff --git a/app/Controllers/Http/RecipeController.js b/app/Controllers/Http/RecipeController.js
index 6c8aa21..9bfd92e 100644
--- a/app/Controllers/Http/RecipeController.js
+++ b/app/Controllers/Http/RecipeController.js
@@ -35,7 +35,7 @@ class RecipeController {
35 const customRecipes = customRecipesArray.map((recipe) => ({ 35 const customRecipes = customRecipesArray.map((recipe) => ({
36 id: recipe.recipeId, 36 id: recipe.recipeId,
37 name: recipe.name, 37 name: recipe.name,
38 ...typeof recipe.data === "string" ? JSON.parse(recipe.data) : recipe.data, 38 ...typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data,
39 })); 39 }));
40 40
41 const recipes = [ 41 const recipes = [
@@ -144,7 +144,7 @@ class RecipeController {
144 results = dbResults.map((recipe) => ({ 144 results = dbResults.map((recipe) => ({
145 id: recipe.recipeId, 145 id: recipe.recipeId,
146 name: recipe.name, 146 name: recipe.name,
147 ...typeof recipe.data === "string" ? JSON.parse(recipe.data) : recipe.data, 147 ...typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data,
148 })); 148 }));
149 } else { 149 } else {
150 let remoteResults = []; 150 let remoteResults = [];
@@ -155,7 +155,7 @@ class RecipeController {
155 const localResults = localResultsArray.map((recipe) => ({ 155 const localResults = localResultsArray.map((recipe) => ({
156 id: recipe.recipeId, 156 id: recipe.recipeId,
157 name: recipe.name, 157 name: recipe.name,
158 ...typeof recipe.data === "string" ? JSON.parse(recipe.data) : recipe.data, 158 ...typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data,
159 })); 159 }));
160 160
161 results = [ 161 results = [
@@ -194,7 +194,8 @@ class RecipeController {
194 // Check if recipe exists in recipes folder 194 // Check if recipe exists in recipes folder
195 if (await Drive.exists(`${service}.tar.gz`)) { 195 if (await Drive.exists(`${service}.tar.gz`)) {
196 return response.send(await Drive.get(`${service}.tar.gz`)); 196 return response.send(await Drive.get(`${service}.tar.gz`));
197 } else if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq 197 }
198 if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq
198 return response.redirect(`https://api.franzinfra.com/v1/recipes/download/${service}`); 199 return response.redirect(`https://api.franzinfra.com/v1/recipes/download/${service}`);
199 } 200 }
200 return response.status(400).send({ 201 return response.status(400).send({
diff --git a/app/Controllers/Http/ServiceController.js b/app/Controllers/Http/ServiceController.js
index 90055b6..a1d26cb 100644
--- a/app/Controllers/Http/ServiceController.js
+++ b/app/Controllers/Http/ServiceController.js
@@ -86,7 +86,7 @@ class ServiceController {
86 const services = (await auth.user.services().fetch()).rows; 86 const services = (await auth.user.services().fetch()).rows;
87 // Convert to array with all data Franz wants 87 // Convert to array with all data Franz wants
88 const servicesArray = services.map((service) => { 88 const servicesArray = services.map((service) => {
89 const settings = typeof service.settings === "string" ? JSON.parse(service.settings) : service.settings; 89 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
90 90
91 return { 91 return {
92 customRecipe: false, 92 customRecipe: false,
@@ -105,7 +105,7 @@ class ServiceController {
105 name: service.name, 105 name: service.name,
106 recipeId: service.recipeId, 106 recipeId: service.recipeId,
107 userId: auth.user.id, 107 userId: auth.user.id,
108 } 108 };
109 }); 109 });
110 110
111 return response.send(servicesArray); 111 return response.send(servicesArray);
@@ -127,7 +127,7 @@ class ServiceController {
127 // Upload custom service icon 127 // Upload custom service icon
128 const icon = request.file('icon', { 128 const icon = request.file('icon', {
129 types: ['image'], 129 types: ['image'],
130 size: '2mb' 130 size: '2mb',
131 }); 131 });
132 const { 132 const {
133 id, 133 id,
@@ -135,17 +135,17 @@ class ServiceController {
135 const service = (await Service.query() 135 const service = (await Service.query()
136 .where('serviceId', id) 136 .where('serviceId', id)
137 .where('userId', auth.user.id).fetch()).rows[0]; 137 .where('userId', auth.user.id).fetch()).rows[0];
138 const settings = typeof service.settings === "string" ? JSON.parse(service.settings) : service.settings; 138 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
139 139
140 let iconId; 140 let iconId;
141 do { 141 do {
142 iconId = uuid() + uuid(); 142 iconId = uuid() + uuid();
143 } while(await fs.exists(path.join(Helpers.tmpPath('uploads'), iconId))); 143 } while (await fs.exists(path.join(Helpers.tmpPath('uploads'), iconId)));
144 144
145 await icon.move(Helpers.tmpPath('uploads'), { 145 await icon.move(Helpers.tmpPath('uploads'), {
146 name: iconId, 146 name: iconId,
147 overwrite: true 147 overwrite: true,
148 }) 148 });
149 149
150 if (!icon.moved()) { 150 if (!icon.moved()) {
151 return response.status(500).send(icon.error()); 151 return response.status(500).send(icon.error());
@@ -175,49 +175,48 @@ class ServiceController {
175 iconUrl: `${Env.get('APP_URL')}/v1/icon/${newSettings.iconId}`, 175 iconUrl: `${Env.get('APP_URL')}/v1/icon/${newSettings.iconId}`,
176 userId: auth.user.id, 176 userId: auth.user.id,
177 }, 177 },
178 status: ["updated"] 178 status: ['updated'],
179 }); 179 });
180 } else { 180 }
181 // Update service info 181 // Update service info
182 const data = request.all(); 182 const data = request.all();
183 const { 183 const {
184 id, 184 id,
185 } = params; 185 } = params;
186 186
187 // Get current settings from db 187 // Get current settings from db
188 const serviceData = (await Service.query() 188 const serviceData = (await Service.query()
189 .where('serviceId', id) 189 .where('serviceId', id)
190 .where('userId', auth.user.id).fetch()).rows[0]; 190 .where('userId', auth.user.id).fetch()).rows[0];
191 191
192 const settings = { 192 const settings = {
193 ...typeof serviceData.settings === "string" ? JSON.parse(serviceData.settings) : serviceData.settings, 193 ...typeof serviceData.settings === 'string' ? JSON.parse(serviceData.settings) : serviceData.settings,
194 ...data, 194 ...data,
195 }; 195 };
196 196
197 // Update data in database 197 // Update data in database
198 await (Service.query() 198 await (Service.query()
199 .where('serviceId', id) 199 .where('serviceId', id)
200 .where('userId', auth.user.id)).update({ 200 .where('userId', auth.user.id)).update({
201 name: data.name, 201 name: data.name,
202 settings: JSON.stringify(settings), 202 settings: JSON.stringify(settings),
203 }); 203 });
204 204
205 // Get updated row 205 // Get updated row
206 const service = (await Service.query() 206 const service = (await Service.query()
207 .where('serviceId', id) 207 .where('serviceId', id)
208 .where('userId', auth.user.id).fetch()).rows[0]; 208 .where('userId', auth.user.id).fetch()).rows[0];
209 209
210 return response.send({ 210 return response.send({
211 data: { 211 data: {
212 id, 212 id,
213 name: service.name, 213 name: service.name,
214 ...settings, 214 ...settings,
215 iconUrl: `${Env.get('APP_URL')}/v1/icon/${settings.iconId}`, 215 iconUrl: `${Env.get('APP_URL')}/v1/icon/${settings.iconId}`,
216 userId: auth.user.id, 216 userId: auth.user.id,
217 }, 217 },
218 status: ["updated"] 218 status: ['updated'],
219 }); 219 });
220 }
221 } 220 }
222 221
223 async icon({ 222 async icon({
@@ -231,7 +230,7 @@ class ServiceController {
231 const iconPath = path.join(Helpers.tmpPath('uploads'), id); 230 const iconPath = path.join(Helpers.tmpPath('uploads'), id);
232 if (!await fs.exists(iconPath)) { 231 if (!await fs.exists(iconPath)) {
233 return response.status(404).send({ 232 return response.status(404).send({
234 status: 'Icon doesn\'t exist' 233 status: 'Icon doesn\'t exist',
235 }); 234 });
236 } 235 }
237 236
@@ -252,7 +251,7 @@ class ServiceController {
252 .where('userId', auth.user.id).fetch()).rows[0]; 251 .where('userId', auth.user.id).fetch()).rows[0];
253 252
254 const settings = { 253 const settings = {
255 ...typeof serviceData.settings === "string" ? JSON.parse(serviceData.settings) : serviceData.settings, 254 ...typeof serviceData.settings === 'string' ? JSON.parse(serviceData.settings) : serviceData.settings,
256 order: data[service], 255 order: data[service],
257 }; 256 };
258 257
@@ -260,16 +259,16 @@ class ServiceController {
260 await (Service.query() // eslint-disable-line no-await-in-loop 259 await (Service.query() // eslint-disable-line no-await-in-loop
261 .where('serviceId', service) 260 .where('serviceId', service)
262 .where('userId', auth.user.id)) 261 .where('userId', auth.user.id))
263 .update({ 262 .update({
264 settings: JSON.stringify(settings), 263 settings: JSON.stringify(settings),
265 }); 264 });
266 } 265 }
267 266
268 // Get new services 267 // Get new services
269 const services = (await auth.user.services().fetch()).rows; 268 const services = (await auth.user.services().fetch()).rows;
270 // Convert to array with all data Franz wants 269 // Convert to array with all data Franz wants
271 const servicesArray = services.map((service) => { 270 const servicesArray = services.map((service) => {
272 const settings = typeof service.settings === "string" ? JSON.parse(service.settings) : service.settings; 271 const settings = typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings;
273 272
274 return { 273 return {
275 customRecipe: false, 274 customRecipe: false,
@@ -288,7 +287,7 @@ class ServiceController {
288 name: service.name, 287 name: service.name,
289 recipeId: service.recipeId, 288 recipeId: service.recipeId,
290 userId: auth.user.id, 289 userId: auth.user.id,
291 } 290 };
292 }); 291 });
293 292
294 return response.send(servicesArray); 293 return response.send(servicesArray);
diff --git a/app/Controllers/Http/StaticController.js b/app/Controllers/Http/StaticController.js
index 265578f..cd38b13 100644
--- a/app/Controllers/Http/StaticController.js
+++ b/app/Controllers/Http/StaticController.js
@@ -31,44 +31,44 @@ class StaticController {
31 isTeamManagementIncludedInCurrentPlan: true, 31 isTeamManagementIncludedInCurrentPlan: true,
32 isTodosEnabled: true, 32 isTodosEnabled: true,
33 isTodosIncludedInCurrentPlan: true, 33 isTodosIncludedInCurrentPlan: true,
34 defaultTrialPlan: "franz-pro-yearly", 34 defaultTrialPlan: 'franz-pro-yearly',
35 subscribeURL: "https://getferdi.com", 35 subscribeURL: 'https://getferdi.com',
36 planSelectionURL: "https://getferdi.com", 36 planSelectionURL: 'https://getferdi.com',
37 isMagicBarEnabled: true, 37 isMagicBarEnabled: true,
38 hasInlineCheckout: true, 38 hasInlineCheckout: true,
39 isPlanSelectionEnabled: false, 39 isPlanSelectionEnabled: false,
40 isTrialStatusBarEnabled: false, 40 isTrialStatusBarEnabled: false,
41 canSkipTrial: true, 41 canSkipTrial: true,
42 pricingConfig: { 42 pricingConfig: {
43 currency: "$", 43 currency: '$',
44 currencyID: "USD", 44 currencyID: 'USD',
45 plans: { 45 plans: {
46 personal: { 46 personal: {
47 monthly: { 47 monthly: {
48 id: "ferdi-free", 48 id: 'ferdi-free',
49 price: 0, 49 price: 0,
50 billed: 0 50 billed: 0,
51 }, 51 },
52 yearly: { 52 yearly: {
53 id: "ferdi-completely-free", 53 id: 'ferdi-completely-free',
54 price: 0, 54 price: 0,
55 billed: 0 55 billed: 0,
56 } 56 },
57 }, 57 },
58 pro: { 58 pro: {
59 monthly: { 59 monthly: {
60 id: "ferdi-still-free", 60 id: 'ferdi-still-free',
61 price: 0, 61 price: 0,
62 billed: 0 62 billed: 0,
63 }, 63 },
64 yearly: { 64 yearly: {
65 id: "ferdi-forever-free", 65 id: 'ferdi-forever-free',
66 price: 0, 66 price: 0,
67 billed: 0 67 billed: 0,
68 } 68 },
69 } 69 },
70 } 70 },
71 } 71 },
72 }); 72 });
73 } 73 }
74 74
diff --git a/app/Controllers/Http/UserController.js b/app/Controllers/Http/UserController.js
index edfccf2..e580e49 100644
--- a/app/Controllers/Http/UserController.js
+++ b/app/Controllers/Http/UserController.js
@@ -38,6 +38,13 @@ class UserController {
38 response, 38 response,
39 auth, 39 auth,
40 }) { 40 }) {
41 if (Env.get('IS_REGISTRATION_ENABLED') == 'false') { // eslint-disable-line eqeqeq
42 return response.status(401).send({
43 message: 'Registration is disabled on this server',
44 status: 401,
45 });
46 }
47
41 // Validate user input 48 // Validate user input
42 const validation = await validateAll(request.all(), { 49 const validation = await validateAll(request.all(), {
43 firstname: 'required', 50 firstname: 'required',
@@ -155,17 +162,17 @@ class UserController {
155 async updateMe({ 162 async updateMe({
156 request, 163 request,
157 response, 164 response,
158 auth 165 auth,
159 }) { 166 }) {
160 let settings = auth.user.settings || {}; 167 let settings = auth.user.settings || {};
161 if (typeof settings === 'string') { 168 if (typeof settings === 'string') {
162 settings = JSON.parse(settings); 169 settings = JSON.parse(settings);
163 } 170 }
164 171
165 let newSettings = { 172 const newSettings = {
166 ...settings, 173 ...settings,
167 ...request.all(), 174 ...request.all(),
168 } 175 };
169 176
170 auth.user.settings = JSON.stringify(newSettings); 177 auth.user.settings = JSON.stringify(newSettings);
171 await auth.user.save(); 178 await auth.user.save();
@@ -188,7 +195,7 @@ class UserController {
188 }, 195 },
189 status: [ 196 status: [
190 'data-updated', 197 'data-updated',
191 ] 198 ],
192 }); 199 });
193 } 200 }
194 201
@@ -197,6 +204,13 @@ class UserController {
197 request, 204 request,
198 response, 205 response,
199 }) { 206 }) {
207 if (Env.get('IS_REGISTRATION_ENABLED') == 'false') { // eslint-disable-line eqeqeq
208 return response.status(401).send({
209 message: 'Registration is disabled on this server',
210 status: 401,
211 });
212 }
213
200 // Validate user input 214 // Validate user input
201 const validation = await validateAll(request.all(), { 215 const validation = await validateAll(request.all(), {
202 email: 'required|email|unique:users,email', 216 email: 'required|email|unique:users,email',
diff --git a/app/Controllers/Http/WorkspaceController.js b/app/Controllers/Http/WorkspaceController.js
index a2200a9..cbb6873 100644
--- a/app/Controllers/Http/WorkspaceController.js
+++ b/app/Controllers/Http/WorkspaceController.js
@@ -168,7 +168,7 @@ class WorkspaceController {
168 id: workspace.workspaceId, 168 id: workspace.workspaceId,
169 name: workspace.name, 169 name: workspace.name,
170 order: workspace.order, 170 order: workspace.order,
171 services: typeof workspace.services === "string" ? JSON.parse(workspace.services) : workspace.services, 171 services: typeof workspace.services === 'string' ? JSON.parse(workspace.services) : workspace.services,
172 userId: auth.user.id, 172 userId: auth.user.id,
173 })); 173 }));
174 } 174 }
diff --git a/app/Exceptions/Handler.js b/app/Exceptions/Handler.js
index cb9e10b..14b840e 100644
--- a/app/Exceptions/Handler.js
+++ b/app/Exceptions/Handler.js
@@ -1,5 +1,6 @@
1 1
2const BaseExceptionHandler = use('BaseExceptionHandler'); 2const BaseExceptionHandler = use('BaseExceptionHandler');
3const Sentry = require('@sentry/node');
3 4
4/** 5/**
5 * This class handles all exceptions thrown during 6 * This class handles all exceptions thrown during
@@ -39,7 +40,8 @@ class ExceptionHandler extends BaseExceptionHandler {
39 * 40 *
40 * @return {void} 41 * @return {void}
41 */ 42 */
42 async report() { 43 async report(error) {
44 Sentry.captureException(error);
43 return true; 45 return true;
44 } 46 }
45} 47}
diff --git a/app/Middleware/HandleDoubleSlash.js b/app/Middleware/HandleDoubleSlash.js
new file mode 100644
index 0000000..456b774
--- /dev/null
+++ b/app/Middleware/HandleDoubleSlash.js
@@ -0,0 +1,24 @@
1'use strict'
2/** @typedef {import('@adonisjs/framework/src/Request')} Request */
3/** @typedef {import('@adonisjs/framework/src/Response')} Response */
4/** @typedef {import('@adonisjs/framework/src/View')} View */
5
6class HandleDoubleSlash {
7 /**
8 * @param {object} ctx
9 * @param {Request} ctx.request
10 * @param {Function} next
11 */
12 async handle ({ request, response }, next) {
13 // Redirect requests that contain duplicate slashes to the right path
14 if (request.url().includes('//')) {
15 return response.redirect(
16 request.url().replace(/\/{2,}/g, '/'),
17 );
18 }
19
20 await next();
21 }
22}
23
24module.exports = HandleDoubleSlash
diff --git a/package-lock.json b/package-lock.json
index 23a9522..b2e96f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
1{ 1{
2 "name": "ferdi-server", 2 "name": "ferdi-server",
3 "version": "1.0.2", 3 "version": "1.0.5",
4 "lockfileVersion": 1, 4 "lockfileVersion": 1,
5 "requires": true, 5 "requires": true,
6 "dependencies": { 6 "dependencies": {
@@ -518,9 +518,9 @@
518 } 518 }
519 }, 519 },
520 "acorn": { 520 "acorn": {
521 "version": "7.0.0", 521 "version": "7.1.1",
522 "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.0.0.tgz", 522 "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
523 "integrity": "sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==" 523 "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg=="
524 }, 524 },
525 "acorn-jsx": { 525 "acorn-jsx": {
526 "version": "5.0.2", 526 "version": "5.0.2",
diff --git a/package.json b/package.json
index 3bcae53..3932b49 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
1{ 1{
2 "name": "ferdi-server", 2 "name": "ferdi-server",
3 "version": "1.0.2", 3 "version": "1.0.5",
4 "adonis-version": "4.1.0", 4 "adonis-version": "4.1.0",
5 "description": "Ferdi server to replace the default Franz server.", 5 "description": "Ferdi server to replace the default Franz server.",
6 "main": "index.js", 6 "main": "index.js",
diff --git a/public/js/transfer.js b/public/js/transfer.js
index d3d23b3..c04a6d3 100644
--- a/public/js/transfer.js
+++ b/public/js/transfer.js
@@ -11,4 +11,4 @@ fileInput.addEventListener('change', () => {
11 submitBtn.disabled = false; 11 submitBtn.disabled = false;
12 }; 12 };
13 reader.readAsText(fileInput.files[0]); 13 reader.readAsText(fileInput.files[0]);
14}) 14});
diff --git a/resources/views/others/index.edge b/resources/views/others/index.edge
index ef3bbfa..9c90fb0 100644
--- a/resources/views/others/index.edge
+++ b/resources/views/others/index.edge
@@ -88,9 +88,17 @@
88 margin: 0.5rem 0; 88 margin: 0.5rem 0;
89 } 89 }
90 90
91 img {
92 height: 200px;
93 margin: 0;
94 }
95
91</style> 96</style>
92<h1>ferdi-server</h1> 97
93<p>You are accessing a custom <a href="https://github.com/kytwb/ferdi">Ferdi</a> server.</p> 98<img src="https://github.com/getferdi/ferdi/raw/develop/build-helpers/images/icon.png" alt="Ferdi logo">
99
100<h1>Ferdi Server</h1>
101<p>You are accessing a custom <a href="https://github.com/getferdi/ferdi">Ferdi</a> server.</p>
94<p> 102<p>
95 To use this server in your Ferdi client, <a href="ferdi://settings/app">open Ferdi's settings</a> and as the 103 To use this server in your Ferdi client, <a href="ferdi://settings/app">open Ferdi's settings</a> and as the
96 <code>server</code>, enter <code id="server"></code> 104 <code>server</code>, enter <code id="server"></code>
@@ -101,7 +109,7 @@
101 109
102<br /> 110<br />
103<small> 111<small>
104 <a href="https://github.com/vantezzen/ferdi-server">ferdi-server</a> is a project by <a 112 <a href="https://github.com/getferdi/server">ferdi-server</a> is a project by <a
105 href="https://vantezzen.io">vantezzen</a>. 113 href="https://getferdi.com">the Ferdi Team</a>.
106</small> 114</small>
107@endsection 115@endsection
diff --git a/server.js b/server.js
index 81f8e73..921df97 100644
--- a/server.js
+++ b/server.js
@@ -1,4 +1,5 @@
1const Sentry = require('@sentry/node'); 1const Sentry = require('@sentry/node');
2
2Sentry.init({ dsn: 'https://34e9a42c1de24048b7bfc980211dd7c8@sentry.io/1838449' }); 3Sentry.init({ dsn: 'https://34e9a42c1de24048b7bfc980211dd7c8@sentry.io/1838449' });
3 4
4/* 5/*
diff --git a/start/kernel.js b/start/kernel.js
index b54fc29..077151a 100644
--- a/start/kernel.js
+++ b/start/kernel.js
@@ -54,6 +54,7 @@ const namedMiddleware = {
54const serverMiddleware = [ 54const serverMiddleware = [
55 'Adonis/Middleware/Static', 55 'Adonis/Middleware/Static',
56 'Adonis/Middleware/Cors', 56 'Adonis/Middleware/Cors',
57 'App/Middleware/HandleDoubleSlash',
57]; 58];
58 59
59Server 60Server
diff --git a/start/routes.js b/start/routes.js
index b5674fd..6385ca5 100644
--- a/start/routes.js
+++ b/start/routes.js
@@ -60,24 +60,32 @@ Route.group(() => {
60}).prefix('v1'); 60}).prefix('v1');
61 61
62// User dashboard 62// User dashboard
63Route.group(() => { 63if (Env.get('IS_DASHBOARD_ENABLED') != 'false') {
64 // Auth 64 Route.group(() => {
65 Route.get('login', ({ view }) => view.render('dashboard.login')).middleware('guest'); 65 // Auth
66 Route.post('login', 'DashboardController.login').middleware('guest'); 66 Route.get('login', ({ view }) => view.render('dashboard.login')).middleware('guest');
67 67 Route.post('login', 'DashboardController.login').middleware('guest');
68 // Dashboard 68
69 Route.get('account', 'DashboardController.account').middleware('auth:session'); 69 // Dashboard
70 Route.post('account', 'DashboardController.edit').middleware('auth:session'); 70 Route.get('account', 'DashboardController.account').middleware('auth:session');
71 Route.get('data', 'DashboardController.data').middleware('auth:session'); 71 Route.post('account', 'DashboardController.edit').middleware('auth:session');
72 Route.get('export', 'DashboardController.export').middleware('auth:session'); 72 Route.get('data', 'DashboardController.data').middleware('auth:session');
73 Route.post('transfer', 'DashboardController.import').middleware('auth:session'); 73 Route.get('export', 'DashboardController.export').middleware('auth:session');
74 Route.get('transfer', ({ view }) => view.render('dashboard.transfer')).middleware('auth:session'); 74 Route.post('transfer', 'DashboardController.import').middleware('auth:session');
75 Route.get('delete', ({ view }) => view.render('dashboard.delete')).middleware('auth:session'); 75 Route.get('transfer', ({ view }) => view.render('dashboard.transfer')).middleware('auth:session');
76 Route.post('delete', 'DashboardController.delete').middleware('auth:session'); 76 Route.get('delete', ({ view }) => view.render('dashboard.delete')).middleware('auth:session');
77 Route.get('logout', 'DashboardController.logout').middleware('auth:session'); 77 Route.post('delete', 'DashboardController.delete').middleware('auth:session');
78 78 Route.get('logout', 'DashboardController.logout').middleware('auth:session');
79 Route.get('*', ({ response }) => response.redirect('/user/account')); 79
80}).prefix('user').middleware('shield'); 80 Route.get('*', ({ response }) => response.redirect('/user/account'));
81 }).prefix('user').middleware('shield');
82} else {
83 Route.group(() => {
84 Route.get('*', ({
85 response,
86 }) => response.send('The user dashboard is disabled on this server\n\nIf you are the server owner, please set IS_DASHBOARD_ENABLED to true to enable the dashboard.'))
87 }).prefix('user');
88}
81 89
82// Recipe creation 90// Recipe creation
83Route.post('new', 'RecipeController.create'); 91Route.post('new', 'RecipeController.create');