unify navigation for authentication pages, add authorization to front end, fixes #650
This commit is contained in:
parent
7f2529a973
commit
5900e62904
17 changed files with 220 additions and 218 deletions
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
import { Link, browserHistory } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import InlineSVG from 'react-inlinesvg';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as IDEActions from '../modules/IDE/actions/ide';
|
import * as IDEActions from '../modules/IDE/actions/ide';
|
||||||
|
@ -93,11 +93,12 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNew() {
|
handleNew() {
|
||||||
if (!this.props.unsavedChanges) {
|
const { unsavedChanges, warnIfUnsavedChanges } = this.props;
|
||||||
|
if (!unsavedChanges) {
|
||||||
this.props.showToast(1500);
|
this.props.showToast(1500);
|
||||||
this.props.setToastText('Opened new sketch.');
|
this.props.setToastText('Opened new sketch.');
|
||||||
this.props.newProject();
|
this.props.newProject();
|
||||||
} else if (this.props.warnIfUnsavedChanges()) {
|
} else if (warnIfUnsavedChanges && warnIfUnsavedChanges()) {
|
||||||
this.props.showToast(1500);
|
this.props.showToast(1500);
|
||||||
this.props.setToastText('Opened new sketch.');
|
this.props.setToastText('Opened new sketch.');
|
||||||
this.props.newProject();
|
this.props.newProject();
|
||||||
|
@ -166,6 +167,8 @@ class Nav extends React.PureComponent {
|
||||||
|
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
this.props.logoutUser();
|
this.props.logoutUser();
|
||||||
|
// if you're on the settings page, probably.
|
||||||
|
browserHistory.push('/');
|
||||||
this.setDropdown('none');
|
this.setDropdown('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -535,13 +538,13 @@ class Nav extends React.PureComponent {
|
||||||
renderUnauthenticatedUserMenu(navDropdownState) {
|
renderUnauthenticatedUserMenu(navDropdownState) {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li>
|
<li className="nav__item">
|
||||||
<Link to="/login">
|
<Link to="/login">
|
||||||
<span className="nav__item-header">Log in</span>
|
<span className="nav__item-header">Log in</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<span className="nav__item-spacer">or</span>
|
<span className="nav__item-spacer">or</span>
|
||||||
<li>
|
<li className="nav__item">
|
||||||
<Link to="/signup">
|
<Link to="/signup">
|
||||||
<span className="nav__item-header">Sign up</span>
|
<span className="nav__item-header">Sign up</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -708,7 +711,7 @@ Nav.propTypes = {
|
||||||
showShareModal: PropTypes.func.isRequired,
|
showShareModal: PropTypes.func.isRequired,
|
||||||
showErrorModal: PropTypes.func.isRequired,
|
showErrorModal: PropTypes.func.isRequired,
|
||||||
unsavedChanges: PropTypes.bool.isRequired,
|
unsavedChanges: PropTypes.bool.isRequired,
|
||||||
warnIfUnsavedChanges: PropTypes.func.isRequired,
|
warnIfUnsavedChanges: PropTypes.func,
|
||||||
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||||
cmController: PropTypes.shape({
|
cmController: PropTypes.shape({
|
||||||
tidyCode: PropTypes.func,
|
tidyCode: PropTypes.func,
|
||||||
|
@ -731,7 +734,8 @@ Nav.defaultProps = {
|
||||||
owner: undefined
|
owner: undefined
|
||||||
},
|
},
|
||||||
cmController: {},
|
cmController: {},
|
||||||
layout: 'project'
|
layout: 'project',
|
||||||
|
warnIfUnsavedChanges: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
|
|
@ -86,7 +86,8 @@ class APIKeyForm extends React.Component {
|
||||||
disabled={this.state.keyLabel === ''}
|
disabled={this.state.keyLabel === ''}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<InlineSVG src={plusIcon} alt="" /> Create
|
<InlineSVG src={plusIcon} className="api-key-form__create-icon" />
|
||||||
|
Create
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { browserHistory } from 'react-router';
|
|
||||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
@ -11,48 +10,37 @@ import AccountForm from '../components/AccountForm';
|
||||||
import { validateSettings } from '../../../utils/reduxFormUtils';
|
import { validateSettings } from '../../../utils/reduxFormUtils';
|
||||||
import GithubButton from '../components/GithubButton';
|
import GithubButton from '../components/GithubButton';
|
||||||
import APIKeyForm from '../components/APIKeyForm';
|
import APIKeyForm from '../components/APIKeyForm';
|
||||||
import NavBasic from '../../../components/NavBasic';
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
|
const ROOT_URL = __process.env.API_URL;
|
||||||
|
|
||||||
class AccountView extends React.Component {
|
class AccountView extends React.Component {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.closeAccountPage = this.closeAccountPage.bind(this);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.body.className = this.props.theme;
|
document.body.className = this.props.theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAccountPage() {
|
|
||||||
browserHistory.push(this.props.previousPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const accessTokensUIEnabled = window.process.env.UI_ACCESS_TOKEN_ENABLED;
|
const accessTokensUIEnabled = window.process.env.UI_ACCESS_TOKEN_ENABLED;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user">
|
<div className="account-settings__container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Account</title>
|
<title>p5.js Web Editor | Account Settings</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<NavBasic onBack={this.closeAccountPage} />
|
<Nav layout="dashboard" />
|
||||||
|
|
||||||
<section className="modal">
|
<section className="account-settings">
|
||||||
<div className="modal-content">
|
<header className="account-settings__header">
|
||||||
<div className="modal__header">
|
<h1 className="account-settings__title">Account Settings</h1>
|
||||||
<h2 className="modal__title">My Account</h2>
|
</header>
|
||||||
</div>
|
{accessTokensUIEnabled &&
|
||||||
<Tabs className="account__tabs">
|
<Tabs className="account__tabs">
|
||||||
<TabList>
|
<TabList>
|
||||||
<div className="tabs__titles">
|
<div className="tabs__titles">
|
||||||
<Tab><h4 className="tabs__title">Account</h4></Tab>
|
<Tab><h4 className="tabs__title">Account</h4></Tab>
|
||||||
{accessTokensUIEnabled && <Tab><h4 className="tabs__title">Access Tokens</h4></Tab>}
|
<Tab><h4 className="tabs__title">Access Tokens</h4></Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
|
@ -67,7 +55,17 @@ class AccountView extends React.Component {
|
||||||
<APIKeyForm {...this.props} />
|
<APIKeyForm {...this.props} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
}
|
||||||
|
{!accessTokensUIEnabled &&
|
||||||
|
<div>
|
||||||
|
<AccountForm {...this.props} />
|
||||||
|
<h2 className="form-container__divider">Social Login</h2>
|
||||||
|
<p className="account__social-text">
|
||||||
|
Link this account with your GitHub account to allow login from both.
|
||||||
|
</p>
|
||||||
|
<GithubButton buttonText="Login with GitHub" />
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -96,7 +94,7 @@ function asyncValidate(formProps, dispatch, props) {
|
||||||
const queryParams = {};
|
const queryParams = {};
|
||||||
queryParams[fieldToValidate] = formProps[fieldToValidate];
|
queryParams[fieldToValidate] = formProps[fieldToValidate];
|
||||||
queryParams.check_type = fieldToValidate;
|
queryParams.check_type = fieldToValidate;
|
||||||
return axios.get('/api/signup/duplicate_check', { params: queryParams })
|
return axios.get(`${ROOT_URL}/signup/duplicate_check`, { params: queryParams })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.exists) {
|
if (response.data.exists) {
|
||||||
const error = {};
|
const error = {};
|
||||||
|
|
|
@ -3,13 +3,10 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { verifyEmailConfirmation } from '../actions';
|
import { verifyEmailConfirmation } from '../actions';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
|
||||||
|
|
||||||
|
|
||||||
class EmailVerificationView extends React.Component {
|
class EmailVerificationView extends React.Component {
|
||||||
|
@ -17,12 +14,6 @@ class EmailVerificationView extends React.Component {
|
||||||
emailVerificationTokenState: null,
|
emailVerificationTokenState: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.closeLoginPage = this.closeLoginPage.bind(this);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const verificationToken = this.verificationToken();
|
const verificationToken = this.verificationToken();
|
||||||
if (verificationToken != null) {
|
if (verificationToken != null) {
|
||||||
|
@ -32,14 +23,6 @@ class EmailVerificationView extends React.Component {
|
||||||
|
|
||||||
verificationToken = () => get(this.props, 'location.query.t', null);
|
verificationToken = () => get(this.props, 'location.query.t', null);
|
||||||
|
|
||||||
closeLoginPage() {
|
|
||||||
browserHistory.push(this.props.previousPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let status = null;
|
let status = null;
|
||||||
const {
|
const {
|
||||||
|
@ -48,7 +31,7 @@ class EmailVerificationView extends React.Component {
|
||||||
|
|
||||||
if (this.verificationToken() == null) {
|
if (this.verificationToken() == null) {
|
||||||
status = (
|
status = (
|
||||||
<p>That link is invalid</p>
|
<p>That link is invalid.</p>
|
||||||
);
|
);
|
||||||
} else if (emailVerificationTokenState === 'checking') {
|
} else if (emailVerificationTokenState === 'checking') {
|
||||||
status = (
|
status = (
|
||||||
|
@ -58,6 +41,7 @@ class EmailVerificationView extends React.Component {
|
||||||
status = (
|
status = (
|
||||||
<p>All done, your email address has been verified.</p>
|
<p>All done, your email address has been verified.</p>
|
||||||
);
|
);
|
||||||
|
setTimeout(() => browserHistory.push('/'), 1000);
|
||||||
} else if (emailVerificationTokenState === 'invalid') {
|
} else if (emailVerificationTokenState === 'invalid') {
|
||||||
status = (
|
status = (
|
||||||
<p>Something went wrong.</p>
|
<p>Something went wrong.</p>
|
||||||
|
@ -65,19 +49,12 @@ class EmailVerificationView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user">
|
<div className="email-verification">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Email Verification</title>
|
<title>p5.js Web Editor | Email Verification</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.closeLoginPage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Login Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Verify your email</h2>
|
<h2 className="form-container__title">Verify your email</h2>
|
||||||
{status}
|
{status}
|
||||||
|
@ -91,7 +68,6 @@ class EmailVerificationView extends React.Component {
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
emailVerificationTokenState: state.user.emailVerificationTokenState,
|
emailVerificationTokenState: state.user.emailVerificationTokenState,
|
||||||
previousPath: state.ide.previousPath
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +79,6 @@ function mapDispatchToProps(dispatch) {
|
||||||
|
|
||||||
|
|
||||||
EmailVerificationView.propTypes = {
|
EmailVerificationView.propTypes = {
|
||||||
previousPath: PropTypes.string.isRequired,
|
|
||||||
emailVerificationTokenState: PropTypes.oneOf([
|
emailVerificationTokenState: PropTypes.oneOf([
|
||||||
'checking', 'verified', 'invalid'
|
'checking', 'verified', 'invalid'
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -2,16 +2,13 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import { Link, browserHistory } from 'react-router';
|
import { Link, browserHistory } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { validateAndLoginUser } from '../actions';
|
import { validateAndLoginUser } from '../actions';
|
||||||
import LoginForm from '../components/LoginForm';
|
import LoginForm from '../components/LoginForm';
|
||||||
import { validateLogin } from '../../../utils/reduxFormUtils';
|
import { validateLogin } from '../../../utils/reduxFormUtils';
|
||||||
import GithubButton from '../components/GithubButton';
|
import GithubButton from '../components/GithubButton';
|
||||||
import GoogleButton from '../components/GoogleButton';
|
import GoogleButton from '../components/GoogleButton';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
|
||||||
|
|
||||||
class LoginView extends React.Component {
|
class LoginView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -34,19 +31,12 @@ class LoginView extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="user">
|
<div className="login">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Login</title>
|
<title>p5.js Web Editor | Login</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.closeLoginPage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Login Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Log In</h2>
|
<h2 className="form-container__title">Log In</h2>
|
||||||
<LoginForm {...this.props} />
|
<LoginForm {...this.props} />
|
||||||
|
|
|
@ -2,55 +2,29 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { browserHistory } from 'react-router';
|
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import NewPasswordForm from '../components/NewPasswordForm';
|
import NewPasswordForm from '../components/NewPasswordForm';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
function NewPasswordView(props) {
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
|
||||||
|
|
||||||
class NewPasswordView extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// need to check if this is a valid token
|
|
||||||
this.props.validateResetPasswordToken(this.props.params.reset_password_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const newPasswordClass = classNames({
|
const newPasswordClass = classNames({
|
||||||
'new-password': true,
|
'new-password': true,
|
||||||
'new-password--invalid': this.props.user.resetPasswordInvalid,
|
'new-password--invalid': props.user.resetPasswordInvalid,
|
||||||
'form-container': true,
|
'form-container': true,
|
||||||
'user': true
|
'user': true
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className="user">
|
<div className="new-password-container">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className={newPasswordClass}>
|
<div className={newPasswordClass}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | New Password</title>
|
<title>p5.js Web Editor | New Password</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close NewPassword Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Set a New Password</h2>
|
<h2 className="form-container__title">Set a New Password</h2>
|
||||||
<NewPasswordForm {...this.props} />
|
<NewPasswordForm {...props} />
|
||||||
<p className="new-password__invalid">
|
<p className="new-password__invalid">
|
||||||
The password reset token is invalid or has expired.
|
The password reset token is invalid or has expired.
|
||||||
</p>
|
</p>
|
||||||
|
@ -59,7 +33,6 @@ class NewPasswordView extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
NewPasswordView.propTypes = {
|
NewPasswordView.propTypes = {
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
|
|
|
@ -1,57 +1,33 @@
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, browserHistory } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
import ResetPasswordForm from '../components/ResetPasswordForm';
|
import ResetPasswordForm from '../components/ResetPasswordForm';
|
||||||
import { validateResetPassword } from '../../../utils/reduxFormUtils';
|
import { validateResetPassword } from '../../../utils/reduxFormUtils';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
function ResetPasswordView(props) {
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
|
||||||
|
|
||||||
class ResetPasswordView extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
this.props.resetPasswordReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const resetPasswordClass = classNames({
|
const resetPasswordClass = classNames({
|
||||||
'reset-password': true,
|
'reset-password': true,
|
||||||
'reset-password--submitted': this.props.user.resetPasswordInitiate,
|
'reset-password--submitted': props.user.resetPasswordInitiate,
|
||||||
'form-container': true,
|
'form-container': true,
|
||||||
'user': true
|
'user': true
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className="user">
|
<div className="reset-password-container">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className={resetPasswordClass}>
|
<div className={resetPasswordClass}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Reset Password</title>
|
<title>p5.js Web Editor | Reset Password</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close ResetPassword Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Reset Your Password</h2>
|
<h2 className="form-container__title">Reset Your Password</h2>
|
||||||
<ResetPasswordForm {...this.props} />
|
<ResetPasswordForm {...props} />
|
||||||
<p className="reset-password__submitted">
|
<p className="reset-password__submitted">
|
||||||
Your password reset email should arrive shortly. If you don't see it, check
|
Your password reset email should arrive shortly. If you don't see it, check
|
||||||
in your spam folder as sometimes it can end up there.
|
in your spam folder as sometimes it can end up there.
|
||||||
|
@ -66,7 +42,6 @@ class ResetPasswordView extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ResetPasswordView.propTypes = {
|
ResetPasswordView.propTypes = {
|
||||||
resetPasswordReset: PropTypes.func.isRequired,
|
resetPasswordReset: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -4,27 +4,17 @@ import { bindActionCreators } from 'redux';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Link, browserHistory } from 'react-router';
|
import { Link, browserHistory } from 'react-router';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
import SignupForm from '../components/SignupForm';
|
import SignupForm from '../components/SignupForm';
|
||||||
import { validateSignup } from '../../../utils/reduxFormUtils';
|
import { validateSignup } from '../../../utils/reduxFormUtils';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
const ROOT_URL = __process.env.API_URL;
|
||||||
|
|
||||||
class SignupView extends React.Component {
|
class SignupView extends React.Component {
|
||||||
constructor(props) {
|
gotoHomePage = () => {
|
||||||
super(props);
|
|
||||||
this.closeSignupPage = this.closeSignupPage.bind(this);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeSignupPage() {
|
|
||||||
browserHistory.push(this.props.previousPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
browserHistory.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,19 +24,12 @@ class SignupView extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="user">
|
<div className="signup">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Signup</title>
|
<title>p5.js Web Editor | Signup</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.closeSignupPage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Signup Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Sign Up</h2>
|
<h2 className="form-container__title">Sign Up</h2>
|
||||||
<SignupForm {...this.props} />
|
<SignupForm {...this.props} />
|
||||||
|
@ -97,7 +80,7 @@ function asyncValidate(formProps, dispatch, props) {
|
||||||
const queryParams = {};
|
const queryParams = {};
|
||||||
queryParams[fieldToValidate] = formProps[fieldToValidate];
|
queryParams[fieldToValidate] = formProps[fieldToValidate];
|
||||||
queryParams.check_type = fieldToValidate;
|
queryParams.check_type = fieldToValidate;
|
||||||
return axios.get('/api/signup/duplicate_check', { params: queryParams })
|
return axios.get(`${ROOT_URL}/signup/duplicate_check`, { params: queryParams })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.exists) {
|
if (response.data.exists) {
|
||||||
errors[fieldToValidate] = response.data.message;
|
errors[fieldToValidate] = response.data.message;
|
||||||
|
@ -120,9 +103,9 @@ function onSubmitFail(errors) {
|
||||||
|
|
||||||
SignupView.propTypes = {
|
SignupView.propTypes = {
|
||||||
previousPath: PropTypes.string.isRequired,
|
previousPath: PropTypes.string.isRequired,
|
||||||
user: {
|
user: PropTypes.shape({
|
||||||
authenticated: PropTypes.bool
|
authenticated: PropTypes.bool
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
SignupView.defaultProps = {
|
SignupView.defaultProps = {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import DashboardView from './modules/User/pages/DashboardView';
|
||||||
import createRedirectWithUsername from './components/createRedirectWithUsername';
|
import createRedirectWithUsername from './components/createRedirectWithUsername';
|
||||||
import { getUser } from './modules/User/actions';
|
import { getUser } from './modules/User/actions';
|
||||||
import { stopSketch } from './modules/IDE/actions/ide';
|
import { stopSketch } from './modules/IDE/actions/ide';
|
||||||
|
import { userIsAuthenticated, userIsNotAuthenticated, userIsAuthorized } from './utils/auth';
|
||||||
|
|
||||||
const checkAuth = (store) => {
|
const checkAuth = (store) => {
|
||||||
store.dispatch(getUser());
|
store.dispatch(getUser());
|
||||||
|
@ -25,9 +26,9 @@ const onRouteChange = (store) => {
|
||||||
const routes = store => (
|
const routes = store => (
|
||||||
<Route path="/" component={App} onChange={() => { onRouteChange(store); }}>
|
<Route path="/" component={App} onChange={() => { onRouteChange(store); }}>
|
||||||
<IndexRoute component={IDEView} onEnter={checkAuth(store)} />
|
<IndexRoute component={IDEView} onEnter={checkAuth(store)} />
|
||||||
<Route path="/login" component={LoginView} />
|
<Route path="/login" component={userIsNotAuthenticated(LoginView)} />
|
||||||
<Route path="/signup" component={SignupView} />
|
<Route path="/signup" component={userIsNotAuthenticated(SignupView)} />
|
||||||
<Route path="/reset-password" component={ResetPasswordView} />
|
<Route path="/reset-password" component={userIsNotAuthenticated(ResetPasswordView)} />
|
||||||
<Route path="/verify" component={EmailVerificationView} />
|
<Route path="/verify" component={EmailVerificationView} />
|
||||||
<Route
|
<Route
|
||||||
path="/reset-password/:reset_password_token"
|
path="/reset-password/:reset_password_token"
|
||||||
|
@ -37,12 +38,11 @@ const routes = store => (
|
||||||
<Route path="/:username/full/:project_id" component={FullView} />
|
<Route path="/:username/full/:project_id" component={FullView} />
|
||||||
<Route path="/full/:project_id" component={FullView} />
|
<Route path="/full/:project_id" component={FullView} />
|
||||||
<Route path="/sketches" component={createRedirectWithUsername('/:username/sketches')} />
|
<Route path="/sketches" component={createRedirectWithUsername('/:username/sketches')} />
|
||||||
<Route path="/:username/assets" component={DashboardView} />
|
<Route path="/:username/assets" component={userIsAuthenticated(userIsAuthorized(DashboardView))} />
|
||||||
<Route path="/assets" component={createRedirectWithUsername('/:username/assets')} />
|
<Route path="/assets" component={createRedirectWithUsername('/:username/assets')} />
|
||||||
<Route path="/account" component={AccountView} />
|
<Route path="/account" component={userIsAuthenticated(AccountView)} />
|
||||||
<Route path="/:username/sketches/:project_id" component={IDEView} />
|
<Route path="/:username/sketches/:project_id" component={IDEView} />
|
||||||
<Route path="/:username/sketches" component={DashboardView} />
|
<Route path="/:username/sketches" component={DashboardView} />
|
||||||
<Route path="/:username/assets" component={DashboardView} />
|
|
||||||
<Route path="/about" component={IDEView} />
|
<Route path="/about" component={IDEView} />
|
||||||
<Route path="/feedback" component={IDEView} />
|
<Route path="/feedback" component={IDEView} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -62,6 +62,10 @@ button {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: #{21 / $base-font-size}em;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: #{21 / $base-font-size}em;
|
font-size: #{21 / $base-font-size}em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,17 @@
|
||||||
|
.account-settings__container {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
background-color: getThemifyVariable('background-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-settings {
|
||||||
|
max-width: #{700 / $base-font-size}rem;
|
||||||
|
align-self: center;
|
||||||
|
padding: 0 #{10 / $base-font-size}rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.account__tabs {
|
.account__tabs {
|
||||||
padding-top: #{20 / $base-font-size}rem;
|
padding-top: #{20 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,11 @@
|
||||||
.api-key-form__create-button {
|
.api-key-form__create-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__create-icon {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.api-key-form__create-button .isvg {
|
.api-key-form__create-button .isvg {
|
||||||
|
|
|
@ -8,3 +8,15 @@
|
||||||
background-color: getThemifyVariable('background-color');
|
background-color: getThemifyVariable('background-color');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login,
|
||||||
|
.signup,
|
||||||
|
.reset-password-container,
|
||||||
|
.new-password-container,
|
||||||
|
.email-verification {
|
||||||
|
height: 100%;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
background-color: getThemifyVariable('background-color');
|
||||||
|
}
|
||||||
|
}
|
29
client/utils/auth.js
Normal file
29
client/utils/auth.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { connectedRouterRedirect } from 'redux-auth-wrapper/history3/redirect';
|
||||||
|
import locationHelperBuilder from 'redux-auth-wrapper/history3/locationHelper';
|
||||||
|
|
||||||
|
const locationHelper = locationHelperBuilder({});
|
||||||
|
|
||||||
|
export const userIsAuthenticated = connectedRouterRedirect({
|
||||||
|
// The url to redirect user to if they fail
|
||||||
|
redirectPath: '/login',
|
||||||
|
// Determine if the user is authenticated or not
|
||||||
|
authenticatedSelector: state => state.user.authenticated === true,
|
||||||
|
// A nice display name for this check
|
||||||
|
wrapperDisplayName: 'UserIsAuthenticated'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userIsNotAuthenticated = connectedRouterRedirect({
|
||||||
|
redirectPath: (state, ownProps) => locationHelper.getRedirectQueryParam(ownProps) || '/',
|
||||||
|
allowRedirectBack: false,
|
||||||
|
authenticatedSelector: state => state.user.authenticated === false,
|
||||||
|
wrapperDisplayName: 'UserIsNotAuthenticated'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userIsAuthorized = connectedRouterRedirect({
|
||||||
|
redirectPath: '/',
|
||||||
|
allowRedirectBack: false,
|
||||||
|
authenticatedSelector: (state, ownProps) => {
|
||||||
|
const { username } = ownProps.params;
|
||||||
|
return state.user.username === username;
|
||||||
|
},
|
||||||
|
});
|
37
package-lock.json
generated
37
package-lock.json
generated
|
@ -11348,6 +11348,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
||||||
},
|
},
|
||||||
|
"lodash.isempty": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
|
||||||
|
"integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4="
|
||||||
|
},
|
||||||
"lodash.isequal": {
|
"lodash.isequal": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
@ -17239,6 +17244,38 @@
|
||||||
"symbol-observable": "^1.0.3"
|
"symbol-observable": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux-auth-wrapper": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-auth-wrapper/-/redux-auth-wrapper-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-UtU64cJk2pWcMMfgWIVoyBVG0p8ZiGJ++vqrvQ5r5ghZZOLRq+M5aTS0RRNneiB+aCCZBzI+txFSaKYVRrv8qQ==",
|
||||||
|
"requires": {
|
||||||
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"lodash.isempty": "^4.4.0",
|
||||||
|
"prop-types": "^15.5.0",
|
||||||
|
"query-string": "^5.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hoist-non-react-statics": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
|
||||||
|
"requires": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query-string": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
|
||||||
|
"requires": {
|
||||||
|
"decode-uri-component": "^0.2.0",
|
||||||
|
"object-assign": "^4.1.0",
|
||||||
|
"strict-uri-encode": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"redux-devtools": {
|
"redux-devtools": {
|
||||||
"version": "3.5.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/redux-devtools/-/redux-devtools-3.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/redux-devtools/-/redux-devtools-3.5.0.tgz",
|
||||||
|
|
|
@ -158,6 +158,7 @@
|
||||||
"react-split-pane": "^0.1.85",
|
"react-split-pane": "^0.1.85",
|
||||||
"react-tabs": "^2.3.0",
|
"react-tabs": "^2.3.0",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
|
"redux-auth-wrapper": "^2.1.0",
|
||||||
"redux-devtools": "^3.4.2",
|
"redux-devtools": "^3.4.2",
|
||||||
"redux-devtools-dock-monitor": "^1.1.3",
|
"redux-devtools-dock-monitor": "^1.1.3",
|
||||||
"redux-devtools-log-monitor": "^1.4.0",
|
"redux-devtools-log-monitor": "^1.4.0",
|
||||||
|
|
|
@ -37,9 +37,8 @@ export function findUserByUsername(username, cb) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
|
|
||||||
|
|
||||||
export function createUser(req, res, next) {
|
export function createUser(req, res, next) {
|
||||||
|
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
|
||||||
random((tokenError, token) => {
|
random((tokenError, token) => {
|
||||||
const user = new User({
|
const user = new User({
|
||||||
username: req.body.username,
|
username: req.body.username,
|
||||||
|
@ -224,6 +223,7 @@ export function emailVerificationInitiate(req, res) {
|
||||||
if (mailErr != null) {
|
if (mailErr != null) {
|
||||||
res.status(500).send({ error: 'Error sending mail' });
|
res.status(500).send({ error: 'Error sending mail' });
|
||||||
} else {
|
} else {
|
||||||
|
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
|
||||||
user.verified = User.EmailConfirmation.Resent;
|
user.verified = User.EmailConfirmation.Resent;
|
||||||
user.verifiedToken = token;
|
user.verifiedToken = token;
|
||||||
user.verifiedTokenExpires = EMAIL_VERIFY_TOKEN_EXPIRY_TIME; // 24 hours
|
user.verifiedTokenExpires = EMAIL_VERIFY_TOKEN_EXPIRY_TIME; // 24 hours
|
||||||
|
@ -240,7 +240,7 @@ export function emailVerificationInitiate(req, res) {
|
||||||
export function verifyEmail(req, res) {
|
export function verifyEmail(req, res) {
|
||||||
const token = req.query.t;
|
const token = req.query.t;
|
||||||
|
|
||||||
User.findOne({ verifiedToken: token, verifiedTokenExpires: { $gt: Date.now() } }, (err, user) => {
|
User.findOne({ verifiedToken: token, verifiedTokenExpires: { $gt: new Date() } }, (err, user) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(401).json({ success: false, message: 'Token is invalid or has expired.' });
|
res.status(401).json({ success: false, message: 'Token is invalid or has expired.' });
|
||||||
return;
|
return;
|
||||||
|
@ -316,6 +316,7 @@ export function updateSettings(req, res) {
|
||||||
saveUser(res, user);
|
saveUser(res, user);
|
||||||
});
|
});
|
||||||
} else if (user.email !== req.body.email) {
|
} else if (user.email !== req.body.email) {
|
||||||
|
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
|
||||||
user.verified = User.EmailConfirmation.Sent;
|
user.verified = User.EmailConfirmation.Sent;
|
||||||
|
|
||||||
user.email = req.body.email;
|
user.email = req.body.email;
|
||||||
|
|
Loading…
Reference in a new issue