diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index e45e3f095..5dabc5b6f 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -96,8 +96,6 @@ class UserDetail extends React.Component { bulkReject, } = this.props; - console.log(rejectedComments, totalComments); - // if totalComments is 0, you're dividing by zero let rejectedPercent = rejectedComments / totalComments * 100; diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 95595c265..3874e6c94 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -291,5 +291,36 @@ export default { }, }, }), + SetCommentStatus: ({ variables: { status } }) => ({ + updateQueries: { + CoralAdmin_UserDetail: prev => { + const increment = { + rejectedComments: { + $apply: count => (count < prev.totalComments ? count + 1 : count), + }, + }; + + const decrement = { + rejectedComments: { + $apply: count => (count > 0 ? count - 1 : 0), + }, + }; + + // If rejected then increment rejectedComments by one + if (status === 'REJECTED') { + const updated = update(prev, increment); + return updated; + } + + // If approved then decrement rejectedComments by one + if (status === 'ACCEPTED') { + const updated = update(prev, decrement); + return updated; + } + + return prev; + }, + }, + }), }, }; diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js index bf75fa1a8..778214b8b 100644 --- a/client/coral-framework/graphql/fragments.js +++ b/client/coral-framework/graphql/fragments.js @@ -27,6 +27,7 @@ export default { 'UpdateAssetStatusResponse', 'UpdateSettingsResponse', 'ChangePasswordResponse', - 'UpdateEmailAddressResponse' + 'UpdateEmailAddressResponse', + 'AttachLocalAuthResponse' ), }; diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 228911d4a..995ca1720 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -1,6 +1,5 @@ import { gql } from 'react-apollo'; import withMutation from '../hocs/withMutation'; -import update from 'immutability-helper'; function convertItemType(item_type) { switch (item_type) { @@ -168,36 +167,6 @@ export const withSetCommentStatus = withMutation( errors: null, }, }, - updateQueries: { - CoralAdmin_UserDetail: prev => { - const increment = { - rejectedComments: { - $apply: count => - count < prev.totalComments ? count + 1 : count, - }, - }; - - const decrement = { - rejectedComments: { - $apply: count => (count > 0 ? count - 1 : 0), - }, - }; - - // If rejected then increment rejectedComments by one - if (status === 'REJECTED') { - const updated = update(prev, increment); - return updated; - } - - // If approved then decrement rejectedComments by one - if (status === 'ACCEPTED') { - const updated = update(prev, decrement); - return updated; - } - - return prev; - }, - }, update: proxy => { const fragment = gql` fragment Talk_SetCommentStatus_Comment on Comment { diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index d33e7acc9..d7ca5fce9 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -27,6 +27,5 @@ export { withSetCommentStatus, withChangePassword, withChangeUsername, - withUpdateEmailAddress, } from 'coral-framework/graphql/mutations'; export { compose } from 'recompose'; diff --git a/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.css b/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.css new file mode 100644 index 000000000..039ffae30 --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.css @@ -0,0 +1,8 @@ +.errorMsg { + color: #FA4643; + font-size: 0.9em; +} + +.warningIcon { + color: #FA4643; +} diff --git a/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.js b/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.js new file mode 100644 index 000000000..f39a8fc08 --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.js @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './ErrorMessage.css'; +import { Icon } from 'plugin-api/beta/client/components/ui'; + +const ErrorMessage = ({ children }) => ( +
+ + {children} +
+); + +ErrorMessage.propTypes = { + children: PropTypes.node, +}; + +export default ErrorMessage; diff --git a/plugins/talk-plugin-facebook-auth/client/components/InputField.css b/plugins/talk-plugin-facebook-auth/client/components/InputField.css new file mode 100644 index 000000000..3442befde --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/components/InputField.css @@ -0,0 +1,80 @@ + +.detailItem { + margin-bottom: 12px; +} + +.detailItemContainer { + display: flex; +} + +.columnDisplay { + flex-direction: column; + + .detailItemMessage { + padding: 4px 0 0; + } +} + +.detailItemContent { + border: solid 1px #787D80; + border-radius: 2px; + background-color: white; + height: 30px; + display: inline-block; + width: 230px; + display: flex; + box-sizing: border-box; + + > .detailIcon { + font-size: 1.2em; + padding: 0 5px; + color: #787D80; + line-height: 30px; + } + + &.error { + border: solid 2px #FA4643; + } + + &.disabled { + background-color: #E0E0E0; + } +} + +.detailLabel { + color: #4C4C4D; + font-size: 1em; + display: block; + margin-bottom: 4px; +} + +.detailValue { + background: transparent; + border: none; + font-size: 1em; + color: #000; + outline: none; + flex: 1; + height: 100%; + box-sizing: border-box; +} + +.detailItemMessage { + flex-grow: 1; + display: flex; + align-items: center; + padding-left: 6px; + padding-top: 16px; + + .warningIcon, .checkIcon { + font-size: 17px; + } +} + +.checkIcon { + color: #00CD73; +} + +.warningIcon { + color: #FA4643; +} diff --git a/plugins/talk-plugin-facebook-auth/client/components/InputField.js b/plugins/talk-plugin-facebook-auth/client/components/InputField.js new file mode 100644 index 000000000..26937d5b9 --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/components/InputField.js @@ -0,0 +1,94 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import styles from './InputField.css'; +import ErrorMessage from './ErrorMessage'; +import { Icon } from 'plugin-api/beta/client/components/ui'; + +const InputField = ({ + id = '', + label = '', + type = 'text', + name = '', + onChange = () => {}, + showError = true, + hasError = false, + errorMsg = '', + children, + columnDisplay = false, + showSuccess = false, + validationType = '', + icon = '', + value = '', + defaultValue = '', + disabled = false, +}) => { + const inputValue = { + ...(value ? { value } : {}), + ...(defaultValue ? { defaultValue } : {}), + }; + + return ( +
+
+ {label && ( + + )} +
+ {icon && } + +
+
+ {!hasError && + showSuccess && + value && } + {hasError && showError && {errorMsg}} +
+
+ {children} +
+ ); +}; + +InputField.propTypes = { + id: PropTypes.string, + disabled: PropTypes.bool, + label: PropTypes.string, + type: PropTypes.string, + name: PropTypes.string.isRequired, + onChange: PropTypes.func, + value: PropTypes.string, + defaultValue: PropTypes.string, + icon: PropTypes.string, + showError: PropTypes.bool, + hasError: PropTypes.bool, + errorMsg: PropTypes.string, + children: PropTypes.node, + columnDisplay: PropTypes.bool, + showSuccess: PropTypes.bool, + validationType: PropTypes.string, +}; + +export default InputField; diff --git a/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.css b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.css new file mode 100644 index 000000000..41b531147 --- /dev/null +++ b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.css @@ -0,0 +1,82 @@ +.dialog { + border: none; + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); + width: 320px; + top: 50%; + transform: translateY(-50%); + padding: 20px; + border-radius: 4px; + font-family: Helvetica,Helvetica Neue,Verdana,sans-serif; + color:#3B4A53; +} + +.title { + font-size: 1.3em; + margin: 15px 0; + text-align: center; +} + +.description { + font-size: 1em; + line-height: 20px; + margin: 0; + margin-bottom: 15px; +} + +.list { + padding: 0; + margin: 20px 0; + list-style: none; +} + +.item { + display: flex; + margin-bottom: 20px; + + .itemIcon { + flex-grow: 0; + } + + .text { + flex-grow: 1; + padding-left: 10px; + } + + > i.itemIcon { + font-size: 1.3em; + } +} + +.button { + color: #787D80; + border-radius: 2px; + background-color: transparent; + height: 30px; + font-size: 0.9em; + line-height: normal; + width: 100%; + display: inline-block; + text-align: center; + line-height: 30px; + font-size: 1em; + + &:hover { + background-color: #eaeaea; + cursor: pointer; + } + + &.cancel { + background-color: transparent; + color: #787D80; + } + + &.proceed { + background-color: #3498DB; + color: white; + } + + &.danger { + background-color: #FA4643; + color: white; + } +} \ No newline at end of file diff --git a/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js new file mode 100644 index 000000000..691915c0f --- /dev/null +++ b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js @@ -0,0 +1,153 @@ +import React from 'react'; +import isMatch from 'lodash/isEqual'; +import isEqualWith from 'lodash/isEqual'; +import PropTypes from 'prop-types'; +import { Dialog } from 'plugin-api/beta/client/components/ui'; +import validate from 'coral-framework/helpers/validate'; +import errorMsj from 'coral-framework/helpers/error'; +import { getErrorMessages } from 'coral-framework/utils'; +import styles from './AddEmailAddressDialog.css'; + +import AddEmailContent from './AddEmailContent'; +import VerifyEmailAddress from './VerifyEmailAddress'; +import EmailAddressAdded from './EmailAddressAdded'; + +const initialState = { + step: 0, + showErrors: false, + errors: {}, + formData: {}, +}; + +class AddEmailAddressDialog extends React.Component { + state = initialState; + validKeys = ['emailAddress', 'confirmPassword', 'confirmEmailAddress']; + + onChange = e => { + const { name, value, type } = e.target; + this.setState( + state => ({ + formData: { + ...state.formData, + [name]: value, + }, + }), + () => { + this.fieldValidation(value, type, name); + } + ); + }; + + fieldValidation = (value, type, name) => { + if (!value.length) { + this.addError({ + [name]: 'Field is required', + }); + } else if (!validate[type](value)) { + this.addError({ [name]: errorMsj[type] }); + } else { + this.removeError(name); + } + }; + + addError = err => { + this.setState(({ errors }) => ({ + errors: { ...errors, ...err }, + })); + }; + + removeError = errKey => { + this.setState(state => { + const { [errKey]: _, ...errors } = state.errors; + return { + errors, + }; + }); + }; + + hasError = err => { + return Object.keys(this.state.errors).indexOf(err) !== -1; + }; + + formHasError = () => { + const formHasErrors = !!Object.keys(this.state.errors).length; + const formIncomplete = !isEqualWith( + Object.keys(this.state.formData), + this.validKeys, + isMatch + ); + + return formHasErrors || formIncomplete; + }; + + showErrors = () => { + this.setState({ + showErrors: true, + }); + }; + + confirmChanges = async () => { + if (this.formHasError()) { + this.showErrors(); + return; + } + + const { emailAddress, confirmPassword } = this.state.formData; + const { attachLocalAuth } = this.props; + + try { + await attachLocalAuth({ + email: emailAddress, + password: confirmPassword, + }); + this.props.notify('success', 'Email Added!'); + this.goToNextStep(); + } catch (err) { + this.props.notify('error', getErrorMessages(err)); + } + }; + + goToNextStep = () => { + this.setState(({ step }) => ({ + step: step + 1, + })); + }; + + render() { + const { errors, formData, showErrors, step } = this.state; + const { root: { settings } } = this.props; + + return ( + + {step === 0 && ( + + )} + {step === 1 && + !settings.requireEmailConfirmation && ( + {}} /> + )} + {step === 1 && + settings.requireEmailConfirmation && ( + {}} + /> + )} + + ); + } +} + +AddEmailAddressDialog.propTypes = { + attachLocalAuth: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, + root: PropTypes.object.isRequired, +}; + +export default AddEmailAddressDialog; diff --git a/plugins/talk-plugin-local-auth/client/components/AddEmailContent.js b/plugins/talk-plugin-local-auth/client/components/AddEmailContent.js new file mode 100644 index 000000000..4a2ce7430 --- /dev/null +++ b/plugins/talk-plugin-local-auth/client/components/AddEmailContent.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './AddEmailAddressDialog.css'; +import { Icon } from 'plugin-api/beta/client/components/ui'; +import cn from 'classnames'; +import InputField from './InputField'; +import { t } from 'plugin-api/beta/client/services'; + +const AddEmailContent = ({ + formData, + errors, + showErrors, + confirmChanges, + onChange, +}) => ( +
+

+ {t('talk-plugin-local-auth.add_email.content.title')} +

+

+ {t('talk-plugin-local-auth.add_email.content.description')} +

+ + +
+ + + + + +
+); + +AddEmailContent.propTypes = { + formData: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + showErrors: PropTypes.bool.isRequired, + confirmChanges: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default AddEmailContent; diff --git a/plugins/talk-plugin-local-auth/client/components/EmailAddressAdded.js b/plugins/talk-plugin-local-auth/client/components/EmailAddressAdded.js new file mode 100644 index 000000000..6d5775f3c --- /dev/null +++ b/plugins/talk-plugin-local-auth/client/components/EmailAddressAdded.js @@ -0,0 +1,32 @@ +import React from 'react'; +import cn from 'classnames'; +import PropTypes from 'prop-types'; +import styles from './AddEmailAddressDialog.css'; +import { t } from 'plugin-api/beta/client/services'; + +const EmailAddressAdded = ({ done }) => ( +
+

+ {t('talk-plugin-local-auth.add_email.added.title')} +

+

+ {t('talk-plugin-local-auth.add_email.added.description')} +

+ {t('talk-plugin-local-auth.add_email.added.subtitle')} +

+ {t('talk-plugin-local-auth.add_email.added.description_2')}{' '} + {t('talk-plugin-local-auth.add_email.added.path')}. +

+
+ + {t('talk-plugin-local-auth.add_email.done')} + +
+
+); + +EmailAddressAdded.propTypes = { + done: PropTypes.func.isRequired, +}; + +export default EmailAddressAdded; diff --git a/plugins/talk-plugin-local-auth/client/components/InputField.css b/plugins/talk-plugin-local-auth/client/components/InputField.css index d0dc51494..fae31c80a 100644 --- a/plugins/talk-plugin-local-auth/client/components/InputField.css +++ b/plugins/talk-plugin-local-auth/client/components/InputField.css @@ -62,6 +62,7 @@ flex: 1; height: 100%; box-sizing: border-box; + padding: 0 6px; } .detailItemMessage { @@ -81,4 +82,4 @@ .warningIcon { color: #FA4643; -} \ No newline at end of file +} diff --git a/plugins/talk-plugin-local-auth/client/components/InputField.js b/plugins/talk-plugin-local-auth/client/components/InputField.js index 944f5e7cf..930c15582 100644 --- a/plugins/talk-plugin-local-auth/client/components/InputField.js +++ b/plugins/talk-plugin-local-auth/client/components/InputField.js @@ -44,7 +44,7 @@ const InputField = ({
diff --git a/plugins/talk-plugin-local-auth/client/components/VerifyEmailAddress.js b/plugins/talk-plugin-local-auth/client/components/VerifyEmailAddress.js new file mode 100644 index 000000000..cc1ff3626 --- /dev/null +++ b/plugins/talk-plugin-local-auth/client/components/VerifyEmailAddress.js @@ -0,0 +1,28 @@ +import React from 'react'; +import cn from 'classnames'; +import PropTypes from 'prop-types'; +import styles from './AddEmailAddressDialog.css'; +import { t } from 'plugin-api/beta/client/services'; + +const VerifyEmailAddress = ({ emailAddress, done }) => ( +
+

+ {t('talk-plugin-local-auth.add_email.verify.title')} +

+

+ {t('talk-plugin-local-auth.add_email.verify.description', emailAddress)} +

+
+ + {t('talk-plugin-local-auth.add_email.done')} + +
+
+); + +VerifyEmailAddress.propTypes = { + emailAddress: PropTypes.string.isRequired, + done: PropTypes.func.isRequired, +}; + +export default VerifyEmailAddress; diff --git a/plugins/talk-plugin-local-auth/client/containers/AddEmailAddressDialog.js b/plugins/talk-plugin-local-auth/client/containers/AddEmailAddressDialog.js new file mode 100644 index 000000000..841dc8420 --- /dev/null +++ b/plugins/talk-plugin-local-auth/client/containers/AddEmailAddressDialog.js @@ -0,0 +1,30 @@ +import { compose, gql } from 'react-apollo'; +import { bindActionCreators } from 'redux'; +import { connect, withFragments, excludeIf } from 'plugin-api/beta/client/hocs'; +import AddEmailAddressDialog from '../components/AddEmailAddressDialog'; +import { notify } from 'coral-framework/actions/notification'; + +import { withAttachLocalAuth } from '../hocs'; + +const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch); + +const withData = withFragments({ + root: gql` + fragment TalkPluginLocalAuth_AddEmailAddressDialog_root on RootQuery { + me { + id + email + } + settings { + requireEmailConfirmation + } + } + `, +}); + +export default compose( + connect(null, mapDispatchToProps), + withAttachLocalAuth, + withData, + excludeIf(({ root: { me } }) => !me || me.email) +)(AddEmailAddressDialog); diff --git a/plugins/talk-plugin-local-auth/client/hocs/index.js b/plugins/talk-plugin-local-auth/client/hocs/index.js index bcd439af2..573fa6b10 100644 --- a/plugins/talk-plugin-local-auth/client/hocs/index.js +++ b/plugins/talk-plugin-local-auth/client/hocs/index.js @@ -1,6 +1,50 @@ +import { withMutation } from 'plugin-api/beta/client/hocs'; import { gql } from 'react-apollo'; import update from 'immutability-helper'; -import withMutation from 'coral-framework/hocs/withMutation'; + +export const withAttachLocalAuth = withMutation( + gql` + mutation AttachLocalAuth($input: AttachLocalAuthInput!) { + attachLocalAuth(input: $input) { + ...AttachLocalAuthResponse + } + } + `, + { + props: ({ mutate }) => ({ + attachLocalAuth: input => { + return mutate({ + variables: { + input, + }, + update: proxy => { + const AttachLocalAuthQuery = gql` + query Talk_AttachLocalAuth { + me { + id + email + } + } + `; + + const prev = proxy.readQuery({ query: AttachLocalAuthQuery }); + + const data = update(prev, { + me: { + email: { $set: input.email }, + }, + }); + + proxy.writeQuery({ + query: AttachLocalAuthQuery, + data, + }); + }, + }); + }, + }), + } +); export const withUpdateEmailAddress = withMutation( gql` diff --git a/plugins/talk-plugin-local-auth/client/index.js b/plugins/talk-plugin-local-auth/client/index.js index d5f9f8636..9c89e0490 100644 --- a/plugins/talk-plugin-local-auth/client/index.js +++ b/plugins/talk-plugin-local-auth/client/index.js @@ -1,4 +1,5 @@ import ChangePassword from './containers/ChangePassword'; +import AddEmailAddressDialog from './containers/AddEmailAddressDialog'; import Profile from './containers/Profile'; import translations from './translations.yml'; import graphql from './graphql'; @@ -8,6 +9,7 @@ export default { slots: { profileHeader: [Profile], profileSettings: [ChangePassword], + stream: [AddEmailAddressDialog], }, ...graphql, }; diff --git a/plugins/talk-plugin-local-auth/client/translations.yml b/plugins/talk-plugin-local-auth/client/translations.yml index 0c3216009..b41ad5302 100644 --- a/plugins/talk-plugin-local-auth/client/translations.yml +++ b/plugins/talk-plugin-local-auth/client/translations.yml @@ -36,6 +36,29 @@ en: change_email_msg: "Email Address Changed - Your email address has been successfully changed. This email address will now be used for signing in and email notifications." changed_username_success_msg: "Username Changed - Your username has been successfully changed. You will not be able to change your user name for 14 days." change_username_attempt: "Username can't be updated. Usernames can only be changed every 14 days." + add_email: + add_email_address: "Add Email Address" + enter_email_address: "Enter Email Address:" + invalid_email_address: "Invalid Email address" + confirm_email_address: "Confirm Email Address:" + email_does_not_match: "Email Address does not match" + insert_password: "Insert Password:" + done: "done" + content: + title: "Add an Email Address" + description: "For your added security, we require users to add an email address to their accounts. Your email address will be used to:" + item_1: "Receive updates regarding any changes to your account (email address, username, password, etc.)" + item_2: "Allow you to download your comments." + item_3: "Send comment notifications that you have chosen to receive." + verify: + title: "Verify Your Email Address" + description: "We’ve sent an email to {0} to verify your account. You must verify your email address so that it can be used for account change confirmations and notifications." + added: + title: "Email Address Added" + description: "Your email address has been added to your account." + subtitle: "Need to change your email address?" + description_2: "You can change your account settings by visiting" + path: "My Profile > Settings" es: talk-plugin-local-auth: change_password: diff --git a/plugins/talk-plugin-local-auth/server/mutators.js b/plugins/talk-plugin-local-auth/server/mutators.js index 6798584f5..389574d2d 100644 --- a/plugins/talk-plugin-local-auth/server/mutators.js +++ b/plugins/talk-plugin-local-auth/server/mutators.js @@ -5,7 +5,6 @@ const { ErrIncorrectPassword, } = require('./errors'); const { get } = require('lodash'); -const bcrypt = require('bcryptjs'); // hasLocalProfile checks a user's profiles to see if they already have a local // profile associated with their account. @@ -89,7 +88,7 @@ async function attachUserLocalAuth(ctx, email, password) { await Users.isValidPassword(password); // Hash the new password. - const hashedPassword = await bcrypt.hash(password, 10); + const hashedPassword = await Users.hashPassword(password); try { // Associate the account with the user. diff --git a/services/users.js b/services/users.js index d5486b3f8..061fae6fb 100644 --- a/services/users.js +++ b/services/users.js @@ -560,7 +560,7 @@ class Users { throw new ErrPasswordTooShort(); } - const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); + const hashedPassword = await Users.hashPassword(password); return User.update( { id }, @@ -637,7 +637,7 @@ class Users { Users.isValidPassword(password), ]); - const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); + const hashedPassword = await Users.hashPassword(password); let user = new User({ username, @@ -814,6 +814,10 @@ class Users { return { user, redirect, version }; } + static async hashPassword(password) { + return bcrypt.hash(password, SALT_ROUNDS); + } + // TODO: update doc static async resetPassword(token, password) { const { user, redirect, version } = await this.verifyPasswordResetToken( @@ -824,7 +828,7 @@ class Users { throw new ErrPasswordTooShort(); } - const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); + const hashedPassword = await Users.hashPassword(password); // Update the user's password. await User.update(