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')