diff options
author | vantezzen <hello@vantezzen.io> | 2020-04-02 14:22:55 +0200 |
---|---|---|
committer | vantezzen <hello@vantezzen.io> | 2020-04-02 14:22:55 +0200 |
commit | 2ff924e0cc3494489abf0853080f316cc009e18a (patch) | |
tree | a6bb51f77c126f6174a595dda0dbfbfd03b006c8 | |
parent | Add new home page (diff) | |
parent | Merge pull request #22 from getferdi/dependabot/npm_and_yarn/acorn-7.1.1 (diff) | |
download | ferdium-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.example | 2 | ||||
-rw-r--r-- | .github/workflows/main.yml | 18 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | README.md | 42 | ||||
-rw-r--r-- | app/Controllers/Http/DashboardController.js | 11 | ||||
-rw-r--r-- | app/Controllers/Http/RecipeController.js | 9 | ||||
-rw-r--r-- | app/Controllers/Http/ServiceController.js | 103 | ||||
-rw-r--r-- | app/Controllers/Http/StaticController.js | 36 | ||||
-rw-r--r-- | app/Controllers/Http/UserController.js | 22 | ||||
-rw-r--r-- | app/Controllers/Http/WorkspaceController.js | 2 | ||||
-rw-r--r-- | app/Exceptions/Handler.js | 4 | ||||
-rw-r--r-- | app/Middleware/HandleDoubleSlash.js | 24 | ||||
-rw-r--r-- | package-lock.json | 8 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | public/js/transfer.js | 2 | ||||
-rw-r--r-- | resources/views/others/index.edge | 16 | ||||
-rw-r--r-- | server.js | 1 | ||||
-rw-r--r-- | start/kernel.js | 1 | ||||
-rw-r--r-- | start/routes.js | 44 |
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 | |||
19 | HASH_DRIVER=bcrypt | 19 | HASH_DRIVER=bcrypt |
20 | 20 | ||
21 | IS_CREATION_ENABLED=true | 21 | IS_CREATION_ENABLED=true |
22 | IS_DASHBOARD_ENABLED=true | ||
23 | IS_REGISTRATION_ENABLED=true | ||
22 | CONNECT_WITH_FRANZ=true \ No newline at end of file | 24 | CONNECT_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 @@ | |||
1 | name: Trigger Docker Hub build | ||
2 | |||
3 | on: | ||
4 | release: | ||
5 | types: [published] | ||
6 | |||
7 | jobs: | ||
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/' | ||
@@ -19,4 +19,5 @@ public/terms.html | |||
19 | public/privacy.html | 19 | public/privacy.html |
20 | 20 | ||
21 | resources/announcements/*.json | 21 | resources/announcements/*.json |
22 | !resources/announcements/version.json \ No newline at end of file | 22 | !resources/announcements/version.json |
23 | npm-debug.log | ||
@@ -3,16 +3,25 @@ | |||
3 | </p> | 3 | </p> |
4 | 4 | ||
5 | # ferdi-server | 5 | # ferdi-server |
6 | Unofficial Franz server replacement for use with the Ferdi Client. | 6 | Official 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) |
12 | A 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) |
15 | You 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? | ||
22 | A custom server allows you to manage the data of all registered users yourself and add your own recipes to the repository. | ||
23 | |||
24 | If 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 |
29 | The easiest way to set up ferdi-server on your server is with Docker. | 38 | The easiest way to set up Ferdi server on your server is with Docker. |
30 | 39 | ||
31 | The 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). | 40 | The 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). |
32 | After 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. | 41 | After 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 |
107 | franz-server's configuration is saved inside the `.env` file. Besides AdonisJS's settings, ferdi-server has the following custom settings: | 118 | franz-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 | ||
116 | To 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. | 129 | To 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 | ||
132 | Please 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 |
119 | ferdi-server allows to extends the Franz recipe catalogue with custom Ferdi recipes. | 135 | ferdi-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 | ||
2 | const BaseExceptionHandler = use('BaseExceptionHandler'); | 2 | const BaseExceptionHandler = use('BaseExceptionHandler'); |
3 | const 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 | |||
6 | class 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 | |||
24 | module.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 |
@@ -1,4 +1,5 @@ | |||
1 | const Sentry = require('@sentry/node'); | 1 | const Sentry = require('@sentry/node'); |
2 | |||
2 | Sentry.init({ dsn: 'https://34e9a42c1de24048b7bfc980211dd7c8@sentry.io/1838449' }); | 3 | Sentry.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 = { | |||
54 | const serverMiddleware = [ | 54 | const 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 | ||
59 | Server | 60 | Server |
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 |
63 | Route.group(() => { | 63 | if (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 |
83 | Route.post('new', 'RecipeController.create'); | 91 | Route.post('new', 'RecipeController.create'); |