diff options
Diffstat (limited to 'app/Controllers/Http')
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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { app } from '@adonisjs/core/services/app' | 2 | import { app } from '@adonisjs/core/services/app'; |
3 | import path from 'node:path' | 3 | import path from 'node:path'; |
4 | import fs from 'fs-extra' | 4 | import fs from 'fs-extra'; |
5 | 5 | ||
6 | export default class AnnouncementsController { | 6 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | 2 | ||
3 | export default class EmptyController { | 3 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | 2 | ||
3 | export default class FeaturesController { | 3 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { schema, rules, validator } from '@adonisjs/validator' | 2 | import { schema, rules, validator } from '@adonisjs/validator'; |
3 | import crypto from 'node:crypto' | 3 | import crypto from 'node:crypto'; |
4 | 4 | ||
5 | export default class AccountController { | 5 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | 2 | ||
3 | export default class DataController { | 3 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | 2 | ||
3 | export default class DeleteController { | 3 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import 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 |
4 | function deepParseToJSON(obj: any): Record<string, unknown> { | 4 | function 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { schema, rules, validator } from '@adonisjs/validator' | 2 | import { schema, rules, validator } from '@adonisjs/validator'; |
3 | import User from '#app/Models/User' | 3 | import User from '#app/Models/User'; |
4 | 4 | ||
5 | export default class ForgotPasswordController { | 5 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | 2 | ||
3 | export default class LogOutController { | 3 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { schema, rules, validator } from '@adonisjs/validator' | 2 | import { schema, rules, validator } from '@adonisjs/validator'; |
3 | import User from '#app/Models/User' | 3 | import User from '#app/Models/User'; |
4 | import crypto from 'node:crypto' | 4 | import crypto from 'node:crypto'; |
5 | import { handleVerifyAndReHash } from '../../../../helpers/PasswordHash.js' | 5 | import { handleVerifyAndReHash } from '../../../../helpers/PasswordHash.js'; |
6 | 6 | ||
7 | export default class LoginController { | 7 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { schema, rules, validator } from '@adonisjs/validator' | 2 | import { schema, rules, validator } from '@adonisjs/validator'; |
3 | import Token from '#app/Models/Token' | 3 | import Token from '#app/Models/Token'; |
4 | import moment from 'moment' | 4 | import moment from 'moment'; |
5 | import crypto from 'node:crypto' | 5 | import crypto from 'node:crypto'; |
6 | 6 | ||
7 | export default class ResetPasswordController { | 7 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { schema, validator } from '@adonisjs/validator' | 2 | import { schema, validator } from '@adonisjs/validator'; |
3 | import Service from '#app/Models/Service' | 3 | import Service from '#app/Models/Service'; |
4 | import Workspace from '#app/Models/Workspace' | 4 | import Workspace from '#app/Models/Workspace'; |
5 | import { v4 as uuidv4 } from 'uuid' | 5 | import { v4 as uuidv4 } from 'uuid'; |
6 | 6 | ||
7 | const importSchema = schema.create({ | 7 | const 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 | ||
15 | export default class TransferController { | 15 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import fs from 'fs-extra' | 2 | import fs from 'fs-extra'; |
3 | import { app } from '@adonisjs/core/services/app' | 3 | import { app } from '@adonisjs/core/services/app'; |
4 | import path from 'node:path' | 4 | import path from 'node:path'; |
5 | import Recipe from '#app/Models/Recipe' | 5 | import Recipe from '#app/Models/Recipe'; |
6 | import { isCreationEnabled } from '#config/app' | 6 | import { isCreationEnabled } from '#config/app'; |
7 | import { validator, schema, rules } from '@adonisjs/validator' | 7 | import { validator, schema, rules } from '@adonisjs/validator'; |
8 | import targz from 'targz' | 8 | import targz from 'targz'; |
9 | import semver from 'semver' | 9 | import semver from 'semver'; |
10 | import Drive from '@ioc:Adonis/Core/Drive' | 10 | import 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 | ||
23 | const searchSchema = schema.create({ | 23 | const searchSchema = schema.create({ |
24 | needle: schema.string(), | 24 | needle: schema.string(), |
25 | }) | 25 | }); |
26 | 26 | ||
27 | const downloadSchema = schema.create({ | 27 | const 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 | ||
33 | const compress = (src: string, dest: string) => | 33 | const 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 | ||
50 | export default class RecipesController { | 50 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { schema } from '@adonisjs/validator' | 2 | import { schema } from '@adonisjs/validator'; |
3 | import Service from '#app/Models/Service' | 3 | import Service from '#app/Models/Service'; |
4 | import { url } from '#config/app' | 4 | import { url } from '#config/app'; |
5 | import { v4 as uuid } from 'uuid' | 5 | import { v4 as uuid } from 'uuid'; |
6 | import * as fs from 'fs-extra' | 6 | import * as fs from 'fs-extra'; |
7 | import path from 'node:path' | 7 | import path from 'node:path'; |
8 | import { app } from '@adonisjs/core/services/app' | 8 | import { app } from '@adonisjs/core/services/app'; |
9 | import sanitize from 'sanitize-filename' | 9 | import sanitize from 'sanitize-filename'; |
10 | 10 | ||
11 | const createSchema = schema.create({ | 11 | const createSchema = schema.create({ |
12 | name: schema.string(), | 12 | name: schema.string(), |
13 | recipeId: schema.string(), | 13 | recipeId: schema.string(), |
14 | }) | 14 | }); |
15 | 15 | ||
16 | export default class ServiceController { | 16 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { schema, rules } from '@adonisjs/validator' | 2 | import { schema, rules } from '@adonisjs/validator'; |
3 | import User from '#app/Models/User' | 3 | import User from '#app/Models/User'; |
4 | import { connectWithFranz, isRegistrationEnabled } from '../../../config/app.js' | 4 | import { |
5 | import crypto from 'node:crypto' | 5 | connectWithFranz, |
6 | import { v4 as uuid } from 'uuid' | 6 | isRegistrationEnabled, |
7 | import Workspace from '#app/Models/Workspace' | 7 | } from '../../../config/app.js'; |
8 | import Service from '#app/Models/Service' | 8 | import crypto from 'node:crypto'; |
9 | import { v4 as uuid } from 'uuid'; | ||
10 | import Workspace from '#app/Models/Workspace'; | ||
11 | import 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 |
11 | import { handleVerifyAndReHash } from '../../../helpers/PasswordHash.js' | 14 | import { handleVerifyAndReHash } from '../../../helpers/PasswordHash.js'; |
12 | 15 | ||
13 | const newPostSchema = schema.create({ | 16 | const 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 | ||
20 | const franzImportSchema = schema.create({ | 26 | const 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 |
27 | const franzRequest = (route: any, method: any, auth: any) => | 36 | const 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 | ||
48 | export default class UsersController { | 57 | export 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 @@ | |||
1 | import type { HttpContext } from '@adonisjs/core/http' | 1 | import type { HttpContext } from '@adonisjs/core/http'; |
2 | import { validator, schema } from '@adonisjs/validator' | 2 | import { validator, schema } from '@adonisjs/validator'; |
3 | import Workspace from '#app/Models/Workspace' | 3 | import Workspace from '#app/Models/Workspace'; |
4 | import { v4 as uuid } from 'uuid' | 4 | import { v4 as uuid } from 'uuid'; |
5 | 5 | ||
6 | const createSchema = schema.create({ | 6 | const createSchema = schema.create({ |
7 | name: schema.string(), | 7 | name: schema.string(), |
8 | }) | 8 | }); |
9 | 9 | ||
10 | const editSchema = schema.create({ | 10 | const editSchema = schema.create({ |
11 | name: schema.string(), | 11 | name: schema.string(), |
12 | }) | 12 | }); |
13 | 13 | ||
14 | const deleteSchema = schema.create({ | 14 | const deleteSchema = schema.create({ |
15 | id: schema.string(), | 15 | id: schema.string(), |
16 | }) | 16 | }); |
17 | 17 | ||
18 | export default class WorkspaceController { | 18 | export 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 | } |