From 9857406921acb2761c6bcefbcb0276eca0b76785 Mon Sep 17 00:00:00 2001 From: Kelvin Oghenerhoro Omereshone Date: Sun, 22 Sep 2024 21:25:15 +0100 Subject: [PATCH] feat(mellow-svelte): add actions --- .../api/controllers/auth/callback.js | 91 +++++++++++++++ .../api/controllers/auth/forgot-password.js | 53 +++++++++ .../api/controllers/auth/login.js | 81 +++++++++++++ .../api/controllers/auth/redirect.js | 22 ++++ .../api/controllers/auth/resend-link.js | 44 ++++++++ .../api/controllers/auth/reset-password.js | 58 ++++++++++ .../api/controllers/auth/signup.js | 88 +++++++++++++++ .../api/controllers/auth/verify-email.js | 82 ++++++++++++++ .../api/controllers/auth/view-check-email.js | 21 ++++ .../controllers/auth/view-forgot-password.js | 15 +++ .../api/controllers/auth/view-link-expired.js | 15 +++ .../api/controllers/auth/view-login.js | 15 +++ .../controllers/auth/view-reset-password.js | 33 ++++++ .../api/controllers/auth/view-signup.js | 14 +++ .../api/controllers/auth/view-success.js | 38 +++++++ .../controllers/dashboard/view-dashboard.js | 15 +++ .../api/controllers/example/index.js | 22 ---- .../home/{index.js => view-home.js} | 7 +- .../api/controllers/user/delete-profile.js | 52 +++++++++ .../api/controllers/user/logout.js | 19 ++++ .../api/controllers/user/update-profile.js | 106 ++++++++++++++++++ .../api/controllers/user/view-profile.js | 15 +++ 22 files changed, 878 insertions(+), 28 deletions(-) create mode 100644 templates/mellow-svelte/api/controllers/auth/callback.js create mode 100644 templates/mellow-svelte/api/controllers/auth/forgot-password.js create mode 100644 templates/mellow-svelte/api/controllers/auth/login.js create mode 100644 templates/mellow-svelte/api/controllers/auth/redirect.js create mode 100644 templates/mellow-svelte/api/controllers/auth/resend-link.js create mode 100644 templates/mellow-svelte/api/controllers/auth/reset-password.js create mode 100644 templates/mellow-svelte/api/controllers/auth/signup.js create mode 100644 templates/mellow-svelte/api/controllers/auth/verify-email.js create mode 100644 templates/mellow-svelte/api/controllers/auth/view-check-email.js create mode 100644 templates/mellow-svelte/api/controllers/auth/view-forgot-password.js create mode 100644 templates/mellow-svelte/api/controllers/auth/view-link-expired.js create mode 100644 templates/mellow-svelte/api/controllers/auth/view-login.js create mode 100644 templates/mellow-svelte/api/controllers/auth/view-reset-password.js create mode 100644 templates/mellow-svelte/api/controllers/auth/view-signup.js create mode 100644 templates/mellow-svelte/api/controllers/auth/view-success.js create mode 100644 templates/mellow-svelte/api/controllers/dashboard/view-dashboard.js delete mode 100644 templates/mellow-svelte/api/controllers/example/index.js rename templates/mellow-svelte/api/controllers/home/{index.js => view-home.js} (68%) create mode 100644 templates/mellow-svelte/api/controllers/user/delete-profile.js create mode 100644 templates/mellow-svelte/api/controllers/user/logout.js create mode 100644 templates/mellow-svelte/api/controllers/user/update-profile.js create mode 100644 templates/mellow-svelte/api/controllers/user/view-profile.js diff --git a/templates/mellow-svelte/api/controllers/auth/callback.js b/templates/mellow-svelte/api/controllers/auth/callback.js new file mode 100644 index 00000000..167022f9 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/callback.js @@ -0,0 +1,91 @@ +module.exports = { + friendlyName: 'Callback', + + description: 'Callback auth.', + + inputs: { + provider: { + isIn: ['google'], + required: true + }, + code: { + type: 'string', + required: true + } + }, + + exits: { + success: { + responseType: 'redirect' + } + }, + fn: async function ({ code, provider }, exits) { + const req = this.req + const googleUser = await sails.wish.provider(provider).user(code) + + User.findOrCreate( + { or: [{ googleId: googleUser.id }, { email: googleUser.email }] }, + { + googleId: googleUser.id, + email: googleUser.email, + fullName: googleUser.name, + googleAvatarUrl: googleUser.picture, + googleAccessToken: googleUser.accessToken, + googleIdToken: googleUser.idToken, + emailStatus: googleUser.verified_email ? 'verified' : 'unverified' + } + ).exec(async (error, user, wasCreated) => { + if (error) throw error + + if (!wasCreated && googleUser.verified_email) { + await User.updateOne({ id: user.id }).set({ + emailStatus: 'verified' + }) + } + if (!wasCreated && user.googleId !== googleUser.id) { + // Checks if the user email has changed since last log in + // And then update the email change candidate which will be used be used to prompt the user to update their email + await User.updateOne({ id: user.id }).set({ + emailChangeCandidate: googleUser.email + }) + } + if (!wasCreated && user.email !== googleUser.email) { + // Checks if the user email has changed since last log in + // And then update the email change candidate which will be used be used to prompt the user to update their email + await User.updateOne({ id: user.id }).set({ + emailChangeCandidate: googleUser.email + }) + } + + // Checks if the user name has changed since last log in + // And then update the name if changed + if (!wasCreated && user.fullName !== googleUser.name) { + await User.updateOne({ id: user.id }).set({ + fullName: googleUser.name + }) + } + + if (!wasCreated && user.googleAvatarUrl !== googleUser.picture) { + await User.updateOne({ id: user.id }).set({ + googleAvatarUrl: googleUser.picture + }) + } + + if (!wasCreated && user.googleAccessToken !== googleUser.accessToken) { + await User.updateOne({ id: user.id }).set({ + googleAccessToken: googleUser.accessToken + }) + } + + if (!wasCreated && user.googleIdToken !== googleUser.idToken) { + await User.updateOne({ id: user.id }).set({ + googleIdToken: googleUser.idToken + }) + } + + req.session.userId = user.id + const urlToRedirectTo = '/dashboard' + return exits.success(urlToRedirectTo) + }) + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/forgot-password.js b/templates/mellow-svelte/api/controllers/auth/forgot-password.js new file mode 100644 index 00000000..5ba541dd --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/forgot-password.js @@ -0,0 +1,53 @@ +module.exports = { + friendlyName: 'Forgot password', + + description: + 'Send a password recovery notification to the user with the specified email address.', + + inputs: { + email: { + description: + 'The email address of the alleged user who wants to recover their password.', + example: 'kelvin@boringstack.com', + type: 'string', + required: true, + isEmail: true + } + }, + + exits: { + success: { + description: + 'The email address might have matched a user in the database. (If so, a recovery email was sent.)', + responseType: 'redirect' + } + }, + + fn: async function ({ email }) { + const userExists = await User.count({ email: this.req.session.userEmail }) + if (!userExists) { + return '/check-email' + } + + const token = await sails.helpers.strings.random('url-friendly') + + const user = await User.updateOne({ email }).set({ + passwordResetToken: token, + passwordResetTokenExpiresAt: + Date.now() + sails.config.custom.passwordResetTokenTTL + }) + + await sails.helpers.mail.send.with({ + to: user.email, + subject: 'Password reset instructions', + template: 'email-reset-password', + templateData: { + fullName: user.fullName, + token + } + }) + + this.req.session.userEmail = user.email + return '/check-email' + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/login.js b/templates/mellow-svelte/api/controllers/auth/login.js new file mode 100644 index 00000000..b088f7de --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/login.js @@ -0,0 +1,81 @@ +module.exports = { + friendlyName: 'Login', + + description: 'Log in using the provided email and password combination.', + + extendedDescription: `This action attempts to look up the user record in the database with the +specified email address. Then, if such a user exists, it uses +bcrypt to compare the hashed password from the database with the provided +password attempt.`, + + inputs: { + email: { + description: 'The email to try in this attempt, e.g. "irl@example.com".', + type: 'string', + isEmail: true, + required: true + }, + + password: { + description: + 'The unencrypted password to try in this attempt, e.g. "passwordlol".', + type: 'string', + required: true + }, + + rememberMe: { + description: "Whether to extend the lifetime of the user's session.", + type: 'boolean' + } + }, + + exits: { + success: { + description: 'The requesting user agent has been successfully logged in.', + extendedDescription: `Under the covers, this stores the id of the logged-in user in the session +as the \`userId\` key. The next time this user agent sends a request, assuming +it includes a cookie (like a web browser), Sails will automatically make this +user id available as req.session.userId in the corresponding action. (Also note +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' + } + }, + + fn: async function ({ email, password, rememberMe }) { + const user = await User.findOne({ + email: email.toLowerCase() + }) + + if (!user) { + throw { + badCombo: { + problems: [{ login: 'Wrong email/password.' }] + } + } + } + + 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 = + sails.config.custom.rememberMeCookieMaxAge + } + + this.req.session.userId = user.id + return '/dashboard' + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/redirect.js b/templates/mellow-svelte/api/controllers/auth/redirect.js new file mode 100644 index 00000000..f8ce7637 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/redirect.js @@ -0,0 +1,22 @@ +module.exports = { + friendlyName: 'Redirect', + + description: 'Redirect auth.', + + inputs: { + provider: { + isIn: ['google'], + required: true + } + }, + + exits: { + success: { + responseType: 'redirect' + } + }, + + fn: async function ({ provider }) { + return sails.wish.provider(provider).redirect() + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/resend-link.js b/templates/mellow-svelte/api/controllers/auth/resend-link.js new file mode 100644 index 00000000..9e29ad72 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/resend-link.js @@ -0,0 +1,44 @@ +module.exports = { + friendlyName: 'Resend link', + + description: '', + + inputs: {}, + + exits: { + success: { + responseType: 'redirect' + }, + userNotFound: { + responseType: 'notFound' + } + }, + + fn: async function () { + 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 + + await sails.helpers.mail.send.with({ + subject: 'Verify your email', + template: 'email-verify-account', + to: unverifiedUser.email, + templateData: { + token: unverifiedUser.emailProofToken, + fullName: unverifiedUser.fullName + } + }) + return '/check-email' + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/reset-password.js b/templates/mellow-svelte/api/controllers/auth/reset-password.js new file mode 100644 index 00000000..c66f9a9e --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/reset-password.js @@ -0,0 +1,58 @@ +module.exports = { + friendlyName: 'Reset password', + + description: '', + + inputs: { + token: { + description: 'The verification token from the email.', + example: 'lyCap0N9i8wKYz7rhrEPog' + }, + password: { + type: 'string', + required: true, + minLength: 8 + } + }, + + exits: { + success: { + responseType: 'redirect' + }, + invalidOrExpiredToken: { + responseType: 'expired', + description: 'The provided token is expired, invalid, or already used up.' + }, + badSignupRequest: { + responseType: 'badRequest', + description: + 'The provided fullName, password and/or email address are invalid.', + extendedDescription: + 'If this request was sent from a graphical user interface, the request ' + + 'parameters should have been validated/coerced _before_ they were sent.' + } + }, + + fn: async function ({ token, password }) { + if (!token) { + throw 'invalidOrExpiredToken' + } + + const user = await User.findOne({ passwordResetToken: token }) + + if (!user || user.passwordResetTokenExpiresAt <= Date.now()) { + throw 'invalidOrExpiredToken' + } + await User.updateOne({ id: user.id }).set({ + password, + passwordResetToken: '', + passwordResetTokenExpiresAt: 0 + }) + + this.req.session.userId = user.id + + delete this.req.session.userEmail + + return '/reset-password/success' + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/signup.js b/templates/mellow-svelte/api/controllers/auth/signup.js new file mode 100644 index 00000000..10d329f1 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/signup.js @@ -0,0 +1,88 @@ +module.exports = { + friendlyName: 'Register', + + description: 'Register auth.', + + inputs: { + fullName: { + type: 'string', + maxLength: 120, + required: true + }, + email: { + type: 'string', + isEmail: true, + required: true + }, + password: { + type: 'string', + required: true, + minLength: 8 + } + }, + + exits: { + badSignupRequest: { + responseType: 'badRequest', + description: + 'The provided fullName, password and/or email address are invalid.', + extendedDescription: + 'If this request was sent from a graphical user interface, the request ' + + 'parameters should have been validated/coerced _before_ they were sent.' + }, + success: { + responseType: 'redirect' + } + }, + + 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, + emailProofTokenExpiresAt: + Date.now() + sails.config.custom.emailProofTokenTTL + }).fetch() + } catch (error) { + if (error.code === 'E_UNIQUE') { + throw { + badSignupRequest: { + problems: [ + { + email: 'An account with this email address already exists.' + } + ] + } + } + } else if (error.name === 'UsageError') { + throw { + badSignupRequest: { + problems: [ + { + signup: + 'Apologies, but something went wrong with signing you up. Please try again.' + } + ] + } + } + } + } + + await sails.helpers.mail.send.with({ + subject: 'Verify your email', + template: 'email-verify-account', + to: unverifiedUser.email, + templateData: { + token: unverifiedUser.emailProofToken, + fullName: unverifiedUser.fullName + } + }) + this.req.session.userEmail = unverifiedUser.email + return '/check-email' + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/verify-email.js b/templates/mellow-svelte/api/controllers/auth/verify-email.js new file mode 100644 index 00000000..c3a04e52 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/verify-email.js @@ -0,0 +1,82 @@ +module.exports = { + friendlyName: 'Verify email', + + description: `Confirm a new user's email address, or an existing user's request for an email address change, + then redirect to either a special landing page (for newly-signed up users), or the account page + (for existing users who just changed their email address).`, + + inputs: { + token: { + description: 'The verification token from the email.', + example: 'lyCap0N9i8wKYz7rhrEPog' + } + }, + + exits: { + success: { + description: + 'Email address confirmed and requesting user logged in. Since this looks like a browser, redirecting...', + responseType: 'redirect' + }, + invalidOrExpiredToken: { + responseType: 'expired', + description: 'The provided token is expired, invalid, or already used up.' + }, + 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.' + } + }, + + fn: async function ({ token }) { + if (!token) { + throw 'invalidOrExpiredToken' + } + + const user = await User.findOne({ emailProofToken: token }) + + if (!user || user.emailProofTokenExpiresAt <= Date.now()) { + throw 'invalidOrExpiredToken' + } + + if (user.emailStatus == 'unverified') { + await User.updateOne({ id: user.id }).set({ + emailStatus: 'verified', + emailProofToken: '', + emailProofTokenExpiresAt: 0 + }) + + this.req.session.userId = user.id + delete this.req.session.userEmail + + return '/verify-email/success' + } else if (user.emailStatus == 'change-requested') { + if (!user.emailChangeCandidate) { + throw new Error( + `Consistency violation: Could not update user because this user record's emailChangeCandidate ("${user.emailChangeCandidate}") is missing. (This should never happen.)` + ) + } + + if ((await User.count({ email: user.emailChangeCandidate })) > 0) { + throw 'emailAlreadyInUse' + } + + await User.updateOne({ id: user.id }).set({ + emailStatus: 'confirmed', + emailProofToken: '', + emailProofTokenExpiresAt: 0, + email: user.emailChangeCandidate, + emailChangeCandidate: '' + }) + this.req.session.userId = user.id + return '/' + } else { + throw new Error( + `Consistency violation: User ${user.id} has an email proof token, but somehow also has an emailStatus of "${user.emailStatus}"! (This should never happen.)` + ) + } + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/view-check-email.js b/templates/mellow-svelte/api/controllers/auth/view-check-email.js new file mode 100644 index 00000000..025df714 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/view-check-email.js @@ -0,0 +1,21 @@ +module.exports = { + friendlyName: 'View verify email', + + description: 'Display "Verify email" page.', + + exits: { + success: { + responseType: 'inertia' + } + }, + + fn: async function () { + let message = `We sent a link to the email address you provided. Please check your inbox and follow the instructions.` + return { + page: 'auth/check-email', + props: { + message + } + } + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/view-forgot-password.js b/templates/mellow-svelte/api/controllers/auth/view-forgot-password.js new file mode 100644 index 00000000..c56d150d --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/view-forgot-password.js @@ -0,0 +1,15 @@ +module.exports = { + friendlyName: 'View forgot password', + + description: 'Display "Forgot password" page.', + + exits: { + success: { + responseType: 'inertia' + } + }, + + fn: async function () { + return { page: 'auth/forgot-password' } + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/view-link-expired.js b/templates/mellow-svelte/api/controllers/auth/view-link-expired.js new file mode 100644 index 00000000..a82cf1e3 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/view-link-expired.js @@ -0,0 +1,15 @@ +module.exports = { + friendlyName: 'View link expired', + + description: 'Display "Link expired" page.', + + exits: { + success: { + responseType: 'inertia' + } + }, + + fn: async function () { + return { page: 'link-expired' } + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/view-login.js b/templates/mellow-svelte/api/controllers/auth/view-login.js new file mode 100644 index 00000000..55acb996 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/view-login.js @@ -0,0 +1,15 @@ +module.exports = { + friendlyName: 'View login', + + description: 'Display "Login" page.', + + exits: { + success: { + responseType: 'inertia' + } + }, + + fn: async function () { + return { page: 'auth/login' } + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/view-reset-password.js b/templates/mellow-svelte/api/controllers/auth/view-reset-password.js new file mode 100644 index 00000000..2b565f4b --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/view-reset-password.js @@ -0,0 +1,33 @@ +module.exports = { + friendlyName: 'View reset password', + + description: 'Display "Reset password" page.', + + inputs: { + token: { + description: 'The reset token from the email.', + example: 'lyCap0N9i8wKYz7rhrEPog' + } + }, + exits: { + success: { + responseType: 'inertia' + }, + invalidOrExpiredToken: { + responseType: 'expired', + description: 'The provided token is expired, invalid, or already used up.' + } + }, + + fn: async function ({ token }) { + if (!token) { + throw 'invalidOrExpiredToken' + } + const user = await User.findOne({ passwordResetToken: token }) + + if (!user || user.passwordResetTokenExpiresAt <= Date.now()) { + throw 'invalidOrExpiredToken' + } + return { page: 'auth/reset-password', props: { token } } + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/view-signup.js b/templates/mellow-svelte/api/controllers/auth/view-signup.js new file mode 100644 index 00000000..c63f6846 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/view-signup.js @@ -0,0 +1,14 @@ +module.exports = { + friendlyName: 'View signup', + + description: 'Display "Signup" page.', + exits: { + success: { + responseType: 'inertia' + } + }, + + fn: async function () { + return { page: 'auth/signup' } + } +} diff --git a/templates/mellow-svelte/api/controllers/auth/view-success.js b/templates/mellow-svelte/api/controllers/auth/view-success.js new file mode 100644 index 00000000..eec6ef54 --- /dev/null +++ b/templates/mellow-svelte/api/controllers/auth/view-success.js @@ -0,0 +1,38 @@ +module.exports = { + friendlyName: 'View success email', + + description: 'Display "Success" page.', + inputs: { + operation: { + isIn: ['verify-email', 'reset-password'] + } + }, + exits: { + success: { + responseType: 'inertia' + } + }, + + fn: async function ({ operation }) { + let message + const pageTitle = `${sails.helpers.capitalize(operation)}` + let pageHeading + switch (operation) { + case 'verify-email': + message = 'Email has been successfully verified' + pageHeading = 'Email verification successful' + break + case 'reset-password': + message = 'Password has been successful reset' + pageHeading = 'Password reset successful' + } + return { + page: 'auth/success', + props: { + pageTitle, + pageHeading, + message + } + } + } +} diff --git a/templates/mellow-svelte/api/controllers/dashboard/view-dashboard.js b/templates/mellow-svelte/api/controllers/dashboard/view-dashboard.js new file mode 100644 index 00000000..c1420876 --- /dev/null +++ b/templates/mellow-svelte/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-svelte/api/controllers/example/index.js b/templates/mellow-svelte/api/controllers/example/index.js deleted file mode 100644 index 118af332..00000000 --- a/templates/mellow-svelte/api/controllers/example/index.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - friendlyName: 'Example', - - description: 'Example index.', - - inputs: {}, - - exits: { - success: { - responseType: 'inertia' - } - }, - - fn: async function () { - return { - page: 'example', - props: { - quote: "You can shine no matter what you're made of - Bigweld" - } - } - } -} diff --git a/templates/mellow-svelte/api/controllers/home/index.js b/templates/mellow-svelte/api/controllers/home/view-home.js similarity index 68% rename from templates/mellow-svelte/api/controllers/home/index.js rename to templates/mellow-svelte/api/controllers/home/view-home.js index 5aee250e..93bdb1d5 100644 --- a/templates/mellow-svelte/api/controllers/home/index.js +++ b/templates/mellow-svelte/api/controllers/home/view-home.js @@ -12,11 +12,6 @@ module.exports = { }, fn: async function () { - return { - page: 'index', - props: { - name: 'Inertia' - } - } + return { page: 'index' } } } diff --git a/templates/mellow-svelte/api/controllers/user/delete-profile.js b/templates/mellow-svelte/api/controllers/user/delete-profile.js new file mode 100644 index 00000000..51448e7d --- /dev/null +++ b/templates/mellow-svelte/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-svelte/api/controllers/user/logout.js b/templates/mellow-svelte/api/controllers/user/logout.js new file mode 100644 index 00000000..73dcb67e --- /dev/null +++ b/templates/mellow-svelte/api/controllers/user/logout.js @@ -0,0 +1,19 @@ +module.exports = { + friendlyName: 'Logout', + + description: 'Logout user.', + + inputs: {}, + + exits: { + success: { + responseType: 'inertiaRedirect' + } + }, + + fn: async function () { + sails.inertia.flushShared('loggedInUser') + delete this.req.session.userId + return '/login' + } +} diff --git a/templates/mellow-svelte/api/controllers/user/update-profile.js b/templates/mellow-svelte/api/controllers/user/update-profile.js new file mode 100644 index 00000000..67aac57e --- /dev/null +++ b/templates/mellow-svelte/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.' + }, + confirmPassword: { + 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, + confirmPassword + }) { + 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 !== confirmPassword) { + 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-svelte/api/controllers/user/view-profile.js b/templates/mellow-svelte/api/controllers/user/view-profile.js new file mode 100644 index 00000000..8d1727ea --- /dev/null +++ b/templates/mellow-svelte/api/controllers/user/view-profile.js @@ -0,0 +1,15 @@ +module.exports = { + friendlyName: 'View profile', + + description: 'Display "Profile" page.', + + exits: { + success: { + responseType: 'inertia' + } + }, + + fn: async function () { + return { page: 'dashboard/profile' } + } +}