From fff1304709a65599e4d4409b1aa809c050f3d22a Mon Sep 17 00:00:00 2001 From: Kelvin Oghenerhoro Omereshone Date: Thu, 22 Feb 2024 07:39:00 +0100 Subject: [PATCH] [feat] handle validation errors in mellow vue (#82) * feat(mellow-vue): update badRequest response * feat(mellow-vue): update inertia-sails and install sails-flash * chore(mellow-vue): turn of typechecking js files * feat(mellow-vue): update login action and page to display validation errors * feat(mellow-vue): modify signup action to handle validation errors * feat(mellow-vue): display validation errors --- .../mellow-vue/api/controllers/auth/login.js | 23 +++++++-- .../mellow-vue/api/controllers/auth/signup.js | 49 +++++++++++-------- .../mellow-vue/api/responses/badRequest.js | 49 ++++++++++--------- .../mellow-vue/assets/js/pages/login.vue | 9 +++- .../mellow-vue/assets/js/pages/signup.vue | 15 ++++++ templates/mellow-vue/jsconfig.json | 2 +- templates/mellow-vue/package-lock.json | 28 +++++++++-- templates/mellow-vue/package.json | 3 +- 8 files changed, 123 insertions(+), 55 deletions(-) diff --git a/templates/mellow-vue/api/controllers/auth/login.js b/templates/mellow-vue/api/controllers/auth/login.js index 5541f661..cd30be41 100644 --- a/templates/mellow-vue/api/controllers/auth/login.js +++ b/templates/mellow-vue/api/controllers/auth/login.js @@ -12,6 +12,7 @@ password attempt.`, email: { description: 'The email to try in this attempt, e.g. "irl@example.com".', type: 'string', + isEmail: true, required: true }, @@ -39,6 +40,9 @@ that, thanks to the included "custom" hook, when a relevant request is received from a logged-in user, that user's entire record from the database will be fetched and exposed as a shared data via loggedInUser prop.)`, responseType: 'redirect' + }, + badCombo: { + responseType: 'badRequest' } }, @@ -48,12 +52,23 @@ and exposed as a shared data via loggedInUser prop.)`, }) if (!user) { - throw 'badCombo' + throw { + badCombo: { + problems: [{ login: 'Wrong email/password.' }] + } + } } - await sails.helpers.passwords - .checkPassword(password, user.password) - .intercept('incorrect', 'badCombo') + try { + await sails.helpers.passwords.checkPassword(password, user.password) + } catch (e) { + sails.log.error(e.message) + throw { + badCombo: { + problems: [{ login: 'Wrong email/password.' }] + } + } + } if (rememberMe) { this.req.session.cookie.maxAge = diff --git a/templates/mellow-vue/api/controllers/auth/signup.js b/templates/mellow-vue/api/controllers/auth/signup.js index 95f6e5d1..9bf4d8ae 100644 --- a/templates/mellow-vue/api/controllers/auth/signup.js +++ b/templates/mellow-vue/api/controllers/auth/signup.js @@ -30,13 +30,6 @@ module.exports = { 'If this request was sent from a graphical user interface, the request ' + 'parameters should have been validated/coerced _before_ they were sent.' }, - emailAlreadyInUse: { - statusCode: 409, - viewTemplatePath: '500', - description: 'The email address is no longer available.', - extendedDescription: - 'This is an edge case that is not always anticipated by websites and APIs. Since it is pretty rare, the 500 server error page is used as a simple catch-all. If this becomes important in the future, this could easily be expanded into a custom error page or resolution flow. But for context: this behavior of showing the 500 server error page mimics how popular apps like Slack behave under the same circumstances.' - }, success: { responseType: 'redirect' } @@ -45,26 +38,40 @@ module.exports = { fn: async function ({ fullName, email: userEmail, password }) { const email = userEmail.toLowerCase() - const unverifiedUser = await User.create({ - email, - password, - fullName, - tosAcceptedByIp: this.req.ip, - emailProofToken: sails.helpers.strings.random('url-friendly'), - emailProofTokenExpiresAt: - Date.now() + sails.config.custom.emailProofTokenTTL - }) - .intercept('E_UNIQUE', 'emailAlreadyInUse') - .intercept({ name: 'UsageError' }, () => { + try { + unverifiedUser = await User.create({ + email, + password, + fullName, + tosAcceptedByIp: this.req.ip, + emailProofToken: sails.helpers.strings.random('url-friendly'), + emailProofTokenExpiresAt: + Date.now() + sails.config.custom.emailProofTokenTTL + }).fetch() + } catch (error) { + if (error.code === 'E_UNIQUE') { throw { badSignupRequest: { problems: [ - `"signup" Apologies, but something went wrong with signing you up. Please try again.` + { + email: 'An account with this email address already exists.' + } ] } } - }) - .fetch() + } else if (error.name === 'UsageError') { + throw { + badSignupRequest: { + problems: [ + { + signup: + 'Apologies, but something went wrong with signing you up. Please try again.' + } + ] + } + } + } + } this.req.session.userEmail = unverifiedUser.email diff --git a/templates/mellow-vue/api/responses/badRequest.js b/templates/mellow-vue/api/responses/badRequest.js index 06ce0117..142fcd6b 100644 --- a/templates/mellow-vue/api/responses/badRequest.js +++ b/templates/mellow-vue/api/responses/badRequest.js @@ -36,34 +36,39 @@ module.exports = function badRequest(optionalData) { // Check if it's an Inertia request if (req.header('X-Inertia')) { - console.log(optionalData) - if ( - optionalData && - optionalData.code && - (optionalData.code === 'E_MISSING_OR_INVALID_PARAMS' || - optionalData.name === 'UsageError') - ) { + if (optionalData && optionalData.problems) { const errors = {} optionalData.problems.forEach((problem) => { - const regex = /"(.*?)"/ - const matches = problem.match(regex) + if (typeof problem === 'object') { + Object.keys(problem).forEach((propertyName) => { + const sanitizedProblem = problem[propertyName].replace(/\.$/, '') // Trim trailing dot + if (!errors[propertyName]) { + errors[propertyName] = [sanitizedProblem] + } else { + errors[propertyName].push(sanitizedProblem) + } + }) + } else { + const regex = /"(.*?)"/ + const matches = problem.match(regex) - if (matches && matches.length > 1) { - const propertyName = matches[1] - const sanitizedProblem = problem.replace(/"([^"]+)"/, '$1') - if (!errors[propertyName]) { - errors[propertyName] = [sanitizedProblem] - } else { - errors[propertyName].push(sanitizedProblem) + if (matches && matches.length > 1) { + const propertyName = matches[1] + const sanitizedProblem = problem + .replace(/"([^"]+)"/, '$1') + .replace('\n', '') + .replace('ยท', '') + .trim() + if (!errors[propertyName]) { + errors[propertyName] = [sanitizedProblem] + } else { + errors[propertyName].push(sanitizedProblem) + } } } }) - - if (req.session) { - req.session.errors = errors - } - - return res.status(statusCodeToSet).redirect('back') + req.session.errors = errors + return res.redirect(303, 'back') } } diff --git a/templates/mellow-vue/assets/js/pages/login.vue b/templates/mellow-vue/assets/js/pages/login.vue index 20444370..5ac7ec24 100644 --- a/templates/mellow-vue/assets/js/pages/login.vue +++ b/templates/mellow-vue/assets/js/pages/login.vue @@ -54,6 +54,13 @@ const disableLoginButton = computed(() => {

Log into your account

Welcome back, please enter your details

+

+ {{ form.errors?.login || form.errors?.email }} +

+
{ { Welcome! Please enter your details to sign up

+

+ {{ form.errors?.signup }} +

{ class="block w-full rounded-lg border border-gray/50 bg-white py-3 pl-10 pr-3 shadow-sm placeholder:text-lg placeholder:text-gray focus:outline-none focus:ring-1 focus:ring-gray-100" v-model="form.fullName" /> +

+ {{ form.errors.fullName }} +