aboutsummaryrefslogtreecommitdiffstats
path: root/app/Controllers/Http/Dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'app/Controllers/Http/Dashboard')
-rw-r--r--app/Controllers/Http/Dashboard/AccountController.ts80
-rw-r--r--app/Controllers/Http/Dashboard/DataController.ts24
-rw-r--r--app/Controllers/Http/Dashboard/DeleteController.ts20
-rw-r--r--app/Controllers/Http/Dashboard/ExportController.ts56
-rw-r--r--app/Controllers/Http/Dashboard/ForgotPasswordController.ts41
-rw-r--r--app/Controllers/Http/Dashboard/LogOutController.ts12
-rw-r--r--app/Controllers/Http/Dashboard/LoginController.ts81
-rw-r--r--app/Controllers/Http/Dashboard/ResetPasswordController.ts85
-rw-r--r--app/Controllers/Http/Dashboard/TransferController.ts128
9 files changed, 527 insertions, 0 deletions
diff --git a/app/Controllers/Http/Dashboard/AccountController.ts b/app/Controllers/Http/Dashboard/AccountController.ts
new file mode 100644
index 0000000..3c4e919
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/AccountController.ts
@@ -0,0 +1,80 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2import { schema, rules, validator } from '@ioc:Adonis/Core/Validator';
3import crypto from 'node:crypto';
4
5export default class AccountController {
6 /**
7 * Shows the user account page
8 */
9 public async show({ auth, view }: HttpContextContract) {
10 return view.render('dashboard/account', {
11 username: auth.user?.username,
12 email: auth.user?.email,
13 lastname: auth.user?.lastname,
14 });
15 }
16
17 /**
18 * Stores user account data
19 */
20 public async store({
21 auth,
22 request,
23 response,
24 session,
25 view,
26 }: HttpContextContract) {
27 try {
28 await validator.validate({
29 schema: schema.create({
30 username: schema.string([
31 rules.required(),
32 rules.unique({
33 table: 'users',
34 column: 'username',
35 caseInsensitive: true,
36 whereNot: { id: auth.user?.id },
37 }),
38 ]),
39 email: schema.string([
40 rules.required(),
41 rules.unique({
42 table: 'users',
43 column: 'email',
44 caseInsensitive: true,
45 whereNot: { id: auth.user?.id },
46 }),
47 ]),
48 lastname: schema.string([rules.required()]),
49 }),
50 data: request.only(['username', 'email', 'lastname']),
51 });
52 } catch (error) {
53 session.flash(error.messages);
54 return response.redirect('/user/account');
55 }
56
57 // Update user account
58 const { user } = auth;
59 if (user) {
60 user.username = request.input('username');
61 user.lastname = request.input('lastname');
62 user.email = request.input('email');
63 if (request.input('password')) {
64 const hashedPassword = crypto
65 .createHash('sha256')
66 .update(request.input('password'))
67 .digest('base64');
68 user.password = hashedPassword;
69 }
70 await user.save();
71 }
72
73 return view.render('dashboard/account', {
74 username: user?.username,
75 lastname: user?.lastname,
76 email: user?.email,
77 success: user !== undefined,
78 });
79 }
80}
diff --git a/app/Controllers/Http/Dashboard/DataController.ts b/app/Controllers/Http/Dashboard/DataController.ts
new file mode 100644
index 0000000..f77702f
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/DataController.ts
@@ -0,0 +1,24 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2
3export default class DataController {
4 /**
5 * Display the data page
6 */
7 public async show({ view, auth }: HttpContextContract) {
8 const { user } = auth;
9
10 const services = await user?.related('services').query();
11 const workspaces = await user?.related('workspaces').query();
12
13 return view.render('dashboard/data', {
14 username: user?.username,
15 lastname: user?.lastname,
16 mail: user?.email,
17 created: user?.created_at.toFormat('yyyy-MM-dd HH:mm:ss'),
18 updated: user?.updated_at.toFormat('yyyy-MM-dd HH:mm:ss'),
19 stringify: JSON.stringify,
20 services,
21 workspaces,
22 });
23 }
24}
diff --git a/app/Controllers/Http/Dashboard/DeleteController.ts b/app/Controllers/Http/Dashboard/DeleteController.ts
new file mode 100644
index 0000000..ef8188c
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/DeleteController.ts
@@ -0,0 +1,20 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2
3export default class DeleteController {
4 /**
5 * Display the delete page
6 */
7 public async show({ view }: HttpContextContract) {
8 return view.render('dashboard/delete');
9 }
10
11 /**
12 * Delete user and session
13 */
14 public async delete({ auth, response }: HttpContextContract) {
15 auth.user?.delete();
16 auth.use('web').logout();
17
18 return response.redirect('/user/login');
19 }
20}
diff --git a/app/Controllers/Http/Dashboard/ExportController.ts b/app/Controllers/Http/Dashboard/ExportController.ts
new file mode 100644
index 0000000..7155eab
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/ExportController.ts
@@ -0,0 +1,56 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2
3// eslint-disable-next-line @typescript-eslint/no-explicit-any
4function deepParseToJSON(obj: any): Record<string, unknown> {
5 if (typeof obj !== 'object' || obj === null) {
6 try {
7 // Try to parse the object as JSON
8 return JSON.parse(obj) as Record<string, unknown>;
9 } catch {
10 // If parsing fails, return the original value
11 return obj;
12 }
13 }
14
15 // If obj is an object, recursively parse its keys
16 if (Array.isArray(obj)) {
17 // If obj is an array, recursively parse each element
18 return obj.map(item => deepParseToJSON(item)) as unknown as Record<
19 string,
20 unknown
21 >;
22 } else {
23 // If obj is an object, recursively parse its keys
24 const parsedObj: Record<string, unknown> = {};
25 for (const key in obj) {
26 if (obj.hasOwnProperty(key)) {
27 parsedObj[key] = deepParseToJSON(obj[key]);
28 }
29 }
30 return parsedObj;
31 }
32}
33
34export default class ExportController {
35 /**
36 * Display the export page
37 */
38 public async show({ auth, response }: HttpContextContract) {
39 const user = auth.user!;
40 const services = await user.related('services').query();
41 const workspaces = await user.related('workspaces').query();
42
43 const exportData = {
44 username: user.username,
45 lastname: user.lastname,
46 mail: user.email,
47 services: deepParseToJSON(JSON.parse(JSON.stringify(services))),
48 workspaces: deepParseToJSON(JSON.parse(JSON.stringify(workspaces))),
49 };
50
51 return response
52 .header('Content-Type', 'application/force-download')
53 .header('Content-disposition', 'attachment; filename=export.ferdium-data')
54 .send(exportData);
55 }
56}
diff --git a/app/Controllers/Http/Dashboard/ForgotPasswordController.ts b/app/Controllers/Http/Dashboard/ForgotPasswordController.ts
new file mode 100644
index 0000000..da05bbd
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/ForgotPasswordController.ts
@@ -0,0 +1,41 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2import { schema, rules, validator } from '@ioc:Adonis/Core/Validator';
3import User from 'App/Models/User';
4
5export default class ForgotPasswordController {
6 /**
7 * Display the forgot password form
8 */
9 public async show({ view }: HttpContextContract) {
10 return view.render('dashboard/forgotPassword');
11 }
12
13 /**
14 * Send forget password email to user
15 */
16 public async forgotPassword({ view, request }: HttpContextContract) {
17 try {
18 await validator.validate({
19 schema: schema.create({
20 mail: schema.string([rules.email(), rules.required()]),
21 }),
22 data: request.only(['mail']),
23 });
24 } catch {
25 return view.render('others/message', {
26 heading: 'Cannot reset your password',
27 text: 'Please enter a valid email address',
28 });
29 }
30
31 try {
32 const user = await User.findByOrFail('email', request.input('mail'));
33 await user.forgotPassword();
34 } catch {}
35
36 return view.render('others/message', {
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.',
39 });
40 }
41}
diff --git a/app/Controllers/Http/Dashboard/LogOutController.ts b/app/Controllers/Http/Dashboard/LogOutController.ts
new file mode 100644
index 0000000..41cbd31
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/LogOutController.ts
@@ -0,0 +1,12 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2
3export default class LogOutController {
4 /**
5 * Login a user
6 */
7 public async logout({ auth, response }: HttpContextContract) {
8 auth.logout();
9
10 return response.redirect('/user/login');
11 }
12}
diff --git a/app/Controllers/Http/Dashboard/LoginController.ts b/app/Controllers/Http/Dashboard/LoginController.ts
new file mode 100644
index 0000000..ffb9eeb
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/LoginController.ts
@@ -0,0 +1,81 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2import { schema, rules, validator } from '@ioc:Adonis/Core/Validator';
3import User from 'App/Models/User';
4import crypto from 'node:crypto';
5import { handleVerifyAndReHash } from '../../../../helpers/PasswordHash';
6
7export default class LoginController {
8 /**
9 * Display the login form
10 */
11 public async show({ view }: HttpContextContract) {
12 return view.render('dashboard/login');
13 }
14
15 /**
16 * Login a user
17 */
18 public async login({
19 request,
20 response,
21 auth,
22 session,
23 }: HttpContextContract) {
24 try {
25 await validator.validate({
26 schema: schema.create({
27 mail: schema.string([rules.email(), rules.required()]),
28 password: schema.string([rules.required()]),
29 }),
30 data: request.only(['mail', 'password']),
31 });
32 } catch {
33 session.flash({
34 type: 'danger',
35 message: 'Invalid mail or password',
36 });
37 session.flashExcept(['password']);
38
39 return response.redirect('/user/login');
40 }
41
42 try {
43 const { mail, password } = request.all();
44
45 // Check if user with email exists
46 const user = await User.query().where('email', mail).first();
47 if (!user?.email) {
48 throw new Error('User credentials not valid (Invalid email)');
49 }
50
51 const hashedPassword = crypto
52 .createHash('sha256')
53 .update(password)
54 .digest('base64');
55
56 // Verify password
57 let isMatchedPassword = false;
58 try {
59 isMatchedPassword = await handleVerifyAndReHash(user, hashedPassword);
60 } catch (error) {
61 return response.internalServerError({ message: error.message });
62 }
63
64 if (!isMatchedPassword) {
65 throw new Error('User credentials not valid (Invalid password)');
66 }
67
68 await auth.use('web').login(user);
69
70 return response.redirect('/user/account');
71 } catch {
72 session.flash({
73 type: 'danger',
74 message: 'Invalid mail or password',
75 });
76 session.flashExcept(['password']);
77
78 return response.redirect('/user/login');
79 }
80 }
81}
diff --git a/app/Controllers/Http/Dashboard/ResetPasswordController.ts b/app/Controllers/Http/Dashboard/ResetPasswordController.ts
new file mode 100644
index 0000000..0b9053f
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/ResetPasswordController.ts
@@ -0,0 +1,85 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2import { schema, rules, validator } from '@ioc:Adonis/Core/Validator';
3import Token from 'App/Models/Token';
4import moment from 'moment';
5import crypto from 'node:crypto';
6
7export default class ResetPasswordController {
8 /**
9 * Display the reset password form
10 */
11 public async show({ view, request }: HttpContextContract) {
12 const { token } = request.qs();
13
14 if (token) {
15 return view.render('dashboard/resetPassword', { token });
16 }
17
18 return view.render('others/message', {
19 heading: 'Invalid token',
20 text: 'Please make sure you are using a valid and recent link to reset your password.',
21 });
22 }
23
24 /**
25 * Resets user password
26 */
27 public async resetPassword({
28 response,
29 request,
30 session,
31 view,
32 }: HttpContextContract) {
33 try {
34 await validator.validate({
35 schema: schema.create({
36 password: schema.string([rules.required(), rules.confirmed()]),
37 token: schema.string([rules.required()]),
38 }),
39 data: request.only(['password', 'password_confirmation', 'token']),
40 });
41 } catch {
42 session.flash({
43 type: 'danger',
44 message: 'Passwords do not match',
45 });
46
47 return response.redirect(`/user/reset?token=${request.input('token')}`);
48 }
49
50 const tokenRow = await Token.query()
51 .preload('user')
52 .where('token', request.input('token'))
53 .where('type', 'forgot_password')
54 .where('is_revoked', false)
55 .where(
56 'updated_at',
57 '>=',
58 moment().subtract(24, 'hours').format('YYYY-MM-DD HH:mm:ss'),
59 )
60 .first();
61
62 if (!tokenRow) {
63 return view.render('others/message', {
64 heading: 'Cannot reset your password',
65 text: 'Please make sure you are using a valid and recent link to reset your password and that your passwords entered match.',
66 });
67 }
68
69 // Update user password
70 const hashedPassword = crypto
71 .createHash('sha256')
72 .update(request.input('password'))
73 .digest('base64');
74 tokenRow.user.password = hashedPassword;
75 await tokenRow.user.save();
76
77 // Delete token to prevent it from being used again
78 await tokenRow.delete();
79
80 return view.render('others/message', {
81 heading: 'Reset password',
82 text: 'Successfully reset your password. You can now login to your account using your new password.',
83 });
84 }
85}
diff --git a/app/Controllers/Http/Dashboard/TransferController.ts b/app/Controllers/Http/Dashboard/TransferController.ts
new file mode 100644
index 0000000..a005c1b
--- /dev/null
+++ b/app/Controllers/Http/Dashboard/TransferController.ts
@@ -0,0 +1,128 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2import { schema, validator } from '@ioc:Adonis/Core/Validator';
3import Service from 'App/Models/Service';
4import Workspace from 'App/Models/Workspace';
5import { v4 as uuidv4 } from 'uuid';
6
7const importSchema = schema.create({
8 username: schema.string(),
9 lastname: schema.string(),
10 mail: schema.string(),
11 services: schema.array().anyMembers(),
12 workspaces: schema.array().anyMembers(),
13});
14
15export default class TransferController {
16 /**
17 * Display the transfer page
18 */
19 public async show({ view }: HttpContextContract) {
20 return view.render('dashboard/transfer');
21 }
22
23 public async import({
24 auth,
25 request,
26 response,
27 session,
28 view,
29 }: HttpContextContract) {
30 let file;
31 try {
32 file = await validator.validate({
33 schema: importSchema,
34 data: JSON.parse(request.body().file),
35 });
36 } catch {
37 session.flash({
38 message: 'Invalid Ferdium account file',
39 });
40
41 return response.redirect('/user/transfer');
42 }
43
44 if (!file?.services || !file.workspaces) {
45 session.flash({
46 type: 'danger',
47 message: 'Invalid Ferdium account file (2)',
48 });
49 return response.redirect('/user/transfer');
50 }
51
52 const serviceIdTranslation = {};
53
54 // Import services
55 try {
56 for (const service of file.services) {
57 // Get new, unused uuid
58 let serviceId;
59 do {
60 serviceId = uuidv4();
61 } while (
62 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member
63 (await Service.query().where('serviceId', serviceId)).length > 0
64 );
65
66 // eslint-disable-next-line no-await-in-loop
67 await Service.create({
68 userId: auth.user?.id,
69 serviceId,
70 name: service.name,
71 recipeId: service.recipe_id,
72 settings: JSON.stringify(service.settings),
73 });
74
75 // @ts-expect-error Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{}'
76 serviceIdTranslation[service.service_id] = serviceId;
77 }
78 } catch (error) {
79 // eslint-disable-next-line no-console
80 console.log(error);
81 const errorMessage = `Could not import your services into our system.\nError: ${error}`;
82 return view.render('others/message', {
83 heading: 'Error while importing',
84 text: errorMessage,
85 });
86 }
87
88 // Import workspaces
89 try {
90 for (const workspace of file.workspaces) {
91 let workspaceId;
92
93 do {
94 workspaceId = uuidv4();
95 } while (
96 // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member
97 (await Workspace.query().where('workspaceId', workspaceId)).length > 0
98 );
99
100 const services = workspace.services.map(
101 // @ts-expect-error Parameter 'service' implicitly has an 'any' type.
102 service => serviceIdTranslation[service],
103 );
104
105 // eslint-disable-next-line no-await-in-loop
106 await Workspace.create({
107 userId: auth.user?.id,
108 workspaceId,
109 name: workspace.name,
110 order: workspace.order,
111 services: JSON.stringify(services),
112 data: JSON.stringify(workspace.data),
113 });
114 }
115 } catch (error) {
116 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}`;
117 return view.render('others/message', {
118 heading: 'Error while importing',
119 text: errorMessage,
120 });
121 }
122
123 return view.render('others/message', {
124 heading: 'Successfully imported',
125 text: 'Your account has been imported, you can now login as usual!',
126 });
127 }
128}