diff --git a/.eslintrc b/.eslintrc index 7751b12..3f9efd0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,17 +9,15 @@ "experimentalObjectRestSpread": true } }, - "plugins": [ - "react" - ], + "plugins": ["react"], "extends": ["eslint:recommended", "plugin:react/recommended"], - globals: { - 'Meteor': true, - 'document': true, - 'describe': true, - 'it': true + "globals": { + "Meteor": true, + "document": true, + "describe": true, + "it": true }, - rules: { + "rules": { "react/prop-types": 0 } } diff --git a/README.md b/README.md index c1028b0..d323081 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ class Authentication extends Component { + ) } @@ -149,6 +150,7 @@ Currently available states are: | resetPwd | Set a new password (After reset, a "token" prop must be passed to AccountsReactComponent) | signIn | Login form | signUp | Registration form +| resendVerification | Resend email with verification link @@ -175,12 +177,14 @@ The following is a list with details about each configurable option. | lowercaseUsername | Boolean | false | Transform username field to lowercase upon registration | loginAfterSignup | Boolean | true | Login automatically after sign up | overrideLoginErrors | Boolean | true | Show general error on failed login (without specifying which field was wrong) -| sendVerificationEmail | Boolean | false | Send email verification after successful registration +| sendVerificationEmail | Boolean | true | Send email verification after successful registration | setDenyRules | Boolean | true | Apply default deny rules on Meteor.users collection +| disableConfigureLoginService | Boolean | true | Disable `configureLoginService()` insecure method | **Appearance** | | | | hideSignInLink | Boolean | false | When set to true, asks to never show the link to the sign in page | hideSignUpLink | Boolean | false | When set to true, asks to never show the link to the sign up page | showForgotPasswordLink | Boolean | false | Specifies whether to display a link to the forgot password page/form +| showResendVerificationLink | Boolean | false | Specifies whether to display a link to the resend verification page/form | showLabels | Boolean | true | Specifies whether to display text labels above input elements | showPlaceholders | Boolean | true | Specifies whether to display place-holder text inside input elements | **Client side validation** | | | @@ -351,7 +355,8 @@ The default object used is the following signUp: '/sign-up', forgotPwd: '/forgot-password', changePwd: '/change-password', - resetPwd: '/reset-password' + resetPwd: '/reset-password', + resendVerification: '/resend-verification' } ``` @@ -391,6 +396,7 @@ The supported properties are listed in the following table. | placeholder | String | | The field's (input) placeholder text. The place-holder is shown only if showPlaceholders option is set to true | re | RegExp | | Specify a regular expression to validate against. (example below) | required | Boolean | | If set to true the corresponding field cannot be left blank +| autocomplete | String | | `` autocomplete tag value **The original user accounts package supports several more properties. Pull requests are more then welcome!** @@ -463,7 +469,8 @@ AccountsReact.addFields('signUp', [ minLength: 4, maxLength: 70, required: true, - errStr: 'This field must contain at least 4 characters and no more than 70' + errStr: 'This field must contain at least 4 characters and no more than 70', + autocomplete: 'name' } ]) ``` diff --git a/lib/AccountsReact.js b/lib/AccountsReact.js index 449134d..c966016 100644 --- a/lib/AccountsReact.js +++ b/lib/AccountsReact.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor' import { Accounts } from 'meteor/accounts-base' import regExp from './utils/regExp' import merge from './utils/deepmerge' +import './AccountsReactComponent/methods' class AccountsReact_ { constructor () { @@ -15,7 +16,7 @@ class AccountsReact_ { confirmPassword: true, defaultState: 'signIn', disableForgotPassword: false, - enablePasswordChange: false, + enablePasswordChange: true, focusFirstInput: !Meteor.isCordova, forbidClientAccountCreation: false, lowercaseUsername: false, @@ -24,6 +25,7 @@ class AccountsReact_ { passwordSignupFields: 'EMAIL_ONLY', sendVerificationEmail: true, setDenyRules: true, + disableConfigureLoginService: true, /* ----------------------------- Appearance @@ -32,6 +34,7 @@ class AccountsReact_ { hideSignInLink: false, hideSignUpLink: false, showForgotPasswordLink: false, + showResendVerificationLink: false, showLabels: true, showPlaceholders: true, @@ -63,11 +66,12 @@ class AccountsReact_ { ----------------------------- */ mapStateToRoute: { - signIn: '/sign-in', - signUp: '/sign-up', - forgotPwd: '/forgot-password', - changePwd: '/change-password', - resetPwd: '/reset-password' + signIn: '/sign-in', + signUp: '/sign-up', + forgotPwd: '/forgot-password', + changePwd: '/change-password', + resetPwd: '/reset-password', + resendVerification: '/resend-verification' }, @@ -95,7 +99,8 @@ class AccountsReact_ { _id: 'password', displayName: 'Password', type: 'password', - placeholder: 'Enter your password' + placeholder: 'Enter your password', + autocomplete: 'current-password' } ], @@ -125,7 +130,8 @@ class AccountsReact_ { placeholder: 'Enter your password', minLength: 6, maxLength: 32, - errStr: 'Please enter a strong password between 6 and 32 characters' + errStr: 'Please enter a strong password between 6 and 32 characters', + autocomplete: 'new-password' }, { _id: 'confirmPassword', @@ -134,6 +140,7 @@ class AccountsReact_ { placeholder: 'Re-enter your password', errStr: 'Password doesn\'t match', exclude: true, + autocomplete: 'new-password', func: (fields, fieldObj, value, model, errorsArray) => { if (!this.config.confirmPassword) { return true @@ -174,7 +181,8 @@ class AccountsReact_ { _id: 'currentPassword', displayName: 'Current password', type: 'password', - placeholder: 'Enter your current password' + placeholder: 'Enter your current password', + autocomplete: 'current-password' }, { _id: 'password', @@ -183,7 +191,8 @@ class AccountsReact_ { placeholder: 'Enter a new password', minLength: 6, maxLength: 32, - errStr: 'Please enter a strong password between 6 and 32 characters' + errStr: 'Please enter a strong password between 6 and 32 characters', + autocomplete: 'new-password' } ], @@ -194,7 +203,20 @@ class AccountsReact_ { _id: 'password', displayName: 'New password', type: 'password', - placeholder: 'Enter a new password' + placeholder: 'Enter a new password', + autocomplete: 'new-password' + } + ], + + /* Resend email verification */ + + resendVerification: [ + { + _id: 'email', + displayName: 'Email', + placeholder: 'Enter your email', + re: regExp.Email, + errStr: 'Please enter a valid email' } ] }, @@ -209,14 +231,16 @@ class AccountsReact_ { forgotPwd: 'Send Reset Link', resetPwd: 'Save New Password', signIn: 'Login', - signUp: 'Register' + signUp: 'Register', + resendVerification: 'Send Verification Link' }, title: { changePwd: 'Change Password', forgotPwd: 'Forgot Password', resetPwd: 'Reset Password', signIn: 'Login', - signUp: 'Create Your Account' + signUp: 'Create Your Account', + resendVerification: 'Resend Verification Link' }, links: { toChangePwd: 'Change your password', @@ -224,7 +248,7 @@ class AccountsReact_ { toForgotPwd: 'Forgot your password?', toSignIn: 'Already have an account? Sign in!', toSignUp: 'Don\'t have an account? Register', - toResendVerification: 'Resend email verification' + toResendVerification: 'Verification email lost? Resend' }, info: { emailSent: 'An email has been sent to your inbox', @@ -237,7 +261,9 @@ class AccountsReact_ { }, errors: { loginForbidden: 'There was a problem with your login', - captchaVerification: 'There was a problem with the recaptcha verification, please try again' + captchaVerification: 'There was a problem with the recaptcha verification, please try again', + userNotFound: 'User not found', + userAlreadyVerified: 'User already verified!' }, forgotPwdSubmitSuccess: 'A password reset link has been sent to your email!', loginForbiddenMessage: 'There was a problem with your login' @@ -268,7 +294,7 @@ class AccountsReact_ { this.loadReCaptcha() this.setAccountCreationPolicy() this.overrideLoginErrors() - this.disableForgotPassword() + this.disableMethods() this.setDenyRules() this._init = true @@ -378,10 +404,39 @@ class AccountsReact_ { } } - disableForgotPassword () { - if (this.config.disableForgotPassword && Meteor.isServer) { - Meteor.server.method_handlers.forgotPassword = () => { // Override forgotPassword method directly. - throw new Meteor.Error('Forgot password is disabled') + disableMethods () { + if (Meteor.isServer) { + // Override methods directly. + if (this.config.disableForgotPassword) { + Meteor.server.method_handlers.forgotPassword = () => { + throw new Meteor.Error('forgotPassword is disabled') + } + + Meteor.server.method_handlers.resetPassword = () => { + throw new Meteor.Error('resetPassword is disabled') + } + } + + if (!this.config.enablePasswordChange) { + Meteor.server.method_handlers.changePassword = () => { + throw new Meteor.Error('changePassword is disabled') + } + } + + if (!this.config.sendVerificationEmail) { + Meteor.server.method_handlers.verifyEmail = () => { + throw new Meteor.Error('verifyEmail is disabled') + } + + Accounts.sendVerificationEmail = () => { + throw new Meteor.Error('disabled', 'sendVerificationEmail is disabled') + } + } + + if (this.config.disableConfigureLoginService) { + Meteor.server.method_handlers.configureLoginService = () => { + throw new Meteor.Error('configureLoginService is disabled') + } } } } diff --git a/lib/AccountsReactComponent/forgotPwd.js b/lib/AccountsReactComponent/forgotPwd.js index ba35b33..15aaa81 100644 --- a/lib/AccountsReactComponent/forgotPwd.js +++ b/lib/AccountsReactComponent/forgotPwd.js @@ -46,7 +46,7 @@ class ForgotPassword extends Component { errors={errors} /> - {emailSent &&

{texts.info.emailSent}

} + {emailSent &&

{texts.forgotPwdSubmitSuccess}

} {!hideSignInLink && (
diff --git a/lib/AccountsReactComponent/index.js b/lib/AccountsReactComponent/index.js index 036cb07..720f472 100644 --- a/lib/AccountsReactComponent/index.js +++ b/lib/AccountsReactComponent/index.js @@ -5,6 +5,7 @@ import SignUp from './signUp' import ForgotPwd from './forgotPwd' import ChangePwd from './changePwd' import ResetPwd from './resetPwd' +import ResendVerification from './resendVerification' import AccountsReact from '../AccountsReact' import merge from '../utils/deepmerge' @@ -16,7 +17,7 @@ class AccountsReactComponent extends React.Component { render () { ensureComponentsExist() - + // State priority -> 1.internal 2. provided by route/state prop (from parent component) 3. default state from config let state = this.state.internalState || this.props.state if (!state) { @@ -37,6 +38,7 @@ class AccountsReactComponent extends React.Component { case 'forgotPwd': form = ForgotPwd; break; case 'changePwd': form = ChangePwd; break; case 'resetPwd': form = ResetPwd; break; + case 'resendVerification': form = ResendVerification; break; default: return null } @@ -45,7 +47,11 @@ class AccountsReactComponent extends React.Component { this.props.config ]) - if (!defaults.enablePasswordChange && state === 'changePwd') { + if ((defaults.forbidClientAccountCreation && state === 'signUp') || + (defaults.disableForgotPassword && (state === 'forgotPwd' || state === 'resetPwd')) || + (!defaults.enablePasswordChange && state === 'changePwd') || + (!defaults.sendVerificationEmail && state === 'resendVerification')) + { return null } diff --git a/lib/AccountsReactComponent/methods/ARResendVerificationEmail.js b/lib/AccountsReactComponent/methods/ARResendVerificationEmail.js new file mode 100644 index 0000000..86f01bf --- /dev/null +++ b/lib/AccountsReactComponent/methods/ARResendVerificationEmail.js @@ -0,0 +1,42 @@ +import { Accounts } from 'meteor/accounts-base' +import { ValidatedMethod } from 'meteor/mdg:validated-method' +import { check } from 'meteor/check' +import AccountsReact from '../../AccountsReact' + +// Based on https://github.com/meteor-useraccounts/core/blob/2e8986813b51f321f908d2f6211f6f81f76cd627/lib/server_methods.js#L124 +const ARResendVerificationEmail = new ValidatedMethod({ + name: 'ARResendVerificationEmail', + validate: ({ email }) => { + /* This validation runs on both client and server */ + + if (Meteor.userId()) { + throw new Meteor.Error('Error', 'Already logged in') + } + + check(email, String); + }, + run ({ email }) { + if (Meteor.isServer) { + var user = Meteor.users.findOne({ "emails.address": email }); + + // Send the standard error back to the client if no user exist with this e-mail + if (!user) { + throw new Meteor.Error('UserNotFound', AccountsReact.config.texts.errors.userNotFound); + } + + try { + Accounts.sendVerificationEmail(user._id); + } catch (error) { + if (error.error === 'disabled') { + throw error; + } else { + // Handle error when email already verified + // https://github.com/dwinston/send-verification-email-bug + throw new Meteor.Error('UserAlreadyVerified', AccountsReact.config.texts.errors.userAlreadyVerified); + } + } + } + } +}) + +export default ARResendVerificationEmail diff --git a/lib/AccountsReactComponent/methods/index.js b/lib/AccountsReactComponent/methods/index.js index 77c778f..e5fa84b 100644 --- a/lib/AccountsReactComponent/methods/index.js +++ b/lib/AccountsReactComponent/methods/index.js @@ -1,6 +1,7 @@ /* globals Meteor: true */ import { Accounts } from 'meteor/accounts-base' import ARCreateAccount from './ARCreateAccount' +import ARResendVerificationEmail from './ARResendVerificationEmail' import AccountsReact from '../../AccountsReact' // Create user export const createUser = (newUser, callback) => { @@ -28,3 +29,8 @@ export const changePassword = (oldPassword, newPassword, callback) => { export const resetPassword = (token, newPassword, callback) => { Accounts.resetPassword(token, newPassword, callback) } + +// Resend verification link +export const resendVerification = (email, callback) => { + ARResendVerificationEmail.call({ email }, callback) +} diff --git a/lib/AccountsReactComponent/resendVerification.js b/lib/AccountsReactComponent/resendVerification.js new file mode 100644 index 0000000..39ef29e --- /dev/null +++ b/lib/AccountsReactComponent/resendVerification.js @@ -0,0 +1,86 @@ +import React, { Component, Fragment } from 'react' +import BaseForm from './baseForm' +import { validateForm } from '../utils/' +import { getModel, redirect } from './commonUtils' +import { resendVerification } from './methods' + +class ResendVerification extends Component { + constructor () { + super() + this.state = { + emailSent: false, + errors: [] + } + + this.getModel = getModel.bind(this) + this.redirect = redirect.bind(this) + } + + render () { + const { + currentState, + defaults + } = this.props + + const { + texts, + hideSignInLink + } = defaults + + const { + errors, + emailSent + } = this.state + + const model = this.getModel() + + return ( + + + + + {emailSent &&

{texts.info.emailSent}

} + + {!hideSignInLink && ( +
+ {texts.links.toSignIn} + + )} + + ) + } + + onSubmit = () => { + // Validate form + if (!validateForm(this.getModel(), this)) return + + this.sendVerificationLink() + } + + sendVerificationLink = () => { + // Send the verification link to the desired email + + resendVerification(this.state.email, err => { + if (err) { + this.setState({ errors: [{ _id: '__globals', errStr: err.reason }], emailSent: false }) + } else { + this.setState({ errors: [], emailSent: true }) + } + + this.props.defaults.onSubmitHook(err, this.props.currentState) + }) + } + + redirectToSignIn = () => { + this.redirect('signIn', this.props.defaults.redirects.toSignIn) + } +} + +export default ResendVerification diff --git a/lib/AccountsReactComponent/signIn.js b/lib/AccountsReactComponent/signIn.js index b25bdab..6bd2663 100644 --- a/lib/AccountsReactComponent/signIn.js +++ b/lib/AccountsReactComponent/signIn.js @@ -25,7 +25,11 @@ class SignIn extends Component { const { texts, hideSignUpLink, - showForgotPasswordLink + showForgotPasswordLink, + showResendVerificationLink, + sendVerificationEmail, + forbidClientAccountCreation, + disableForgotPassword } = defaults const model = this.getModel() @@ -41,20 +45,27 @@ class SignIn extends Component { errors={this.state.errors} /> - + {!forbidClientAccountCreation && ( + + )} - {!hideSignUpLink && ( + {!forbidClientAccountCreation && !hideSignUpLink && ( {texts.links.toSignUp} )} - {showForgotPasswordLink && ( + {!disableForgotPassword && showForgotPasswordLink && ( {texts.links.toForgotPwd} )} + {sendVerificationEmail && showResendVerificationLink && ( + + {texts.links.toResendVerification} + + )} ) } @@ -88,6 +99,10 @@ class SignIn extends Component { redirectToForgotPwd = () => { this.redirect('forgotPwd', this.props.defaults.redirects.toForgotPwd) } + + redirectToResendVerification = () => { + this.redirect('resendVerification', this.props.defaults.redirects.toResendVerification) + } } const linkStyle = { diff --git a/lib/AccountsReactComponent/signUp.js b/lib/AccountsReactComponent/signUp.js index 4cc687e..a84aef9 100644 --- a/lib/AccountsReactComponent/signUp.js +++ b/lib/AccountsReactComponent/signUp.js @@ -10,7 +10,8 @@ class SignUp extends Component { constructor () { super() this.state = { - errors: [] + errors: [], + signUpSuccessful: false } this.getModel = getModel.bind(this) @@ -26,9 +27,14 @@ class SignUp extends Component { const { texts, hideSignInLink, - showReCaptcha + showReCaptcha, + sendVerificationEmail } = defaults + const { + signUpSuccessful + } = this.state + return ( + {signUpSuccessful && sendVerificationEmail &&

{texts.info.signUpVerifyEmail}

} + {!hideSignInLink && ( {texts.links.toSignIn} @@ -93,13 +101,17 @@ class SignUp extends Component { } else { this.setState({ errors: [{ _id: '__globals', errStr: err.reason }] }) } - } else if (loginAfterSignup) { - const { password } = this.getModel() - const { username, email } = newUser + } else { + this.setState({ signUpSuccessful: true }) + + if (loginAfterSignup) { + const { password } = this.getModel() + const { username, email } = newUser - login(username, email, password, err => { - if (err) { return } // ? - }) + login(username, email, password, err => { + if (err) { return } // ? + }) + } } onSubmitHook(err, this.props.currentState) diff --git a/package.js b/package.js index e2a8b30..38ec98e 100644 --- a/package.js +++ b/package.js @@ -13,7 +13,8 @@ Package.onUse(api => { 'ecmascript', 'accounts-base', 'accounts-password', - 'mdg:validated-method@1.1.0' + 'mdg:validated-method@1.1.0', + 'check' ], ['client', 'server']) api.use('react-meteor-data@0.2.16', 'client')