diff --git a/.env b/.env new file mode 100644 index 0000000..21685c1 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +GOOGLE_API_KEY=AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI +REACT_APP_API_BASE_PATH=http://localhost:3500 +REACT_APP_AUTH0_CLIENT_ID=3CGKzjS2nVSqHxHHE64RhvvKY6e0TYpK +REACT_APP_AUTH0_CLIENT_DOMAIN=dronetest.auth0.com +REACT_APP_SOCKET_URL=http://localhost:3500 diff --git a/.env.example b/.env.example index 4a42b68..3830cb2 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -REACT_APP_API_BASE_PATH=https://kb-dsp-server.herokuapp.com -REACT_APP_SOCKET_URL=https://kb-dsp-server.herokuapp.com -REACT_APP_AUTH0_CLIEND_ID=3CGKzjS2nVSqHxHHE64RhvvKY6e0TYpK -REACT_APP_AUTH0_DOMAIN=dronetest.auth0.com -REACT_APP_GOOGLE_API_KEY=AIzaSyCR3jfBdv9prCBYBOf-fPUDhjPP4K05YjE +GOOGLE_API_KEY=AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI +REACT_APP_API_BASE_PATH=http://localhost:3500 +REACT_APP_AUTH0_CLIENT_ID=3CGKzjS2nVSqHxHHE64RhvvKY6e0TYpK +REACT_APP_AUTH0_CLIENT_DOMAIN=dronetest.auth0.com +REACT_APP_SOCKET_URL=http://localhost:3500 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 20c36db..e043f42 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ node_modules dist coverage .tmp -/.env diff --git a/README.md b/README.md index f8cfdec..6389d32 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,24 @@ See Guild https://github.com/lorenwest/node-config/wiki/Configuration-Files |`REACT_APP_AUTH0_DOMAIN`| The React app auth0 domain`| Environment variables will be loaded from the .env file during build. Create the .env file based on the provided env.example +### Auth0 setup +- Create an account on auth0. +- Click on clients in left side menu, it will redirect you to client page. Click on CREATE CLIENT button + to create a new client. +- Copy the client id and client domain and export them as environment variables. +- Add `http://localhost:3000` as Allowed callback url's in client settings. + +### Add social connections + +### Facebook social connection +- To add facebook social connection to auth0, you have to create a facebook app. + Go to facebook [developers](https://developers.facebook.com/apps) and create a new app. +- Copy the app secret and app id to auth0 social connections facebook tab. +- You have to setup the oauth2 callback in app oauth settings. +- For more information visit auth0 [docs](https://auth0.com/docs/connections/social/facebook) + +### Google social connection +- For more information on how to connect google oauth2 client, visit official [docs](https://auth0.com/docs/connections/social/google) ## Install dependencies `npm i` diff --git a/config/default.js b/config/default.js index f1bbbf3..d35b608 100644 --- a/config/default.js +++ b/config/default.js @@ -1,6 +1,6 @@ /* eslint-disable import/no-commonjs */ /** - * Main config file + * Main config file for the server which is hosting the reat app */ module.exports = { // below env variables are NOT visible in frontend diff --git a/src/api/User.js b/src/api/User.js index 70c1728..53466dc 100644 --- a/src/api/User.js +++ b/src/api/User.js @@ -24,7 +24,6 @@ class UserApi { login(email, password) { const url = `${this.basePath}/api/v1/login`; - return reqwest({ url, method: 'post', @@ -40,7 +39,7 @@ class UserApi { }); } - register(name, email, password) { + register(firstName, lastName, email, password) { const url = `${this.basePath}/api/v1/register`; return reqwest({ url, @@ -48,15 +47,15 @@ class UserApi { type: 'json', contentType: 'application/json', data: JSON.stringify({ - firstName: name, - lastName: name, + firstName, + lastName, email, phone: '1', password, })}); } - registerSocialUser(name, email) { + registerSocialUser(name, email, token) { const url = `${this.basePath}/api/v1/login/social`; return reqwest({ @@ -64,6 +63,9 @@ class UserApi { method: 'post', type: 'json', contentType: 'application/json', + headers: { + Authorization: `Bearer ${token}`, + }, data: JSON.stringify({ name, email, diff --git a/src/components/AdminHeader/AdminHeader.jsx b/src/components/AdminHeader/AdminHeader.jsx index a822dd2..32da13e 100644 --- a/src/components/AdminHeader/AdminHeader.jsx +++ b/src/components/AdminHeader/AdminHeader.jsx @@ -2,6 +2,8 @@ import React from 'react'; import CSSModules from 'react-css-modules'; import {Link} from 'react-router'; import styles from './AdminHeader.scss'; +import Dropdown from '../Dropdown'; +import Notification from '../Notification'; export const AdminHeader = () => ( ); diff --git a/src/components/AdminHeader/AdminHeader.scss b/src/components/AdminHeader/AdminHeader.scss index dcabbf5..ca74fa9 100644 --- a/src/components/AdminHeader/AdminHeader.scss +++ b/src/components/AdminHeader/AdminHeader.scss @@ -10,4 +10,8 @@ composes: pages from '../Header/Header.scss' } +.notifications { + composes: notifications from '../Header/Header.scss' +} + diff --git a/src/components/Dropdown/Dropdown.jsx b/src/components/Dropdown/Dropdown.jsx index d0a020b..5412451 100644 --- a/src/components/Dropdown/Dropdown.jsx +++ b/src/components/Dropdown/Dropdown.jsx @@ -3,18 +3,17 @@ import CSSModules from 'react-css-modules'; import ReactDropdown, {DropdownTrigger, DropdownContent} from 'react-simple-dropdown'; import styles from './Dropdown.scss'; -export const Dropdown = ({title, children}) => ( -
- - {title} - - {children} - - -
+export const Dropdown = ({onRef, title, children}) => ( + + {title} + + {children} + + ); Dropdown.propTypes = { + onRef: PropTypes.func, title: PropTypes.any.isRequired, children: PropTypes.any.isRequired, }; diff --git a/src/components/Dropdown/Dropdown.scss b/src/components/Dropdown/Dropdown.scss index 9acd9d2..61de447 100644 --- a/src/components/Dropdown/Dropdown.scss +++ b/src/components/Dropdown/Dropdown.scss @@ -1,18 +1,12 @@ -.dropdown { - :global { - .dropdown { - display: inline-block; - } - - - .dropdown--active .dropdown__content { - display: block; - } +:global { + .dropdown { + display: inline-block; + } + .dropdown--active .dropdown__content { + display: block; } } - - .content { display: none; position: absolute; diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 5467cd4..55e9742 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -8,64 +8,86 @@ import Dropdown from '../Dropdown'; import Notification from '../Notification'; import styles from './Header.scss'; -export const Header = ({ +/** + * TODO: This component cries: 'REFACTOR ME!' + * Seriously, it is such a mess now, should be split into separate sub-components! + */ + +export function Header({ location, selectedCategory, categories, user, notifications, - routes, handleNotification, toggleNotif, loggedUser, -}) => ( + handleNotification, logoutAction, toggleNotif, loggedUser, +}) { + // Holds a reference to the function which hides the user dropdown (Profile, + // Logout, etc.). + let hideUserDropdown; - + ); +} Header.propTypes = { - routes: PropTypes.any.isRequired, + // routes: PropTypes.any.isRequired, location: PropTypes.string.isRequired, selectedCategory: PropTypes.string.isRequired, categories: PropTypes.array.isRequired, notifications: PropTypes.array.isRequired, user: PropTypes.object.isRequired, handleNotification: PropTypes.func, + logoutAction: PropTypes.func.isRequired, toggleNotif: PropTypes.bool, loggedUser: PropTypes.bool, }; diff --git a/src/components/MapHistory/MapHistory.scss b/src/components/MapHistory/MapHistory.scss index 317e61e..dfffff0 100644 --- a/src/components/MapHistory/MapHistory.scss +++ b/src/components/MapHistory/MapHistory.scss @@ -24,7 +24,7 @@ left:0; right:0; margin:0 auto; - width:520px; + width:85%; height: 80px; background-color: #FFF; .slider{ diff --git a/src/components/TextField/TextField.scss b/src/components/TextField/TextField.scss index 46a04ab..56f5f52 100644 --- a/src/components/TextField/TextField.scss +++ b/src/components/TextField/TextField.scss @@ -2,6 +2,7 @@ width: 100%; border: 1px solid #ebebeb; + input[type="password"], input[type="text"] { width: 100%; padding: 0 10px; diff --git a/src/config/index.js b/src/config/index.js index c6b575d..23bf78c 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,23 +1,16 @@ +/* eslint-disable import/no-commonjs */ /** - * Copyright (c) 2016 Topcoder Inc, All rights reserved. + * Main config file for the react app */ - -/** - * Webapp configuration - * - * @author TCSCODER - * @version 1.0.0 - */ - -const config = { +module.exports = { api: { basePath: process.env.REACT_APP_API_BASE_PATH || 'http://localhost:3500', }, socket: { url: process.env.REACT_APP_SOCKET_URL || 'http://localhost:3500', }, - AUTH0_CLIEND_ID: process.env.REACT_APP_AUTH0_CLIEND_ID || '3CGKzjS2nVSqHxHHE64RhvvKY6e0TYpK', - AUTH0_DOMAIN: process.env.REACT_APP_AUTH0_DOMAIN || 'dronetest.auth0.com', -}; + AUTH0_CLIENT_ID: process.env.REACT_APP_AUTH0_CLIENT_ID || 'h7p6V93Shau3SSvqGrl6V4xrATlkrVGm', + AUTH0_CLIENT_DOMAIN: process.env.REACT_APP_AUTH0_CLIENT_DOMAIN || 'spanhawk.auth0.com', + AUTH0_CALLBACK: 'http://localhost:3000', -export default config; +}; diff --git a/src/containers/HeaderContainer.js b/src/containers/HeaderContainer.js index 9128459..a9a2110 100644 --- a/src/containers/HeaderContainer.js +++ b/src/containers/HeaderContainer.js @@ -1,12 +1,17 @@ import Header from 'components/Header'; import {asyncConnect} from 'redux-connect'; -import {toggleNotification, loginAction} from '../store/modules/global'; +import {actions, toggleNotification, logoutAction} from '../store/modules/global'; const resolve = [{ promise: () => Promise.resolve(), }]; -const mapState = (state) => state.global; +const mapState = (state) => ({...state.global}); + +/* + TODO: This is not used anymore, should be checked if this is safe to remove + (i.e. if the toggleNotification and loginAction actions are part of + the acetions object, injected into the asyncConnect call below). const mapDispatchToProps = (dispatch) => ({ handleNotification: (value) => { @@ -14,6 +19,6 @@ const mapDispatchToProps = (dispatch) => ({ }, handleLogin: (userObj) => dispatch(loginAction(userObj)), }); +*/ -export default asyncConnect(resolve, mapState, mapDispatchToProps)(Header); - +export default asyncConnect(resolve, mapState, {...actions, logoutAction})(Header); diff --git a/src/routes/Home/components/LoginModal/LoginModal.jsx b/src/routes/Home/components/LoginModal/LoginModal.jsx index 5bd807e..eb7799c 100644 --- a/src/routes/Home/components/LoginModal/LoginModal.jsx +++ b/src/routes/Home/components/LoginModal/LoginModal.jsx @@ -7,6 +7,12 @@ import Button from 'components/Button'; import Checkbox from 'components/Checkbox'; import TextField from 'components/TextField'; import styles from './LoginModal.scss'; +import APIService from '../../../../services/APIService'; +import {toastr} from 'react-redux-toastr'; +import {defaultAuth0Service} from '../../../../services/AuthService'; + +const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i; + /* * customStyles */ @@ -50,10 +56,11 @@ FormField.propTypes = { */ class LogInModal extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); this.state = { modalLoginIsOpen: false, + showForgetPassword: false, }; } @@ -62,28 +69,60 @@ class LogInModal extends React.Component { } closeLoginModal() { - this.setState({modalLoginIsOpen: false}); + this.setState({modalLoginIsOpen: false, showForgetPassword: false}); } login() { this.setState({modalLoginIsOpen: true}); } - handleLogin(handleLoggedIn, loggedUser) { - handleLoggedIn(); - const _self = this; - setTimeout(() => { - handleLoggedIn(); - if (loggedUser) { - _self.setState({modalLoginIsOpen: false}); - _self.setState({modalSignupIsOpen: false}); + forgetPassword() { + this.setState({showForgetPassword: true}); + } + + /** + * Login using google social network, + * this method internally uses auth0 service + */ + googleLogin() { + defaultAuth0Service.login({connection: 'google-oauth2'}, (error) => { + if (error) { + const message = error.message || 'something went wrong, please try again'; + toastr.error(message); } - }, 100); + }); } - render() { - const {handleSubmit, fields, handleLoggedIn, loggedUser, hasError, errorText} = this.props; + /** + * Login using facebook social network, + * this method internally uses auth0 service + */ + facebookLogin() { + defaultAuth0Service.login({connection: 'facebook'}, (error) => { + if (error) { + const message = error.message || 'something went wrong, please try again'; + toastr.error(message); + } + }); + } + + /** + * This method is invoked when reset password request is submitted + */ + handleForgetPassword(data) { + APIService.forgotPassword({email: data.emailUp}).then(() => { + toastr.success('', 'Reset password link emailed to your email address'); + this.closeLoginModal(); + }).catch((reason) => { + const message = reason.response.body.error || 'something went wrong, please try again'; + toastr.error(message); + this.closeLoginModal(); + }); + } + render() { + const _self = this; + const {handleSubmit, fields, hasError, errorText} = this.props; return (
@@ -100,81 +139,98 @@ class LogInModal extends React.Component {
-
Login to Your Account
+ {this.state.showForgetPassword === false &&
Login to Your Account
} + {this.state.showForgetPassword === true &&
Reset forgotten password
}
+ {this.state.showForgetPassword === false && +
+ - - - - - {/* login with end */} -
-
-
or
-
-
- {/* or end */} -
- {hasError && {errorText.error}} -
- - - + + {/* login with end */} +
+
+
or
+
+
+ {/* or end */}
- - - + {hasError && {errorText}} +
+ + + +
+
+ + + +
+
+ {/* input end */} +
+
+ this.props.fields.remember.onChange(!this.props.fields.remember.value)} + id="remember" + > + Remember me + +
+
-
- {/* input end */} -
-
- this.props.fields.remember.onChange(!this.props.fields.remember.value)} - id="remember" +
+ +
+
+ Don’t have an account? Sign Up +
+ + } + { this.state.showForgetPassword === true && +
_self.handleForgetPassword(data))}> +
+ {hasError && {errorText}} +
+ + + +
+
+
+
- -
-
- -
-
- Don’t have an account? Sign Up -
- + + } - -
); } } LogInModal.propTypes = { - handleSubmit: PropTypes.func.isRequired, + handleSubmit: PropTypes.func, fields: PropTypes.object, - handleLoggedIn: PropTypes.func.isRequired, - loggedUser: PropTypes.bool, + loginAction: PropTypes.func.isRequired, hasError: PropTypes.bool, errorText: PropTypes.string, }; @@ -183,9 +239,21 @@ const fields = ['remember', 'email', 'password', 'emailUp', 'passwordUp']; const validate = (values) => { const errors = {}; + if (!values.emailUp && !values.email) { + errors.emailUp = 'Email is required'; + } else if (!EMAIL_REGEX.test(values.emailUp) && !values.email) { + errors.emailUp = 'Invalid email address'; + } + + if (errors.emailUp && (values.emailUp || values.email)) { + return errors; + } else if (values.emailUp) { + return errors; + } + if (!values.email) { errors.email = 'Email is required'; - } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) { + } else if (!EMAIL_REGEX.test(values.email)) { errors.email = 'Invalid email address'; } if (!values.password) { diff --git a/src/routes/Home/components/SignupModal/SignupModal.jsx b/src/routes/Home/components/SignupModal/SignupModal.jsx index 213d6e6..f51bf4b 100644 --- a/src/routes/Home/components/SignupModal/SignupModal.jsx +++ b/src/routes/Home/components/SignupModal/SignupModal.jsx @@ -6,6 +6,8 @@ import Modal from 'react-modal'; import Button from 'components/Button'; import TextField from 'components/TextField'; import styles from './SignupModal.scss'; +import {defaultAuth0Service} from '../../../../services/AuthService'; +import {toastr} from 'react-redux-toastr'; /* * customStyles @@ -79,6 +81,32 @@ class SignupModal extends React.Component { }, 100); } + /** + * Login using google social network, + * this method internally uses auth0 service + */ + googleLogin() { + defaultAuth0Service.login({connection: 'google-oauth2'}, (error) => { + if (error) { + const message = error.message || 'something went wrong, please try again'; + toastr.error(message); + } + }); + } + + /** + * Login using facebook social network, + * this method internally uses auth0 service + */ + facebookLogin() { + defaultAuth0Service.login({connection: 'facebook'}, (error) => { + if (error) { + const message = error.message || 'something went wrong, please try again'; + toastr.error(message); + } + }); + } + render() { const {handleSubmit, fields, handleSigned, signedUser, hasError, errorText} = this.props; @@ -103,14 +131,14 @@ class SignupModal extends React.Component {
- + Sign Up with Google Plus @@ -123,12 +151,22 @@ class SignupModal extends React.Component {
{/* or end */}
- {hasError && {errorText.error}} + {hasError && {errorText}}
+
+ + + +
+
+ + + +
@@ -166,7 +204,7 @@ SignupModal.propTypes = { errorText: PropTypes.string, }; -const fields = ['email', 'password', 'emailUp', 'passwordUp']; +const fields = ['email', 'password', 'firstName', 'lastName', 'emailUp', 'passwordUp']; const validate = (values) => { const errors = {}; @@ -175,6 +213,12 @@ const validate = (values) => { } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) { errors.email = 'Invalid email address'; } + if (!values.firstName) { + errors.firstName = 'First Name is required'; + } + if (!values.lastName) { + errors.lastName = 'Last Name is required'; + } if (!values.password) { errors.password = 'Password is required'; } diff --git a/src/routes/Home/containers/LoginModalContainer.js b/src/routes/Home/containers/LoginModalContainer.js index 5bf37b8..9092a5b 100644 --- a/src/routes/Home/containers/LoginModalContainer.js +++ b/src/routes/Home/containers/LoginModalContainer.js @@ -1,11 +1,9 @@ import {connect} from 'react-redux'; -import {sendLoginRequest, loginAction} from '../../../store/modules/global'; +import {loginAction} from '../../../store/modules/global'; import LogInModal from '../components/LoginModal'; -const mapState = (state) => ({...state.global, onSubmit: sendLoginRequest}); +const mapState = (state) => ({...state.global}); -const mapDispatchToProps = (dispatch) => ({ - handleLoggedIn: (value) => dispatch(loginAction(value)), -}); - -export default connect(mapState, mapDispatchToProps)(LogInModal); +export default connect(mapState, { + loginAction, +})(LogInModal); diff --git a/src/routes/ResetPassword/components/ResetPasswordView.jsx b/src/routes/ResetPassword/components/ResetPasswordView.jsx new file mode 100644 index 0000000..3fe1671 --- /dev/null +++ b/src/routes/ResetPassword/components/ResetPasswordView.jsx @@ -0,0 +1,80 @@ +import React, {Component} from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './ResetPasswordView.scss'; +import TextField from '../../../components/TextField'; +import FormField from '../../../components/FormField'; +import Button from '../../../components/Button'; +import {reduxForm} from 'redux-form'; +import {sendRequest} from '../modules/ResetPassword'; +import {browserHistory} from 'react-router'; +import {toastr} from 'react-redux-toastr'; + +class ResetPasswordView extends Component { + + /** + * This function is called when the form is submitted + * This is triggered by handleSubmit + */ + onSubmit(data) { + sendRequest(data).then(() => { + toastr.success('', 'Password reset successfuly, kindly login again'); + browserHistory.push('/'); + }).catch((reason) => { + const message = reason.response.body.error || 'something went wrong, please try again'; + toastr.error(message); + }); + } + + render() { + const {fields, handleSubmit, location: {query: {token}}} = this.props; + const _self = this; + return ( +
+ _self.onSubmit({...data, code: token}))}> +
+ + + + +
+
+ + + + +
+ + {/* add-package end */} +
+ +
+ + {/* form end */} +
+ ); + } +} + +ResetPasswordView.propTypes = { + fields: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired, + handleSubmit: React.PropTypes.func.isRequired, +}; + +const form = reduxForm({ + form: 'resetPasswordForm', + fields: ['password', 'email'], + validate(values) { + const errors = {}; + if (!values.password) { + errors.password = 'required'; + } + if (!values.email) { + errors.email = 'required'; + } + + return errors; + }, +}); + +export default form(CSSModules(ResetPasswordView, styles)); diff --git a/src/routes/ResetPassword/components/ResetPasswordView.scss b/src/routes/ResetPassword/components/ResetPasswordView.scss new file mode 100644 index 0000000..b008991 --- /dev/null +++ b/src/routes/ResetPassword/components/ResetPasswordView.scss @@ -0,0 +1,48 @@ +.reset-password-form { + padding: 50px 0 14px; + margin: 0 300px; + height: calc(100vh - 60px - 42px - 50px); // header height - breadcrumb height - footer height + h4 { + font-weight: bold; + font-size: 20px; + color: #525051; + margin-top: 40px; + border-top: 1px solid #e7e8ea; + padding-top: 25px; + } + :global { + .form-field { + width: 100%; + &.error { + color: #ff3100; + > div:first-child { + border: 1px solid #ff3100; + } + } + } + } +} +.row { + display: flex; + margin-bottom: 22px; + label { + display: block; + flex: 0 0 20%; + align-self: center; + font-size: 14px; + color: #343434; + font-weight: bold; + } + + .input-with-label { + flex: 0 0 20%; + display: flex; + align-items: center; + .input { + flex: 0 0 66%; + } + } +} +.actions { + text-align: right; +} diff --git a/src/routes/ResetPassword/containers/ResetPasswordContainer.js b/src/routes/ResetPassword/containers/ResetPasswordContainer.js new file mode 100644 index 0000000..f6309c7 --- /dev/null +++ b/src/routes/ResetPassword/containers/ResetPasswordContainer.js @@ -0,0 +1,12 @@ +import {asyncConnect} from 'redux-connect'; +import {actions} from '../modules/ResetPassword'; + +import ResetPasswordView from '../components/ResetPasswordView'; + +const resolve = [{ + promise: () => Promise.resolve(), +}]; + +const mapState = (state) => state.resetPassword; + +export default asyncConnect(resolve, mapState, actions)(ResetPasswordView); diff --git a/src/routes/ResetPassword/index.js b/src/routes/ResetPassword/index.js new file mode 100644 index 0000000..eeb7dbb --- /dev/null +++ b/src/routes/ResetPassword/index.js @@ -0,0 +1,20 @@ +import {injectReducer} from '../../store/reducers'; + +export default (store) => ({ + path: 'reset-password', + name: 'Reset password', /* Breadcrumb name */ + staticName: true, + getComponent(nextState, cb) { + require.ensure([], (require) => { + const Dashboard = require('./containers/ResetPasswordContainer').default; + const reducer = require('./modules/ResetPassword').default; + + injectReducer(store, {key: 'resetPassword', reducer}); + if (!nextState.location.query.token) { + cb(new Error('Invalid route invocation')); + } else { + cb(null, Dashboard); + } + }, 'ResetPassword'); + }, +}); diff --git a/src/routes/ResetPassword/modules/ResetPassword.js b/src/routes/ResetPassword/modules/ResetPassword.js new file mode 100644 index 0000000..a5bcb9e --- /dev/null +++ b/src/routes/ResetPassword/modules/ResetPassword.js @@ -0,0 +1,23 @@ +import {handleActions} from 'redux-actions'; +import APIService from 'services/APIService'; +// ------------------------------------ +// Actions +// ------------------------------------ + +export const actions = { +}; + +export const sendRequest = (values) => new Promise((resolve, reject) => { + APIService.resetPassword(values).then((result) => { + resolve(result); + }).catch((reason) => { + reject(reason); + }); +}); + +// ------------------------------------ +// Reducer +// ------------------------------------ +export default handleActions({ +}, { +}); diff --git a/src/routes/index.js b/src/routes/index.js index b89d765..0d9e590 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -22,14 +22,23 @@ import AvailablePackagesRoute from './AvailablePackages'; import AdminDashboard from './Admin/AdminDashboard'; import NoFlyZones from './Admin/NoFlyZones'; import ProviderDetailsRoute from './ProviderDetails'; +import ResetPasswordRoute from './ResetPassword'; +import {defaultAuth0Service} from '../services/AuthService'; export const createRoutes = (store) => ({ path: '/', name: 'CoreLayout', indexRoute: { onEnter: (nextState, replace, cb) => { - replace('/dashboard'); - cb(); + // parse the hash if present + if (nextState.location.hash) { + defaultAuth0Service.parseHash(nextState.location.hash); + replace('/dashboard'); + cb(); + } else { + replace('/dashboard'); + cb(); + } }, }, childRoutes: [ @@ -59,10 +68,10 @@ export const createRoutes = (store) => ({ BrowseProviderRoute(store), DroneDetailsRoute(store), AvailablePackagesRoute(store), + ProviderDetailsRoute(store), ], }, - ProviderDetailsRoute(store), - + ResetPasswordRoute(store), // admin routes { path: 'admin', diff --git a/src/services/APIService.js b/src/services/APIService.js index 783359b..0be05bb 100644 --- a/src/services/APIService.js +++ b/src/services/APIService.js @@ -484,6 +484,8 @@ const regAndAuth = () => authorize().then( ); export default class APIService { + + static fetchMyRequestStatus(filterByStatus) { return (new Promise((resolve) => { resolve(); @@ -501,68 +503,50 @@ export default class APIService { } static fetchMissionList() { - return regAndAuth().then((authRes) => { - const accessToken = authRes.body.accessToken; - - return request - .get(`${config.api.basePath}/api/v1/missions`) - .set('Authorization', `Bearer ${accessToken}`) - .end() - .then((res) => res.body.items.map((item) => ({ - ...item, - downloadLink: `${config.api.basePath}/api/v1/missions/${item.id}/download?token=${accessToken}`, - }))); - }); + const token = this.accessToken; + return request + .get(`${config.api.basePath}/api/v1/missions`) + .set('Authorization', `Bearer ${this.accessToken}`) + .send() + .end() + .then((res) => res.body.items.map((item) => ({ + ...item, + downloadLink: `${config.api.basePath}/api/v1/missions/${item.id}/download?token=${token}`, + }))); } static getMission(id) { - return regAndAuth().then((authRes) => { - const accessToken = authRes.body.accessToken; - - return request - .get(`${config.api.basePath}/api/v1/missions/${id}`) - .set('Authorization', `Bearer ${accessToken}`) - .end() - .then((res) => res.body); - }); + return request + .get(`${config.api.basePath}/api/v1/missions/${id}`) + .set('Authorization', `Bearer ${this.accessToken}`) + .end() + .then((res) => res.body); } static createMission(values) { - return regAndAuth().then((authRes) => { - const accessToken = authRes.body.accessToken; - - return request - .post(`${config.api.basePath}/api/v1/missions`) - .set('Authorization', `Bearer ${accessToken}`) - .send(values) - .end() - .then((res) => res.body); - }); + return request + .post(`${config.api.basePath}/api/v1/missions`) + .set('Authorization', `Bearer ${this.accessToken}`) + .send(values) + .end() + .then((res) => res.body); } static updateMission(id, values) { - return regAndAuth().then((authRes) => { - const accessToken = authRes.body.accessToken; - - return request - .put(`${config.api.basePath}/api/v1/missions/${id}`) - .set('Authorization', `Bearer ${accessToken}`) - .send(values) - .end() - .then((res) => res.body); - }); + return request + .put(`${config.api.basePath}/api/v1/missions/${id}`) + .set('Authorization', `Bearer ${this.accessToken}`) + .send(values) + .end() + .then((res) => res.body); } static deleteMission(id) { - return regAndAuth().then((authRes) => { - const accessToken = authRes.body.accessToken; - - return request - .del(`${config.api.basePath}/api/v1/missions/${id}`) - .set('Authorization', `Bearer ${accessToken}`) - .end() - .then((res) => res.body); - }); + return request + .del(`${config.api.basePath}/api/v1/missions/${id}`) + .set('Authorization', `Bearer ${this.accessToken}`) + .end() + .then((res) => res.body); } /** @@ -641,4 +625,28 @@ export default class APIService { .del(`${config.api.basePath}/api/v1/nfz/${id}`) .end(); } + + /** + * Reset the user password + * @param {Object} entity the client request payload + */ + static resetPassword(entity) { + return request + .post(`${config.api.basePath}/api/v1/reset-password`) + .set('Content-Type', 'application/json') + .send(entity) + .end(); + } + + /** + * Send the forgot password link to user's email account + * @param {Object} entity the client request payload + */ + static forgotPassword(entity) { + return request + .post(`${config.api.basePath}/api/v1/forgot-password`) + .set('Content-Type', 'application/json') + .send(entity) + .end(); + } } diff --git a/src/services/AuthService.js b/src/services/AuthService.js new file mode 100644 index 0000000..e9c70e9 --- /dev/null +++ b/src/services/AuthService.js @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2016 Topcoder Inc, All rights reserved. + */ + +/** + * auth0 Authentication service for the app. + * + * @author TCSCODER + * @version 1.0.0 + */ + +import Auth0 from 'auth0-js'; +import config from '../config'; +import UserApi from '../api/User'; +import _ from 'lodash'; + + +const userApi = new UserApi(config.api.basePath); +const idTokenKey = 'id_token'; + +class AuthService { + + /** + * Default constructor + * @param {String} clientId the auth0 client id + * @param {String} domain the auth0 domain + */ + constructor(clientId, domain) { + this.auth0 = new Auth0({ + clientID: clientId, + domain, + responseType: 'token', + callbackURL: config.AUTH0_CALLBACK, + }); + this.login = this.login.bind(this); + this.parseHash = this.parseHash.bind(this); + this.loggedIn = this.loggedIn.bind(this); + this.logout = this.logout.bind(this); + this.getProfile = this.getProfile.bind(this); + this.getHeader = this.getHeader.bind(this); + } + + /** + * Redirects the user to appropriate social network for oauth2 authentication + * + * @param {Object} params any params to pass to auth0 client + * @param {Function} onError function to execute on error + */ + login(params, onError) { + // redirects the call to auth0 instance + this.auth0.login(params, onError); + } + + /** + * Parse the hash fragment of url + * This method will actually parse the token + * will create a user profile if not already present and save the id token in local storage + * if there is some error delete the access token + * @param {String} hash the hash fragment + */ + parseHash(hash) { + const _self = this; + const authResult = _self.auth0.parseHash(hash); + if (authResult && authResult.idToken) { + _self.setToken(authResult.idToken); + // get social profile + _self.getProfile((error, profile) => { + if (error) { + // remove the id token + _self.removeToken(); + throw error; + } else { + userApi.registerSocialUser(profile.name, profile.email, _self.getToken()).then( + (authResult) => { + localStorage.setItem('userInfo', JSON.stringify(authResult)); + }).catch((reason) => { + // remove the id token + _self.removeToken(); + throw reason; + }); + } + }); + } + } + + /** + * Check if the user is logged in + * @param {String} hash the hash fragment + */ + loggedIn() { + // Checks if there is a saved token and it's still valid + return !!this.getToken(); + } + + /** + * Set the id token to be stored in local storage + * @param {String} idToken the token to store + */ + setToken(idToken) { + // Saves user token to localStorage + localStorage.setItem(idTokenKey, idToken); + } + + /** + * Get the stored id token from local storage + */ + getToken() { + // Retrieves the user token from localStorage + return localStorage.getItem(idTokenKey); + } + + /** + * Remove the id token from local storage + */ + removeToken() { + // Clear user token and profile data from localStorage + localStorage.removeItem(idTokenKey); + } + + /** + * Logout the user from the application, delete the id token + */ + logout() { + this.removeToken(); + } + + /** + * Get the authorization header for API access + */ + getHeader() { + return { + Authorization: `Bearer ${this.getToken()}`, + }; + } + + /** + * Get the profile of currently logged in user + * + * @param {callback} the callback function to call after operation finishes + * @return {Object} the profile of logged in user + */ + getProfile(callback) { + this.auth0.getProfile(this.getToken(), callback); + } +} + +const defaultAuth0Service = new AuthService(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_DOMAIN); + +export {AuthService as default, defaultAuth0Service}; diff --git a/src/static/img/myDrones/drone-lg.png b/src/static/img/myDrones/drone-lg.png index 2283147..448d226 100644 Binary files a/src/static/img/myDrones/drone-lg.png and b/src/static/img/myDrones/drone-lg.png differ diff --git a/src/static/img/myDrones/drone-spec.png b/src/static/img/myDrones/drone-spec.png index 6f08ad2..d645448 100644 Binary files a/src/static/img/myDrones/drone-spec.png and b/src/static/img/myDrones/drone-spec.png differ diff --git a/src/static/img/myDrones/my-drone-1.png b/src/static/img/myDrones/my-drone-1.png index 02d3b2b..eb58cc1 100644 Binary files a/src/static/img/myDrones/my-drone-1.png and b/src/static/img/myDrones/my-drone-1.png differ diff --git a/src/static/img/myDrones/my-drone-2.png b/src/static/img/myDrones/my-drone-2.png index b9402c3..5a3b4d9 100644 Binary files a/src/static/img/myDrones/my-drone-2.png and b/src/static/img/myDrones/my-drone-2.png differ diff --git a/src/static/img/myDrones/my-drone-3.png b/src/static/img/myDrones/my-drone-3.png index b347080..846771d 100644 Binary files a/src/static/img/myDrones/my-drone-3.png and b/src/static/img/myDrones/my-drone-3.png differ diff --git a/src/static/img/myDrones/my-drone-4.png b/src/static/img/myDrones/my-drone-4.png index 2722254..622dbce 100644 Binary files a/src/static/img/myDrones/my-drone-4.png and b/src/static/img/myDrones/my-drone-4.png differ diff --git a/src/store/modules/global.js b/src/store/modules/global.js index 0e0357c..4e06868 100644 --- a/src/store/modules/global.js +++ b/src/store/modules/global.js @@ -3,38 +3,49 @@ import {browserHistory} from 'react-router'; import UserApi from 'api/User.js'; import config from '../../config'; +import APIService from 'services/APIService'; + const userApi = new UserApi(config.api.basePath); +//------------------------------------------------------------------------------ +// Constants + +const LOGIN_ACTION_FAILURE = 'LOGIN_ACTION_FAILURE'; +const LOGIN_ACTION_SUCCESS = 'LOGIN_ACTION_SUCCESS'; + +const LOGIN_REDIRECT = { + admin: '/admin', + consumer: '/browse-provider', + pilot: '/pilot', + provider: '/dashboard', +}; + +const LOGOUT_ACTION = 'LOGOUT_ACTION'; +const USER_INFO_KEY = 'userInfo'; + // ------------------------------------ // Actions // ------------------------------------ + +// TODO: Any use of these local variables should be eliminated! +// Their current usage should be entirely replaced using the redux state, +// and action payloads! let isLogged = false; let hasError = false; let errorText = ''; +let userInfo = {}; -export const sendLoginRequest = (values) => new Promise((resolve) => { - userApi.login(values.email, values.password).then((authResult) => { - isLogged = true; - hasError = false; - if (authResult.user.role === 'consumer') { - browserHistory.push('/browse-provider'); - } else if (authResult.user.role === 'provider') { - browserHistory.push('/dashboard'); - } else if (authResult.user.role === 'admin') { - browserHistory.push('/admin'); - } else if (authResult.user.role === 'pilot') { - browserHistory.push('/pilot'); - } - }).catch((err) => { - isLogged = false; - hasError = true; - errorText = JSON.parse(err.responseText); - }); - resolve(); -}); +function loadUserInfo() { + userInfo = localStorage.getItem(USER_INFO_KEY); + if (userInfo) { + userInfo = JSON.parse(userInfo); + APIService.accessToken = userInfo.accessToken; + } + return userInfo; +} export const sendSignupRequest = (values) => new Promise((resolve) => { - userApi.register('name', values.email, values.password).then(() => { + userApi.register(values.firstName, values.lastName, values.email, values.password).then(() => { isLogged = true; hasError = false; browserHistory.push('/browse-provider'); @@ -48,14 +59,32 @@ export const sendSignupRequest = (values) => new Promise((resolve) => { export const toggleNotification = createAction('TOGGLE_NOTIFICATION'); -export const loginAction = createAction('LOGIN_ACTION'); +export const loginAction = (data) => (dispatch) => { + userApi.login(data.email, data.password).then((res) => { + localStorage.setItem(USER_INFO_KEY, JSON.stringify(res)); + dispatch({type: LOGIN_ACTION_SUCCESS}); + browserHistory.push(LOGIN_REDIRECT[res.user.role]); + }).catch((failure) => { + dispatch({ + type: LOGIN_ACTION_FAILURE, + payload: JSON.parse(failure.response).error, + }); + }); +}; + +export const logoutAction = () => (dispatch) => { + browserHistory.push('/home'); + dispatch({ + type: LOGOUT_ACTION, + }); +}; export const signupAction = createAction('SIGNUP_ACTION'); export const actions = { - toggleNotification, loginAction, + toggleNotification, loginAction, logoutAction, }; -// console.log(loginAction(true)) + // ------------------------------------ // Reducer // ------------------------------------ @@ -63,24 +92,45 @@ export default handleActions({ [toggleNotification]: (state, action) => ({ ...state, toggleNotif: action.payload, }), - [loginAction]: (state) => ({ - ...state, loggedUser: isLogged, hasError, errorText, + [LOGIN_ACTION_FAILURE]: (state, action) => ({ + ...state, + loggedUser: false, + hasError: true, + errorText: action.payload, + user: {}, + }), + [LOGIN_ACTION_SUCCESS]: (state) => ({ + ...state, + loggedUser: true, + hasError: false, + errorText: '', + user: (loadUserInfo() ? loadUserInfo().user : {}), }), + [LOGOUT_ACTION]: (state) => { + localStorage.removeItem(USER_INFO_KEY); + APIService.accessToken = ''; + isLogged = false; + return ({ + ...state, + loggedUser: false, + hasError, + errorText, + user: {}, + }); + }, [signupAction]: (state) => ({ - ...state, loggedUser: isLogged, hasError, errorText, + ...state, loggedUser: isLogged, hasError, errorText, user: (loadUserInfo() ? loadUserInfo().user : {}), }), }, { toggleNotif: false, - loggedUser: false, + loggedUser: Boolean(loadUserInfo()), location: 'Jakarta, Indonesia', selectedCategory: 'Category', categories: [ {name: 'Category1'}, {name: 'Category2'}, ], - user: { - name: 'John Doe', - }, + user: loadUserInfo() ? loadUserInfo().user : {}, notifications: [ { id: 1,