diff options
author | vantezzen <properly@protonmail.com> | 2019-08-28 12:10:16 +0200 |
---|---|---|
committer | vantezzen <properly@protonmail.com> | 2019-08-28 12:10:16 +0200 |
commit | 4f39dce953ea8e197105ee9042fa376c120087ce (patch) | |
tree | da953ea5837b8547c04ec1d3de383dfd4f2ca502 | |
parent | Merge branch 'master' of https://github.com/vantezzen/ferdi-server (diff) | |
download | ferdium-server-4f39dce953ea8e197105ee9042fa376c120087ce.tar.gz ferdium-server-4f39dce953ea8e197105ee9042fa376c120087ce.tar.zst ferdium-server-4f39dce953ea8e197105ee9042fa376c120087ce.zip |
Add import Franz account feature
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | app/Controllers/Http/UserController.js | 170 | ||||
-rw-r--r-- | package-lock.json | 5 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | public/import.html | 92 | ||||
-rw-r--r-- | start/routes.js | 2 |
6 files changed, 273 insertions, 1 deletions
@@ -12,4 +12,6 @@ database/development.sqlite | |||
12 | database/adonis.sqlite | 12 | database/adonis.sqlite |
13 | 13 | ||
14 | # Uploaded recipes | 14 | # Uploaded recipes |
15 | recipes/ \ No newline at end of file | 15 | recipes/ |
16 | |||
17 | .DS_Store \ No newline at end of file | ||
diff --git a/app/Controllers/Http/UserController.js b/app/Controllers/Http/UserController.js index 084b023..7c6cece 100644 --- a/app/Controllers/Http/UserController.js +++ b/app/Controllers/Http/UserController.js | |||
@@ -1,10 +1,39 @@ | |||
1 | 'use strict' | 1 | 'use strict' |
2 | 2 | ||
3 | const User = use('App/Models/User'); | 3 | const User = use('App/Models/User'); |
4 | const Service = use('App/Models/Service'); | ||
5 | const Workspace = use('App/Models/Workspace'); | ||
4 | const { | 6 | const { |
5 | validateAll | 7 | validateAll |
6 | } = use('Validator'); | 8 | } = use('Validator'); |
9 | |||
7 | const atob = require('atob'); | 10 | const atob = require('atob'); |
11 | const btoa = require('btoa'); | ||
12 | const fetch = require('node-fetch'); | ||
13 | const uuid = require('uuid/v4'); | ||
14 | const crypto = require('crypto'); | ||
15 | |||
16 | const franzRequest = async (route, method, auth) => { | ||
17 | return new Promise(async (resolve, reject) => { | ||
18 | const base = 'https://api.franzinfra.com/v1/'; | ||
19 | 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'; | ||
20 | |||
21 | try { | ||
22 | const rawResponse = await fetch(base + route, { | ||
23 | method, | ||
24 | headers: { | ||
25 | 'Authorization': 'Bearer ' + auth, | ||
26 | 'User-Agent': user | ||
27 | }, | ||
28 | }); | ||
29 | const content = await rawResponse.json(); | ||
30 | |||
31 | resolve(content); | ||
32 | } catch (e) { | ||
33 | reject(); | ||
34 | } | ||
35 | }) | ||
36 | } | ||
8 | 37 | ||
9 | class UserController { | 38 | class UserController { |
10 | 39 | ||
@@ -127,6 +156,147 @@ class UserController { | |||
127 | locale: "en-US" | 156 | locale: "en-US" |
128 | }); | 157 | }); |
129 | } | 158 | } |
159 | |||
160 | |||
161 | |||
162 | async import({ | ||
163 | request, | ||
164 | response | ||
165 | }) { | ||
166 | // Validate user input | ||
167 | const validation = await validateAll(request.all(), { | ||
168 | email: 'required|email|unique:users,email', | ||
169 | password: 'required' | ||
170 | }); | ||
171 | if (validation.fails()) { | ||
172 | let errorMessage = "There was an error while trying to import your account:\n"; | ||
173 | for (const message of validation.messages()) { | ||
174 | if (message.validation == 'required') { | ||
175 | errorMessage += '- Please make sure to supply your ' + message.field + '\n' | ||
176 | } else if (message.validation == 'unique') { | ||
177 | errorMessage += '- There is already a user with this email.\n' | ||
178 | } else { | ||
179 | errorMessage += message.message + '\n'; | ||
180 | } | ||
181 | } | ||
182 | return response.status(401).send(errorMessage) | ||
183 | } | ||
184 | |||
185 | const { | ||
186 | email, | ||
187 | password | ||
188 | } = request.all() | ||
189 | |||
190 | const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); | ||
191 | |||
192 | const base = 'https://api.franzinfra.com/v1/'; | ||
193 | 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'; | ||
194 | |||
195 | // Try to get an authentication token | ||
196 | let token; | ||
197 | try { | ||
198 | const basicToken = btoa(email + ':' + hashedPassword) | ||
199 | |||
200 | const rawResponse = await fetch(base + 'auth/login', { | ||
201 | method: 'POST', | ||
202 | headers: { | ||
203 | 'Authorization': 'Basic ' + basicToken, | ||
204 | 'User-Agent': userAgent | ||
205 | }, | ||
206 | }); | ||
207 | const content = await rawResponse.json(); | ||
208 | |||
209 | if (!content.message || content.message !== 'Successfully logged in') { | ||
210 | const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again'; | ||
211 | return response.status(401).send(errorMessage) | ||
212 | } | ||
213 | |||
214 | token = content.token; | ||
215 | } catch (e) { | ||
216 | return response.status(401).send({ | ||
217 | "message": "Cannot login to Franz", | ||
218 | "error": e | ||
219 | }) | ||
220 | } | ||
221 | |||
222 | // Get user information | ||
223 | let userInf; | ||
224 | try { | ||
225 | userInf = await franzRequest('me', 'GET', token) | ||
226 | } catch (e) { | ||
227 | const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later.\nError: ' + e; | ||
228 | return response.status(401).send(errorMessage) | ||
229 | } | ||
230 | |||
231 | // Create user in DB | ||
232 | let user; | ||
233 | try { | ||
234 | user = await User.create({ | ||
235 | email: userInf.email, | ||
236 | password: hashedPassword, | ||
237 | username: userInf.firstname | ||
238 | }); | ||
239 | } catch (e) { | ||
240 | const errorMessage = 'Could not create your user in our system.\nError: ' + e; | ||
241 | return response.status(401).send(errorMessage) | ||
242 | } | ||
243 | |||
244 | let serviceIdTranslation = {}; | ||
245 | |||
246 | // Import services | ||
247 | try { | ||
248 | const services = await franzRequest('me/services', 'GET', token) | ||
249 | |||
250 | for (const service of services) { | ||
251 | // Get new, unused uuid | ||
252 | let serviceId; | ||
253 | do { | ||
254 | serviceId = uuid(); | ||
255 | } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0) | ||
256 | |||
257 | await Service.create({ | ||
258 | userId: user.id, | ||
259 | serviceId, | ||
260 | name: service.name, | ||
261 | recipeId: service.recipeId, | ||
262 | settings: JSON.stringify(service) | ||
263 | }); | ||
264 | |||
265 | serviceIdTranslation[service.id] = serviceId; | ||
266 | } | ||
267 | } catch (e) { | ||
268 | const errorMessage = 'Could not import your services into our system.\nError: ' + e; | ||
269 | return response.status(401).send(errorMessage) | ||
270 | } | ||
271 | |||
272 | // Import workspaces | ||
273 | try { | ||
274 | const workspaces = await franzRequest('workspace', 'GET', token) | ||
275 | |||
276 | for (const workspace of workspaces) { | ||
277 | let workspaceId; | ||
278 | do { | ||
279 | workspaceId = uuid(); | ||
280 | } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0) | ||
281 | |||
282 | const services = workspace.services.map(service => serviceIdTranslation[service]) | ||
283 | |||
284 | await Workspace.create({ | ||
285 | userId: auth.user.id, | ||
286 | workspaceId, | ||
287 | name: workspace.name, | ||
288 | order: workspace.order, | ||
289 | services: JSON.stringify(services), | ||
290 | data: JSON.stringify({}) | ||
291 | }); | ||
292 | } | ||
293 | } catch (e) { | ||
294 | const errorMessage = 'Could not import your workspaces into our system.\nError: ' + e; | ||
295 | return response.status(401).send(errorMessage) | ||
296 | } | ||
297 | |||
298 | return response.send('Your account has been imported. You can now use your Franz account in Ferdi.') | ||
299 | } | ||
130 | } | 300 | } |
131 | 301 | ||
132 | module.exports = UserController | 302 | module.exports = UserController |
diff --git a/package-lock.json b/package-lock.json index b6ccb17..dfc1308 100644 --- a/package-lock.json +++ b/package-lock.json | |||
@@ -836,6 +836,11 @@ | |||
836 | "to-regex": "^3.0.1" | 836 | "to-regex": "^3.0.1" |
837 | } | 837 | } |
838 | }, | 838 | }, |
839 | "btoa": { | ||
840 | "version": "1.2.1", | ||
841 | "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", | ||
842 | "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" | ||
843 | }, | ||
839 | "buffer-alloc": { | 844 | "buffer-alloc": { |
840 | "version": "1.2.0", | 845 | "version": "1.2.0", |
841 | "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", | 846 | "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", |
diff --git a/package.json b/package.json index 6ae22ed..e63e7de 100644 --- a/package.json +++ b/package.json | |||
@@ -27,6 +27,7 @@ | |||
27 | "@adonisjs/lucid": "^6.1.3", | 27 | "@adonisjs/lucid": "^6.1.3", |
28 | "@adonisjs/validator": "^5.0.6", | 28 | "@adonisjs/validator": "^5.0.6", |
29 | "atob": "^2.1.2", | 29 | "atob": "^2.1.2", |
30 | "btoa": "^1.2.1", | ||
30 | "fs-extra": "^8.1.0", | 31 | "fs-extra": "^8.1.0", |
31 | "node-fetch": "^2.6.0", | 32 | "node-fetch": "^2.6.0", |
32 | "pg": "^7.12.1", | 33 | "pg": "^7.12.1", |
diff --git a/public/import.html b/public/import.html new file mode 100644 index 0000000..9e2592d --- /dev/null +++ b/public/import.html | |||
@@ -0,0 +1,92 @@ | |||
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>Import Franz account</title> | ||
9 | |||
10 | <link rel="stylesheet" href="css/vanilla.css"> | ||
11 | |||
12 | <style> | ||
13 | input { | ||
14 | margin-bottom: 1rem; | ||
15 | width: 100%; | ||
16 | padding: 0.5rem; | ||
17 | } | ||
18 | |||
19 | button { | ||
20 | display: flex; | ||
21 | overflow: hidden; | ||
22 | padding: 12px 12px; | ||
23 | cursor: pointer; | ||
24 | width: 100%; | ||
25 | -webkit-user-select: none; | ||
26 | -moz-user-select: none; | ||
27 | -ms-user-select: none; | ||
28 | user-select: none; | ||
29 | transition: all 150ms linear; | ||
30 | text-align: center; | ||
31 | white-space: nowrap; | ||
32 | text-decoration: none !important; | ||
33 | text-transform: none; | ||
34 | text-transform: capitalize; | ||
35 | color: #fff; | ||
36 | border: 0 none; | ||
37 | border-radius: 4px; | ||
38 | font-size: 13px; | ||
39 | font-weight: 500; | ||
40 | line-height: 1.3; | ||
41 | -webkit-appearance: none; | ||
42 | -moz-appearance: none; | ||
43 | appearance: none; | ||
44 | justify-content: center; | ||
45 | align-items: center; | ||
46 | flex: 0 0 160px; | ||
47 | box-shadow: 2px 5px 10px #e4e4e4; | ||
48 | color: #FFFFFF; | ||
49 | background: #161616; | ||
50 | } | ||
51 | |||
52 | #dropzone { | ||
53 | width: 100%; | ||
54 | height: 30vh; | ||
55 | background-color: #ebebeb; | ||
56 | |||
57 | display: flex; | ||
58 | align-items: center; | ||
59 | justify-content: center; | ||
60 | text-align: center; | ||
61 | |||
62 | cursor: pointer; | ||
63 | } | ||
64 | |||
65 | #dropzone p { | ||
66 | font-size: 0.85rem; | ||
67 | } | ||
68 | |||
69 | #files { | ||
70 | display: none; | ||
71 | } | ||
72 | |||
73 | </style> | ||
74 | </head> | ||
75 | |||
76 | <body> | ||
77 | <h1>Import a Franz account</h1> | ||
78 | <p>Please login using your Franz account. We will create a new Ferdi account with the same credentials.</p> | ||
79 | <form action="import" method="post"> | ||
80 | <label for="email">E-Mail address</label><br /> | ||
81 | <input type="email" name="email" placeholder="joe@example.com" required><br /> | ||
82 | |||
83 | <label for="password">Password</label><br /> | ||
84 | <input type="password" name="password" placeholder="********" required><br /> | ||
85 | |||
86 | <button type="submit" id="submitbutton">Import Franz account</button> | ||
87 | </form> | ||
88 | |||
89 | <script src="js/new.js"></script> | ||
90 | </body> | ||
91 | |||
92 | </html> | ||
diff --git a/start/routes.js b/start/routes.js index 43cbafe..4b82f12 100644 --- a/start/routes.js +++ b/start/routes.js | |||
@@ -61,6 +61,8 @@ Route.group(() => { | |||
61 | // Dashboard | 61 | // Dashboard |
62 | Route.post('new', 'RecipeController.create') | 62 | Route.post('new', 'RecipeController.create') |
63 | Route.get('new', ({ response }) => response.redirect('/new.html')) | 63 | Route.get('new', ({ response }) => response.redirect('/new.html')) |
64 | Route.post('import', 'UserController.import') | ||
65 | Route.get('import', ({ response }) => response.redirect('/import.html')) | ||
64 | 66 | ||
65 | Route.get('/', () => { | 67 | Route.get('/', () => { |
66 | return { | 68 | return { |