unify navigation for authentication pages, add authorization to front end, fixes #650

This commit is contained in:
Cassie Tarakajian 2019-09-19 13:38:27 -04:00
parent 7f2529a973
commit 5900e62904
17 changed files with 220 additions and 218 deletions

View file

@ -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) {

View file

@ -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>

View file

@ -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 = {};

View file

@ -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'
]), ]),

View file

@ -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} />

View file

@ -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>
@ -58,7 +32,6 @@ class NewPasswordView extends React.Component {
</div> </div>
</div> </div>
); );
}
} }
NewPasswordView.propTypes = { NewPasswordView.propTypes = {

View file

@ -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&apos;t see it, check Your password reset email should arrive shortly. If you don&apos;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.
@ -65,7 +41,6 @@ class ResetPasswordView extends React.Component {
</div> </div>
</div> </div>
); );
}
} }
ResetPasswordView.propTypes = { ResetPasswordView.propTypes = {

View file

@ -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 = {

View file

@ -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>

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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
View 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
View file

@ -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",

View file

@ -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",

View file

@ -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;