├── .babelrc ├── .env ├── .gitignore ├── README.md ├── karma.conf.js ├── package.json ├── scripts └── prepublish.sh ├── src ├── app.css ├── app.js ├── containers │ └── App │ │ ├── App.js │ │ ├── App.spec.js │ │ └── styles.module.css ├── routes.js ├── styles │ ├── base.css │ ├── colors.css │ └── queries.css ├── utils │ ├── AuthService.js │ ├── constants.js │ └── jwtHelper.js └── views │ └── Main │ ├── Container.js │ ├── Home │ ├── Home.js │ └── styles.module.css │ ├── Instructor │ ├── Instructor.js │ └── styles.module.css │ ├── Login │ ├── Login.js │ └── styles.module.css │ ├── NewInstructor │ ├── NewInstructor.js │ └── styles.module.css │ ├── Profile │ ├── Profile.js │ └── styles.module.css │ ├── routes.js │ └── styles.module.css ├── tests.webpack.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | }, 7 | "production": { 8 | "presets": [] 9 | }, 10 | "test": { 11 | "presets": [] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3solution/react-user-authentication-master/2f05ef87105da32992475a3ed729062efae08f2d/.env -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | dist 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Authentication for Front End Masters 2 | 3 | ## Running the App 4 | 5 | Install the dependencies: 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | Start the app: 12 | 13 | ```bash 14 | npm start 15 | ``` 16 | 17 | The app will be served at `localhost:3000`. -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv; 2 | var path = require('path'); 3 | 4 | var webpackConfig = require('./webpack.config'); 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | basePath: '', 9 | frameworks: ['mocha', 'chai'], 10 | files: [ 11 | 'tests.webpack.js' 12 | ], 13 | 14 | preprocessors: { 15 | // add webpack as preprocessor 16 | 'tests.webpack.js': ['webpack', 'sourcemap'], 17 | }, 18 | 19 | webpack: webpackConfig, 20 | webpackServer: { 21 | noInfo: true 22 | }, 23 | 24 | plugins: [ 25 | 'karma-mocha', 26 | 'karma-chai', 27 | 'karma-webpack', 28 | 'karma-phantomjs-launcher', 29 | 'karma-spec-reporter', 30 | 'karma-sourcemap-loader' 31 | ], 32 | 33 | reporters: ['spec'], 34 | port: 9876, 35 | colors: true, 36 | logLevel: config.LOG_INFO, 37 | browsers: ['PhantomJS'], 38 | singleRun: !argv.watch 39 | }) 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-react-sample", 3 | "version": "1.0.0", 4 | "description": "A minimal reactJS sample application showing auth0 integration", 5 | "repository": "https://github.com/auth0-samples/auth0-react-sample", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "npm-run-all --parallel dev-server", 9 | "dev-server": "cross-env NODE_ENV=development hjs-dev-server", 10 | "clean": "rimraf dist", 11 | "build": "npm run clean && cross-env NODE_ENV=production webpack", 12 | "publish_pages": "gh-pages -d dist", 13 | "ghpages": "npm run build && npm run publish_pages", 14 | "test": "cross-env NODE_ENV=test karma start karma.conf.js", 15 | "test:watch": "npm run test -- --watch" 16 | }, 17 | "devDependencies": { 18 | "autoprefixer": "^6.3.6", 19 | "babel-core": "^6.7.7", 20 | "babel-loader": "^6.2.4", 21 | "babel-plugin-transform-es2015-modules-umd": "^6.8.0", 22 | "babel-polyfill": "^6.7.4", 23 | "babel-preset-es2015": "^6.6.0", 24 | "babel-preset-react": "^6.5.0", 25 | "babel-preset-react-hmre": "^1.1.1", 26 | "babel-preset-stage-0": "^6.5.0", 27 | "babel-register": "^6.7.2", 28 | "chai": "^3.5.0", 29 | "chai-enzyme": "^0.4.2", 30 | "cheerio": "^0.20.0", 31 | "cross-env": "^1.0.8", 32 | "css-loader": "^0.23.1", 33 | "cssnano": "^3.5.2", 34 | "dotenv": "^2.0.0", 35 | "enzyme": "^2.2.0", 36 | "expect": "^1.18.0", 37 | "file-loader": "^0.8.5", 38 | "gh-pages": "^0.11.0", 39 | "hjs-webpack": "^8.1.0", 40 | "jasmine-core": "^2.4.1", 41 | "json-loader": "^0.5.4", 42 | "karma": "^0.13.22", 43 | "karma-chai": "^0.1.0", 44 | "karma-jasmine": "^0.3.8", 45 | "karma-mocha": "^1.0.1", 46 | "karma-phantomjs-launcher": "^1.0.0", 47 | "karma-sourcemap-loader": "^0.3.7", 48 | "karma-spec-reporter": "0.0.26", 49 | "karma-webpack": "^1.7.0", 50 | "mocha": "^2.4.5", 51 | "npm-run-all": "^2.3.0", 52 | "phantomjs-polyfill": "0.0.2", 53 | "phantomjs-prebuilt": "^2.1.7", 54 | "postcss-loader": "^0.9.1", 55 | "precss": "^1.4.0", 56 | "prettyjson": "^1.1.3", 57 | "react-addons-test-utils": "^15.0.2", 58 | "sinon": "^1.17.4", 59 | "style-loader": "^0.13.1", 60 | "transform-loader": "^0.2.3", 61 | "url-loader": "^0.5.7", 62 | "webpack": "^1.13.0", 63 | "yargs": "^4.7.1" 64 | }, 65 | "dependencies": { 66 | "auth0-lock": "^11.27.0", 67 | "bootstrap": "^3.3.7", 68 | "classnames": "^2.2.5", 69 | "express-jwt": "^5.0.0", 70 | "jwt-decode": "^2.1.0", 71 | "md5": "^2.2.1", 72 | "react": "^15.3.1", 73 | "react-bootstrap": "^0.30.0-rc.1", 74 | "react-dom": "^15.3.1", 75 | "react-router": "^2.8.0", 76 | "react-router-bootstrap": "^0.23.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scripts/prepublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "=> Compiling..." 4 | echo "" 5 | rm -rf ./dist 6 | NODE_ENV=production ./node_modules/.bin/webpack 7 | echo "" 8 | echo "=> Complete" 9 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3solution/react-user-authentication-master/2f05ef87105da32992475a3ed729062efae08f2d/src/app.css -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import 'bootstrap/dist/css/bootstrap.css' 5 | import './app.css' 6 | 7 | import App from 'containers/App/App' 8 | 9 | import {browserHistory} from 'react-router' 10 | import makeRoutes from './routes' 11 | 12 | const routes = makeRoutes() 13 | 14 | const mountNode = document.querySelector('#root'); 15 | ReactDOM.render( 16 | , 18 | mountNode) 19 | -------------------------------------------------------------------------------- /src/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Router } from 'react-router' 3 | 4 | class App extends React.Component { 5 | static contextTypes = { 6 | router: PropTypes.object 7 | } 8 | 9 | static propTypes = { 10 | history: PropTypes.object.isRequired, 11 | routes: PropTypes.element.isRequired 12 | } 13 | 14 | get content() { 15 | return ( 16 | 19 | ) 20 | } 21 | 22 | render () { 23 | return ( 24 |
25 | {this.content} 26 |
27 | ) 28 | } 29 | } 30 | 31 | export default App 32 | -------------------------------------------------------------------------------- /src/containers/App/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { expect } from 'chai' 3 | import { shallow } from 'enzyme' 4 | 5 | import App from './App' 6 | import styles from './styles.module.css' 7 | 8 | describe('', () => { 9 | let wrapper 10 | let history = {} 11 | beforeEach(() => { 12 | wrapper = 13 | shallow() 14 | }) 15 | 16 | it('has a Router component', () => { 17 | expect(wrapper.find('Router')) 18 | .to.have.length(1) 19 | }) 20 | 21 | it('passes a history prop', () => { 22 | const props = wrapper.find('Router').props(); 23 | 24 | expect(props.history) 25 | .to.be.defined 26 | }) 27 | 28 | }) 29 | -------------------------------------------------------------------------------- /src/containers/App/styles.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | } 4 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {browserHistory, Router, Route, Redirect} from 'react-router' 3 | 4 | import makeMainRoutes from './views/Main/routes' 5 | 6 | export const makeRoutes = () => { 7 | const main = makeMainRoutes() 8 | 9 | return ( 10 | 11 | {main} 12 | 13 | ) 14 | } 15 | 16 | export default makeRoutes 17 | -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --topbar-height: 80px; 3 | --padding: 25px; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark: #404040; 3 | --light-gray: #a2a2a2; 4 | --white: #ffffff; 5 | --highlight: #48b5e9; 6 | --heading-color: var(--highlight); 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/queries.css: -------------------------------------------------------------------------------- 1 | @custom-media --screen-phone (width <= 35.5em); 2 | @custom-media --screen-phone-lg (width > 35.5em); 3 | 4 | @custom-media --screen-sm var(--screen-phone) and (width < 48em); 5 | @custom-media --screen-md (width >= 48em) and (width < 64em); 6 | @custom-media --screen-lg (width >= 64em) and (width < 80em); 7 | @custom-media --screen-xl (width >= 80em); 8 | -------------------------------------------------------------------------------- /src/utils/AuthService.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { isTokenExpired } from './jwtHelper' 3 | import { browserHistory } from 'react-router' 4 | import jwtDecode from 'jwt-decode' 5 | 6 | import { API_URL } from './constants' 7 | 8 | export default class AuthService extends EventEmitter { 9 | constructor() { 10 | super() 11 | 12 | // binds login functions to keep this context 13 | this.login = this.login.bind(this) 14 | } 15 | 16 | _doAuthentication(endpoint, values) { 17 | return this.fetch(`${API_URL}/${endpoint}`, { 18 | method: 'POST', 19 | body: JSON.stringify(values), 20 | headers: { 'Content-Type': 'application/json' } 21 | }) 22 | } 23 | 24 | login(user, password) { 25 | return this._doAuthentication('users/authenticate', { user, password }) 26 | } 27 | 28 | signup(username, email, password) { 29 | return this._doAuthentication('users', { username, email, password }) 30 | } 31 | 32 | isAuthenticated() { 33 | // Checks if there is a saved token and it's still valid 34 | const token = localStorage.getItem('token') 35 | if (token) { 36 | return !isTokenExpired(token) 37 | } else { 38 | return false 39 | } 40 | } 41 | 42 | isAdmin() { 43 | return jwtDecode(this.getToken()).scope === 'admin' 44 | } 45 | 46 | finishAuthentication(token) { 47 | localStorage.setItem('token', token) 48 | } 49 | 50 | getToken() { 51 | // Retrieves the user token from localStorage 52 | return localStorage.getItem('token') 53 | } 54 | 55 | logout() { 56 | // Clear user token and profile data from localStorage 57 | localStorage.removeItem('token') 58 | } 59 | 60 | _checkStatus(response) { 61 | // raises an error in case response status is not a success 62 | if (response.status >= 200 && response.status < 300) { 63 | return response 64 | } else { 65 | var error = new Error(response.statusText) 66 | error.response = response 67 | return error 68 | } 69 | } 70 | 71 | fetch(url, options) { 72 | // performs api calls sending the required authentication headers 73 | const headers = { 74 | 'Accept': 'application/json', 75 | 'Content-Type': 'application/json' 76 | } 77 | 78 | if (this.isAuthenticated()) { 79 | headers['Authorization'] = 'Bearer ' + this.getToken() 80 | } 81 | 82 | return fetch(url, { 83 | headers, 84 | ...options 85 | }) 86 | .then(response => response.json()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const API_URL = 'https://fem-user-authentication-api.herokuapp.com/api' 2 | -------------------------------------------------------------------------------- /src/utils/jwtHelper.js: -------------------------------------------------------------------------------- 1 | import decode from 'jwt-decode' 2 | 3 | export function getTokenExpirationDate(token) { 4 | const decoded = decode(token) 5 | if(!decoded.exp) { 6 | return null 7 | } 8 | 9 | const date = new Date(0) // The 0 here is the key, which sets the date to the epoch 10 | date.setUTCSeconds(decoded.exp) 11 | return date 12 | } 13 | 14 | export function isTokenExpired(token) { 15 | const date = getTokenExpirationDate(token) 16 | const offsetSeconds = 0 17 | if (date === null) { 18 | return false 19 | } 20 | return !(date.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000))) 21 | } 22 | -------------------------------------------------------------------------------- /src/views/Main/Container.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react' 2 | import { Nav, Navbar, NavItem, Header, Brand } from 'react-bootstrap' 3 | import { Link } from 'react-router' 4 | import { LinkContainer } from 'react-router-bootstrap' 5 | import AuthService from 'utils/AuthService' 6 | 7 | const auth = new AuthService() 8 | 9 | export class Container extends React.Component { 10 | static contextTypes = { 11 | router: T.object 12 | } 13 | 14 | logout(){ 15 | auth.logout() 16 | this.context.router.push('/home') 17 | } 18 | 19 | render() { 20 | let children = null 21 | if (this.props.children) { 22 | children = React.cloneElement(this.props.children, { 23 | auth: this.props.route.auth //sends auth instance to children 24 | }) 25 | } 26 | 27 | return ( 28 |
29 | 30 | 31 | 32 | 33 | React Authentication 34 | 35 | 36 | 37 | 49 | 60 | 61 |
62 | { children } 63 |
64 |
65 | ) 66 | } 67 | } 68 | 69 | export default Container 70 | -------------------------------------------------------------------------------- /src/views/Main/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react' 2 | import {Jumbotron, Button} from 'react-bootstrap' 3 | import AuthService from 'utils/AuthService' 4 | import styles from './styles.module.css' 5 | import { Link } from 'react-router' 6 | 7 | export class Home extends React.Component { 8 | static contextTypes = { 9 | router: T.object 10 | } 11 | 12 | static propTypes = { 13 | auth: T.instanceOf(AuthService) 14 | } 15 | 16 | logout(){ 17 | this.props.auth.logout() 18 | this.context.router.push('/home') 19 | } 20 | 21 | render() { 22 | const { isAuthenticated } = this.props.auth 23 | return ( 24 | 25 |

Welcome!

26 |

This app demonstrates how to add authentication to a React app. More specifically, it covers how to:

27 |
    28 |
  • Add a login/signup area which returns a JSON Web Token that is saved in localStorage
  • 29 |
  • Conditionally hide and show various parts of the application depending on the user's authentication state
  • 30 |
  • Create a profile area which displays user information from the payload of the JWT
  • 31 |
  • Protect client-side routes with the onEnter route event
  • 32 |
  • Make requests for server resources protected by JWT middleware on the server
  • 33 |
  • Make requests for server resources that require a specific scope to be present in the JWT payload
  • 34 |
35 | { !isAuthenticated() && 36 | 37 | 38 | 39 | } 40 | { isAuthenticated() && 41 | 42 | } 43 |
44 | ) 45 | } 46 | } 47 | 48 | export default Home 49 | -------------------------------------------------------------------------------- /src/views/Main/Home/styles.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3solution/react-user-authentication-master/2f05ef87105da32992475a3ed729062efae08f2d/src/views/Main/Home/styles.module.css -------------------------------------------------------------------------------- /src/views/Main/Instructor/Instructor.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react' 2 | import {Panel, Col, Button} from 'react-bootstrap' 3 | import styles from './styles.module.css' 4 | import { Link } from 'react-router' 5 | import jwtDecode from 'jwt-decode' 6 | import md5 from 'md5' 7 | import { API_URL } from './../../../utils/constants' 8 | 9 | function getInstructorListItem (instructor) { 10 | const avatarUrl = `https://www.gravatar.com/avatar/${md5(instructor.email).toLowerCase().trim()}s=200` 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 |

{ instructor.first_name + ' ' + instructor.last_name }

18 |

{ instructor.email }

19 |

{ instructor.company }

20 | 21 |
22 | ) 23 | } 24 | 25 | export class Instructor extends React.Component { 26 | static contextTypes = { 27 | router: T.object 28 | } 29 | 30 | constructor(props, context) { 31 | super(props, context) 32 | this.state = { 33 | instructors: [] 34 | } 35 | this.props.auth.fetch(`${API_URL}/instructors`).then(data => this.setState({ instructors: data })) 36 | } 37 | 38 | onAddInstructorClick() { 39 | this.context.router.push('/instructor/new') 40 | } 41 | 42 | render() { 43 | let instructorList 44 | if (this.state.instructors) { 45 | instructorList = this.state.instructors.map(instructor => getInstructorListItem(instructor)) 46 | } 47 | 48 | const { auth } = this.props 49 | 50 | return ( 51 |
52 |

Front End Masters Instructors

53 | { auth.isAuthenticated() && auth.isAdmin() && 54 | 57 | } 58 |
    59 | {instructorList} 60 |
61 |
62 | ) 63 | } 64 | } 65 | 66 | export default Instructor 67 | -------------------------------------------------------------------------------- /src/views/Main/Instructor/styles.module.css: -------------------------------------------------------------------------------- 1 | h3 { 2 | margin-top: 0; 3 | } 4 | 5 | button { 6 | margin-bottom: 15px !important; 7 | } 8 | 9 | ul { 10 | padding: 0; 11 | } -------------------------------------------------------------------------------- /src/views/Main/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react' 2 | import {Tabs, Col, Tab, ButtonToolbar, Button, FormGroup, FormControl, ControlLabel, Alert} from 'react-bootstrap' 3 | import AuthService from 'utils/AuthService' 4 | import styles from './styles.module.css' 5 | 6 | export class Login extends React.Component { 7 | constructor(context) { 8 | super() 9 | this.state = { 10 | user: '', 11 | username: '', 12 | email: '', 13 | password: '', 14 | loginError: '', 15 | signupError: '' 16 | } 17 | } 18 | static contextTypes = { 19 | router: T.object 20 | } 21 | 22 | static propTypes = { 23 | location: T.object, 24 | auth: T.instanceOf(AuthService) 25 | } 26 | 27 | onLoginSubmit(event) { 28 | event.preventDefault() 29 | const { user, password } = this.state 30 | if (user && password) { 31 | this.props.auth.login(user, password) 32 | .then(result => { 33 | if (!result.token) { 34 | this.setState({loginError: result.message}) 35 | return 36 | } 37 | this.props.auth.finishAuthentication(result.token) 38 | this.context.router.push('/profile') 39 | }) 40 | } 41 | } 42 | 43 | onSignupSubmit(event) { 44 | event.preventDefault() 45 | const { username, email, password } = this.state 46 | if (username && email && password) { 47 | this.props.auth.signup(username, email, password) 48 | .then(result => { 49 | if (!result.token) { 50 | this.setState({signupError: result.message}) 51 | return 52 | } 53 | this.props.auth.finishAuthentication(result.token) 54 | this.context.router.push('/profile') 55 | }) 56 | } 57 | } 58 | 59 | handleChange(event) { 60 | this.setState({[event.target.name]: event.target.value}) 61 | } 62 | 63 | render() { 64 | const { auth } = this.props 65 | return ( 66 | 67 | 68 | 69 |
70 | 71 | Username/Email 72 | 79 | 80 | 81 | Password 82 | 89 | 90 | 91 | 92 |
93 | { this.state.loginError && 94 | {this.state.loginError} 95 | } 96 |
97 | 98 |
99 | 100 | Username 101 | 108 | 109 | 110 | Email 111 | 118 | 119 | 120 | Password 121 | 128 | 129 | 130 |
131 | { this.state.signupError && 132 | {this.state.signupError} 133 | } 134 |
135 |
136 | 137 | ) 138 | } 139 | } 140 | 141 | export default Login 142 | -------------------------------------------------------------------------------- /src/views/Main/Login/styles.module.css: -------------------------------------------------------------------------------- 1 | .tab-content { 2 | margin-top: 15px !important; 3 | } -------------------------------------------------------------------------------- /src/views/Main/NewInstructor/NewInstructor.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react' 2 | import {Panel, Col, Button, FormGroup, FormControl, ControlLabel, Alert} from 'react-bootstrap' 3 | import styles from './styles.module.css' 4 | import jwtDecode from 'jwt-decode' 5 | import md5 from 'md5' 6 | 7 | import { API_URL } from './../../../utils/constants' 8 | 9 | export class NewInstructor extends React.Component { 10 | static contextTypes = { 11 | router: T.object 12 | } 13 | 14 | constructor(props, context) { 15 | super(props, context) 16 | this.state = { 17 | first_name: '', 18 | last_name: '', 19 | email: '', 20 | company: '', 21 | error: '' 22 | } 23 | } 24 | 25 | handleChange(event) { 26 | this.setState({ [event.target.name]: event.target.value }) 27 | } 28 | 29 | onNewInstructorSubmit() { 30 | const { first_name, last_name, email, company } = this.state 31 | const data = { first_name, last_name, email, company } 32 | this.props.auth.fetch(`${API_URL}/instructors`, { 33 | method: 'POST', 34 | body: JSON.stringify(data) 35 | }).then(result => { 36 | if (result.error) { 37 | this.setState({ error: result }) 38 | return 39 | } 40 | this.context.router.push('/instructor') 41 | }) 42 | } 43 | 44 | render() { 45 | return ( 46 | 47 | 48 | First Name 49 | 57 | 58 | 59 | Last Name 60 | 68 | 69 | 70 | Email 71 | 79 | 80 | 81 | Company 82 | 90 | 91 | { this.state.error && 92 | {this.state.error.message} 93 | } 94 | 95 | 96 | ) 97 | } 98 | } 99 | 100 | export default NewInstructor 101 | -------------------------------------------------------------------------------- /src/views/Main/NewInstructor/styles.module.css: -------------------------------------------------------------------------------- 1 | h3 { 2 | margin-top: 0; 3 | } 4 | 5 | button { 6 | margin-bottom: 15px !important; 7 | } 8 | 9 | ul { 10 | padding: 0; 11 | } -------------------------------------------------------------------------------- /src/views/Main/Profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react' 2 | import {Panel, Col} from 'react-bootstrap' 3 | import AuthService from 'utils/AuthService' 4 | import styles from './styles.module.css' 5 | import { Link } from 'react-router' 6 | import jwtDecode from 'jwt-decode' 7 | 8 | export class Profile extends React.Component { 9 | static contextTypes = { 10 | router: T.object 11 | } 12 | 13 | static propTypes = { 14 | auth: T.instanceOf(AuthService) 15 | } 16 | 17 | constructor(props, context) { 18 | super(props, context) 19 | this.state = { 20 | profile: jwtDecode(this.props.auth.getToken()), 21 | payload: jwtDecode(this.props.auth.getToken()) 22 | } 23 | this.state.profile.gravatar = `${this.state.profile.gravatar}?s=200` 24 | } 25 | 26 | render(){ 27 | const { profile, payload } = this.state 28 | return ( 29 |
30 |

Profile

31 | 32 | 33 | Avatar 34 | 35 | 36 |

{profile.username}

37 |
38 |

{ profile.email }

39 |

Payload

40 |
{ JSON.stringify(payload, null, 2) }
41 | 42 |
43 |
44 | ) 45 | } 46 | } 47 | 48 | export default Profile 49 | -------------------------------------------------------------------------------- /src/views/Main/Profile/styles.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3solution/react-user-authentication-master/2f05ef87105da32992475a3ed729062efae08f2d/src/views/Main/Profile/styles.module.css -------------------------------------------------------------------------------- /src/views/Main/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route, IndexRedirect} from 'react-router' 3 | import AuthService from 'utils/AuthService' 4 | import Container from './Container' 5 | import Home from './Home/Home' 6 | import Login from './Login/Login' 7 | import Profile from './Profile/Profile' 8 | import Instructor from './Instructor/Instructor' 9 | import NewInstructor from './NewInstructor/NewInstructor' 10 | 11 | const auth = new AuthService() 12 | 13 | // onEnter callback to validate authentication in private routes 14 | const requireAuth = (nextState, replace) => { 15 | if (!auth.isAuthenticated()) { 16 | replace({ pathname: '/login' }) 17 | } 18 | } 19 | 20 | const requireAdmin = (nextState, replace) => { 21 | if (!auth.isAuthenticated() || !auth.isAdmin()) { 22 | replace({ pathname: '/login' }) 23 | } 24 | } 25 | 26 | export const makeMainRoutes = () => { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | export default makeMainRoutes 40 | -------------------------------------------------------------------------------- /src/views/Main/styles.module.css: -------------------------------------------------------------------------------- 1 | @import url("../../styles/base.css"); 2 | @import url("../../styles/queries.css"); 3 | 4 | .wrapper { 5 | overflow-y: scroll; 6 | display: flex; 7 | margin: 0; 8 | 9 | height: 100vh; 10 | -webkit-box-orient: horizontal; 11 | -o-box-orient: horizontal; 12 | 13 | flex-direction: column; 14 | 15 | @media (--screen-phone-lg) { 16 | flex-direction: row; 17 | } 18 | 19 | } 20 | 21 | .content { 22 | position: relative; 23 | top: var(--topbar-height); 24 | left: 0; 25 | 26 | flex: 1; 27 | order: 1; 28 | 29 | @media (--screen-phone-lg) { 30 | flex: 2; 31 | order: 2; 32 | } 33 | } 34 | 35 | .mainTitle{ 36 | text-align: center; 37 | } 38 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | // some setup first 3 | 4 | var chai = require('chai'); 5 | var chaiEnzyme = require('chai-enzyme'); 6 | 7 | chai.use(chaiEnzyme()) 8 | 9 | var context = require.context('./src', true, /\.spec\.js$/); 10 | context.keys().forEach(context); 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const NODE_ENV = process.env.NODE_ENV; 2 | const dotenv = require('dotenv'); 3 | 4 | const webpack = require('webpack'); 5 | const fs = require('fs'); 6 | const path = require('path'), 7 | join = path.join, 8 | resolve = path.resolve; 9 | 10 | const getConfig = require('hjs-webpack'); 11 | 12 | const isDev = NODE_ENV === 'development'; 13 | const isTest = NODE_ENV === 'test'; 14 | 15 | const root = resolve(__dirname); 16 | const src = join(root, 'src'); 17 | const modules = join(root, 'node_modules'); 18 | const dest = join(root, 'dist'); 19 | 20 | var config = getConfig({ 21 | isDev: isDev, 22 | in: join(src, 'app.js'), 23 | out: dest, 24 | html: function (context) { 25 | return { 26 | 'index.html': context.defaultTemplate({ 27 | title: 'auth0 React Sample', 28 | publicPath: isDev ? 'http://localhost:3000/' : '', 29 | meta: { 30 | 'name': 'auth0 React Sample', 31 | 'description': 'A minimal reactJS sample application showing auth0 integration' 32 | } 33 | }) 34 | } 35 | }, 36 | devServer: { 37 | proxy: { 38 | context: "/api", 39 | options: { 40 | target: "http://localhost:3001" 41 | } 42 | } 43 | } 44 | }); 45 | 46 | // ENV variables 47 | const dotEnvVars = dotenv.config(); 48 | const environmentEnv = dotenv.config({ 49 | path: join(root, 'config', `${NODE_ENV}.config.js`), 50 | silent: true, 51 | }); 52 | const envVariables = 53 | Object.assign({}, dotEnvVars, environmentEnv); 54 | 55 | const defines = 56 | Object.keys(envVariables) 57 | .reduce((memo, key) => { 58 | const val = JSON.stringify(envVariables[key]); 59 | memo[`__${key.toUpperCase()}__`] = val; 60 | return memo; 61 | }, { 62 | __NODE_ENV__: JSON.stringify(NODE_ENV) 63 | }); 64 | 65 | config.plugins = [ 66 | new webpack.DefinePlugin(defines) 67 | ].concat(config.plugins); 68 | // END ENV variables 69 | 70 | // CSS modules 71 | const cssModulesNames = `${isDev ? '[path][name]__[local]__' : ''}[hash:base64:5]`; 72 | 73 | const matchCssLoaders = /(^|!)(css-loader)($|!)/; 74 | 75 | const findLoader = (loaders, match) => { 76 | const found = loaders.filter(l => l && l.loader && l.loader.match(match)) 77 | return found ? found[0] : null; 78 | } 79 | // existing css loader 80 | const cssloader = 81 | findLoader(config.module.loaders, matchCssLoaders); 82 | 83 | const newloader = Object.assign({}, cssloader, { 84 | test: /\.module\.css$/, 85 | include: [src], 86 | loader: cssloader.loader.replace(matchCssLoaders, `$1$2?modules&localIdentName=${cssModulesNames}$3`) 87 | }) 88 | config.module.loaders.push(newloader); 89 | cssloader.test = new RegExp(`^(?!.*(module|bootstrap)).*${cssloader.test.source}`) 90 | cssloader.loader = newloader.loader 91 | 92 | config.module.loaders.push({ 93 | test: /bootstrap\.css$/, 94 | include: [modules], 95 | loader: 'style-loader!css-loader' 96 | }) 97 | 98 | // postcss 99 | config.postcss = [].concat([ 100 | require('precss')({}), 101 | require('autoprefixer')({}), 102 | require('cssnano')({}) 103 | ]) 104 | // END postcss 105 | 106 | // Roots 107 | config.resolve.root = [src, modules] 108 | config.resolve.alias = { 109 | 'css': join(src, 'styles'), 110 | 'containers': join(src, 'containers'), 111 | 'components': join(src, 'components'), 112 | 'utils': join(src, 'utils'), 113 | 114 | 'styles': join(src, 'styles') 115 | } 116 | // end Roots 117 | 118 | // Testing 119 | if (isTest) { 120 | config.externals = { 121 | 'react/addons': true, 122 | 'react/lib/ReactContext': true, 123 | 'react/lib/ExecutionEnvironment': true, 124 | } 125 | config.module.noParse = /[/\\]sinon\.js/; 126 | config.resolve.alias['sinon'] = 'sinon/pkg/sinon'; 127 | 128 | config.plugins = config.plugins.filter(p => { 129 | const name = p.constructor.toString(); 130 | const fnName = name.match(/^function (.*)\((.*\))/) 131 | 132 | const idx = [ 133 | 'DedupePlugin', 134 | 'UglifyJsPlugin' 135 | ].indexOf(fnName[1]); 136 | return idx < 0; 137 | }) 138 | } 139 | // End Testing 140 | 141 | module.exports = config; 142 | --------------------------------------------------------------------------------