From e503468660a13760010a94ecda5f0625c6f47f87 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 13 Oct 2023 14:12:03 +0200 Subject: Server re-build with latest AdonisJS framework & Typescript (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: setup first basis structure * chore: ensure styling is loaded correctly * chore: comply to new routing syntax by replace . with / in routes/resource locations * chore: add login controller * chore: correctly use views with slash instead of dot * chore: working login + tests * chore: clean up tests * chore: add password-forgot endpoint and matching test * chore: add delete page test * chore: add logout test * chore: add reset-password route and tests * chore: remove obsolete comment * chore: add account-page and tests * chore: add data page & first step of the test * chore: add transfer/import data feature and tests * chore: add export and basic test * chore: add all static api routes with tests * Regenerate 'pnpm-lock.json' and fix bad merge conflict WIP: - Tests have been commented out since they dont work - Server doesn't start * easier dev and test runs * - remove --require-pragma from reformat-files so formatting works properly - run pnpm reformat-files over codebase - remove .json files from .eslintignore - add invalid.json file to .eslintignore - configure prettier properly in eslint config - add type jsdoc to prettier config - run adonis generate:manifest command to regenerate ace-manifest.json - specify volta in package.json - introduce typecheck npm script - remove unused .mjs extension from npm scripts - install missing type definition dependencies - add pnpm.allowedDeprecatedVersions to package.json - fix invalid extends in tsconfig.json causing TS issues throughout codebase - remove @ts-ignore throughout codebase which is not relevant anymore - enable some of the tsconfig options - remove outdated eslint-disable from codebase - change deprecated faker.company.companyName() to faker.company.name() - fix TS issues inside transfer.spec.ts * - update to latest node and pnpm versions - upgrade all non-major dependencies to latest - install missing @types/luxon dependency - add cuid to pnpm.allowedDeprecatedVersions - add esModuleInterop config option to tsconfig - migrate more deprecated faker methods to new ones - add more temporary ts-ignore to code * - update eslint config - remove trailingComma: all since default in prettier v3 - add typecheck command to prepare-code npm script - upgrade various dependencies to latest major version - update tsconfig to include only useful config options - disable some lint issues and fix others * - add test command to prepare-code - disable strictPropertyInitialization flag in tsconfig which creates issues with adonis models - update precommit hook to excute pnpm prepare-code - remove ts-ignore statements from all models * fix node and pnpm dependency update * add cross env (so that we can develop on windows) * add signup endpoint (TODO: JWT auth) * Add login endpoint * Add me and updateMe endpoints * Add service endpoint * refactor: change endpoints to use jwt * add recipes endpoint * add workspaces endpoint * fix web controllors for login and post import * Update node deps * Change auth middleware (for web) and exempt api from CSRF * Add import endpoint (franz import) * Fix export/import logic * Fix service and workspace data in user/data * Fix partial lint * chore: workaround lint issues * fix: migration naming had two . * Sync back node with recipes repo * Temporarily ignore typescript * Fix adonisrc to handle public folder static assets * Fix issue with production database * add Legacy Password Provider * Fix lint errors * Fix issue on login errors frontend * add Legacy Password Provider * Fix issue with customIcons * Fix issue with auth tokens * Update 'node' to '18.18.0' * make docker work * improve docker entrypoint (test api performance) * Add migration database script * NODE_ENV on recipes * prefer @ts-expect-error over @ts-ignore * small fixes * Update 'pnpm' to '8.7.6' * fix error catch * Automatically generate JWT Public and Private keys * Use custom Adonis5-jwt * Update code to use secret (old way, no breaking changes) * Normalize appKey * Trick to make JWT tokens on client work with new version * Fix error with new JWT logic * Change migration and how we store JWT * Fix 500 response code (needs to be 401) * Improve logic and fix bugs * Fix build and entrypoint logic * Catch error if appKey changes * Add newToken logic * Fix lint (ignore any errors) * Add build for PRs * pnpm reformat-files result * Fix some tests * Fix reset password not working (test failing) * Restore csrfTokens (disabled by accident) * Fix pnpm start command with .env * Disable failing tests on the transfer endpoint (TODO) * Add tests to PR build * Fix build * Remove unnecessary assertStatus * Add typecheck * hash password on UserFactory (fix build) * Add JWT_USE_PEM true by default (increase security) * fix name of github action --------- Co-authored-by: Vijay A Co-authored-by: Balaji Vijayakumar Co-authored-by: MCMXC <16797721+mcmxcdev@users.noreply.github.com> Co-authored-by: André Oliveira --- app/Controllers/Http/DashboardController.ts | 327 ++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 app/Controllers/Http/DashboardController.ts (limited to 'app/Controllers/Http/DashboardController.ts') diff --git a/app/Controllers/Http/DashboardController.ts b/app/Controllers/Http/DashboardController.ts new file mode 100644 index 0000000..a6f5b44 --- /dev/null +++ b/app/Controllers/Http/DashboardController.ts @@ -0,0 +1,327 @@ +// import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class DashboardController {} + +/* +const { validateAll } = use('Validator'); + +const Service = use('App/Models/Service'); +const Workspace = use('App/Models/Workspace'); +const Persona = use('Persona'); + +const crypto = require('crypto'); +const { v4: uuid } = require('uuid'); + +class DashboardController { + async login({ request, response, auth, session }) { + const validation = await validateAll(request.all(), { + mail: 'required|email', + password: 'required', + }); + if (validation.fails()) { + session + .withErrors({ + type: 'danger', + message: 'Invalid mail or password', + }) + .flashExcept(['password']); + return response.redirect('back'); + } + + const { mail, password } = request.all(); + + const hashedPassword = crypto + .createHash('sha256') + .update(password) + .digest('base64'); + + try { + await auth.authenticator('session').attempt(mail, hashedPassword); + } catch (error) { + session.flash({ + type: 'danger', + message: 'Invalid mail or password', + }); + return response.redirect('back'); + } + return response.redirect('/user/account'); + } + + async forgotPassword({ request, view }) { + const validation = await validateAll(request.all(), { + mail: 'required|email', + }); + if (validation.fails()) { + return view.render('others.message', { + heading: 'Cannot reset your password', + text: 'If your provided E-Mail address is linked to an account, we have just sent an E-Mail to that address.', + }); + } + try { + await Persona.forgotPassword(request.input('mail')); + // eslint-disable-next-line no-empty + } catch (e) {} + + return view.render('others.message', { + heading: 'Reset password', + text: 'If your provided E-Mail address is linked to an account, we have just sent an E-Mail to that address.', + }); + } + + async resetPassword({ request, view }) { + const validation = await validateAll(request.all(), { + password: 'required', + password_confirmation: 'required', + token: 'required', + }); + if (validation.fails()) { + session.withErrors({ + type: 'danger', + message: 'Passwords do not match', + }); + return response.redirect('back'); + } + + const payload = { + password: crypto + .createHash('sha256') + .update(request.input('password')) + .digest('base64'), + password_confirmation: crypto + .createHash('sha256') + .update(request.input('password_confirmation')) + .digest('base64'), + }; + + try { + await Persona.updatePasswordByToken(request.input('token'), payload); + } catch (e) { + return view.render('others.message', { + heading: 'Cannot reset your password', + text: 'Please make sure you are using a valid and recent link to reset your password and that your passwords entered match.', + }); + } + + return view.render('others.message', { + heading: 'Reset password', + text: 'Successfully reset your password. You can now login to your account using your new password.', + }); + } + + async account({ auth, view, response }) { + try { + await auth.check(); + } catch (error) { + return response.redirect('/user/login'); + } + + return view.render('dashboard.account', { + username: auth.user.username, + email: auth.user.email, + lastname: auth.user.lastname, + }); + } + + async edit({ auth, request, session, view, response }) { + let validation = await validateAll(request.all(), { + username: 'required', + email: 'required', + lastname: 'required', + }); + if (validation.fails()) { + session.withErrors(validation.messages()).flashExcept(['password']); + return response.redirect('back'); + } + + // Check new username + if (request.input('username') !== auth.user.username) { + validation = await validateAll(request.all(), { + username: 'required|unique:users,username', + email: 'required', + }); + if (validation.fails()) { + session.withErrors(validation.messages()).flashExcept(['password']); + return response.redirect('back'); + } + } + + // Check new email + if (request.input('email') !== auth.user.email) { + validation = await validateAll(request.all(), { + username: 'required', + email: 'required|email|unique:users,email', + }); + if (validation.fails()) { + session.withErrors(validation.messages()).flashExcept(['password']); + return response.redirect('back'); + } + } + + // Update user account + const { user } = auth; + user.username = request.input('username'); + user.lastname = request.input('lastname'); + user.email = request.input('email'); + if (request.input('password')) { + const hashedPassword = crypto + .createHash('sha256') + .update(request.input('password')) + .digest('base64'); + user.password = hashedPassword; + } + user.save(); + + return view.render('dashboard.account', { + username: user.username, + email: user.email, + success: true, + }); + } + + async data({ auth, view }) { + const general = auth.user; + const services = (await auth.user.services().fetch()).toJSON(); + const workspaces = (await auth.user.workspaces().fetch()).toJSON(); + + return view.render('dashboard.data', { + username: general.username, + lastname: general.lastname, + mail: general.email, + created: general.created_at, + updated: general.updated_at, + stringify: JSON.stringify, + services, + workspaces, + }); + } + + async export({ auth, response }) { + const general = auth.user; + const services = (await auth.user.services().fetch()).toJSON(); + const workspaces = (await auth.user.workspaces().fetch()).toJSON(); + + const exportData = { + username: general.username, + lastname: general.lastname, + mail: general.email, + services, + workspaces, + }; + + return response + .header('Content-Type', 'application/force-download') + .header('Content-disposition', 'attachment; filename=export.ferdium-data') + .send(exportData); + } + + async import({ auth, request, session, response, view }) { + const validation = await validateAll(request.all(), { + file: 'required', + }); + if (validation.fails()) { + session.withErrors(validation.messages()).flashExcept(['password']); + return response.redirect('back'); + } + + let file; + try { + file = JSON.parse(request.input('file')); + } catch (e) { + session.flash({ type: 'danger', message: 'Invalid Ferdium account file' }); + return response.redirect('back'); + } + + if (!file || !file.services || !file.workspaces) { + session.flash({ + type: 'danger', + message: 'Invalid Ferdium account file (2)', + }); + return response.redirect('back'); + } + + const serviceIdTranslation = {}; + + // Import services + try { + for (const service of file.services) { + // Get new, unused uuid + let serviceId; + do { + serviceId = uuid(); + } while ( + (await Service.query().where('serviceId', serviceId).fetch()).rows + .length > 0 + ); // eslint-disable-line no-await-in-loop + + await Service.create({ + // eslint-disable-line no-await-in-loop + userId: auth.user.id, + serviceId, + name: service.name, + recipeId: service.recipeId, + settings: JSON.stringify(service.settings), + }); + + serviceIdTranslation[service.serviceId] = serviceId; + } + } catch (e) { + const errorMessage = `Could not import your services into our system.\nError: ${e}`; + return view.render('others.message', { + heading: 'Error while importing', + text: errorMessage, + }); + } + + // Import workspaces + try { + for (const workspace of file.workspaces) { + let workspaceId; + do { + workspaceId = uuid(); + } while ( + (await Workspace.query().where('workspaceId', workspaceId).fetch()) + .rows.length > 0 + ); // eslint-disable-line no-await-in-loop + + const services = workspace.services.map( + service => serviceIdTranslation[service], + ); + + await Workspace.create({ + // eslint-disable-line no-await-in-loop + userId: auth.user.id, + workspaceId, + name: workspace.name, + order: workspace.order, + services: JSON.stringify(services), + data: JSON.stringify(workspace.data), + }); + } + } catch (e) { + const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`; + return view.render('others.message', { + heading: 'Error while importing', + text: errorMessage, + }); + } + + return view.render('others.message', { + heading: 'Successfully imported', + text: 'Your account has been imported, you can now login as usual!', + }); + } + + logout({ auth, response }) { + auth.authenticator('session').logout(); + return response.redirect('/user/login'); + } + + delete({ auth, response }) { + auth.user.delete(); + auth.authenticator('session').logout(); + return response.redirect('/user/login'); + } +} + +module.exports = DashboardController; +*/ -- cgit v1.2.3-70-g09d2