Skip to content

Commit

Permalink
[feat] handle validation errors in mellow vue (#82)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
DominusKelvin authored Feb 22, 2024
1 parent 022be64 commit fff1304
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 55 deletions.
23 changes: 19 additions & 4 deletions templates/mellow-vue/api/controllers/auth/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ password attempt.`,
email: {
description: 'The email to try in this attempt, e.g. "[email protected]".',
type: 'string',
isEmail: true,
required: true
},

Expand Down Expand Up @@ -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'
}
},

Expand All @@ -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 =
Expand Down
49 changes: 28 additions & 21 deletions templates/mellow-vue/api/controllers/auth/signup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand All @@ -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

Expand Down
49 changes: 27 additions & 22 deletions templates/mellow-vue/api/responses/badRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
}

Expand Down
9 changes: 8 additions & 1 deletion templates/mellow-vue/assets/js/pages/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ const disableLoginButton = computed(() => {
<h1 class="text-2xl">Log into your account</h1>
<p class="text-lg text-gray">Welcome back, please enter your details</p>
</section>
<p
class="b my-4 rounded-sm border-red-400 bg-red-100 p-4 text-red-500"
v-if="form.errors.email || form.errors.login"
>
{{ form.errors?.login || form.errors?.email }}
</p>

<form
@submit.prevent="form.post('/login')"
class="mb-4 flex flex-col space-y-6"
Expand All @@ -74,7 +81,7 @@ const disableLoginButton = computed(() => {
</svg>
</span>
<input
type="email"
type="text"
id="email"
placeholder="Your email"
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"
Expand Down
15 changes: 15 additions & 0 deletions templates/mellow-vue/assets/js/pages/signup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ const disableSignupButton = computed(() => {
Welcome! Please enter your details to sign up
</p>
</section>
<p
class="b my-4 rounded-sm border-red-400 bg-red-100 p-4 text-red-500"
v-if="form.errors.signup"
>
{{ form.errors?.signup }}
</p>
<form
@submit.prevent="form.post('/signup')"
class="mb-4 flex flex-col space-y-6"
Expand Down Expand Up @@ -101,6 +107,9 @@ const disableSignupButton = computed(() => {
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"
/>
<p class="absolute text-red-500" v-if="form.errors.fullName">
{{ form.errors.fullName }}
</p>
</label>
<label for="email" class="relative block"
><span class="block text-lg">Email</span>
Expand All @@ -124,6 +133,9 @@ const disableSignupButton = computed(() => {
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.email"
/>
<p class="absolute text-red-500" v-if="form.errors.email">
{{ form.errors.email }}
</p>
</label>
<label for="password" class="relative block"
><span class="block text-lg">Password</span>
Expand Down Expand Up @@ -214,6 +226,9 @@ const disableSignupButton = computed(() => {
</svg>
</button>
</span>
<p class="absolute text-red-500" v-if="form.errors.password">
{{ form.errors.password }}
</p>
</label>
<ul class="flex justify-between text-sm">
<li
Expand Down
2 changes: 1 addition & 1 deletion templates/mellow-vue/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// silences wrong TS error, we don't compile, we only typecheck
"outDir": "./irrelevant/unused",
"allowJs": true,
"checkJs": true,
"checkJs": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
Expand Down
28 changes: 23 additions & 5 deletions templates/mellow-vue/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion templates/mellow-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
"@sailshq/connect-redis": "^3.2.1",
"@sailshq/lodash": "^3.10.3",
"@sailshq/socket.io-redis": "^5.2.0",
"inertia-sails": "^0.1.7",
"inertia-sails": "^0.1.8",
"nodemailer": "^6.9.4",
"sails": "^1.5.2",
"sails-flash": "^0.0.1",
"sails-hook-mail": "^0.0.7",
"sails-hook-organics": "^2.2.2",
"sails-hook-orm": "^4.0.0",
Expand Down

0 comments on commit fff1304

Please sign in to comment.