├── .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 |
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 |
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;
--------------------------------------------------------------------------------