diff --git a/backend/grant/user/models.py b/backend/grant/user/models.py index 2b4cf2b2..454f88c4 100644 --- a/backend/grant/user/models.py +++ b/backend/grant/user/models.py @@ -156,10 +156,7 @@ class User(db.Model, UserMixin): db.session.commit() if _send_email: - send_email(user.email_address, 'signup', { - 'display_name': user.display_name, - 'confirm_url': make_url(f'/email/verify?code={ev.code}') - }) + user.send_verification_email() return user @@ -212,6 +209,12 @@ class User(db.Model, UserMixin): def login(self): login_user(self) + def send_verification_email(self): + send_email(self.email_address, 'signup', { + 'display_name': self.display_name, + 'confirm_url': make_url(f'/email/verify?code={self.email_verification.code}') + }) + def send_recovery_email(self): existing = self.email_recovery if existing: diff --git a/backend/grant/user/views.py b/backend/grant/user/views.py index edd2404c..e7881851 100644 --- a/backend/grant/user/views.py +++ b/backend/grant/user/views.py @@ -170,11 +170,18 @@ def update_user_password(current_password, password): def update_user_email(email, password): if not g.current_user.check_password(password): return {"message": "Password is incorrect"}, 403 - print('set_email') g.current_user.set_email(email) return None, 200 +@blueprint.route("/me/resend-verification", methods=["PUT"]) +@requires_auth +@endpoint.api() +def resend_email_verification(): + g.current_user.send_verification_email() + return None, 200 + + @blueprint.route("/logout", methods=["POST"]) @requires_auth @endpoint.api() diff --git a/frontend/client/api/api.ts b/frontend/client/api/api.ts index e74eade3..b15b3420 100644 --- a/frontend/client/api/api.ts +++ b/frontend/client/api/api.ts @@ -294,3 +294,7 @@ export function getRFP(rfpId: number | string): Promise<{ data: RFP }> { return res; }); } + +export function resendEmailVerification(): Promise<{ data: void }> { + return axios.put(`/api/v1/users/me/resend-verification`); +} diff --git a/frontend/client/components/Settings/Account.less b/frontend/client/components/Settings/Account.less index b22d160c..8ed97b11 100644 --- a/frontend/client/components/Settings/Account.less +++ b/frontend/client/components/Settings/Account.less @@ -1,5 +1,10 @@ .AccountSettings { &-form { + &-resend { + position: relative; + margin-bottom: 1rem; + } + & > .ant-form-item { margin-bottom: 0.75rem; } diff --git a/frontend/client/components/Settings/Account.tsx b/frontend/client/components/Settings/Account.tsx index b06e3a8d..8326a39d 100644 --- a/frontend/client/components/Settings/Account.tsx +++ b/frontend/client/components/Settings/Account.tsx @@ -1,13 +1,15 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Form, Input, Button, Alert } from 'antd'; +import { Form, Input, Button, Alert, message } from 'antd'; import { FormComponentProps } from 'antd/lib/form'; -import { updateUserEmail } from 'api/api'; +import Loader from 'components/Loader'; +import { updateUserEmail, resendEmailVerification } from 'api/api'; import { AppState } from 'store/reducers'; import './Account.less'; interface StateProps { email: string; + emailVerified: boolean; } type Props = FormComponentProps & StateProps; @@ -17,6 +19,8 @@ const STATE = { emailChangePending: false, emailChangeSuccess: false, emailChangeError: '', + isResendingVerification: false, + hasResentVerification: false, }; type State = typeof STATE; @@ -25,12 +29,14 @@ class AccountSettings extends React.Component { state: State = { ...STATE }; render() { - const { email, form } = this.props; + const { email, emailVerified, form } = this.props; const { emailChangeError, emailChangePending, emailChangeSuccess, newEmail, + isResendingVerification, + hasResentVerification, } = this.state; return ( @@ -40,6 +46,36 @@ class AccountSettings extends React.Component { onSubmit={this.handleSubmit} layout="vertical" > + {!emailVerified && + !hasResentVerification && ( + + You should have a verification in your inbox. If you can't find it, + check your spam folder. Still don't see it?{' '} + Click here to resend. + {isResendingVerification && } + + } + /> + )} + {!emailVerified && + hasResentVerification && ( + + )} {form.getFieldDecorator('email', { initialValue: newEmail || email, @@ -139,10 +175,25 @@ class AccountSettings extends React.Component { } }); }; + + private resendEmailVerification = async () => { + if (this.state.isResendingVerification) { + return; + } + this.setState({ isResendingVerification: true }); + try { + await resendEmailVerification(); + this.setState({ hasResentVerification: true }); + } catch (err) { + message.error(err.message || err.toString()); + } + this.setState({ isResendingVerification: false }); + }; } const FormWrappedAccountSettings = Form.create()(AccountSettings); export default connect(state => ({ email: state.auth.user ? state.auth.user.emailAddress || '' : '', + emailVerified: !!state.auth.user && !!state.auth.user.emailVerified, }))(FormWrappedAccountSettings);