Skip to content

Commit

Permalink
Add suport for resending verification email
Browse files Browse the repository at this point in the history
- Tighten security overriding unused methods
- Disable rendering of unused routes
- Add option to disable unsecure method configureLoginService
  (meteor/meteor#7745)
- Fix .eslintrc json syntax
- Update docs with new features
- Still missing tests
  • Loading branch information
bolaum committed May 18, 2018
1 parent 32ab8f3 commit 658d775
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 50 deletions.
16 changes: 7 additions & 9 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class Authentication extends Component {
<Route exact path='/forgot-password' component={arState} />
<Route exact path='/change-password' component={arState} />
<Route exact path='/reset-password/:token' component={arState} />
<Route exact path='/resend-verification' component={arState} />
</Switch>
)
}
Expand Down Expand Up @@ -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

<a name='Configuration' />

Expand All @@ -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** | | |
Expand Down Expand Up @@ -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'
}
```

Expand Down Expand Up @@ -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 | | `<input>` autocomplete tag value

**The original user accounts package supports several more properties. Pull requests are more then welcome!**

Expand Down Expand Up @@ -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'
}
])
```
Expand Down
95 changes: 75 additions & 20 deletions lib/AccountsReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -15,7 +16,7 @@ class AccountsReact_ {
confirmPassword: true,
defaultState: 'signIn',
disableForgotPassword: false,
enablePasswordChange: false,
enablePasswordChange: true,
focusFirstInput: !Meteor.isCordova,
forbidClientAccountCreation: false,
lowercaseUsername: false,
Expand All @@ -24,6 +25,7 @@ class AccountsReact_ {
passwordSignupFields: 'EMAIL_ONLY',
sendVerificationEmail: true,
setDenyRules: true,
disableConfigureLoginService: true,

/* -----------------------------
Appearance
Expand All @@ -32,6 +34,7 @@ class AccountsReact_ {
hideSignInLink: false,
hideSignUpLink: false,
showForgotPasswordLink: false,
showResendVerificationLink: false,
showLabels: true,
showPlaceholders: true,

Expand Down Expand Up @@ -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'
},


Expand Down Expand Up @@ -95,7 +99,8 @@ class AccountsReact_ {
_id: 'password',
displayName: 'Password',
type: 'password',
placeholder: 'Enter your password'
placeholder: 'Enter your password',
autocomplete: 'current-password'
}
],

Expand Down Expand Up @@ -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',
Expand All @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -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'
}
],

Expand All @@ -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'
}
]
},
Expand All @@ -209,22 +231,24 @@ 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',
toResetPwd: 'Reset your password',
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',
Expand All @@ -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'
Expand Down Expand Up @@ -268,7 +294,7 @@ class AccountsReact_ {
this.loadReCaptcha()
this.setAccountCreationPolicy()
this.overrideLoginErrors()
this.disableForgotPassword()
this.disableMethods()
this.setDenyRules()

this._init = true
Expand Down Expand Up @@ -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')
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/AccountsReactComponent/forgotPwd.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ForgotPassword extends Component {
errors={errors}
/>

{emailSent && <p className='email-sent'>{texts.info.emailSent}</p>}
{emailSent && <p className='email-sent'>{texts.forgotPwdSubmitSuccess}</p>}

{!hideSignInLink && (
<a className='signIn-link' onMouseDown={this.redirectToSignIn} href=''>
Expand Down
10 changes: 8 additions & 2 deletions lib/AccountsReactComponent/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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) {
Expand All @@ -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
}

Expand All @@ -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
}

Expand Down
42 changes: 42 additions & 0 deletions lib/AccountsReactComponent/methods/ARResendVerificationEmail.js
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 658d775

Please sign in to comment.