From 97dd53de1f476d77acbc8c13b50789990d201423 Mon Sep 17 00:00:00 2001 From: Kelvin Oghenerhoro Omereshone Date: Wed, 18 Sep 2024 16:18:10 +0100 Subject: [PATCH 01/52] finish mellow vue (#113) * chore(mellow-vue): add license * chore(mellow-vue): update README * chore(mellow-vue): update introduction * feat(mellow-vue): redirect to dashboard * feat(mellow-vue): redirect to dashboard * fix(mellow-vue): rename to view-home * feat(mellow-vue): clean up route * feat: update how the hero section looks * feat(mellow-vue): update cards for home page * feat: abstract layout for the app * feat: adjust page to use Layout * fix: adjust margin * fix: typo fix * feat: update title * feat: update page titles * feat: update footer * fix: remove flex grow * feat: add navigation to dashboard * feat: add policy for dashboard * feat: rename Layout to AppLayout * fix: update use of Layout * feat: use an auth folder to organize auth pages * feat: remove type file * feat: update page structure * chore: make routes more readable * feat: change migration strategy to alter * feat: simplify logic for password recovery * feat: style dashboard * feat(mellow-vue): add page title * feat(mellow-vue): add profile edit forms * feat(mellow-vue): update app layout * feat(mellow-vue): remove block * feat(mellow-vue): add reuseable components * chore(mellow-vue): update shipwright * feat(mellow-vue): refactor to use input components * chore(mellow-vue): remove custom defined types * feat(mellow-vue): use GoogleButton * feat(mellow-vue): implement GoogleButton * feat(mellow-vue): change text to name * feat(mellow-vue): use input components * fix(mellow-vue): change name to fullName * feat(mellow-vue): use input components * fix(mellow-vue): remove unnecessary props * feat(mellow-vue): add handling of back string in inertiaRedirect * feat(mellow-vue): add route to delete profile * feat(mellow-vue): add call out to install extension * feat(mellow-vue): add template for new email confirmation * fix(mellow-vue): remove await * chore(mellow-vue): prettier * fix(mellow-vue): resolve forgot password logic * feat(mellow-vue): update logic to verify email * feat(mellow-vue): add action to delete profile * fix(mellow-vue): wrong link in email-verify-new-email * fix(mellow-vue): make loading spinner stay in the center * fix(mellow-vue): resolve issue with suffix-icon not showing * feat(mellow-vue): add complete svg for suffix icon * feat(mellow-vue): add action to update profile * feat(mellow-vue): add onError handler * feat(mellow-vue): fix logout issues * feat(mellow-vue): make error message full width * feat(mellow-vue): add router for logout * fix(mellow-vue): wrong syntax for disabling button * feat(mellow-vue): add action to delete account * feat(mellow-vue): remove helper and use Sails Mail instead * feat(mellow-vue): add form handler to delete account * feat(mellow-vue): redirect to dashboard * chore(mellow-vue): remove type entry * feat(mellow-vue): remove Japa test runner * feat(mellow-vue): setup test with native Node runner * chore(mellow-vue): update all dependencies --- templates/mellow-vue/LICENSE | 21 + templates/mellow-vue/README.md | 49 +- .../api/controllers/auth/callback.js | 2 +- .../api/controllers/auth/forgot-password.js | 9 +- .../mellow-vue/api/controllers/auth/login.js | 4 +- .../api/controllers/auth/resend-link.js | 19 +- .../mellow-vue/api/controllers/auth/signup.js | 7 +- .../api/controllers/auth/verify-email.js | 4 +- .../api/controllers/auth/view-check-email.js | 9 +- .../controllers/auth/view-forgot-password.js | 2 +- .../api/controllers/auth/view-login.js | 2 +- .../controllers/auth/view-reset-password.js | 2 +- .../api/controllers/auth/view-signup.js | 2 +- .../api/controllers/auth/view-success.js | 2 +- .../controllers/dashboard/view-dashboard.js | 15 + .../home/{index.js => view-home.js} | 0 .../api/controllers/user/delete-profile.js | 52 + .../mellow-vue/api/controllers/user/logout.js | 2 +- .../api/controllers/user/update-profile.js | 106 + .../api/controllers/user/view-profile.js | 2 +- templates/mellow-vue/api/helpers/mail/send.js | 240 - .../api/responses/inertiaRedirect.js | 4 + .../assets/js/components/GoogleButton.vue | 37 + .../assets/js/components/InputBase.vue | 43 + .../assets/js/components/InputButton.vue | 38 + .../assets/js/components/InputEmail.vue | 21 + .../assets/js/components/InputPassword.vue | 102 + .../assets/js/components/InputText.vue | 32 + .../assets/js/layouts/AppLayout.vue | 105 + .../js/pages/{ => auth}/check-email.vue | 2 +- .../js/pages/{ => auth}/forgot-password.vue | 6 +- .../js/pages/{ => auth}/link-expired.vue | 2 +- .../mellow-vue/assets/js/pages/auth/login.vue | 97 + .../js/pages/{ => auth}/reset-password.vue | 0 .../assets/js/pages/auth/signup.vue | 167 + .../assets/js/pages/{ => auth}/success.vue | 4 +- .../assets/js/pages/dashboard/index.vue | 31 + .../mellow-vue/assets/js/pages/index.vue | 997 +--- .../mellow-vue/assets/js/pages/login.vue | 268 -- .../mellow-vue/assets/js/pages/profile.vue | 170 - .../mellow-vue/assets/js/pages/signup.vue | 365 -- .../assets/js/pages/user/profile.vue | 163 + templates/mellow-vue/bin/test.js | 59 - templates/mellow-vue/config/models.js | 2 +- templates/mellow-vue/config/policies.js | 3 +- templates/mellow-vue/config/routes.js | 16 +- templates/mellow-vue/jsconfig.json | 2 +- templates/mellow-vue/package-lock.json | 4055 +++++------------ templates/mellow-vue/package.json | 46 +- templates/mellow-vue/tests/e2e/login.spec.js | 0 templates/mellow-vue/tests/e2e/signup.spec.js | 64 - .../mellow-vue/tests/unit/helpers.test.js | 83 + templates/mellow-vue/types/index.d.ts | 173 - .../views/emails/email-reset-password.ejs | 39 +- .../views/emails/email-verify-new-email.ejs | 57 + 55 files changed, 2521 insertions(+), 5283 deletions(-) create mode 100644 templates/mellow-vue/LICENSE create mode 100644 templates/mellow-vue/api/controllers/dashboard/view-dashboard.js rename templates/mellow-vue/api/controllers/home/{index.js => view-home.js} (100%) create mode 100644 templates/mellow-vue/api/controllers/user/delete-profile.js create mode 100644 templates/mellow-vue/api/controllers/user/update-profile.js delete mode 100644 templates/mellow-vue/api/helpers/mail/send.js create mode 100644 templates/mellow-vue/assets/js/components/GoogleButton.vue create mode 100644 templates/mellow-vue/assets/js/components/InputBase.vue create mode 100644 templates/mellow-vue/assets/js/components/InputButton.vue create mode 100644 templates/mellow-vue/assets/js/components/InputEmail.vue create mode 100644 templates/mellow-vue/assets/js/components/InputPassword.vue create mode 100644 templates/mellow-vue/assets/js/components/InputText.vue create mode 100644 templates/mellow-vue/assets/js/layouts/AppLayout.vue rename templates/mellow-vue/assets/js/pages/{ => auth}/check-email.vue (98%) rename templates/mellow-vue/assets/js/pages/{ => auth}/forgot-password.vue (98%) rename templates/mellow-vue/assets/js/pages/{ => auth}/link-expired.vue (98%) create mode 100644 templates/mellow-vue/assets/js/pages/auth/login.vue rename templates/mellow-vue/assets/js/pages/{ => auth}/reset-password.vue (100%) create mode 100644 templates/mellow-vue/assets/js/pages/auth/signup.vue rename templates/mellow-vue/assets/js/pages/{ => auth}/success.vue (96%) create mode 100644 templates/mellow-vue/assets/js/pages/dashboard/index.vue delete mode 100644 templates/mellow-vue/assets/js/pages/login.vue delete mode 100644 templates/mellow-vue/assets/js/pages/profile.vue delete mode 100644 templates/mellow-vue/assets/js/pages/signup.vue create mode 100644 templates/mellow-vue/assets/js/pages/user/profile.vue delete mode 100644 templates/mellow-vue/bin/test.js delete mode 100644 templates/mellow-vue/tests/e2e/login.spec.js delete mode 100644 templates/mellow-vue/tests/e2e/signup.spec.js create mode 100644 templates/mellow-vue/tests/unit/helpers.test.js delete mode 100644 templates/mellow-vue/types/index.d.ts create mode 100644 templates/mellow-vue/views/emails/email-verify-new-email.ejs diff --git a/templates/mellow-vue/LICENSE b/templates/mellow-vue/LICENSE new file mode 100644 index 00000000..fad35b8d --- /dev/null +++ b/templates/mellow-vue/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 The Sailscasts Company + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/templates/mellow-vue/README.md b/templates/mellow-vue/README.md index 8f2d6d75..e3fb30c8 100644 --- a/templates/mellow-vue/README.md +++ b/templates/mellow-vue/README.md @@ -1,9 +1,46 @@ -# Vue +
+The Boring JavaScript Stack +
-This template should help get you started developing with a modern Sails fullstack application with Sails and Vue 3. +# Mellow -This scaffold contains: +## Introduction -- Sails -- Vue -- Tailwind +Mellow provides a minimal and simple starting point for building fullstack Sails applications with authentication. Styled with Tailwind, Mellow ships with authentication Sails actions and Vue pages/components that can be easily customized based on your application's needs. + +Mellow is powered by Tailwind and Vue and made possible by Inertia.js + +Key features of Mellow include: + +1. **Sails.js Backend**: Leveraging the power and simplicity of Sails.js for robust server-side operations. +2. **Vue.js Frontend**: Utilizing Vue.js for building dynamic and responsive user interfaces. +3. **Tailwind CSS Styling**: Employing Tailwind CSS for rapid and flexible UI development with utility-first classes. +4. **Inertia.js Integration**: Bridging the gap between the Sails.js backend and Vue.js frontend, allowing for SPA-like experiences without the complexity of building an API. +5. **Authentication Out-of-the-Box**: Providing pre-built authentication actions and pages that can be easily customized to fit your application's needs. + +## Why "Mellow"? + +The name "Mellow" reflects this template's philosophy: + +1. **Ease of Use**: A smooth, hassle-free development experience. +2. **Balanced Approach**: Combining powerful tools without overwhelming complexity. +3. **Flexibility**: Adapting to your needs without rigid constraints. +4. **Stability**: Providing a calm, reliable foundation for your projects. + +## Official Documentation + +Documentation for Mellow can be found on [The Boring JavaScript Stack docs](https://docs.sailscasts.com/boring-stack/templates#mellow). + +## Community and Support + +Join our community of developers using The Boring JavaScript Stack: + +- **Discord**: Real-time chat with other developers on our [Discord server](https://sailscasts.com/chat) +- **GitHub**: Report issues and contribute to the project on [GitHub](https://github.com/sailscasts/boring-stack/issues) +- **YouTube**: Watch tutorials and updates on our [YouTube channel](https://youtube.com/@sailscasts) + +## License + +Mellow is open-sourced software licensed under the MIT license. This means you're free to use, modify, and distribute the software, subject to the conditions of the MIT license. We encourage contributions from the community to help improve and evolve Mellow for everyone's benefit. + +For full license details, please see the [LICENSE](LICENSE.md) file in the project repository. diff --git a/templates/mellow-vue/api/controllers/auth/callback.js b/templates/mellow-vue/api/controllers/auth/callback.js index d2776486..928dc808 100644 --- a/templates/mellow-vue/api/controllers/auth/callback.js +++ b/templates/mellow-vue/api/controllers/auth/callback.js @@ -85,7 +85,7 @@ module.exports = { } req.session.userId = user.id - const urlToRedirectTo = '/' + const urlToRedirectTo = '/dashboard' return exits.success(urlToRedirectTo) }) } diff --git a/templates/mellow-vue/api/controllers/auth/forgot-password.js b/templates/mellow-vue/api/controllers/auth/forgot-password.js index 85a6580e..5ba541dd 100644 --- a/templates/mellow-vue/api/controllers/auth/forgot-password.js +++ b/templates/mellow-vue/api/controllers/auth/forgot-password.js @@ -24,14 +24,14 @@ module.exports = { }, fn: async function ({ email }) { - const user = await User.findOne({ email }) - if (!user) { - return + const userExists = await User.count({ email: this.req.session.userEmail }) + if (!userExists) { + return '/check-email' } const token = await sails.helpers.strings.random('url-friendly') - await User.update({ id: user.id }).set({ + const user = await User.updateOne({ email }).set({ passwordResetToken: token, passwordResetTokenExpiresAt: Date.now() + sails.config.custom.passwordResetTokenTTL @@ -46,6 +46,7 @@ module.exports = { token } }) + this.req.session.userEmail = user.email return '/check-email' } diff --git a/templates/mellow-vue/api/controllers/auth/login.js b/templates/mellow-vue/api/controllers/auth/login.js index cd30be41..b088f7de 100644 --- a/templates/mellow-vue/api/controllers/auth/login.js +++ b/templates/mellow-vue/api/controllers/auth/login.js @@ -47,7 +47,7 @@ and exposed as a shared data via loggedInUser prop.)`, }, fn: async function ({ email, password, rememberMe }) { - var user = await User.findOne({ + const user = await User.findOne({ email: email.toLowerCase() }) @@ -76,6 +76,6 @@ and exposed as a shared data via loggedInUser prop.)`, } this.req.session.userId = user.id - return '/' + return '/dashboard' } } diff --git a/templates/mellow-vue/api/controllers/auth/resend-link.js b/templates/mellow-vue/api/controllers/auth/resend-link.js index c18aaadf..9e29ad72 100644 --- a/templates/mellow-vue/api/controllers/auth/resend-link.js +++ b/templates/mellow-vue/api/controllers/auth/resend-link.js @@ -15,13 +15,18 @@ module.exports = { }, fn: async function () { - const unverifiedUser = await User.updateOne(this.req.session.userId).set({ - emailStatus: 'unverified', - emailProofToken: sails.helpers.strings.random('url-friendly'), - emailProofTokenExpiresAt: - Date.now() + sails.config.custom.emailProofTokenTTL - }) - if (!unverifiedUser) throw 'userNotFound' + const userExists = await User.count({ email: this.req.session.userEmail }) + if (!userExists) { + return '/check-email' + } + const unverifiedUser = await User.updateOne(this.req.session.userEmail).set( + { + emailStatus: 'unverified', + emailProofToken: sails.helpers.strings.random('url-friendly'), + emailProofTokenExpiresAt: + Date.now() + sails.config.custom.emailProofTokenTTL + } + ) this.req.session.userId = unverifiedUser.id diff --git a/templates/mellow-vue/api/controllers/auth/signup.js b/templates/mellow-vue/api/controllers/auth/signup.js index 9bf4d8ae..10d329f1 100644 --- a/templates/mellow-vue/api/controllers/auth/signup.js +++ b/templates/mellow-vue/api/controllers/auth/signup.js @@ -37,14 +37,14 @@ module.exports = { fn: async function ({ fullName, email: userEmail, password }) { const email = userEmail.toLowerCase() - + const emailProofToken = await sails.helpers.strings.random('url-friendly') try { unverifiedUser = await User.create({ email, password, fullName, tosAcceptedByIp: this.req.ip, - emailProofToken: sails.helpers.strings.random('url-friendly'), + emailProofToken, emailProofTokenExpiresAt: Date.now() + sails.config.custom.emailProofTokenTTL }).fetch() @@ -73,8 +73,6 @@ module.exports = { } } - this.req.session.userEmail = unverifiedUser.email - await sails.helpers.mail.send.with({ subject: 'Verify your email', template: 'email-verify-account', @@ -84,6 +82,7 @@ module.exports = { fullName: unverifiedUser.fullName } }) + this.req.session.userEmail = unverifiedUser.email return '/check-email' } } diff --git a/templates/mellow-vue/api/controllers/auth/verify-email.js b/templates/mellow-vue/api/controllers/auth/verify-email.js index 41aeaed0..c3a04e52 100644 --- a/templates/mellow-vue/api/controllers/auth/verify-email.js +++ b/templates/mellow-vue/api/controllers/auth/verify-email.js @@ -67,7 +67,9 @@ module.exports = { await User.updateOne({ id: user.id }).set({ emailStatus: 'confirmed', emailProofToken: '', - emailProofTokenExpiresAt: 0 + emailProofTokenExpiresAt: 0, + email: user.emailChangeCandidate, + emailChangeCandidate: '' }) this.req.session.userId = user.id return '/' diff --git a/templates/mellow-vue/api/controllers/auth/view-check-email.js b/templates/mellow-vue/api/controllers/auth/view-check-email.js index 891b9766..025df714 100644 --- a/templates/mellow-vue/api/controllers/auth/view-check-email.js +++ b/templates/mellow-vue/api/controllers/auth/view-check-email.js @@ -10,14 +10,9 @@ module.exports = { }, fn: async function () { - let message = null - if (this.req.get('referrer').includes('forgot-password')) { - message = `We sent a password reset link to ${this.req.session.userEmail}` - } else { - message = `We sent an email verification link to ${this.req.session.userEmail}` - } + let message = `We sent a link to the email address you provided. Please check your inbox and follow the instructions.` return { - page: 'check-email', + page: 'auth/check-email', props: { message } diff --git a/templates/mellow-vue/api/controllers/auth/view-forgot-password.js b/templates/mellow-vue/api/controllers/auth/view-forgot-password.js index d5896864..c56d150d 100644 --- a/templates/mellow-vue/api/controllers/auth/view-forgot-password.js +++ b/templates/mellow-vue/api/controllers/auth/view-forgot-password.js @@ -10,6 +10,6 @@ module.exports = { }, fn: async function () { - return { page: 'forgot-password' } + return { page: 'auth/forgot-password' } } } diff --git a/templates/mellow-vue/api/controllers/auth/view-login.js b/templates/mellow-vue/api/controllers/auth/view-login.js index 4f5838d1..55acb996 100644 --- a/templates/mellow-vue/api/controllers/auth/view-login.js +++ b/templates/mellow-vue/api/controllers/auth/view-login.js @@ -10,6 +10,6 @@ module.exports = { }, fn: async function () { - return { page: 'login' } + return { page: 'auth/login' } } } diff --git a/templates/mellow-vue/api/controllers/auth/view-reset-password.js b/templates/mellow-vue/api/controllers/auth/view-reset-password.js index b773f6aa..2b565f4b 100644 --- a/templates/mellow-vue/api/controllers/auth/view-reset-password.js +++ b/templates/mellow-vue/api/controllers/auth/view-reset-password.js @@ -28,6 +28,6 @@ module.exports = { if (!user || user.passwordResetTokenExpiresAt <= Date.now()) { throw 'invalidOrExpiredToken' } - return { page: 'reset-password', props: { token } } + return { page: 'auth/reset-password', props: { token } } } } diff --git a/templates/mellow-vue/api/controllers/auth/view-signup.js b/templates/mellow-vue/api/controllers/auth/view-signup.js index 7d55fef4..c63f6846 100644 --- a/templates/mellow-vue/api/controllers/auth/view-signup.js +++ b/templates/mellow-vue/api/controllers/auth/view-signup.js @@ -9,6 +9,6 @@ module.exports = { }, fn: async function () { - return { page: 'signup' } + return { page: 'auth/signup' } } } diff --git a/templates/mellow-vue/api/controllers/auth/view-success.js b/templates/mellow-vue/api/controllers/auth/view-success.js index 4f2c6a0b..eec6ef54 100644 --- a/templates/mellow-vue/api/controllers/auth/view-success.js +++ b/templates/mellow-vue/api/controllers/auth/view-success.js @@ -27,7 +27,7 @@ module.exports = { pageHeading = 'Password reset successful' } return { - page: 'success', + page: 'auth/success', props: { pageTitle, pageHeading, diff --git a/templates/mellow-vue/api/controllers/dashboard/view-dashboard.js b/templates/mellow-vue/api/controllers/dashboard/view-dashboard.js new file mode 100644 index 00000000..c1420876 --- /dev/null +++ b/templates/mellow-vue/api/controllers/dashboard/view-dashboard.js @@ -0,0 +1,15 @@ +module.exports = { + friendlyName: 'View dashboard', + + description: 'Display "Dashboard" page.', + + exits: { + success: { + responseType: 'inertia' + } + }, + + fn: async function () { + return { page: 'dashboard/index' } + } +} diff --git a/templates/mellow-vue/api/controllers/home/index.js b/templates/mellow-vue/api/controllers/home/view-home.js similarity index 100% rename from templates/mellow-vue/api/controllers/home/index.js rename to templates/mellow-vue/api/controllers/home/view-home.js diff --git a/templates/mellow-vue/api/controllers/user/delete-profile.js b/templates/mellow-vue/api/controllers/user/delete-profile.js new file mode 100644 index 00000000..51448e7d --- /dev/null +++ b/templates/mellow-vue/api/controllers/user/delete-profile.js @@ -0,0 +1,52 @@ +module.exports = { + friendlyName: 'Delete profile', + + description: + "Delete the logged-in user's account after verifying the password.", + + inputs: { + password: { + type: 'string', + required: true, + description: 'The current password of the user to verify before deletion.' + } + }, + + exits: { + success: { + responseType: 'inertiaRedirect', + description: 'User account deleted successfully.' + }, + unauthorized: { + responseType: 'inertiaRedirect', + description: 'User is not logged in.' + } + }, + + fn: async function ({ password }) { + const userId = this.req.session.userId + const user = await User.findOne({ id: userId }).intercept( + 'notFound', + () => { + delete this.req.session.userId + return { unauthorized: '/login' } + } + ) + + const passwordMatch = await sails.helpers.passwords + .checkPassword(password, user.password) + .intercept('incorrect', () => { + delete this.req.session.userId + return { unauthorized: '/login' } + }) + + await User.destroy({ id: userId }).intercept('error', (err) => { + sails.log.error('Error deleting account:', err) + throw 'error' + }) + + delete this.req.session.userId + + return '/login' + } +} diff --git a/templates/mellow-vue/api/controllers/user/logout.js b/templates/mellow-vue/api/controllers/user/logout.js index fb5559a7..73dcb67e 100644 --- a/templates/mellow-vue/api/controllers/user/logout.js +++ b/templates/mellow-vue/api/controllers/user/logout.js @@ -14,6 +14,6 @@ module.exports = { fn: async function () { sails.inertia.flushShared('loggedInUser') delete this.req.session.userId - return '/' + return '/login' } } diff --git a/templates/mellow-vue/api/controllers/user/update-profile.js b/templates/mellow-vue/api/controllers/user/update-profile.js new file mode 100644 index 00000000..725d96ff --- /dev/null +++ b/templates/mellow-vue/api/controllers/user/update-profile.js @@ -0,0 +1,106 @@ +module.exports = { + friendlyName: 'Update profile', + + description: 'Update the profile information of the logged-in user.', + + inputs: { + fullName: { + type: 'string', + required: true, + description: 'The full name of the user.' + }, + email: { + type: 'string', + required: true, + isEmail: true, + description: 'The email address of the user.' + }, + currentPassword: { + type: 'string', + description: 'The current password of the user.', + allowNull: true + }, + password: { + type: 'string', + allowNull: true, + description: 'The new password of the user.' + }, + passwordConfirmation: { + type: 'string', + description: 'The confirmation of the new password.', + allowNull: true + } + }, + + exits: { + success: { + responseType: 'inertiaRedirect', + description: 'Profile updated successfully.' + }, + invalid: { + responseType: 'badRequest', + description: 'The provided inputs are invalid.' + }, + unauthorized: { + responseType: 'inertiaRedirect', + description: 'The provided current password is incorrect.' + } + }, + + fn: async function ({ + fullName, + email, + currentPassword, + password, + passwordConfirmation + }) { + const userId = this.req.session.userId + const user = await User.findOne({ id: userId }).select([ + 'password', + 'email' + ]) + + if (currentPassword) { + await sails.helpers.passwords + .checkPassword(currentPassword, user.password) + .intercept('incorrect', () => { + delete this.req.session.userId + return { unauthorized: '/login' } + }) + } + + const updatedData = { + fullName + } + if (email !== user.email) { + updatedData.emailChangeCandidate = email + updatedData.emailStatus = 'change-requested' + const emailProofToken = sails.helpers.strings.random('url-friendly') + + await sails.helpers.mail.send.with({ + to: email, + subject: 'Confirm your new email address', + template: 'email-verify-new-email', + templateData: { + fullName, + token: emailProofToken + } + }) + } + + if (password) { + if (password !== passwordConfirmation) { + throw { + invalid: { + problems: [{ password: 'Password confirmation does not match.' }] + } + } + } + updatedData.password = password + } + + await User.updateOne({ id: userId }).set(updatedData) + + return 'back' + } +} diff --git a/templates/mellow-vue/api/controllers/user/view-profile.js b/templates/mellow-vue/api/controllers/user/view-profile.js index 98d7d2ff..62d61051 100644 --- a/templates/mellow-vue/api/controllers/user/view-profile.js +++ b/templates/mellow-vue/api/controllers/user/view-profile.js @@ -10,6 +10,6 @@ module.exports = { }, fn: async function () { - return { page: 'profile' } + return { page: 'user/profile' } } } diff --git a/templates/mellow-vue/api/helpers/mail/send.js b/templates/mellow-vue/api/helpers/mail/send.js deleted file mode 100644 index 0cc54a2f..00000000 --- a/templates/mellow-vue/api/helpers/mail/send.js +++ /dev/null @@ -1,240 +0,0 @@ -module.exports = { - friendlyName: 'Send', - - description: 'Send mail.', - - inputs: { - mailer: { - type: 'string', - description: 'The mailer to used.', - extendedDescription: - 'The mailer should be configured properly in config/mails.js. If not specified, the default mailer in sails.config.mail.default will be used', - defaultsTo: sails.config.mail.default, - isIn: ['log', 'smtp'] - }, - template: { - description: - 'The relative path to an EJS template within our `views/emails/` folder -- WITHOUT the file extension.', - extendedDescription: - 'Use strings like "foo" or "foo/bar", but NEVER "foo/bar.ejs" or "/foo/bar". For example, ' + - '"internal/email-contact-form" would send an email using the "views/emails/internal/email-contact-form.ejs" template.', - example: 'email-reset-password', - type: 'string' - }, - - templateData: { - description: - 'A dictionary of data which will be accessible in the EJS template.', - extendedDescription: - 'Each key will be a local variable accessible in the template. For instance, if you supply ' + - 'a dictionary with a `friends` key, and `friends` is an array like `[{name:"Chandra"}, {name:"Mary"}]`),' + - 'then you will be able to access `friends` from the template:\n' + - '```\n' + - '\n' + - '```' + - '\n' + - 'This is EJS, so use `<%= %>` to inject the HTML-escaped content of a variable, `<%= %>` to skip HTML-escaping ' + - 'and inject the data as-is, or `<% %>` to execute some JavaScript code such as an `if` statement or `for` loop.', - type: {}, - defaultsTo: {} - }, - to: { - description: 'The email address of the primary recipient.', - extendedDescription: - 'If this is any address ending in "@example.com", then don\'t actually deliver the message. ' + - 'Instead, just log it to the console.', - example: 'nola.thacker@example.com', - required: true, - isEmail: true - }, - toName: { - description: 'Name of the primary recipient as displayed in their inbox.', - example: 'Nola Thacker' - }, - - subject: { - description: 'The subject of the email.', - example: 'Hello there.', - defaultsTo: '' - }, - - from: { - description: - 'An override for the default "from" email that\'s been configured.', - example: 'anne.martin@example.com', - isEmail: true, - defaultsTo: sails.config.mail.from.address - }, - - fromName: { - description: 'An override for the default "from" name.', - example: 'Anne Martin', - defaultsTo: sails.config.mail.from.name - }, - - layout: { - description: - 'Set to `false` to disable layouts altogether, or provide the path (relative ' + - 'from `views/layouts/`) to an override email layout.', - defaultsTo: 'layout-email', - custom: (layout) => layout === false || typeof layout === 'string' - }, - waitForAcknowledgement: { - description: - 'Whether to wait for acknowledgement (response) that the email was successfully sent (or at least queued for sending) before returning.', - extendedDescription: - 'Otherwise by default, this returns immediately and delivers the request to deliver this email in the background.', - type: 'boolean', - defaultsTo: false - }, - text: { - type: 'string', - example: 'Hello world?' - } - }, - - exits: { - success: { - description: 'All done.' - } - }, - - fn: async function ({ - template, - templateData, - layout, - to, - subject, - mailer, - from: fromAddress, - fromName, - text - }) { - if (template && !template.startsWith('email-')) { - sails.log.warn( - 'The "template" that was passed in to `send()` does not begin with ' + - '"email-" -- but by convention, all email template files in `views/emails/` should ' + - 'be namespaced in this way. (This makes it easier to look up email templates by ' + - 'filename; e.g. when using CMD/CTRL+P in Sublime Text.)\n' + - 'Continuing regardless...' - ) - } - if ( - template && - (template.startsWith('views/') || template.startsWith('views/')) - ) { - throw new Error( - 'The "template" that was passed in to `sendTemplateEmail()` was prefixed with\n' + - '`emails/` or `views/` -- but that part is supposed to be omitted. Instead, please\n' + - 'just specify the path to the desired email template relative from `views/emails/`.\n' + - 'For example:\n' + - " template: 'email-reset-password'\n" + - 'Or:\n' + - " template: 'admin/email-contact-form'\n" + - " [?] If you're unsure or need advice, see https://sailsjs.com/support" - ) - } //• - // Determine appropriate email layout and template to use. - const path = require('path') - const emailTemplatePath = path.join('emails/', template) - let emailTemplateLayout - if (layout) { - emailTemplateLayout = path.relative( - path.dirname(emailTemplatePath), - path.resolve('layouts/', layout) - ) - } else { - emailTemplateLayout = false - } - // Compile HTML template. - // > Note that we set the layout, provide access to core `url` package (for - // > building links and image srcs, etc.) - const url = require('url') - const html = await sails - .renderView(emailTemplatePath, { - layout: emailTemplateLayout, - url, - ...templateData - }) - .intercept((err) => { - err.message = - 'Could not compile view template.\n' + - '(Usually, this means the provided data is invalid, or missing a piece.)\n' + - 'Details:\n' + - err.message - return err - }) - - switch (mailer) { - case 'log': - const logMessage = ` - Mailer is set to log so Sails is logging the email: - -=-=-=-=-=-=-=-=-=-=-=-=-= Email log -=-=-=-=-=-=-=-=-=-=-=-=-= - To: ${to} - Subject: ${subject} - - Body: - ${html} - -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - ` - sails.log(logMessage) - break - case 'smtp': - const nodemailer = getModule('nodemailer') - var transporter = nodemailer.createTransport({ - host: sails.config.smtp.host || sails.config.mail.mailers.smtp.host, - port: sails.config.smtp.port || sails.config.mail.mailers.smtp.port, - auth: { - user: - sails.config.smtp.username || - sails.config.mail.mailers.smtp.username, - pass: - sails.config.smtp.password || - sails.config.mail.mailers.smtp.password - } - }) - - const info = await transporter.sendMail({ - from: { - name: fromName, - address: fromAddress - }, - to, - subject, - text, - html - }) - sails.log.debug('Message sent: %s', info.messageId) - break - default: - sails.log.error(`Unknown mailer: ${mailer}`) - break - } - - return {} - } -} - -/** - * @typedef {function(string): any} GetModuleFunction - */ - -/** - * Get the required module by name. - * @param {string} moduleName - The name of the module to require. - * @returns {any} The required module. - * @throws {Error} When the module is not installed. - */ -function getModule(moduleName) { - let requiredModule - try { - requiredModule = require(moduleName) - } catch (error) { - throw new Error( - `"${moduleName}" is not installed. Please run "npm install ${moduleName}" to install it.` - ) - } - return requiredModule -} diff --git a/templates/mellow-vue/api/responses/inertiaRedirect.js b/templates/mellow-vue/api/responses/inertiaRedirect.js index dc44d1f2..662b1a62 100644 --- a/templates/mellow-vue/api/responses/inertiaRedirect.js +++ b/templates/mellow-vue/api/responses/inertiaRedirect.js @@ -9,6 +9,10 @@ module.exports = function inertiaRedirect(url) { const req = this.req const res = this.res + if (url === 'back') { + url = req.get('referer') || '/' + } + if (req.get(inertiaHeaders.INERTIA)) { res.set(inertiaHeaders.LOCATION, url) } diff --git a/templates/mellow-vue/assets/js/components/GoogleButton.vue b/templates/mellow-vue/assets/js/components/GoogleButton.vue new file mode 100644 index 00000000..493e5209 --- /dev/null +++ b/templates/mellow-vue/assets/js/components/GoogleButton.vue @@ -0,0 +1,37 @@ + diff --git a/templates/mellow-vue/assets/js/components/InputBase.vue b/templates/mellow-vue/assets/js/components/InputBase.vue new file mode 100644 index 00000000..f91ab683 --- /dev/null +++ b/templates/mellow-vue/assets/js/components/InputBase.vue @@ -0,0 +1,43 @@ + + + diff --git a/templates/mellow-vue/assets/js/components/InputButton.vue b/templates/mellow-vue/assets/js/components/InputButton.vue new file mode 100644 index 00000000..5ee60fc8 --- /dev/null +++ b/templates/mellow-vue/assets/js/components/InputButton.vue @@ -0,0 +1,38 @@ + + + diff --git a/templates/mellow-vue/assets/js/components/InputEmail.vue b/templates/mellow-vue/assets/js/components/InputEmail.vue new file mode 100644 index 00000000..c8d24279 --- /dev/null +++ b/templates/mellow-vue/assets/js/components/InputEmail.vue @@ -0,0 +1,21 @@ + + + diff --git a/templates/mellow-vue/assets/js/components/InputPassword.vue b/templates/mellow-vue/assets/js/components/InputPassword.vue new file mode 100644 index 00000000..28a05d59 --- /dev/null +++ b/templates/mellow-vue/assets/js/components/InputPassword.vue @@ -0,0 +1,102 @@ + + + diff --git a/templates/mellow-vue/assets/js/components/InputText.vue b/templates/mellow-vue/assets/js/components/InputText.vue new file mode 100644 index 00000000..329c175c --- /dev/null +++ b/templates/mellow-vue/assets/js/components/InputText.vue @@ -0,0 +1,32 @@ + + + diff --git a/templates/mellow-vue/assets/js/layouts/AppLayout.vue b/templates/mellow-vue/assets/js/layouts/AppLayout.vue new file mode 100644 index 00000000..d6ddd354 --- /dev/null +++ b/templates/mellow-vue/assets/js/layouts/AppLayout.vue @@ -0,0 +1,105 @@ + + + diff --git a/templates/mellow-vue/assets/js/pages/check-email.vue b/templates/mellow-vue/assets/js/pages/auth/check-email.vue similarity index 98% rename from templates/mellow-vue/assets/js/pages/check-email.vue rename to templates/mellow-vue/assets/js/pages/auth/check-email.vue index 67fcbbfc..7137577c 100644 --- a/templates/mellow-vue/assets/js/pages/check-email.vue +++ b/templates/mellow-vue/assets/js/pages/auth/check-email.vue @@ -6,7 +6,7 @@ const { message } = defineProps({ })