aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--app/Controllers/Http/UserController.js170
-rw-r--r--package-lock.json5
-rw-r--r--package.json1
-rw-r--r--public/import.html92
-rw-r--r--start/routes.js2
6 files changed, 273 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index 4c1034f..36723a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,6 @@ database/development.sqlite
12database/adonis.sqlite 12database/adonis.sqlite
13 13
14# Uploaded recipes 14# Uploaded recipes
15recipes/ \ No newline at end of file 15recipes/
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
3const User = use('App/Models/User'); 3const User = use('App/Models/User');
4const Service = use('App/Models/Service');
5const Workspace = use('App/Models/Workspace');
4const { 6const {
5 validateAll 7 validateAll
6} = use('Validator'); 8} = use('Validator');
9
7const atob = require('atob'); 10const atob = require('atob');
11const btoa = require('btoa');
12const fetch = require('node-fetch');
13const uuid = require('uuid/v4');
14const crypto = require('crypto');
15
16const 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
9class UserController { 38class 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
132module.exports = UserController 302module.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
62Route.post('new', 'RecipeController.create') 62Route.post('new', 'RecipeController.create')
63Route.get('new', ({ response }) => response.redirect('/new.html')) 63Route.get('new', ({ response }) => response.redirect('/new.html'))
64Route.post('import', 'UserController.import')
65Route.get('import', ({ response }) => response.redirect('/import.html'))
64 66
65Route.get('/', () => { 67Route.get('/', () => {
66 return { 68 return {