Productivity
-- Let Mellow be the starting point of your next SPA. With authentication - and profile management taken care of, you can focus on your core - business logic -
-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 +
-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' + - 'Welcome back, please enter your details
++ {{ form.errors?.login || form.errors?.email }} +
++ Welcome! Please enter your details to sign up +
++ {{ form.errors?.signup }} +
+ ++ You are logged in as {{ loggedInUser.email }} +
+ + Edit Profile + ++ Mellow handles user management, so you can build what matters. +
+- The default starter template of The Boring JavaScript Stack. Save time - and effort by leveraging Mellow's powerful features and intuitive - design. +
+ Install the official Sails VS Code extension to supercharge your + development experience.
- - - -- Let Mellow be the starting point of your next SPA. With authentication - and profile management taken care of, you can focus on your core - business logic -
-- Experience effortless user authentication and simplified profile - management with Mellow, creating a seamless user journey for - developers and users. +
+ Official documentation for The Boring JavaScript Stack.
- -- Let users manage their profiles with ease using Mellow. It offers a - simple and secure way to update their name, email address, and - password. +
+ Join the community to discuss Sails.js and get help. Connect with + fellow developers and stay updated.
- - -- Mellow is the default starter template for The Boring JavaScript - Stack. It provides authentication and profile management out of the - box. -
-- Chances are you already have scaffolded a new project using mellow - if you are seeing this. Just open up the project in your editor and - start coding away. -
-- For sure! All the code in Mellow is open source so you can copy and - paste and customize to your heart's content. -
-- Made with love 💚 by - Kelvin Omereshone - and contributors -
- ++ Give The Boring JavaScript Stack a star on GitHub. +
+ + + ++ Learn Sails.js and The Boring JavaScript Stack through video tutorials + and courses. +
+ + ++ Let Mellow be the starting point of your next SPA. With authentication + and profile management taken care of, you can focus on your core + business logic +
++ Experience effortless user authentication and simplified profile + management with Mellow, creating a seamless user journey for developers + and users. +
++ Let users manage their profiles with ease using Mellow. It offers a + simple and secure way to update their name, email address, and password. +
++ Mellow is the default starter template for The Boring JavaScript + Stack. It provides authentication and profile management out of the + box. +
++ Chances are you already have scaffolded a new project using Mellow if + you are seeing this. Just open up the project in your editor and start + coding away. +
++ For sure! All the code in Mellow is open source so you can copy and + paste and customize to your heart's content. +
+Update your name and email address
-- Welcome! Please enter your details to sign up -
-- {{ form.errors?.signup }} -
- - - Continue with Google - -+ Update your account's profile information and email address. +
++ Ensure your account is using a long, random password to stay secure. +
++ Once your account is deleted, all of its resources and data will be + permanently deleted. Before deleting your account, please download any + data or information that you wish to retain. +
+Dear <%= fullName %>,
-Someone requested a password reset for your account. If this was not you, please disregard this email. Otherwise, simply click the button below:
-Dear <%= fullName %>,
++ Someone requested a password reset for your account. If this was not you, + please disregard this email. Otherwise, simply click the button below: +
+ -If you have any trouble, try pasting this link in your browser: <%= url.resolve(sails.config.custom.baseUrl,'/reset-password')+'?token='+encodeURIComponent(token) %>
-Sincerely,
-The Boring JavaScript Stack Team
++ If you have any trouble, try pasting this link in your browser: + <%= + url.resolve(sails.config.custom.baseUrl,'/reset-password')+'?token='+encodeURIComponent(token) + %> +
+Sincerely,
+The Boring JavaScript Stack Team
diff --git a/templates/mellow-vue/views/emails/email-verify-new-email.ejs b/templates/mellow-vue/views/emails/email-verify-new-email.ejs new file mode 100644 index 00000000..173aa1ea --- /dev/null +++ b/templates/mellow-vue/views/emails/email-verify-new-email.ejs @@ -0,0 +1,57 @@ ++ We've received a request to change your email address. To complete this + process, please click the button below to verify your new email address: +
+ ++ If you have any trouble, try pasting this link in your browser: + <%= + url.resolve(sails.config.custom.baseUrl,'/verify-email')+'?token='+encodeURIComponent(token) + %> +
++ If you didn't request this change, please ignore this email or contact our + support team if you have any concerns. +
+Sincerely,
+The Mellow Team