From 75b470907bf05dfcb0d1b761ed8c9fd03737695d Mon Sep 17 00:00:00 2001 From: cleverbeagle Date: Tue, 1 Aug 2017 10:11:58 -0500 Subject: [PATCH] - Add support for email verification. - Clean up formatting of accounts email templates. --- imports/api/Users/server/methods.js | 7 +++- .../server/accounts/email-templates.js | 22 ++++++---- imports/ui/layouts/App/App.js | 29 ++++++++++++- imports/ui/layouts/App/App.scss | 20 +++++++++ imports/ui/pages/Signup/Signup.js | 2 + imports/ui/pages/VerifyEmail/VerifyEmail.js | 42 +++++++++++++++++++ 6 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 imports/ui/pages/VerifyEmail/VerifyEmail.js diff --git a/imports/api/Users/server/methods.js b/imports/api/Users/server/methods.js index 80e34fc..d97c515 100644 --- a/imports/api/Users/server/methods.js +++ b/imports/api/Users/server/methods.js @@ -1,9 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { check, Match } from 'meteor/check'; +import { check } from 'meteor/check'; +import { Accounts } from 'meteor/accounts-base'; import editProfile from './edit-profile'; import rateLimit from '../../../modules/rate-limit'; Meteor.methods({ + 'users.sendVerificationEmail': function usersResendVerification() { + return Accounts.sendVerificationEmail(this.userId); + }, 'users.editProfile': function usersEditProfile(profile) { check(profile, { emailAddress: String, @@ -25,6 +29,7 @@ Meteor.methods({ rateLimit({ methods: [ + 'users.resendVerification', 'users.editProfile', ], limit: 5, diff --git a/imports/startup/server/accounts/email-templates.js b/imports/startup/server/accounts/email-templates.js index 6d7b5ad..234065c 100644 --- a/imports/startup/server/accounts/email-templates.js +++ b/imports/startup/server/accounts/email-templates.js @@ -9,6 +9,18 @@ const emailTemplates = Accounts.emailTemplates; emailTemplates.siteName = name; emailTemplates.from = from; +emailTemplates.verifyEmail = { + subject() { + return `[${name}] Verify Your Email Address`; + }, + text(user, url) { + const userEmail = user.emails[0].address; + const urlWithoutHash = url.replace('#/', ''); + if (Meteor.isDevelopment) console.info(`Verify Email Link: ${urlWithoutHash}`); // eslint-disable-line + return `Hey, ${user.profile.name.first}! Welcome to ${name}.\n\nTo verify your email address (${userEmail}), click the link below:\n\n${urlWithoutHash}\n\nIf you feel something is wrong, please contact our support team: ${email}.`; + }, +}; + emailTemplates.resetPassword = { subject() { return `[${name}] Reset Your Password`; @@ -16,13 +28,7 @@ emailTemplates.resetPassword = { text(user, url) { const userEmail = user.emails[0].address; const urlWithoutHash = url.replace('#/', ''); - - if (Meteor.isDevelopment) console.info(`Reset Password Link: ${urlWithoutHash}`); - - return `A password reset has been requested for the account related to this - address (${userEmail}). To reset the password, visit the following link: - \n\n${urlWithoutHash}\n\n If you did not request this reset, please ignore - this email. If you feel something is wrong, please contact our support team: - ${email}.`; + if (Meteor.isDevelopment) console.info(`Reset Password Link: ${urlWithoutHash}`); // eslint-disable-line + return `A password reset has been requested for the account related to this address (${userEmail}).\n\nTo reset the password, visit the following link: \n\n${urlWithoutHash}\n\n If you did not request this reset, please ignore this email. If you feel something is wrong, please contact our support team: ${email}.`; }, }; diff --git a/imports/ui/layouts/App/App.js b/imports/ui/layouts/App/App.js index 58f5b1c..d74449b 100644 --- a/imports/ui/layouts/App/App.js +++ b/imports/ui/layouts/App/App.js @@ -1,10 +1,13 @@ +/* eslint-disable jsx-a11y/no-href*/ + import React from 'react'; import PropTypes from 'prop-types'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; -import { Grid } from 'react-bootstrap'; +import { Grid, Alert, Button } from 'react-bootstrap'; import { Meteor } from 'meteor/meteor'; import { createContainer } from 'meteor/react-meteor-data'; import { Roles } from 'meteor/alanning:roles'; +import { Bert } from 'meteor/themeteorchef:bert'; import Navigation from '../../components/Navigation/Navigation'; import Authenticated from '../../components/Authenticated/Authenticated'; import Public from '../../components/Public/Public'; @@ -16,6 +19,7 @@ import EditDocument from '../../pages/EditDocument/EditDocument'; import Signup from '../../pages/Signup/Signup'; import Login from '../../pages/Login/Login'; import Logout from '../../pages/Logout/Logout'; +import VerifyEmail from '../../pages/VerifyEmail/VerifyEmail'; import RecoverPassword from '../../pages/RecoverPassword/RecoverPassword'; import ResetPassword from '../../pages/ResetPassword/ResetPassword'; import Profile from '../../pages/Profile/Profile'; @@ -27,9 +31,20 @@ import ExamplePage from '../../pages/ExamplePage/ExamplePage'; import './App.scss'; +const handleResendVerificationEmail = (emailAddress) => { + Meteor.call('users.sendVerificationEmail', (error) => { + if (error) { + Bert.alert(error.reason, 'danger'); + } else { + Bert.alert(`Check ${emailAddress} for a verification link!`, 'success'); + } + }); +}; + const App = props => ( {!props.loading ?
+ {props.userId && !props.emailVerified ?

Hey friend! Can you verify your email address ({props.emailAddress}) for us?

: ''} @@ -42,6 +57,7 @@ const App = props => ( + @@ -55,8 +71,16 @@ const App = props => ( ); +App.defaultProps = { + userId: '', + emailAddress: '', +}; + App.propTypes = { loading: PropTypes.bool.isRequired, + userId: PropTypes.string, + emailAddress: PropTypes.string, + emailVerified: PropTypes.bool.isRequired, }; const getUserName = name => ({ @@ -78,5 +102,8 @@ export default createContainer(() => { authenticated: !loggingIn && !!userId, name: name || emailAddress, roles: !loading && Roles.getRolesForUser(userId), + userId, + emailAddress, + emailVerified: user ? user && user.emails && user.emails[0].verified : true, }; }, App); diff --git a/imports/ui/layouts/App/App.scss b/imports/ui/layouts/App/App.scss index 4eb3984..e3d6669 100644 --- a/imports/ui/layouts/App/App.scss +++ b/imports/ui/layouts/App/App.scss @@ -1,3 +1,23 @@ +@import '../../stylesheets/colors'; + .App > .container { margin-bottom: 20px; } + +.App .verify-email { + margin-bottom: 0; + padding: 0; + border-top: none; + border-bottom: 1px solid #e7e7e7; + background: #fff; + color: $gray-dark; + border-radius: 0; + + p { + padding: 19px; + } + + .btn { + padding: 0; + } +} diff --git a/imports/ui/pages/Signup/Signup.js b/imports/ui/pages/Signup/Signup.js index d69c1e3..f750c28 100644 --- a/imports/ui/pages/Signup/Signup.js +++ b/imports/ui/pages/Signup/Signup.js @@ -2,6 +2,7 @@ import React from 'react'; import { Row, Col, FormGroup, ControlLabel, Button } from 'react-bootstrap'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; +import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Bert } from 'meteor/themeteorchef:bert'; import OAuthLoginButtons from '../../components/OAuthLoginButtons/OAuthLoginButtons'; @@ -71,6 +72,7 @@ class Signup extends React.Component { if (error) { Bert.alert(error.reason, 'danger'); } else { + Meteor.call('users.sendVerificationEmail'); Bert.alert('Welcome!', 'success'); history.push('/documents'); } diff --git a/imports/ui/pages/VerifyEmail/VerifyEmail.js b/imports/ui/pages/VerifyEmail/VerifyEmail.js new file mode 100644 index 0000000..0539e95 --- /dev/null +++ b/imports/ui/pages/VerifyEmail/VerifyEmail.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Alert } from 'react-bootstrap'; +import { Accounts } from 'meteor/accounts-base'; +import { Bert } from 'meteor/themeteorchef:bert'; + +class VerifyEmail extends React.Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + + componentDidMount() { + const { match, history } = this.props; + Accounts.verifyEmail(match.params.token, (error) => { + if (error) { + Bert.alert(error.reason, 'danger'); + this.setState({ error: `${error.reason}. Please try again.` }); + } else { + setTimeout(() => { + Bert.alert('All set, thanks!', 'success'); + history.push('/documents'); + }, 2000); + } + }); + } + + render() { + return (
+ + {!this.state.error ? 'Verifying...' : this.state.error} + +
); + } +} + +VerifyEmail.propTypes = { + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, +}; + +export default VerifyEmail;