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, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy