├── .gitignore ├── app ├── pages │ ├── Master │ │ ├── style.scss │ │ ├── package.json │ │ └── Master.jsx │ ├── Home │ │ ├── package.json │ │ └── Home.jsx │ └── Login │ │ ├── package.json │ │ └── Login.jsx ├── components │ └── AuthStatus │ │ ├── package.json │ │ └── AuthStatus.jsx ├── actions │ └── AuthActions.js ├── util │ ├── StyleClasses.js │ └── RouteHelpers.js ├── main.jsx ├── stores │ ├── AuthStore.sampleData.json │ └── AuthStore.js └── routes.jsx ├── readme.md ├── server-local.js ├── .eslintrc ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /app/pages/Master/style.scss: -------------------------------------------------------------------------------- 1 | .this { 2 | min-height: 100%; 3 | display: flex; 4 | flex-grow: 1; 5 | } -------------------------------------------------------------------------------- /app/pages/Home/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Home", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Home.jsx" 6 | } -------------------------------------------------------------------------------- /app/pages/Login/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Login", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Login.jsx" 6 | } -------------------------------------------------------------------------------- /app/pages/Master/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Master", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Master.jsx" 6 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### Boot it up 2 | 3 | ``` 4 | npm install 5 | 6 | # start up the asset server 7 | npm run dev-server 8 | 9 | ``` 10 | -------------------------------------------------------------------------------- /app/components/AuthStatus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AuthStatus", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./AuthStatus.jsx" 6 | } -------------------------------------------------------------------------------- /app/actions/AuthActions.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | login: {children: ['completed', 'failed']}, 5 | logout: {} 6 | }); -------------------------------------------------------------------------------- /app/util/StyleClasses.js: -------------------------------------------------------------------------------- 1 | import cx from 'classnames'; 2 | 3 | module.exports = function(style, rules){ 4 | return cx(rules).split(' ').map( className => style[className] ).join(' '); 5 | }; -------------------------------------------------------------------------------- /server-local.js: -------------------------------------------------------------------------------- 1 | // This is a little middleware so that we can preserve pushState 2 | var server = require('pushstate-server'); 3 | 4 | server.start({ 5 | port: process.env.PORT || 8080, 6 | directory: './build' 7 | }); -------------------------------------------------------------------------------- /app/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | import routes from './routes.jsx'; 4 | 5 | Router.run(routes, Router.HistoryLocation, (Root) => { 6 | React.render(, document.body); 7 | }); -------------------------------------------------------------------------------- /app/pages/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Home extends React.Component { 4 | render () { 5 | return ( 6 |
7 |

This is a secret homepage!

8 |
9 | ); 10 | } 11 | } -------------------------------------------------------------------------------- /app/stores/AuthStore.sampleData.json: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "iwritecode@preact.com:wearehiring!": { 3 | "jwt": "DOESNTMATTER.eyJleHAiOi0xLCJpZCI6IjEiLCJuYW1lIjoiR29vbGV5IiwiZW1haWwiOiJnb29sZXlAcHJlYWN0LmNvbSJ9.DOESNTMATTER" 4 | }, 5 | "awesome@preact.com:asdfasdf": { 6 | "jwt": "DOESNTMATTER.eyJleHAiOi0xLCJpZCI6IjIiLCJuYW1lIjoiSGFybGFuIExld2lzIiwiZW1haWwiOiJoYXJsYW5AcHJlYWN0LmNvbSJ9.DOESNTMATTER" 7 | } 8 | } -------------------------------------------------------------------------------- /app/pages/Master/Master.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | import style from './style.scss'; 4 | 5 | import AuthStatus from 'components/AuthStatus'; 6 | 7 | var RouteHandler = Router.RouteHandler; 8 | 9 | var Master = React.createClass({ 10 | render () { 11 | return ( 12 |
13 | 14 |
15 | 16 |
17 | ); 18 | } 19 | }); 20 | 21 | module.exports = Master; -------------------------------------------------------------------------------- /app/routes.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | import Router from 'react-router'; 3 | var Route = Router.Route; 4 | var DefaultRoute = Router.DefaultRoute; 5 | 6 | import Master from './pages/Master'; 7 | import Home from './pages/Home'; 8 | import Login from './pages/Login'; 9 | 10 | import { LoginRequired } from './util/RouteHelpers'; 11 | 12 | module.exports = ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); -------------------------------------------------------------------------------- /app/util/RouteHelpers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | 4 | import AuthStore from 'stores/AuthStore'; 5 | 6 | var LoginRequired = React.createClass({ 7 | statics: { 8 | willTransitionTo: function (transition, params, query, callback) { 9 | if(!AuthStore.loggedIn()){ 10 | // go over to login page 11 | transition.redirect('/login', null, { redirect: transition.path }); 12 | } 13 | callback(); 14 | } 15 | }, 16 | render () { 17 | return ( 18 | 19 | ); 20 | } 21 | }); 22 | 23 | module.exports = { LoginRequired }; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "ecmaFeatures": { 11 | "jsx": true 12 | }, 13 | "rules": { 14 | "quotes": [2, "single"], 15 | "eol-last": 0, 16 | "strict": 0, 17 | "new-cap": [2, { "capIsNewExceptions": ["State", "FluxMixin", "StoreWatchMixin"] }], 18 | 19 | "react/display-name": 0, 20 | "react/jsx-boolean-value": 1, 21 | "react/jsx-no-undef": 1, 22 | "react/jsx-quotes": 1, 23 | "react/jsx-sort-prop-types": 1, 24 | "react/jsx-sort-props": 1, 25 | "react/jsx-uses-react": 1, 26 | "react/jsx-uses-vars": 1, 27 | "react/no-did-mount-set-state": 1, 28 | "react/no-did-update-set-state": 1, 29 | "react/no-multi-comp": 0, 30 | "no-underscore-dangle": 0, 31 | "react/no-unknown-property": 1, 32 | "react/prop-types": 1, 33 | "react/react-in-jsx-scope": 1, 34 | "react/self-closing-comp": 1, 35 | "react/sort-comp": 1, 36 | "react/wrap-multilines": 1 37 | } 38 | } -------------------------------------------------------------------------------- /app/components/AuthStatus/AuthStatus.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | import Reflux from 'reflux'; 4 | 5 | import AuthStore from 'stores/AuthStore'; 6 | import AuthActions from 'actions/AuthActions'; 7 | 8 | var AuthStatus = React.createClass({ 9 | mixins: [ 10 | Router.Navigation, 11 | Reflux.connect(AuthStore), 12 | Reflux.ListenerMixin 13 | ], 14 | 15 | componentWillMount () { 16 | // TODO: is there a smarter way to do this? 17 | this.setState(AuthStore.getState()); 18 | }, 19 | 20 | componentDidMount () { 21 | this.listenTo(AuthStore, this.onAuthChange); 22 | }, 23 | 24 | onAuthChange(auth) { 25 | this.setState(auth); 26 | }, 27 | 28 | handleLogout() { 29 | AuthActions.logout(); 30 | this.transitionTo('/login'); 31 | }, 32 | 33 | render() { 34 | if(this.state.user){ 35 | return ( 36 |
37 | Hi, { this.state.user.name }! 38 |
39 | 40 |
41 |
42 | ); 43 | } 44 | } 45 | }); 46 | 47 | 48 | module.exports = AuthStatus; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack= require('webpack'); 2 | 3 | var path = require('path'); 4 | var node_modules = path.resolve(__dirname, 'node_modules'); 5 | var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js'); 6 | 7 | var autoprefixer= require('autoprefixer-core'); 8 | 9 | module.exports = { 10 | entry: [ 11 | './app/main.jsx', 12 | 'webpack/hot/dev-server' 13 | ], 14 | output: { 15 | path: __dirname + '/build', 16 | publicPath: "http://localhost:8081/build", 17 | filename: 'bundle.js' 18 | }, 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.jsx?$/, 23 | loader: 'react-hot!babel!eslint', exclude: /node_modules/ 24 | }, 25 | { 26 | test: /\.scss$/, 27 | loader: 'style!css?module&localIdentName=[path][name]___[local]---[hash:base64:5]!sass!postcss-loader' 28 | }, 29 | { 30 | test: /\.(png|jpg)$/, 31 | loader: 'url-loader?limit=8192' // inline base64 URLs for <=8k images, direct URLs for the rest 32 | }, 33 | { 34 | test: /\.(svg)$/, 35 | loader: 'raw-loader' 36 | } 37 | ] 38 | }, 39 | postcss: [ 40 | autoprefixer 41 | ], 42 | resolve: { 43 | modulesDirectories: ['node_modules', 'app'] 44 | }, 45 | plugins: [ 46 | new webpack.optimize.CommonsChunkPlugin('main', null, false) 47 | ] 48 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "app/main.jsx", 6 | "scripts": { 7 | "dev-server": "node server-local.js | webpack-dev-server --port 8081 --hot --devtool eval --display-error-details --progress --colors --content-base build" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "autoprefixer-core": "^5.2.0", 13 | "classnames": "^2.1.2", 14 | "js-md5": "^0.3.0", 15 | "postcss-loader": "^0.4.4", 16 | "react": "^0.13.3", 17 | "react-inlinesvg": "^0.4.1", 18 | "react-router": "^0.13.3", 19 | "reflux": "~0.2.4", 20 | "superagent": "^1.2.0" 21 | }, 22 | "devDependencies": { 23 | "babel-core": "^5.4.7", 24 | "babel-eslint": "^3.1.10", 25 | "babel-loader": "^5.1.3", 26 | "css-loader": "^0.14.3", 27 | "eslint": "^0.21.2", 28 | "eslint-loader": "^0.11.2", 29 | "eslint-plugin-react": "^2.4.0", 30 | "express": "^4.12.4", 31 | "extract-text-webpack-plugin": "^0.8.1", 32 | "jsx-loader": "^0.13.2", 33 | "karma": "^0.12.37", 34 | "node-sass": "^3.1.2", 35 | "proxy-middleware": "^0.11.1", 36 | "pushstate-server": "^1.6.0", 37 | "react-hot-loader": "^1.2.8", 38 | "sass-loader": "^0.5.0", 39 | "style-loader": "^0.12.3", 40 | "webpack": "^1.9.10", 41 | "webpack-dev-server": "^1.9.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/pages/Login/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | import Reflux from 'reflux'; 4 | 5 | import AuthStore from 'stores/AuthStore'; 6 | import AuthActions from 'actions/AuthActions'; 7 | 8 | var Login = React.createClass({ 9 | mixins: [ 10 | Router.State, 11 | Router.Navigation, 12 | Reflux.connect(AuthStore), 13 | Reflux.ListenerMixin 14 | ], 15 | 16 | componentDidMount () { 17 | this.listenTo(AuthStore, this._onAuthChange); 18 | }, 19 | 20 | _onAuthChange(auth) { 21 | this.setState(auth); 22 | 23 | if(this.state.loggedIn){ 24 | var redirectUrl = this.getQuery().redirect || '/'; 25 | this.replaceWith(redirectUrl); 26 | } 27 | }, 28 | 29 | _handleSubmit(event) { 30 | event.preventDefault(); 31 | 32 | AuthActions.login( 33 | React.findDOMNode(this.refs.email).value, 34 | React.findDOMNode(this.refs.password).value 35 | ); 36 | }, 37 | 38 | render() { 39 | var errorMessage; 40 | if (this.state.error) { 41 | errorMessage = ( 42 |
43 | { this.state.error } 44 |
45 | ); 46 | } 47 | 48 | var formContent; 49 | if (this.state.user) { 50 | formContent = ( 51 |
52 |

53 | You're logged in as { this.state.user.name }. 54 |

55 |
56 | ); 57 | } else { 58 | formContent = ( 59 |
60 | { errorMessage } 61 | Email: 62 |
63 | Password: 64 |
65 | 66 |
67 | ); 68 | } 69 | return ( 70 |
71 | { formContent } 72 |
73 | ); 74 | } 75 | }); 76 | 77 | 78 | module.exports = Login; -------------------------------------------------------------------------------- /app/stores/AuthStore.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import Actions from 'actions/AuthActions'; 3 | 4 | var renderTimeout = 250; // set a timeout to simulate async response time 5 | 6 | var AuthStore = Reflux.createStore({ 7 | listenables: Actions, 8 | 9 | init () { 10 | // pull cached token if one exists 11 | this.jwt = localStorage.getItem('jwt'); 12 | 13 | this.claims = this.parseJwt(); 14 | this.error = false; 15 | this.loading = false; 16 | }, 17 | 18 | getState () { 19 | return { 20 | loading: this.loading, 21 | error: this.error, 22 | user: this.userFromClaims(), 23 | loggedIn: this.loggedIn() 24 | }; 25 | }, 26 | 27 | userFromClaims () { 28 | // will want to do some cleanup of the claims 29 | // because they're designed to be very small field names for xfer size 30 | return this.claims; 31 | }, 32 | 33 | loggedIn () { 34 | // helper 35 | return this.claims !== null; 36 | }, 37 | 38 | changed () { 39 | this.trigger(this.getState()); 40 | }, 41 | 42 | onLogin (email, password) { 43 | this.loading = true; 44 | this.changed(); 45 | 46 | // fake API simulation 47 | setTimeout(function() { 48 | var auths = require('./AuthStore.sampleData.json'); 49 | Actions.login.completed(auths[`${email}:${password}`]); 50 | }, renderTimeout); 51 | }, 52 | 53 | onLoginCompleted (authResponse) { 54 | if(authResponse){ 55 | this.jwt = authResponse.jwt; 56 | this.claims = this.parseJwt(); 57 | this.error = false; 58 | 59 | localStorage.setItem('jwt', this.jwt); 60 | } else { 61 | this.error = 'Username or password invalid.'; 62 | } 63 | 64 | this.loading = false; 65 | this.changed(); 66 | }, 67 | 68 | onLogout () { 69 | // clear it all 70 | this.jwt = null; 71 | this.claims = null; 72 | this.error = false; 73 | this.loading = false; 74 | localStorage.removeItem('jwt'); 75 | }, 76 | 77 | parseJwt () { 78 | if(this.jwt === null){ return null; } 79 | return JSON.parse(atob(this.jwt.split('.')[1])); 80 | } 81 | 82 | }); 83 | 84 | module.exports = AuthStore; --------------------------------------------------------------------------------