summaryrefslogtreecommitdiffstats
path: root/app/Controllers
diff options
context:
space:
mode:
authorLibravatar MCMXC <16797721+mcmxcdev@users.noreply.github.com>2024-02-10 18:37:40 -0700
committerLibravatar Vijay A <vraravam@users.noreply.github.com>2024-02-13 06:59:44 +0530
commite1c47572a6235fd8fd20af888ac3a11c7ae1369d (patch)
tree2dccff36a441916d7014037cef3f7ce84a790cad /app/Controllers
parentrefactor: project maintenance (diff)
downloadferdium-server-e1c47572a6235fd8fd20af888ac3a11c7ae1369d.tar.gz
ferdium-server-e1c47572a6235fd8fd20af888ac3a11c7ae1369d.tar.zst
ferdium-server-e1c47572a6235fd8fd20af888ac3a11c7ae1369d.zip
updates
Diffstat (limited to 'app/Controllers')
-rw-r--r--app/Controllers/Http/Api/Static/AnnouncementsController.ts18
-rw-r--r--app/Controllers/Http/Api/Static/EmptyController.ts4
-rw-r--r--app/Controllers/Http/Api/Static/FeaturesController.ts4
-rw-r--r--app/Controllers/Http/Dashboard/AccountController.ts30
-rw-r--r--app/Controllers/Http/Dashboard/DataController.ts10
-rw-r--r--app/Controllers/Http/Dashboard/DeleteController.ts10
-rw-r--r--app/Controllers/Http/Dashboard/ExportController.ts27
-rw-r--r--app/Controllers/Http/Dashboard/ForgotPasswordController.ts18
-rw-r--r--app/Controllers/Http/Dashboard/LogOutController.ts6
-rw-r--r--app/Controllers/Http/Dashboard/LoginController.ts49
-rw-r--r--app/Controllers/Http/Dashboard/ResetPasswordController.ts49
-rw-r--r--app/Controllers/Http/Dashboard/TransferController.ts67
-rw-r--r--app/Controllers/Http/HealthController.ts2
-rw-r--r--app/Controllers/Http/HomeController.ts2
-rw-r--r--app/Controllers/Http/RecipeController.ts165
-rw-r--r--app/Controllers/Http/ServiceController.ts173
-rw-r--r--app/Controllers/Http/UserController.ts229
-rw-r--r--app/Controllers/Http/WorkspaceController.ts85
18 files changed, 514 insertions, 434 deletions
diff --git a/app/Controllers/Http/Api/Static/AnnouncementsController.ts b/app/Controllers/Http/Api/Static/AnnouncementsController.ts
index 4ae9d0e..090ee1c 100644
--- a/app/Controllers/Http/Api/Static/AnnouncementsController.ts
+++ b/app/Controllers/Http/Api/Static/AnnouncementsController.ts
@@ -1,16 +1,20 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { app } from '@adonisjs/core/services/app' 2import { app } from '@adonisjs/core/services/app';
3import path from 'node:path' 3import path from 'node:path';
4import fs from 'fs-extra' 4import fs from 'fs-extra';
5 5
6export default class AnnouncementsController { 6export default class AnnouncementsController {
7 public async show({ response, params }: HttpContext) { 7 public async show({ response, params }: HttpContext) {
8 const announcement = path.join(app.resourcesPath(), 'announcements', `${params.version}.json`) 8 const announcement = path.join(
9 app.resourcesPath(),
10 'announcements',
11 `${params.version}.json`,
12 );
9 13
10 if (await fs.pathExists(announcement)) { 14 if (await fs.pathExists(announcement)) {
11 return response.download(announcement) 15 return response.download(announcement);
12 } 16 }
13 17
14 return response.status(404).send('No announcement found.') 18 return response.status(404).send('No announcement found.');
15 } 19 }
16} 20}
diff --git a/app/Controllers/Http/Api/Static/EmptyController.ts b/app/Controllers/Http/Api/Static/EmptyController.ts
index ff05b1c..80d70b7 100644
--- a/app/Controllers/Http/Api/Static/EmptyController.ts
+++ b/app/Controllers/Http/Api/Static/EmptyController.ts
@@ -1,7 +1,7 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2 2
3export default class EmptyController { 3export default class EmptyController {
4 public async show({ response }: HttpContext) { 4 public async show({ response }: HttpContext) {
5 return response.send([]) 5 return response.send([]);
6 } 6 }
7} 7}
diff --git a/app/Controllers/Http/Api/Static/FeaturesController.ts b/app/Controllers/Http/Api/Static/FeaturesController.ts
index 9e14c10..ce964de 100644
--- a/app/Controllers/Http/Api/Static/FeaturesController.ts
+++ b/app/Controllers/Http/Api/Static/FeaturesController.ts
@@ -1,4 +1,4 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2 2
3export default class FeaturesController { 3export default class FeaturesController {
4 public async show({ response }: HttpContext) { 4 public async show({ response }: HttpContext) {
@@ -9,6 +9,6 @@ export default class FeaturesController {
9 isSettingsWSEnabled: false, 9 isSettingsWSEnabled: false,
10 isMagicBarEnabled: true, 10 isMagicBarEnabled: true,
11 isTodosEnabled: true, 11 isTodosEnabled: true,
12 }) 12 });
13 } 13 }
14} 14}
diff --git a/app/Controllers/Http/Dashboard/AccountController.ts b/app/Controllers/Http/Dashboard/AccountController.ts
index 5870f19..a748c75 100644
--- a/app/Controllers/Http/Dashboard/AccountController.ts
+++ b/app/Controllers/Http/Dashboard/AccountController.ts
@@ -1,6 +1,6 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { schema, rules, validator } from '@adonisjs/validator' 2import { schema, rules, validator } from '@adonisjs/validator';
3import crypto from 'node:crypto' 3import crypto from 'node:crypto';
4 4
5export default class AccountController { 5export default class AccountController {
6 /** 6 /**
@@ -11,7 +11,7 @@ export default class AccountController {
11 username: auth.user?.username, 11 username: auth.user?.username,
12 email: auth.user?.email, 12 email: auth.user?.email,
13 lastname: auth.user?.lastname, 13 lastname: auth.user?.lastname,
14 }) 14 });
15 } 15 }
16 16
17 /** 17 /**
@@ -42,26 +42,26 @@ export default class AccountController {
42 lastname: schema.string([rules.required()]), 42 lastname: schema.string([rules.required()]),
43 }), 43 }),
44 data: request.only(['username', 'email', 'lastname']), 44 data: request.only(['username', 'email', 'lastname']),
45 }) 45 });
46 } catch (error) { 46 } catch (error) {
47 session.flash(error.messages) 47 session.flash(error.messages);
48 return response.redirect('/user/account') 48 return response.redirect('/user/account');
49 } 49 }
50 50
51 // Update user account 51 // Update user account
52 const { user } = auth 52 const { user } = auth;
53 if (user) { 53 if (user) {
54 user.username = request.input('username') 54 user.username = request.input('username');
55 user.lastname = request.input('lastname') 55 user.lastname = request.input('lastname');
56 user.email = request.input('email') 56 user.email = request.input('email');
57 if (request.input('password')) { 57 if (request.input('password')) {
58 const hashedPassword = crypto 58 const hashedPassword = crypto
59 .createHash('sha256') 59 .createHash('sha256')
60 .update(request.input('password')) 60 .update(request.input('password'))
61 .digest('base64') 61 .digest('base64');
62 user.password = hashedPassword 62 user.password = hashedPassword;
63 } 63 }
64 await user.save() 64 await user.save();
65 } 65 }
66 66
67 return view.render('dashboard/account', { 67 return view.render('dashboard/account', {
@@ -69,6 +69,6 @@ export default class AccountController {
69 lastname: user?.lastname, 69 lastname: user?.lastname,
70 email: user?.email, 70 email: user?.email,
71 success: user !== undefined, 71 success: user !== undefined,
72 }) 72 });
73 } 73 }
74} 74}
diff --git a/app/Controllers/Http/Dashboard/DataController.ts b/app/Controllers/Http/Dashboard/DataController.ts
index 8a77329..5f22979 100644
--- a/app/Controllers/Http/Dashboard/DataController.ts
+++ b/app/Controllers/Http/Dashboard/DataController.ts
@@ -1,14 +1,14 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2 2
3export default class DataController { 3export default class DataController {
4 /** 4 /**
5 * Display the data page 5 * Display the data page
6 */ 6 */
7 public async show({ view, auth }: HttpContext) { 7 public async show({ view, auth }: HttpContext) {
8 const { user } = auth 8 const { user } = auth;
9 9
10 const services = await user?.related('services').query() 10 const services = await user?.related('services').query();
11 const workspaces = await user?.related('workspaces').query() 11 const workspaces = await user?.related('workspaces').query();
12 12
13 return view.render('dashboard/data', { 13 return view.render('dashboard/data', {
14 username: user?.username, 14 username: user?.username,
@@ -19,6 +19,6 @@ export default class DataController {
19 stringify: JSON.stringify, 19 stringify: JSON.stringify,
20 services, 20 services,
21 workspaces, 21 workspaces,
22 }) 22 });
23 } 23 }
24} 24}
diff --git a/app/Controllers/Http/Dashboard/DeleteController.ts b/app/Controllers/Http/Dashboard/DeleteController.ts
index bd824b0..76e41ca 100644
--- a/app/Controllers/Http/Dashboard/DeleteController.ts
+++ b/app/Controllers/Http/Dashboard/DeleteController.ts
@@ -1,20 +1,20 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2 2
3export default class DeleteController { 3export default class DeleteController {
4 /** 4 /**
5 * Display the delete page 5 * Display the delete page
6 */ 6 */
7 public async show({ view }: HttpContext) { 7 public async show({ view }: HttpContext) {
8 return view.render('dashboard/delete') 8 return view.render('dashboard/delete');
9 } 9 }
10 10
11 /** 11 /**
12 * Delete user and session 12 * Delete user and session
13 */ 13 */
14 public async delete({ auth, response }: HttpContext) { 14 public async delete({ auth, response }: HttpContext) {
15 auth.user?.delete() 15 auth.user?.delete();
16 auth.use('web').logout() 16 auth.use('web').logout();
17 17
18 return response.redirect('/user/login') 18 return response.redirect('/user/login');
19 } 19 }
20} 20}
diff --git a/app/Controllers/Http/Dashboard/ExportController.ts b/app/Controllers/Http/Dashboard/ExportController.ts
index 5b6df70..6b20a82 100644
--- a/app/Controllers/Http/Dashboard/ExportController.ts
+++ b/app/Controllers/Http/Dashboard/ExportController.ts
@@ -1,30 +1,33 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2 2
3// eslint-disable-next-line @typescript-eslint/no-explicit-any 3// eslint-disable-next-line @typescript-eslint/no-explicit-any
4function deepParseToJSON(obj: any): Record<string, unknown> { 4function deepParseToJSON(obj: any): Record<string, unknown> {
5 if (typeof obj !== 'object' || obj === null) { 5 if (typeof obj !== 'object' || obj === null) {
6 try { 6 try {
7 // Try to parse the object as JSON 7 // Try to parse the object as JSON
8 return JSON.parse(obj) as Record<string, unknown> 8 return JSON.parse(obj) as Record<string, unknown>;
9 } catch { 9 } catch {
10 // If parsing fails, return the original value 10 // If parsing fails, return the original value
11 return obj 11 return obj;
12 } 12 }
13 } 13 }
14 14
15 // If obj is an object, recursively parse its keys 15 // If obj is an object, recursively parse its keys
16 if (Array.isArray(obj)) { 16 if (Array.isArray(obj)) {
17 // If obj is an array, recursively parse each element 17 // If obj is an array, recursively parse each element
18 return obj.map((item) => deepParseToJSON(item)) as unknown as Record<string, unknown> 18 return obj.map(item => deepParseToJSON(item)) as unknown as Record<
19 string,
20 unknown
21 >;
19 } else { 22 } else {
20 // If obj is an object, recursively parse its keys 23 // If obj is an object, recursively parse its keys
21 const parsedObj: Record<string, unknown> = {} 24 const parsedObj: Record<string, unknown> = {};
22 for (const key in obj) { 25 for (const key in obj) {
23 if (obj.hasOwnProperty(key)) { 26 if (obj.hasOwnProperty(key)) {
24 parsedObj[key] = deepParseToJSON(obj[key]) 27 parsedObj[key] = deepParseToJSON(obj[key]);
25 } 28 }
26 } 29 }
27 return parsedObj 30 return parsedObj;
28 } 31 }
29} 32}
30 33
@@ -33,9 +36,9 @@ export default class ExportController {
33 * Display the export page 36 * Display the export page
34 */ 37 */
35 public async show({ auth, response }: HttpContext) { 38 public async show({ auth, response }: HttpContext) {
36 const user = auth.user! 39 const user = auth.user!;
37 const services = await user.related('services').query() 40 const services = await user.related('services').query();
38 const workspaces = await user.related('workspaces').query() 41 const workspaces = await user.related('workspaces').query();
39 42
40 const exportData = { 43 const exportData = {
41 username: user.username, 44 username: user.username,
@@ -43,11 +46,11 @@ export default class ExportController {
43 mail: user.email, 46 mail: user.email,
44 services: deepParseToJSON(JSON.parse(JSON.stringify(services))), 47 services: deepParseToJSON(JSON.parse(JSON.stringify(services))),
45 workspaces: deepParseToJSON(JSON.parse(JSON.stringify(workspaces))), 48 workspaces: deepParseToJSON(JSON.parse(JSON.stringify(workspaces))),
46 } 49 };
47 50
48 return response 51 return response
49 .header('Content-Type', 'application/force-download') 52 .header('Content-Type', 'application/force-download')
50 .header('Content-disposition', 'attachment; filename=export.ferdium-data') 53 .header('Content-disposition', 'attachment; filename=export.ferdium-data')
51 .send(exportData) 54 .send(exportData);
52 } 55 }
53} 56}
diff --git a/app/Controllers/Http/Dashboard/ForgotPasswordController.ts b/app/Controllers/Http/Dashboard/ForgotPasswordController.ts
index f7b1d0e..1878c4d 100644
--- a/app/Controllers/Http/Dashboard/ForgotPasswordController.ts
+++ b/app/Controllers/Http/Dashboard/ForgotPasswordController.ts
@@ -1,13 +1,13 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { schema, rules, validator } from '@adonisjs/validator' 2import { schema, rules, validator } from '@adonisjs/validator';
3import User from '#app/Models/User' 3import User from '#app/Models/User';
4 4
5export default class ForgotPasswordController { 5export default class ForgotPasswordController {
6 /** 6 /**
7 * Display the forgot password form 7 * Display the forgot password form
8 */ 8 */
9 public async show({ view }: HttpContext) { 9 public async show({ view }: HttpContext) {
10 return view.render('dashboard/forgotPassword') 10 return view.render('dashboard/forgotPassword');
11 } 11 }
12 12
13 /** 13 /**
@@ -20,22 +20,22 @@ export default class ForgotPasswordController {
20 mail: schema.string([rules.email(), rules.required()]), 20 mail: schema.string([rules.email(), rules.required()]),
21 }), 21 }),
22 data: request.only(['mail']), 22 data: request.only(['mail']),
23 }) 23 });
24 } catch { 24 } catch {
25 return view.render('others/message', { 25 return view.render('others/message', {
26 heading: 'Cannot reset your password', 26 heading: 'Cannot reset your password',
27 text: 'Please enter a valid email address', 27 text: 'Please enter a valid email address',
28 }) 28 });
29 } 29 }
30 30
31 try { 31 try {
32 const user = await User.findByOrFail('email', request.input('mail')) 32 const user = await User.findByOrFail('email', request.input('mail'));
33 await user.forgotPassword() 33 await user.forgotPassword();
34 } catch {} 34 } catch {}
35 35
36 return view.render('others/message', { 36 return view.render('others/message', {
37 heading: 'Reset password', 37 heading: 'Reset password',
38 text: 'If your provided E-Mail address is linked to an account, we have just sent an E-Mail to that address.', 38 text: 'If your provided E-Mail address is linked to an account, we have just sent an E-Mail to that address.',
39 }) 39 });
40 } 40 }
41} 41}
diff --git a/app/Controllers/Http/Dashboard/LogOutController.ts b/app/Controllers/Http/Dashboard/LogOutController.ts
index 5d250c4..f085d00 100644
--- a/app/Controllers/Http/Dashboard/LogOutController.ts
+++ b/app/Controllers/Http/Dashboard/LogOutController.ts
@@ -1,12 +1,12 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2 2
3export default class LogOutController { 3export default class LogOutController {
4 /** 4 /**
5 * Login a user 5 * Login a user
6 */ 6 */
7 public async logout({ auth, response }: HttpContext) { 7 public async logout({ auth, response }: HttpContext) {
8 auth.logout() 8 auth.logout();
9 9
10 return response.redirect('/user/login') 10 return response.redirect('/user/login');
11 } 11 }
12} 12}
diff --git a/app/Controllers/Http/Dashboard/LoginController.ts b/app/Controllers/Http/Dashboard/LoginController.ts
index 5a54448..3367a2f 100644
--- a/app/Controllers/Http/Dashboard/LoginController.ts
+++ b/app/Controllers/Http/Dashboard/LoginController.ts
@@ -1,15 +1,15 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { schema, rules, validator } from '@adonisjs/validator' 2import { schema, rules, validator } from '@adonisjs/validator';
3import User from '#app/Models/User' 3import User from '#app/Models/User';
4import crypto from 'node:crypto' 4import crypto from 'node:crypto';
5import { handleVerifyAndReHash } from '../../../../helpers/PasswordHash.js' 5import { handleVerifyAndReHash } from '../../../../helpers/PasswordHash.js';
6 6
7export default class LoginController { 7export default class LoginController {
8 /** 8 /**
9 * Display the login form 9 * Display the login form
10 */ 10 */
11 public async show({ view }: HttpContext) { 11 public async show({ view }: HttpContext) {
12 return view.render('dashboard/login') 12 return view.render('dashboard/login');
13 } 13 }
14 14
15 /** 15 /**
@@ -23,51 +23,54 @@ export default class LoginController {
23 password: schema.string([rules.required()]), 23 password: schema.string([rules.required()]),
24 }), 24 }),
25 data: request.only(['mail', 'password']), 25 data: request.only(['mail', 'password']),
26 }) 26 });
27 } catch { 27 } catch {
28 session.flash({ 28 session.flash({
29 type: 'danger', 29 type: 'danger',
30 message: 'Invalid mail or password', 30 message: 'Invalid mail or password',
31 }) 31 });
32 session.flashExcept(['password']) 32 session.flashExcept(['password']);
33 33
34 return response.redirect('/user/login') 34 return response.redirect('/user/login');
35 } 35 }
36 36
37 try { 37 try {
38 const { mail, password } = request.all() 38 const { mail, password } = request.all();
39 39
40 // Check if user with email exists 40 // Check if user with email exists
41 const user = await User.query().where('email', mail).first() 41 const user = await User.query().where('email', mail).first();
42 if (!user?.email) { 42 if (!user?.email) {
43 throw new Error('User credentials not valid (Invalid email)') 43 throw new Error('User credentials not valid (Invalid email)');
44 } 44 }
45 45
46 const hashedPassword = crypto.createHash('sha256').update(password).digest('base64') 46 const hashedPassword = crypto
47 .createHash('sha256')
48 .update(password)
49 .digest('base64');
47 50
48 // Verify password 51 // Verify password
49 let isMatchedPassword = false 52 let isMatchedPassword = false;
50 try { 53 try {
51 isMatchedPassword = await handleVerifyAndReHash(user, hashedPassword) 54 isMatchedPassword = await handleVerifyAndReHash(user, hashedPassword);
52 } catch (error) { 55 } catch (error) {
53 return response.internalServerError({ message: error.message }) 56 return response.internalServerError({ message: error.message });
54 } 57 }
55 58
56 if (!isMatchedPassword) { 59 if (!isMatchedPassword) {
57 throw new Error('User credentials not valid (Invalid password)') 60 throw new Error('User credentials not valid (Invalid password)');
58 } 61 }
59 62
60 await auth.use('web').login(user) 63 await auth.use('web').login(user);
61 64
62 return response.redirect('/user/account') 65 return response.redirect('/user/account');
63 } catch { 66 } catch {
64 session.flash({ 67 session.flash({
65 type: 'danger', 68 type: 'danger',
66 message: 'Invalid mail or password', 69 message: 'Invalid mail or password',
67 }) 70 });
68 session.flashExcept(['password']) 71 session.flashExcept(['password']);
69 72
70 return response.redirect('/user/login') 73 return response.redirect('/user/login');
71 } 74 }
72 } 75 }
73} 76}
diff --git a/app/Controllers/Http/Dashboard/ResetPasswordController.ts b/app/Controllers/Http/Dashboard/ResetPasswordController.ts
index b62b5d2..261d773 100644
--- a/app/Controllers/Http/Dashboard/ResetPasswordController.ts
+++ b/app/Controllers/Http/Dashboard/ResetPasswordController.ts
@@ -1,30 +1,35 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { schema, rules, validator } from '@adonisjs/validator' 2import { schema, rules, validator } from '@adonisjs/validator';
3import Token from '#app/Models/Token' 3import Token from '#app/Models/Token';
4import moment from 'moment' 4import moment from 'moment';
5import crypto from 'node:crypto' 5import crypto from 'node:crypto';
6 6
7export default class ResetPasswordController { 7export default class ResetPasswordController {
8 /** 8 /**
9 * Display the reset password form 9 * Display the reset password form
10 */ 10 */
11 public async show({ view, request }: HttpContext) { 11 public async show({ view, request }: HttpContext) {
12 const { token } = request.qs() 12 const { token } = request.qs();
13 13
14 if (token) { 14 if (token) {
15 return view.render('dashboard/resetPassword', { token }) 15 return view.render('dashboard/resetPassword', { token });
16 } 16 }
17 17
18 return view.render('others/message', { 18 return view.render('others/message', {
19 heading: 'Invalid token', 19 heading: 'Invalid token',
20 text: 'Please make sure you are using a valid and recent link to reset your password.', 20 text: 'Please make sure you are using a valid and recent link to reset your password.',
21 }) 21 });
22 } 22 }
23 23
24 /** 24 /**
25 * Resets user password 25 * Resets user password
26 */ 26 */
27 public async resetPassword({ response, request, session, view }: HttpContext) { 27 public async resetPassword({
28 response,
29 request,
30 session,
31 view,
32 }: HttpContext) {
28 try { 33 try {
29 await validator.validate({ 34 await validator.validate({
30 schema: schema.create({ 35 schema: schema.create({
@@ -32,14 +37,14 @@ export default class ResetPasswordController {
32 token: schema.string([rules.required()]), 37 token: schema.string([rules.required()]),
33 }), 38 }),
34 data: request.only(['password', 'password_confirmation', 'token']), 39 data: request.only(['password', 'password_confirmation', 'token']),
35 }) 40 });
36 } catch { 41 } catch {
37 session.flash({ 42 session.flash({
38 type: 'danger', 43 type: 'danger',
39 message: 'Passwords do not match', 44 message: 'Passwords do not match',
40 }) 45 });
41 46
42 return response.redirect(`/user/reset?token=${request.input('token')}`) 47 return response.redirect(`/user/reset?token=${request.input('token')}`);
43 } 48 }
44 49
45 const tokenRow = await Token.query() 50 const tokenRow = await Token.query()
@@ -47,30 +52,34 @@ export default class ResetPasswordController {
47 .where('token', request.input('token')) 52 .where('token', request.input('token'))
48 .where('type', 'forgot_password') 53 .where('type', 'forgot_password')
49 .where('is_revoked', false) 54 .where('is_revoked', false)
50 .where('updated_at', '>=', moment().subtract(24, 'hours').format('YYYY-MM-DD HH:mm:ss')) 55 .where(
51 .first() 56 'updated_at',
57 '>=',
58 moment().subtract(24, 'hours').format('YYYY-MM-DD HH:mm:ss'),
59 )
60 .first();
52 61
53 if (!tokenRow) { 62 if (!tokenRow) {
54 return view.render('others/message', { 63 return view.render('others/message', {
55 heading: 'Cannot reset your password', 64 heading: 'Cannot reset your password',
56 text: 'Please make sure you are using a valid and recent link to reset your password and that your passwords entered match.', 65 text: 'Please make sure you are using a valid and recent link to reset your password and that your passwords entered match.',
57 }) 66 });
58 } 67 }
59 68
60 // Update user password 69 // Update user password
61 const hashedPassword = crypto 70 const hashedPassword = crypto
62 .createHash('sha256') 71 .createHash('sha256')
63 .update(request.input('password')) 72 .update(request.input('password'))
64 .digest('base64') 73 .digest('base64');
65 tokenRow.user.password = hashedPassword 74 tokenRow.user.password = hashedPassword;
66 await tokenRow.user.save() 75 await tokenRow.user.save();
67 76
68 // Delete token to prevent it from being used again 77 // Delete token to prevent it from being used again
69 await tokenRow.delete() 78 await tokenRow.delete();
70 79
71 return view.render('others/message', { 80 return view.render('others/message', {
72 heading: 'Reset password', 81 heading: 'Reset password',
73 text: 'Successfully reset your password. You can now login to your account using your new password.', 82 text: 'Successfully reset your password. You can now login to your account using your new password.',
74 }) 83 });
75 } 84 }
76} 85}
diff --git a/app/Controllers/Http/Dashboard/TransferController.ts b/app/Controllers/Http/Dashboard/TransferController.ts
index 0296973..ab50bcf 100644
--- a/app/Controllers/Http/Dashboard/TransferController.ts
+++ b/app/Controllers/Http/Dashboard/TransferController.ts
@@ -1,8 +1,8 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { schema, validator } from '@adonisjs/validator' 2import { schema, validator } from '@adonisjs/validator';
3import Service from '#app/Models/Service' 3import Service from '#app/Models/Service';
4import Workspace from '#app/Models/Workspace' 4import Workspace from '#app/Models/Workspace';
5import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid';
6 6
7const importSchema = schema.create({ 7const importSchema = schema.create({
8 username: schema.string(), 8 username: schema.string(),
@@ -10,52 +10,52 @@ const importSchema = schema.create({
10 mail: schema.string(), 10 mail: schema.string(),
11 services: schema.array().anyMembers(), 11 services: schema.array().anyMembers(),
12 workspaces: schema.array().anyMembers(), 12 workspaces: schema.array().anyMembers(),
13}) 13});
14 14
15export default class TransferController { 15export default class TransferController {
16 /** 16 /**
17 * Display the transfer page 17 * Display the transfer page
18 */ 18 */
19 public async show({ view }: HttpContext) { 19 public async show({ view }: HttpContext) {
20 return view.render('dashboard/transfer') 20 return view.render('dashboard/transfer');
21 } 21 }
22 22
23 public async import({ auth, request, response, session, view }: HttpContext) { 23 public async import({ auth, request, response, session, view }: HttpContext) {
24 let file 24 let file;
25 try { 25 try {
26 file = await validator.validate({ 26 file = await validator.validate({
27 schema: importSchema, 27 schema: importSchema,
28 data: JSON.parse(request.body().file), 28 data: JSON.parse(request.body().file),
29 }) 29 });
30 } catch { 30 } catch {
31 session.flash({ 31 session.flash({
32 message: 'Invalid Ferdium account file', 32 message: 'Invalid Ferdium account file',
33 }) 33 });
34 34
35 return response.redirect('/user/transfer') 35 return response.redirect('/user/transfer');
36 } 36 }
37 37
38 if (!file?.services || !file.workspaces) { 38 if (!file?.services || !file.workspaces) {
39 session.flash({ 39 session.flash({
40 type: 'danger', 40 type: 'danger',
41 message: 'Invalid Ferdium account file (2)', 41 message: 'Invalid Ferdium account file (2)',
42 }) 42 });
43 return response.redirect('/user/transfer') 43 return response.redirect('/user/transfer');
44 } 44 }
45 45
46 const serviceIdTranslation = {} 46 const serviceIdTranslation = {};
47 47
48 // Import services 48 // Import services
49 try { 49 try {
50 for (const service of file.services) { 50 for (const service of file.services) {
51 // Get new, unused uuid 51 // Get new, unused uuid
52 let serviceId 52 let serviceId;
53 do { 53 do {
54 serviceId = uuidv4() 54 serviceId = uuidv4();
55 } while ( 55 } while (
56 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member 56 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member
57 (await Service.query().where('serviceId', serviceId)).length > 0 57 (await Service.query().where('serviceId', serviceId)).length > 0
58 ) 58 );
59 59
60 // eslint-disable-next-line no-await-in-loop 60 // eslint-disable-next-line no-await-in-loop
61 await Service.create({ 61 await Service.create({
@@ -67,37 +67,38 @@ export default class TransferController {
67 typeof service.settings === 'string' 67 typeof service.settings === 'string'
68 ? service.settings 68 ? service.settings
69 : JSON.stringify(service.settings), 69 : JSON.stringify(service.settings),
70 }) 70 });
71 71
72 // @ts-expect-error Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{}' 72 // @ts-expect-error Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{}'
73 serviceIdTranslation[service.service_id || service.serviceId] = serviceId 73 serviceIdTranslation[service.service_id || service.serviceId] =
74 serviceId;
74 } 75 }
75 } catch (error) { 76 } catch (error) {
76 // eslint-disable-next-line no-console 77 // eslint-disable-next-line no-console
77 console.log(error) 78 console.log(error);
78 const errorMessage = `Could not import your services into our system.\nError: ${error}` 79 const errorMessage = `Could not import your services into our system.\nError: ${error}`;
79 return view.render('others/message', { 80 return view.render('others/message', {
80 heading: 'Error while importing', 81 heading: 'Error while importing',
81 text: errorMessage, 82 text: errorMessage,
82 }) 83 });
83 } 84 }
84 85
85 // Import workspaces 86 // Import workspaces
86 try { 87 try {
87 for (const workspace of file.workspaces) { 88 for (const workspace of file.workspaces) {
88 let workspaceId 89 let workspaceId;
89 90
90 do { 91 do {
91 workspaceId = uuidv4() 92 workspaceId = uuidv4();
92 } while ( 93 } while (
93 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member 94 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member
94 (await Workspace.query().where('workspaceId', workspaceId)).length > 0 95 (await Workspace.query().where('workspaceId', workspaceId)).length > 0
95 ) 96 );
96 97
97 const services = workspace.services.map( 98 const services = workspace.services.map(
98 // @ts-expect-error Parameter 'service' implicitly has an 'any' type. 99 // @ts-expect-error Parameter 'service' implicitly has an 'any' type.
99 (service) => serviceIdTranslation[service] 100 service => serviceIdTranslation[service],
100 ) 101 );
101 102
102 // eslint-disable-next-line no-await-in-loop 103 // eslint-disable-next-line no-await-in-loop
103 await Workspace.create({ 104 await Workspace.create({
@@ -107,20 +108,22 @@ export default class TransferController {
107 order: workspace.order, 108 order: workspace.order,
108 services: JSON.stringify(services), 109 services: JSON.stringify(services),
109 data: 110 data:
110 typeof workspace.data === 'string' ? workspace.data : JSON.stringify(workspace.data), 111 typeof workspace.data === 'string'
111 }) 112 ? workspace.data
113 : JSON.stringify(workspace.data),
114 });
112 } 115 }
113 } catch (error) { 116 } catch (error) {
114 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}` 117 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}`;
115 return view.render('others/message', { 118 return view.render('others/message', {
116 heading: 'Error while importing', 119 heading: 'Error while importing',
117 text: errorMessage, 120 text: errorMessage,
118 }) 121 });
119 } 122 }
120 123
121 return view.render('others/message', { 124 return view.render('others/message', {
122 heading: 'Successfully imported', 125 heading: 'Successfully imported',
123 text: 'Your account has been imported, you can now login as usual!', 126 text: 'Your account has been imported, you can now login as usual!',
124 }) 127 });
125 } 128 }
126} 129}
diff --git a/app/Controllers/Http/HealthController.ts b/app/Controllers/Http/HealthController.ts
index 59094e2..bf185d8 100644
--- a/app/Controllers/Http/HealthController.ts
+++ b/app/Controllers/Http/HealthController.ts
@@ -5,6 +5,6 @@ export default class HealthController {
5 return { 5 return {
6 api: 'success', 6 api: 'success',
7 db: 'success', 7 db: 'success',
8 } 8 };
9 } 9 }
10} 10}
diff --git a/app/Controllers/Http/HomeController.ts b/app/Controllers/Http/HomeController.ts
index 669d970..bae3bc2 100644
--- a/app/Controllers/Http/HomeController.ts
+++ b/app/Controllers/Http/HomeController.ts
@@ -4,6 +4,6 @@ export default class HomeController {
4 public async index() { 4 public async index() {
5 // TODO: Actually do something instead of alwayas returning success. 5 // TODO: Actually do something instead of alwayas returning success.
6 6
7 return { hello: 'world' } 7 return { hello: 'world' };
8 } 8 }
9} 9}
diff --git a/app/Controllers/Http/RecipeController.ts b/app/Controllers/Http/RecipeController.ts
index e43bcf8..d30c59f 100644
--- a/app/Controllers/Http/RecipeController.ts
+++ b/app/Controllers/Http/RecipeController.ts
@@ -1,13 +1,13 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import fs from 'fs-extra' 2import fs from 'fs-extra';
3import { app } from '@adonisjs/core/services/app' 3import { app } from '@adonisjs/core/services/app';
4import path from 'node:path' 4import path from 'node:path';
5import Recipe from '#app/Models/Recipe' 5import Recipe from '#app/Models/Recipe';
6import { isCreationEnabled } from '#config/app' 6import { isCreationEnabled } from '#config/app';
7import { validator, schema, rules } from '@adonisjs/validator' 7import { validator, schema, rules } from '@adonisjs/validator';
8import targz from 'targz' 8import targz from 'targz';
9import semver from 'semver' 9import semver from 'semver';
10import Drive from '@ioc:Adonis/Core/Drive' 10import Drive from '@ioc:Adonis/Core/Drive';
11 11
12// TODO: This file needs to be refactored and cleaned up to include types 12// TODO: This file needs to be refactored and cleaned up to include types
13 13
@@ -18,17 +18,17 @@ const createSchema = schema.create({
18 // author: 'required|accepted', 18 // author: 'required|accepted',
19 author: schema.string(), 19 author: schema.string(),
20 svg: schema.string([rules.url()]), 20 svg: schema.string([rules.url()]),
21}) 21});
22 22
23const searchSchema = schema.create({ 23const searchSchema = schema.create({
24 needle: schema.string(), 24 needle: schema.string(),
25}) 25});
26 26
27const downloadSchema = schema.create({ 27const downloadSchema = schema.create({
28 // TODO: Check if this is correct 28 // TODO: Check if this is correct
29 // recipe: 'required|accepted', 29 // recipe: 'required|accepted',
30 recipe: schema.string(), 30 recipe: schema.string(),
31}) 31});
32 32
33const compress = (src: string, dest: string) => 33const compress = (src: string, dest: string) =>
34 new Promise((resolve, reject) => { 34 new Promise((resolve, reject) => {
@@ -37,30 +37,34 @@ const compress = (src: string, dest: string) =>
37 src, 37 src,
38 dest, 38 dest,
39 }, 39 },
40 (err) => { 40 err => {
41 if (err) { 41 if (err) {
42 reject(err) 42 reject(err);
43 } else { 43 } else {
44 resolve(dest) 44 resolve(dest);
45 } 45 }
46 } 46 },
47 ) 47 );
48 }) 48 });
49 49
50export default class RecipesController { 50export default class RecipesController {
51 // List official and custom recipes 51 // List official and custom recipes
52 public async list({ response }: HttpContext) { 52 public async list({ response }: HttpContext) {
53 const officialRecipes = fs.readJsonSync(path.join(app.appRoot, 'recipes', 'all.json')) 53 const officialRecipes = fs.readJsonSync(
54 const customRecipesArray = await Recipe.all() 54 path.join(app.appRoot, 'recipes', 'all.json'),
55 const customRecipes = customRecipesArray.map((recipe) => ({ 55 );
56 const customRecipesArray = await Recipe.all();
57 const customRecipes = customRecipesArray.map(recipe => ({
56 id: recipe.recipeId, 58 id: recipe.recipeId,
57 name: recipe.name, 59 name: recipe.name,
58 ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data), 60 ...(typeof recipe.data === 'string'
59 })) 61 ? JSON.parse(recipe.data)
62 : recipe.data),
63 }));
60 64
61 const recipes = [...officialRecipes, ...customRecipes] 65 const recipes = [...officialRecipes, ...customRecipes];
62 66
63 return response.send(recipes) 67 return response.send(recipes);
64 } 68 }
65 69
66 // TODO: Test this endpoint 70 // TODO: Test this endpoint
@@ -68,45 +72,52 @@ export default class RecipesController {
68 public async create({ request, response }: HttpContext) { 72 public async create({ request, response }: HttpContext) {
69 // Check if recipe creation is enabled 73 // Check if recipe creation is enabled
70 if (isCreationEnabled === 'false') { 74 if (isCreationEnabled === 'false') {
71 return response.send("This server doesn't allow the creation of new recipes.") 75 return response.send(
76 "This server doesn't allow the creation of new recipes.",
77 );
72 } 78 }
73 79
74 // Validate user input 80 // Validate user input
75 let data 81 let data;
76 try { 82 try {
77 data = await request.validate({ schema: createSchema }) 83 data = await request.validate({ schema: createSchema });
78 } catch (error) { 84 } catch (error) {
79 return response.status(401).send({ 85 return response.status(401).send({
80 message: 'Invalid POST arguments', 86 message: 'Invalid POST arguments',
81 messages: error.messages, 87 messages: error.messages,
82 status: 401, 88 status: 401,
83 }) 89 });
84 } 90 }
85 91
86 if (!data.id) { 92 if (!data.id) {
87 return response.send('Please provide an ID') 93 return response.send('Please provide an ID');
88 } 94 }
89 95
90 // Check for invalid characters 96 // Check for invalid characters
91 if (/\.+/.test(data.id) || /\/+/.test(data.id)) { 97 if (/\.+/.test(data.id) || /\/+/.test(data.id)) {
92 return response.send('Invalid recipe name. Your recipe name may not contain "." or "/"') 98 return response.send(
99 'Invalid recipe name. Your recipe name may not contain "." or "/"',
100 );
93 } 101 }
94 102
95 // Clear temporary recipe folder 103 // Clear temporary recipe folder
96 await fs.emptyDir(app.tmpPath('recipe')) 104 await fs.emptyDir(app.tmpPath('recipe'));
97 105
98 // Move uploaded files to temporary path 106 // Move uploaded files to temporary path
99 const files = request.file('files') 107 const files = request.file('files');
100 if (!files) { 108 if (!files) {
101 return response.abort('Error processsing files.') 109 return response.abort('Error processsing files.');
102 } 110 }
103 await files.move(app.tmpPath('recipe')) 111 await files.move(app.tmpPath('recipe'));
104 112
105 // Compress files to .tar.gz file 113 // Compress files to .tar.gz file
106 const source = app.tmpPath('recipe') 114 const source = app.tmpPath('recipe');
107 const destination = path.join(app.appRoot, `/recipes/archives/${data.id}.tar.gz`) 115 const destination = path.join(
116 app.appRoot,
117 `/recipes/archives/${data.id}.tar.gz`,
118 );
108 119
109 compress(source, destination) 120 compress(source, destination);
110 121
111 // Create recipe in db 122 // Create recipe in db
112 await Recipe.create({ 123 await Recipe.create({
@@ -121,47 +132,55 @@ export default class RecipesController {
121 svg: data.svg, 132 svg: data.svg,
122 }, 133 },
123 }), 134 }),
124 }) 135 });
125 136
126 return response.send('Created new recipe') 137 return response.send('Created new recipe');
127 } 138 }
128 139
129 // Search official and custom recipes 140 // Search official and custom recipes
130 public async search({ request, response }: HttpContext) { 141 public async search({ request, response }: HttpContext) {
131 // Validate user input 142 // Validate user input
132 let data 143 let data;
133 try { 144 try {
134 data = await request.validate({ schema: searchSchema }) 145 data = await request.validate({ schema: searchSchema });
135 } catch (error) { 146 } catch (error) {
136 return response.status(401).send({ 147 return response.status(401).send({
137 message: 'Please provide a needle', 148 message: 'Please provide a needle',
138 messages: error.messages, 149 messages: error.messages,
139 status: 401, 150 status: 401,
140 }) 151 });
141 } 152 }
142 153
143 const { needle } = data 154 const { needle } = data;
144 155
145 // Get results 156 // Get results
146 let results 157 let results;
147 158
148 if (needle === 'ferdium:custom') { 159 if (needle === 'ferdium:custom') {
149 const dbResults = await Recipe.all() 160 const dbResults = await Recipe.all();
150 results = dbResults.map((recipe) => ({ 161 results = dbResults.map(recipe => ({
151 id: recipe.recipeId, 162 id: recipe.recipeId,
152 name: recipe.name, 163 name: recipe.name,
153 ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data), 164 ...(typeof recipe.data === 'string'
154 })) 165 ? JSON.parse(recipe.data)
166 : recipe.data),
167 }));
155 } else { 168 } else {
156 const localResultsArray = await Recipe.query().where('name', 'LIKE', `%${needle}%`) 169 const localResultsArray = await Recipe.query().where(
157 results = localResultsArray.map((recipe) => ({ 170 'name',
171 'LIKE',
172 `%${needle}%`,
173 );
174 results = localResultsArray.map(recipe => ({
158 id: recipe.recipeId, 175 id: recipe.recipeId,
159 name: recipe.name, 176 name: recipe.name,
160 ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data), 177 ...(typeof recipe.data === 'string'
161 })) 178 ? JSON.parse(recipe.data)
179 : recipe.data),
180 }));
162 } 181 }
163 182
164 return response.send(results) 183 return response.send(results);
165 } 184 }
166 185
167 public popularRecipes({ response }: HttpContext) { 186 public popularRecipes({ response }: HttpContext) {
@@ -169,63 +188,67 @@ export default class RecipesController {
169 fs 188 fs
170 .readJsonSync(path.join(app.appRoot, 'recipes', 'all.json')) 189 .readJsonSync(path.join(app.appRoot, 'recipes', 'all.json'))
171 // eslint-disable-next-line @typescript-eslint/no-explicit-any 190 // eslint-disable-next-line @typescript-eslint/no-explicit-any
172 .filter((recipe: any) => recipe.featured) 191 .filter((recipe: any) => recipe.featured),
173 ) 192 );
174 } 193 }
175 194
176 // TODO: test this endpoint 195 // TODO: test this endpoint
177 public update({ request, response }: HttpContext) { 196 public update({ request, response }: HttpContext) {
178 const updates = [] 197 const updates = [];
179 const recipes = request.all() 198 const recipes = request.all();
180 const allJson = fs.readJsonSync(path.join(app.appRoot, 'recipes', 'all.json')) 199 const allJson = fs.readJsonSync(
200 path.join(app.appRoot, 'recipes', 'all.json'),
201 );
181 202
182 for (const recipe of Object.keys(recipes)) { 203 for (const recipe of Object.keys(recipes)) {
183 const version = recipes[recipe] 204 const version = recipes[recipe];
184 205
185 // Find recipe in local recipe repository 206 // Find recipe in local recipe repository
186 // eslint-disable-next-line @typescript-eslint/no-explicit-any 207 // eslint-disable-next-line @typescript-eslint/no-explicit-any
187 const localRecipe = allJson.find((r: any) => r.id === recipe) 208 const localRecipe = allJson.find((r: any) => r.id === recipe);
188 if (localRecipe && semver.lt(version, localRecipe.version)) { 209 if (localRecipe && semver.lt(version, localRecipe.version)) {
189 updates.push(recipe) 210 updates.push(recipe);
190 } 211 }
191 } 212 }
192 213
193 return response.send(updates) 214 return response.send(updates);
194 } 215 }
195 216
196 // TODO: test this endpoint 217 // TODO: test this endpoint
197 // Download a recipe 218 // Download a recipe
198 public async download({ response, params }: HttpContext) { 219 public async download({ response, params }: HttpContext) {
199 // Validate user input 220 // Validate user input
200 let data 221 let data;
201 try { 222 try {
202 data = await validator.validate({ 223 data = await validator.validate({
203 data: params, 224 data: params,
204 schema: downloadSchema, 225 schema: downloadSchema,
205 }) 226 });
206 } catch (error) { 227 } catch (error) {
207 return response.status(401).send({ 228 return response.status(401).send({
208 message: 'Please provide a recipe ID', 229 message: 'Please provide a recipe ID',
209 messages: error.messages, 230 messages: error.messages,
210 status: 401, 231 status: 401,
211 }) 232 });
212 } 233 }
213 234
214 const service = data.recipe 235 const service = data.recipe;
215 236
216 // Check for invalid characters 237 // Check for invalid characters
217 if (/\.+/.test(service) || /\/+/.test(service)) { 238 if (/\.+/.test(service) || /\/+/.test(service)) {
218 return response.send('Invalid recipe name') 239 return response.send('Invalid recipe name');
219 } 240 }
220 241
221 // Check if recipe exists in recipes folder 242 // Check if recipe exists in recipes folder
222 if (await Drive.exists(`${service}.tar.gz`)) { 243 if (await Drive.exists(`${service}.tar.gz`)) {
223 return response.type('.tar.gz').send(await Drive.get(`${service}.tar.gz`)) 244 return response
245 .type('.tar.gz')
246 .send(await Drive.get(`${service}.tar.gz`));
224 } 247 }
225 248
226 return response.status(400).send({ 249 return response.status(400).send({
227 message: 'Recipe not found', 250 message: 'Recipe not found',
228 code: 'recipe-not-found', 251 code: 'recipe-not-found',
229 }) 252 });
230 } 253 }
231} 254}
diff --git a/app/Controllers/Http/ServiceController.ts b/app/Controllers/Http/ServiceController.ts
index 9988244..8fec844 100644
--- a/app/Controllers/Http/ServiceController.ts
+++ b/app/Controllers/Http/ServiceController.ts
@@ -1,49 +1,49 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { schema } from '@adonisjs/validator' 2import { schema } from '@adonisjs/validator';
3import Service from '#app/Models/Service' 3import Service from '#app/Models/Service';
4import { url } from '#config/app' 4import { url } from '#config/app';
5import { v4 as uuid } from 'uuid' 5import { v4 as uuid } from 'uuid';
6import * as fs from 'fs-extra' 6import * as fs from 'fs-extra';
7import path from 'node:path' 7import path from 'node:path';
8import { app } from '@adonisjs/core/services/app' 8import { app } from '@adonisjs/core/services/app';
9import sanitize from 'sanitize-filename' 9import sanitize from 'sanitize-filename';
10 10
11const createSchema = schema.create({ 11const createSchema = schema.create({
12 name: schema.string(), 12 name: schema.string(),
13 recipeId: schema.string(), 13 recipeId: schema.string(),
14}) 14});
15 15
16export default class ServiceController { 16export default class ServiceController {
17 // Create a new service for user 17 // Create a new service for user
18 public async create({ request, response, auth }: HttpContext) { 18 public async create({ request, response, auth }: HttpContext) {
19 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 19 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
20 const user = auth.user ?? request.user 20 const user = auth.user ?? request.user;
21 21
22 if (!user) { 22 if (!user) {
23 return response.unauthorized('Missing or invalid api token') 23 return response.unauthorized('Missing or invalid api token');
24 } 24 }
25 25
26 // Validate user input 26 // Validate user input
27 const data = request.all() 27 const data = request.all();
28 28
29 try { 29 try {
30 await request.validate({ schema: createSchema }) 30 await request.validate({ schema: createSchema });
31 } catch (error) { 31 } catch (error) {
32 return response.status(401).send({ 32 return response.status(401).send({
33 message: 'Invalid POST arguments', 33 message: 'Invalid POST arguments',
34 messages: error.messages, 34 messages: error.messages,
35 status: 401, 35 status: 401,
36 }) 36 });
37 } 37 }
38 38
39 // Get new, unused uuid 39 // Get new, unused uuid
40 let serviceId 40 let serviceId;
41 do { 41 do {
42 serviceId = uuid() 42 serviceId = uuid();
43 } while ( 43 } while (
44 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member 44 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member
45 (await Service.query().where('serviceId', serviceId)).length > 0 45 (await Service.query().where('serviceId', serviceId)).length > 0
46 ) 46 );
47 47
48 await Service.create({ 48 await Service.create({
49 userId: user.id, 49 userId: user.id,
@@ -51,7 +51,7 @@ export default class ServiceController {
51 name: data.name, 51 name: data.name,
52 recipeId: data.recipeId, 52 recipeId: data.recipeId,
53 settings: JSON.stringify(data), 53 settings: JSON.stringify(data),
54 }) 54 });
55 55
56 return response.send({ 56 return response.send({
57 data: { 57 data: {
@@ -72,26 +72,28 @@ export default class ServiceController {
72 ...data, 72 ...data,
73 }, 73 },
74 status: ['created'], 74 status: ['created'],
75 }) 75 });
76 } 76 }
77 77
78 // List all services a user has created 78 // List all services a user has created
79 public async list({ request, response, auth }: HttpContext) { 79 public async list({ request, response, auth }: HttpContext) {
80 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 80 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
81 const user = auth.user ?? request.user 81 const user = auth.user ?? request.user;
82 82
83 if (!user) { 83 if (!user) {
84 return response.unauthorized('Missing or invalid api token') 84 return response.unauthorized('Missing or invalid api token');
85 } 85 }
86 86
87 const { id } = user 87 const { id } = user;
88 const services = await user.related('services').query() 88 const services = await user.related('services').query();
89 89
90 // Convert to array with all data Franz wants 90 // Convert to array with all data Franz wants
91 // eslint-disable-next-line @typescript-eslint/no-explicit-any 91 // eslint-disable-next-line @typescript-eslint/no-explicit-any
92 const servicesArray = services.map((service: any) => { 92 const servicesArray = services.map((service: any) => {
93 const settings = 93 const settings =
94 typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings 94 typeof service.settings === 'string'
95 ? JSON.parse(service.settings)
96 : service.settings;
95 97
96 return { 98 return {
97 customRecipe: false, 99 customRecipe: false,
@@ -113,82 +115,89 @@ export default class ServiceController {
113 name: service.name, 115 name: service.name,
114 recipeId: service.recipeId, 116 recipeId: service.recipeId,
115 userId: id, 117 userId: id,
116 } 118 };
117 }) 119 });
118 120
119 return response.send(servicesArray) 121 return response.send(servicesArray);
120 } 122 }
121 123
122 public async delete({ request, params, auth, response }: HttpContext) { 124 public async delete({ request, params, auth, response }: HttpContext) {
123 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 125 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
124 const user = auth.user ?? request.user 126 const user = auth.user ?? request.user;
125 127
126 if (!user) { 128 if (!user) {
127 return response.unauthorized('Missing or invalid api token') 129 return response.unauthorized('Missing or invalid api token');
128 } 130 }
129 131
130 // Update data in database 132 // Update data in database
131 await Service.query().where('serviceId', params.id).where('userId', user.id).delete() 133 await Service.query()
134 .where('serviceId', params.id)
135 .where('userId', user.id)
136 .delete();
132 137
133 return response.send({ 138 return response.send({
134 message: 'Sucessfully deleted service', 139 message: 'Sucessfully deleted service',
135 status: 200, 140 status: 200,
136 }) 141 });
137 } 142 }
138 143
139 // TODO: Test if icon upload works 144 // TODO: Test if icon upload works
140 public async edit({ request, response, auth, params }: HttpContext) { 145 public async edit({ request, response, auth, params }: HttpContext) {
141 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 146 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
142 const user = auth.user ?? request.user 147 const user = auth.user ?? request.user;
143 148
144 if (!user) { 149 if (!user) {
145 return response.unauthorized('Missing or invalid api token') 150 return response.unauthorized('Missing or invalid api token');
146 } 151 }
147 152
148 const { id } = params 153 const { id } = params;
149 const service = await Service.query() 154 const service = await Service.query()
150 .where('serviceId', id) 155 .where('serviceId', id)
151 .where('userId', user.id) 156 .where('userId', user.id)
152 .firstOrFail() 157 .firstOrFail();
153 158
154 if (request.file('icon')) { 159 if (request.file('icon')) {
155 // Upload custom service icon 160 // Upload custom service icon
156 const icon = request.file('icon', { 161 const icon = request.file('icon', {
157 extnames: ['png', 'jpg', 'jpeg', 'svg'], 162 extnames: ['png', 'jpg', 'jpeg', 'svg'],
158 size: '2mb', 163 size: '2mb',
159 }) 164 });
160 165
161 if (icon === null) { 166 if (icon === null) {
162 return response.badRequest('Icon not uploaded.') 167 return response.badRequest('Icon not uploaded.');
163 } 168 }
164 169
165 const settings = 170 const settings =
166 typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings 171 typeof service.settings === 'string'
172 ? JSON.parse(service.settings)
173 : service.settings;
167 174
168 let iconId 175 let iconId;
169 do { 176 do {
170 iconId = uuid() + uuid() 177 iconId = uuid() + uuid();
171 } while ( 178 } while (
172 // eslint-disable-next-line no-await-in-loop 179 // eslint-disable-next-line no-await-in-loop
173 await fs.exists(path.join(app.tmpPath('uploads'), iconId)) 180 await fs.exists(path.join(app.tmpPath('uploads'), iconId))
174 ) 181 );
175 iconId = `${iconId}.${icon.extname}` 182 iconId = `${iconId}.${icon.extname}`;
176 183
177 await icon.move(app.tmpPath('uploads'), { 184 await icon.move(app.tmpPath('uploads'), {
178 name: iconId, 185 name: iconId,
179 overwrite: true, 186 overwrite: true,
180 }) 187 });
181 188
182 if (icon.state !== 'moved') { 189 if (icon.state !== 'moved') {
183 return response.status(500).send(icon.errors) 190 return response.status(500).send(icon.errors);
184 } 191 }
185 192
186 const newSettings = { 193 const newSettings = {
187 ...settings, 194 ...settings,
188 195
189 iconId, 196 iconId,
190 customIconVersion: settings?.customIconVersion ? settings.customIconVersion + 1 : 1, 197 customIconVersion: settings?.customIconVersion
191 } 198 ? settings.customIconVersion + 1
199 : 1,
200 };
192 201
193 // Update data in database 202 // Update data in database
194 await Service.query() 203 await Service.query()
@@ -197,7 +206,7 @@ export default class ServiceController {
197 .update({ 206 .update({
198 name: service.name, 207 name: service.name,
199 settings: JSON.stringify(newSettings), 208 settings: JSON.stringify(newSettings),
200 }) 209 });
201 210
202 return response.send({ 211 return response.send({
203 data: { 212 data: {
@@ -208,24 +217,28 @@ export default class ServiceController {
208 userId: user.id, 217 userId: user.id,
209 }, 218 },
210 status: ['updated'], 219 status: ['updated'],
211 }) 220 });
212 } 221 }
213 // Update service info 222 // Update service info
214 const data = request.all() 223 const data = request.all();
215 224
216 const settings = { 225 const settings = {
217 ...(typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings), 226 ...(typeof service.settings === 'string'
227 ? JSON.parse(service.settings)
228 : service.settings),
218 ...data, 229 ...data,
219 } 230 };
220 231
221 if (settings.customIcon === 'delete') { 232 if (settings.customIcon === 'delete') {
222 fs.remove(path.join(app.tmpPath('uploads'), settings.iconId)).catch((error) => { 233 fs.remove(path.join(app.tmpPath('uploads'), settings.iconId)).catch(
223 console.error(error) 234 error => {
224 }) 235 console.error(error);
236 },
237 );
225 238
226 settings.iconId = undefined 239 settings.iconId = undefined;
227 settings.customIconVersion = undefined 240 settings.customIconVersion = undefined;
228 settings.customIcon = '' 241 settings.customIcon = '';
229 } 242 }
230 243
231 // Update data in database 244 // Update data in database
@@ -235,13 +248,13 @@ export default class ServiceController {
235 .update({ 248 .update({
236 name: data.name, 249 name: data.name,
237 settings: JSON.stringify(settings), 250 settings: JSON.stringify(settings),
238 }) 251 });
239 252
240 // Get updated row 253 // Get updated row
241 const serviceUpdated = await Service.query() 254 const serviceUpdated = await Service.query()
242 .where('serviceId', id) 255 .where('serviceId', id)
243 .where('userId', user.id) 256 .where('userId', user.id)
244 .firstOrFail() 257 .firstOrFail();
245 258
246 return response.send({ 259 return response.send({
247 data: { 260 data: {
@@ -252,19 +265,19 @@ export default class ServiceController {
252 userId: user.id, 265 userId: user.id,
253 }, 266 },
254 status: ['updated'], 267 status: ['updated'],
255 }) 268 });
256 } 269 }
257 270
258 // TODO: Test if this works 271 // TODO: Test if this works
259 public async reorder({ request, response, auth }: HttpContext) { 272 public async reorder({ request, response, auth }: HttpContext) {
260 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 273 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
261 const user = auth.user ?? request.user 274 const user = auth.user ?? request.user;
262 275
263 if (!user) { 276 if (!user) {
264 return response.unauthorized('Missing or invalid api token') 277 return response.unauthorized('Missing or invalid api token');
265 } 278 }
266 279
267 const data = request.all() 280 const data = request.all();
268 281
269 for (const service of Object.keys(data)) { 282 for (const service of Object.keys(data)) {
270 // Get current settings from db 283 // Get current settings from db
@@ -272,14 +285,14 @@ export default class ServiceController {
272 .where('serviceId', service) 285 .where('serviceId', service)
273 .where('userId', user.id) 286 .where('userId', user.id)
274 287
275 .firstOrFail() 288 .firstOrFail();
276 289
277 const settings = { 290 const settings = {
278 ...(typeof serviceData.settings === 'string' 291 ...(typeof serviceData.settings === 'string'
279 ? JSON.parse(serviceData.settings) 292 ? JSON.parse(serviceData.settings)
280 : serviceData.settings), 293 : serviceData.settings),
281 order: data[service], 294 order: data[service],
282 } 295 };
283 296
284 // Update data in database 297 // Update data in database
285 await Service.query() // eslint-disable-line no-await-in-loop 298 await Service.query() // eslint-disable-line no-await-in-loop
@@ -287,16 +300,18 @@ export default class ServiceController {
287 .where('userId', user.id) 300 .where('userId', user.id)
288 .update({ 301 .update({
289 settings: JSON.stringify(settings), 302 settings: JSON.stringify(settings),
290 }) 303 });
291 } 304 }
292 305
293 // Get new services 306 // Get new services
294 const services = await user.related('services').query() 307 const services = await user.related('services').query();
295 // Convert to array with all data Franz wants 308 // Convert to array with all data Franz wants
296 // eslint-disable-next-line @typescript-eslint/no-explicit-any 309 // eslint-disable-next-line @typescript-eslint/no-explicit-any
297 const servicesArray = services.map((service: any) => { 310 const servicesArray = services.map((service: any) => {
298 const settings = 311 const settings =
299 typeof service.settings === 'string' ? JSON.parse(service.settings) : service.settings 312 typeof service.settings === 'string'
313 ? JSON.parse(service.settings)
314 : service.settings;
300 315
301 return { 316 return {
302 customRecipe: false, 317 customRecipe: false,
@@ -318,34 +333,34 @@ export default class ServiceController {
318 name: service.name, 333 name: service.name,
319 recipeId: service.recipeId, 334 recipeId: service.recipeId,
320 userId: user.id, 335 userId: user.id,
321 } 336 };
322 }) 337 });
323 338
324 return response.send(servicesArray) 339 return response.send(servicesArray);
325 } 340 }
326 341
327 // TODO: Test if this works 342 // TODO: Test if this works
328 public async icon({ params, response }: HttpContext) { 343 public async icon({ params, response }: HttpContext) {
329 let { id } = params 344 let { id } = params;
330 345
331 id = sanitize(id) 346 id = sanitize(id);
332 if (id === '') { 347 if (id === '') {
333 return response.status(404).send({ 348 return response.status(404).send({
334 status: "Icon doesn't exist", 349 status: "Icon doesn't exist",
335 }) 350 });
336 } 351 }
337 352
338 const iconPath = path.join(app.tmpPath('uploads'), id) 353 const iconPath = path.join(app.tmpPath('uploads'), id);
339 354
340 try { 355 try {
341 await fs.access(iconPath) 356 await fs.access(iconPath);
342 } catch { 357 } catch {
343 // File not available. 358 // File not available.
344 return response.status(404).send({ 359 return response.status(404).send({
345 status: "Icon doesn't exist", 360 status: "Icon doesn't exist",
346 }) 361 });
347 } 362 }
348 363
349 return response.download(iconPath) 364 return response.download(iconPath);
350 } 365 }
351} 366}
diff --git a/app/Controllers/Http/UserController.ts b/app/Controllers/Http/UserController.ts
index 088f7b1..667786b 100644
--- a/app/Controllers/Http/UserController.ts
+++ b/app/Controllers/Http/UserController.ts
@@ -1,49 +1,58 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { schema, rules } from '@adonisjs/validator' 2import { schema, rules } from '@adonisjs/validator';
3import User from '#app/Models/User' 3import User from '#app/Models/User';
4import { connectWithFranz, isRegistrationEnabled } from '../../../config/app.js' 4import {
5import crypto from 'node:crypto' 5 connectWithFranz,
6import { v4 as uuid } from 'uuid' 6 isRegistrationEnabled,
7import Workspace from '#app/Models/Workspace' 7} from '../../../config/app.js';
8import Service from '#app/Models/Service' 8import crypto from 'node:crypto';
9import { v4 as uuid } from 'uuid';
10import Workspace from '#app/Models/Workspace';
11import Service from '#app/Models/Service';
9 12
10// TODO: This file needs to be refactored and cleaned up to include types 13// TODO: This file needs to be refactored and cleaned up to include types
11import { handleVerifyAndReHash } from '../../../helpers/PasswordHash.js' 14import { handleVerifyAndReHash } from '../../../helpers/PasswordHash.js';
12 15
13const newPostSchema = schema.create({ 16const newPostSchema = schema.create({
14 firstname: schema.string(), 17 firstname: schema.string(),
15 lastname: schema.string(), 18 lastname: schema.string(),
16 email: schema.string([rules.email(), rules.unique({ table: 'users', column: 'email' })]), 19 email: schema.string([
20 rules.email(),
21 rules.unique({ table: 'users', column: 'email' }),
22 ]),
17 password: schema.string([rules.minLength(8)]), 23 password: schema.string([rules.minLength(8)]),
18}) 24});
19 25
20const franzImportSchema = schema.create({ 26const franzImportSchema = schema.create({
21 email: schema.string([rules.email(), rules.unique({ table: 'users', column: 'email' })]), 27 email: schema.string([
28 rules.email(),
29 rules.unique({ table: 'users', column: 'email' }),
30 ]),
22 password: schema.string([rules.minLength(8)]), 31 password: schema.string([rules.minLength(8)]),
23}) 32});
24 33
25// // TODO: This whole controller needs to be changed such that it can support importing from both Franz and Ferdi 34// // TODO: This whole controller needs to be changed such that it can support importing from both Franz and Ferdi
26// eslint-disable-next-line @typescript-eslint/no-explicit-any 35// eslint-disable-next-line @typescript-eslint/no-explicit-any
27const franzRequest = (route: any, method: any, auth: any) => 36const franzRequest = (route: any, method: any, auth: any) =>
28 new Promise((resolve, reject) => { 37 new Promise((resolve, reject) => {
29 const base = 'https://api.franzinfra.com/v1/' 38 const base = 'https://api.franzinfra.com/v1/';
30 const user = 39 const user =
31 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36' 40 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36';
32 41
33 try { 42 try {
34 fetch(base + route, { 43 fetch(base + route, {
35 method, 44 method,
36 headers: { 45 headers: {
37 'Authorization': `Bearer ${auth}`, 46 Authorization: `Bearer ${auth}`,
38 'User-Agent': user, 47 'User-Agent': user,
39 }, 48 },
40 }) 49 })
41 .then((data) => data.json()) 50 .then(data => data.json())
42 .then((json) => resolve(json)) 51 .then(json => resolve(json));
43 } catch { 52 } catch {
44 reject() 53 reject();
45 } 54 }
46 }) 55 });
47 56
48export default class UsersController { 57export default class UsersController {
49 // Register a new user 58 // Register a new user
@@ -52,44 +61,44 @@ export default class UsersController {
52 return response.status(401).send({ 61 return response.status(401).send({
53 message: 'Registration is disabled on this server', 62 message: 'Registration is disabled on this server',
54 status: 401, 63 status: 401,
55 }) 64 });
56 } 65 }
57 66
58 // Validate user input 67 // Validate user input
59 let data 68 let data;
60 try { 69 try {
61 data = await request.validate({ schema: newPostSchema }) 70 data = await request.validate({ schema: newPostSchema });
62 } catch (error) { 71 } catch (error) {
63 return response.status(401).send({ 72 return response.status(401).send({
64 message: 'Invalid POST arguments', 73 message: 'Invalid POST arguments',
65 messages: error.messages, 74 messages: error.messages,
66 status: 401, 75 status: 401,
67 }) 76 });
68 } 77 }
69 78
70 // Create user in DB 79 // Create user in DB
71 let user 80 let user;
72 try { 81 try {
73 user = await User.create({ 82 user = await User.create({
74 email: data.email, 83 email: data.email,
75 password: data.password, 84 password: data.password,
76 username: data.firstname, 85 username: data.firstname,
77 lastname: data.lastname, 86 lastname: data.lastname,
78 }) 87 });
79 } catch { 88 } catch {
80 return response.status(401).send({ 89 return response.status(401).send({
81 message: 'E-Mail address already in use', 90 message: 'E-Mail address already in use',
82 status: 401, 91 status: 401,
83 }) 92 });
84 } 93 }
85 94
86 // Generate new auth token 95 // Generate new auth token
87 const token = await auth.use('jwt').login(user, { payload: {} }) 96 const token = await auth.use('jwt').login(user, { payload: {} });
88 97
89 return response.send({ 98 return response.send({
90 message: 'Successfully created account', 99 message: 'Successfully created account',
91 token: token.accessToken, 100 token: token.accessToken,
92 }) 101 });
93 } 102 }
94 103
95 // Login using an existing user 104 // Login using an existing user
@@ -98,28 +107,30 @@ export default class UsersController {
98 return response.status(401).send({ 107 return response.status(401).send({
99 message: 'Please provide authorization', 108 message: 'Please provide authorization',
100 status: 401, 109 status: 401,
101 }) 110 });
102 } 111 }
103 112
104 // Get auth data from auth token 113 // Get auth data from auth token
105 const authHeader = atob(request.header('Authorization')!.replace('Basic ', '')).split(':') 114 const authHeader = atob(
115 request.header('Authorization')!.replace('Basic ', ''),
116 ).split(':');
106 117
107 // Check if user with email exists 118 // Check if user with email exists
108 const user = await User.query().where('email', authHeader[0]).first() 119 const user = await User.query().where('email', authHeader[0]).first();
109 if (!user?.email) { 120 if (!user?.email) {
110 return response.status(401).send({ 121 return response.status(401).send({
111 message: 'User credentials not valid', 122 message: 'User credentials not valid',
112 code: 'invalid-credentials', 123 code: 'invalid-credentials',
113 status: 401, 124 status: 401,
114 }) 125 });
115 } 126 }
116 127
117 // Verify password 128 // Verify password
118 let isMatchedPassword = false 129 let isMatchedPassword = false;
119 try { 130 try {
120 isMatchedPassword = await handleVerifyAndReHash(user, authHeader[1]) 131 isMatchedPassword = await handleVerifyAndReHash(user, authHeader[1]);
121 } catch (error) { 132 } catch (error) {
122 return response.internalServerError({ message: error.message }) 133 return response.internalServerError({ message: error.message });
123 } 134 }
124 135
125 if (!isMatchedPassword) { 136 if (!isMatchedPassword) {
@@ -127,28 +138,31 @@ export default class UsersController {
127 message: 'User credentials not valid', 138 message: 'User credentials not valid',
128 code: 'invalid-credentials', 139 code: 'invalid-credentials',
129 status: 401, 140 status: 401,
130 }) 141 });
131 } 142 }
132 143
133 // Generate token 144 // Generate token
134 const token = await auth.use('jwt').login(user, { payload: {} }) 145 const token = await auth.use('jwt').login(user, { payload: {} });
135 146
136 return response.send({ 147 return response.send({
137 message: 'Successfully logged in', 148 message: 'Successfully logged in',
138 token: token.accessToken, 149 token: token.accessToken,
139 }) 150 });
140 } 151 }
141 152
142 // Return information about the current user 153 // Return information about the current user
143 public async me({ request, response, auth }: HttpContext) { 154 public async me({ request, response, auth }: HttpContext) {
144 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 155 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
145 const user = auth.user ?? request.user 156 const user = auth.user ?? request.user;
146 157
147 if (!user) { 158 if (!user) {
148 return response.send('Missing or invalid api token') 159 return response.send('Missing or invalid api token');
149 } 160 }
150 161
151 const settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings 162 const settings =
163 typeof user.settings === 'string'
164 ? JSON.parse(user.settings)
165 : user.settings;
152 166
153 return response.send({ 167 return response.send({
154 accountType: 'individual', 168 accountType: 'individual',
@@ -164,29 +178,29 @@ export default class UsersController {
164 lastname: user.lastname, 178 lastname: user.lastname,
165 locale: 'en-US', 179 locale: 'en-US',
166 ...settings, 180 ...settings,
167 }) 181 });
168 } 182 }
169 183
170 public async updateMe({ request, response, auth }: HttpContext) { 184 public async updateMe({ request, response, auth }: HttpContext) {
171 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 185 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
172 const user = auth.user ?? request.user 186 const user = auth.user ?? request.user;
173 187
174 if (!user) { 188 if (!user) {
175 return response.send('Missing or invalid api token') 189 return response.send('Missing or invalid api token');
176 } 190 }
177 191
178 let settings = user.settings || {} 192 let settings = user.settings || {};
179 if (typeof settings === 'string') { 193 if (typeof settings === 'string') {
180 settings = JSON.parse(settings) 194 settings = JSON.parse(settings);
181 } 195 }
182 196
183 const newSettings = { 197 const newSettings = {
184 ...settings, 198 ...settings,
185 ...request.all(), 199 ...request.all(),
186 } 200 };
187 201
188 user.settings = JSON.stringify(newSettings) 202 user.settings = JSON.stringify(newSettings);
189 await user.save() 203 await user.save();
190 204
191 return response.send({ 205 return response.send({
192 data: { 206 data: {
@@ -205,22 +219,22 @@ export default class UsersController {
205 ...newSettings, 219 ...newSettings,
206 }, 220 },
207 status: ['data-updated'], 221 status: ['data-updated'],
208 }) 222 });
209 } 223 }
210 224
211 public async newToken({ request, response, auth }: HttpContext) { 225 public async newToken({ request, response, auth }: HttpContext) {
212 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 226 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
213 const user = auth.user ?? request.user 227 const user = auth.user ?? request.user;
214 228
215 if (!user) { 229 if (!user) {
216 return response.send('Missing or invalid api token') 230 return response.send('Missing or invalid api token');
217 } 231 }
218 232
219 const token = await auth.use('jwt').generate(user, { payload: {} }) 233 const token = await auth.use('jwt').generate(user, { payload: {} });
220 234
221 return response.send({ 235 return response.send({
222 token: token.accessToken, 236 token: token.accessToken,
223 }) 237 });
224 } 238 }
225 239
226 public async import({ request, response, view }: HttpContext) { 240 public async import({ request, response, view }: HttpContext) {
@@ -228,114 +242,117 @@ export default class UsersController {
228 return response.status(401).send({ 242 return response.status(401).send({
229 message: 'Registration is disabled on this server', 243 message: 'Registration is disabled on this server',
230 status: 401, 244 status: 401,
231 }) 245 });
232 } 246 }
233 247
234 if (connectWithFranz === 'false') { 248 if (connectWithFranz === 'false') {
235 return response.send( 249 return response.send(
236 'We could not import your Franz account data.\n\nIf you are the server owner, please set CONNECT_WITH_FRANZ to true to enable account imports.' 250 'We could not import your Franz account data.\n\nIf you are the server owner, please set CONNECT_WITH_FRANZ to true to enable account imports.',
237 ) 251 );
238 } 252 }
239 253
240 // Validate user input 254 // Validate user input
241 let data 255 let data;
242 try { 256 try {
243 data = await request.validate({ schema: franzImportSchema }) 257 data = await request.validate({ schema: franzImportSchema });
244 } catch (error) { 258 } catch (error) {
245 return view.render('others.message', { 259 return view.render('others.message', {
246 heading: 'Error while importing', 260 heading: 'Error while importing',
247 text: error.messages, 261 text: error.messages,
248 }) 262 });
249 } 263 }
250 264
251 const { email, password } = data 265 const { email, password } = data;
252 266
253 const hashedPassword = crypto.createHash('sha256').update(password).digest('base64') 267 const hashedPassword = crypto
268 .createHash('sha256')
269 .update(password)
270 .digest('base64');
254 271
255 const base = 'https://api.franzinfra.com/v1/' 272 const base = 'https://api.franzinfra.com/v1/';
256 const userAgent = 273 const userAgent =
257 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36' 274 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36';
258 275
259 // Try to get an authentication token 276 // Try to get an authentication token
260 let token 277 let token;
261 try { 278 try {
262 const basicToken = btoa(`${email}:${hashedPassword}`) 279 const basicToken = btoa(`${email}:${hashedPassword}`);
263 const loginBody = { 280 const loginBody = {
264 isZendeskLogin: false, 281 isZendeskLogin: false,
265 } 282 };
266 283
267 const rawResponse = await fetch(`${base}auth/login`, { 284 const rawResponse = await fetch(`${base}auth/login`, {
268 method: 'POST', 285 method: 'POST',
269 body: JSON.stringify(loginBody), 286 body: JSON.stringify(loginBody),
270 headers: { 287 headers: {
271 'Authorization': `Basic ${basicToken}`, 288 Authorization: `Basic ${basicToken}`,
272 'User-Agent': userAgent, 289 'User-Agent': userAgent,
273 'Content-Type': 'application/json', 290 'Content-Type': 'application/json',
274 'accept': '*/*', 291 accept: '*/*',
275 'x-franz-source': 'Web', 292 'x-franz-source': 'Web',
276 }, 293 },
277 }) 294 });
278 const content = await rawResponse.json() 295 const content = await rawResponse.json();
279 296
280 if (!content.message || content.message !== 'Successfully logged in') { 297 if (!content.message || content.message !== 'Successfully logged in') {
281 const errorMessage = 298 const errorMessage =
282 'Could not login into Franz with your supplied credentials. Please check and try again' 299 'Could not login into Franz with your supplied credentials. Please check and try again';
283 return response.status(401).send(errorMessage) 300 return response.status(401).send(errorMessage);
284 } 301 }
285 302
286 token = content.token 303 token = content.token;
287 } catch (error) { 304 } catch (error) {
288 return response.status(401).send({ 305 return response.status(401).send({
289 message: 'Cannot login to Franz', 306 message: 'Cannot login to Franz',
290 error: error, 307 error: error,
291 }) 308 });
292 } 309 }
293 310
294 // Get user information 311 // Get user information
295 // eslint-disable-next-line @typescript-eslint/no-explicit-any 312 // eslint-disable-next-line @typescript-eslint/no-explicit-any
296 let userInf: any = false 313 let userInf: any = false;
297 try { 314 try {
298 userInf = await franzRequest('me', 'GET', token) 315 userInf = await franzRequest('me', 'GET', token);
299 } catch (error) { 316 } catch (error) {
300 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${error}` 317 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${error}`;
301 return response.status(401).send(errorMessage) 318 return response.status(401).send(errorMessage);
302 } 319 }
303 if (!userInf) { 320 if (!userInf) {
304 const errorMessage = 321 const errorMessage =
305 'Could not get your user info from Franz. Please check your credentials or try again later' 322 'Could not get your user info from Franz. Please check your credentials or try again later';
306 return response.status(401).send(errorMessage) 323 return response.status(401).send(errorMessage);
307 } 324 }
308 325
309 // Create user in DB 326 // Create user in DB
310 let user 327 let user;
311 try { 328 try {
312 user = await User.create({ 329 user = await User.create({
313 email: userInf.email, 330 email: userInf.email,
314 password: hashedPassword, 331 password: hashedPassword,
315 username: userInf.firstname, 332 username: userInf.firstname,
316 lastname: userInf.lastname, 333 lastname: userInf.lastname,
317 }) 334 });
318 } catch (error) { 335 } catch (error) {
319 const errorMessage = `Could not create your user in our system.\nError: ${error}` 336 const errorMessage = `Could not create your user in our system.\nError: ${error}`;
320 return response.status(401).send(errorMessage) 337 return response.status(401).send(errorMessage);
321 } 338 }
322 339
323 const serviceIdTranslation = {} 340 const serviceIdTranslation = {};
324 341
325 // Import services 342 // Import services
326 try { 343 try {
327 const services = await franzRequest('me/services', 'GET', token) 344 const services = await franzRequest('me/services', 'GET', token);
328 345
329 // @ts-expect-error 346 // @ts-expect-error
330 for (const service of services) { 347 for (const service of services) {
331 // Get new, unused uuid 348 // Get new, unused uuid
332 let serviceId 349 let serviceId;
333 do { 350 do {
334 serviceId = uuid() 351 serviceId = uuid();
335 } while ( 352 } while (
336 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member 353 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member
337 (await Service.query().where('serviceId', serviceId)).length > 0 354 (await Service.query().where('serviceId', serviceId)).length > 0
338 ) 355 );
339 356
340 // eslint-disable-next-line no-await-in-loop 357 // eslint-disable-next-line no-await-in-loop
341 await Service.create({ 358 await Service.create({
@@ -344,34 +361,34 @@ export default class UsersController {
344 name: service.name, 361 name: service.name,
345 recipeId: service.recipeId, 362 recipeId: service.recipeId,
346 settings: JSON.stringify(service), 363 settings: JSON.stringify(service),
347 }) 364 });
348 365
349 // @ts-expect-error 366 // @ts-expect-error
350 serviceIdTranslation[service.id] = serviceId 367 serviceIdTranslation[service.id] = serviceId;
351 } 368 }
352 } catch (error) { 369 } catch (error) {
353 const errorMessage = `Could not import your services into our system.\nError: ${error}` 370 const errorMessage = `Could not import your services into our system.\nError: ${error}`;
354 return response.status(401).send(errorMessage) 371 return response.status(401).send(errorMessage);
355 } 372 }
356 373
357 // Import workspaces 374 // Import workspaces
358 try { 375 try {
359 const workspaces = await franzRequest('workspace', 'GET', token) 376 const workspaces = await franzRequest('workspace', 'GET', token);
360 377
361 // @ts-expect-error 378 // @ts-expect-error
362 for (const workspace of workspaces) { 379 for (const workspace of workspaces) {
363 let workspaceId 380 let workspaceId;
364 do { 381 do {
365 workspaceId = uuid() 382 workspaceId = uuid();
366 } while ( 383 } while (
367 // eslint-disable-next-line unicorn/no-await-expression-member, no-await-in-loop 384 // eslint-disable-next-line unicorn/no-await-expression-member, no-await-in-loop
368 (await Workspace.query().where('workspaceId', workspaceId)).length > 0 385 (await Workspace.query().where('workspaceId', workspaceId)).length > 0
369 ) 386 );
370 387
371 const services = workspace.services.map( 388 const services = workspace.services.map(
372 // @ts-expect-error 389 // @ts-expect-error
373 (service) => serviceIdTranslation[service] 390 service => serviceIdTranslation[service],
374 ) 391 );
375 392
376 // eslint-disable-next-line no-await-in-loop 393 // eslint-disable-next-line no-await-in-loop
377 await Workspace.create({ 394 await Workspace.create({
@@ -381,15 +398,15 @@ export default class UsersController {
381 order: workspace.order, 398 order: workspace.order,
382 services: JSON.stringify(services), 399 services: JSON.stringify(services),
383 data: JSON.stringify({}), 400 data: JSON.stringify({}),
384 }) 401 });
385 } 402 }
386 } catch (error) { 403 } catch (error) {
387 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}` 404 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}`;
388 return response.status(401).send(errorMessage) 405 return response.status(401).send(errorMessage);
389 } 406 }
390 407
391 return response.send( 408 return response.send(
392 'Your account has been imported. You can now use your Franz/Ferdi account in Ferdium.' 409 'Your account has been imported. You can now use your Franz/Ferdi account in Ferdium.',
393 ) 410 );
394 } 411 }
395} 412}
diff --git a/app/Controllers/Http/WorkspaceController.ts b/app/Controllers/Http/WorkspaceController.ts
index a2bc54e..6cecf69 100644
--- a/app/Controllers/Http/WorkspaceController.ts
+++ b/app/Controllers/Http/WorkspaceController.ts
@@ -1,53 +1,53 @@
1import type { HttpContext } from '@adonisjs/core/http' 1import type { HttpContext } from '@adonisjs/core/http';
2import { validator, schema } from '@adonisjs/validator' 2import { validator, schema } from '@adonisjs/validator';
3import Workspace from '#app/Models/Workspace' 3import Workspace from '#app/Models/Workspace';
4import { v4 as uuid } from 'uuid' 4import { v4 as uuid } from 'uuid';
5 5
6const createSchema = schema.create({ 6const createSchema = schema.create({
7 name: schema.string(), 7 name: schema.string(),
8}) 8});
9 9
10const editSchema = schema.create({ 10const editSchema = schema.create({
11 name: schema.string(), 11 name: schema.string(),
12}) 12});
13 13
14const deleteSchema = schema.create({ 14const deleteSchema = schema.create({
15 id: schema.string(), 15 id: schema.string(),
16}) 16});
17 17
18export default class WorkspaceController { 18export default class WorkspaceController {
19 // Create a new workspace for user 19 // Create a new workspace for user
20 public async create({ request, response, auth }: HttpContext) { 20 public async create({ request, response, auth }: HttpContext) {
21 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 21 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
22 const user = auth.user ?? request.user 22 const user = auth.user ?? request.user;
23 23
24 if (!user) { 24 if (!user) {
25 return response.unauthorized('Missing or invalid api token') 25 return response.unauthorized('Missing or invalid api token');
26 } 26 }
27 27
28 // Validate user input 28 // Validate user input
29 let data 29 let data;
30 try { 30 try {
31 data = await request.validate({ schema: createSchema }) 31 data = await request.validate({ schema: createSchema });
32 } catch (error) { 32 } catch (error) {
33 return response.status(401).send({ 33 return response.status(401).send({
34 message: 'Invalid POST arguments', 34 message: 'Invalid POST arguments',
35 messages: error.messages, 35 messages: error.messages,
36 status: 401, 36 status: 401,
37 }) 37 });
38 } 38 }
39 39
40 // Get new, unused uuid 40 // Get new, unused uuid
41 let workspaceId 41 let workspaceId;
42 do { 42 do {
43 workspaceId = uuid() 43 workspaceId = uuid();
44 } while ( 44 } while (
45 // eslint-disable-next-line unicorn/no-await-expression-member, no-await-in-loop 45 // eslint-disable-next-line unicorn/no-await-expression-member, no-await-in-loop
46 (await Workspace.query().where('workspaceId', workspaceId)).length > 0 46 (await Workspace.query().where('workspaceId', workspaceId)).length > 0
47 ) 47 );
48 48
49 // eslint-disable-next-line unicorn/no-await-expression-member 49 // eslint-disable-next-line unicorn/no-await-expression-member
50 const order = (await user.related('workspaces').query()).length 50 const order = (await user.related('workspaces').query()).length;
51 51
52 await Workspace.create({ 52 await Workspace.create({
53 userId: user.id, 53 userId: user.id,
@@ -56,7 +56,7 @@ export default class WorkspaceController {
56 order, 56 order,
57 services: JSON.stringify([]), 57 services: JSON.stringify([]),
58 data: JSON.stringify(data), 58 data: JSON.stringify(data),
59 }) 59 });
60 60
61 return response.send({ 61 return response.send({
62 userId: user.id, 62 userId: user.id,
@@ -64,30 +64,30 @@ export default class WorkspaceController {
64 id: workspaceId, 64 id: workspaceId,
65 order, 65 order,
66 workspaces: [], 66 workspaces: [],
67 }) 67 });
68 } 68 }
69 69
70 public async edit({ request, response, auth, params }: HttpContext) { 70 public async edit({ request, response, auth, params }: HttpContext) {
71 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 71 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
72 const user = auth.user ?? request.user 72 const user = auth.user ?? request.user;
73 73
74 if (!user) { 74 if (!user) {
75 return response.unauthorized('Missing or invalid api token') 75 return response.unauthorized('Missing or invalid api token');
76 } 76 }
77 77
78 // Validate user input 78 // Validate user input
79 try { 79 try {
80 await request.validate({ schema: editSchema }) 80 await request.validate({ schema: editSchema });
81 } catch (error) { 81 } catch (error) {
82 return response.status(401).send({ 82 return response.status(401).send({
83 message: 'Invalid POST arguments', 83 message: 'Invalid POST arguments',
84 messages: error.messages, 84 messages: error.messages,
85 status: 401, 85 status: 401,
86 }) 86 });
87 } 87 }
88 88
89 const data = request.all() 89 const data = request.all();
90 const { id } = params 90 const { id } = params;
91 91
92 // Update data in database 92 // Update data in database
93 await Workspace.query() 93 await Workspace.query()
@@ -96,13 +96,13 @@ export default class WorkspaceController {
96 .update({ 96 .update({
97 name: data.name, 97 name: data.name,
98 services: JSON.stringify(data.services), 98 services: JSON.stringify(data.services),
99 }) 99 });
100 100
101 // Get updated row 101 // Get updated row
102 const workspace = await Workspace.query() 102 const workspace = await Workspace.query()
103 .where('workspaceId', id) 103 .where('workspaceId', id)
104 .where('userId', user.id) 104 .where('userId', user.id)
105 .firstOrFail() 105 .firstOrFail();
106 106
107 return response.send({ 107 return response.send({
108 id: workspace.workspaceId, 108 id: workspace.workspaceId,
@@ -110,54 +110,57 @@ export default class WorkspaceController {
110 order: workspace.order, 110 order: workspace.order,
111 services: data.services, 111 services: data.services,
112 userId: user.id, 112 userId: user.id,
113 }) 113 });
114 } 114 }
115 115
116 public async delete({ request, response, auth, params }: HttpContext) { 116 public async delete({ request, response, auth, params }: HttpContext) {
117 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 117 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
118 const user = auth.user ?? request.user 118 const user = auth.user ?? request.user;
119 119
120 if (!user) { 120 if (!user) {
121 return response.unauthorized('Missing or invalid api token') 121 return response.unauthorized('Missing or invalid api token');
122 } 122 }
123 123
124 // Validate user input 124 // Validate user input
125 let data 125 let data;
126 try { 126 try {
127 data = await validator.validate({ 127 data = await validator.validate({
128 data: params, 128 data: params,
129 schema: deleteSchema, 129 schema: deleteSchema,
130 }) 130 });
131 } catch (error) { 131 } catch (error) {
132 return response.status(401).send({ 132 return response.status(401).send({
133 message: 'Invalid arguments', 133 message: 'Invalid arguments',
134 messages: error.messages, 134 messages: error.messages,
135 status: 401, 135 status: 401,
136 }) 136 });
137 } 137 }
138 138
139 const { id } = data 139 const { id } = data;
140 140
141 // Update data in database 141 // Update data in database
142 await Workspace.query().where('workspaceId', id).where('userId', user.id).delete() 142 await Workspace.query()
143 .where('workspaceId', id)
144 .where('userId', user.id)
145 .delete();
143 146
144 return response.send({ 147 return response.send({
145 message: 'Successfully deleted workspace', 148 message: 'Successfully deleted workspace',
146 }) 149 });
147 } 150 }
148 151
149 // List all workspaces a user has created 152 // List all workspaces a user has created
150 public async list({ request, response, auth }: HttpContext) { 153 public async list({ request, response, auth }: HttpContext) {
151 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'. 154 // @ts-expect-error Property 'user' does not exist on type 'HttpContextContract'.
152 const user = auth.user ?? request.user 155 const user = auth.user ?? request.user;
153 156
154 if (!user) { 157 if (!user) {
155 return response.unauthorized('Missing or invalid api token') 158 return response.unauthorized('Missing or invalid api token');
156 } 159 }
157 160
158 const workspaces = await user.related('workspaces').query() 161 const workspaces = await user.related('workspaces').query();
159 // Convert to array with all data Franz wants 162 // Convert to array with all data Franz wants
160 let workspacesArray: object[] = [] 163 let workspacesArray: object[] = [];
161 if (workspaces) { 164 if (workspaces) {
162 // eslint-disable-next-line @typescript-eslint/no-explicit-any 165 // eslint-disable-next-line @typescript-eslint/no-explicit-any
163 workspacesArray = workspaces.map((workspace: any) => ({ 166 workspacesArray = workspaces.map((workspace: any) => ({
@@ -169,9 +172,9 @@ export default class WorkspaceController {
169 ? JSON.parse(workspace.services) 172 ? JSON.parse(workspace.services)
170 : workspace.services, 173 : workspace.services,
171 userId: user.id, 174 userId: user.id,
172 })) 175 }));
173 } 176 }
174 177
175 return response.send(workspacesArray) 178 return response.send(workspacesArray);
176 } 179 }
177} 180}