commit 62ddf4ff64b5b7cb9d0756da9108e369d22671da Author: sipp11 Date: Wed Nov 18 02:13:22 2015 +0700 Init which work with DRF-jwt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..825fc67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +npm-debug.log diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..e798d57 --- /dev/null +++ b/Readme.md @@ -0,0 +1,39 @@ +# Add authentication to a React Flux app + +This is a sample that shows how you can add authentication to a React Flux app. Read more about it in [this blog post](https://auth0.com/blog/2015/04/09/adding-authentication-to-your-react-flux-app/) + +## Using it + +Clone this repository as well as [the server](https://github.com/auth0/nodejs-jwt-authentication-sample) for this example. + +First, run the server app in the port `3001`. + +Then, run `npm install` on this project and run `npm run watch` to start the app. Then just navigate to [http://localhost:3000](http://localhost:3000) + +## How does it work? + +To learn more about how this project works and how it has been implemented, please read [this blog post](https://auth0.com/blog/2015/04/09/adding-authentication-to-your-react-flux-app/) + +## Issue Reporting + +If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. + +## License + +MIT + +## What is Auth0? + +Auth0 helps you to: + +* Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. +* Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**. +* Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user. +* Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely. +* Analytics of how, when and where users are logging in. +* Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules). + +## Create a free account in Auth0 + +1. Go to [Auth0](https://auth0.com) and click Sign Up. +2. Use Google, GitHub or Microsoft Account to login. diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/build/.gitignore @@ -0,0 +1 @@ +* diff --git a/index.css b/index.css new file mode 100644 index 0000000..f9030ae --- /dev/null +++ b/index.css @@ -0,0 +1 @@ +@import 'bootstrap'; diff --git a/index.html b/index.html new file mode 100644 index 0000000..e96853c --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + React Browserify SPA seed + + + + +
+ + + + + diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..06b989b --- /dev/null +++ b/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "A Single Page App without a Framework", + "type": "sample", + "tags": [ + "web", + "javascript", + "single-page-app" + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f29db78 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "react-browserify-spa-seed", + "version": "0.0.1", + "description": "Seed project for React, Browserify, Rework SPAs", + "main": "index.js", + "repository": { + "type": "git", + "url": "git@github.com:mgonto/react-browserify-spa-seed.git" + }, + "authors": [ + "Martin Gontovnikas (http://gon.to/)" + ], + "browserify": { + "transform": [ + "babelify" + ] + }, + "scripts": { + "start": "npm run build && serve .", + "build": "npm run build-js && npm run build-css", + "watch": "npm run watch-js & npm run watch-css & serve .", + "test": "npm run lint -s && npm run build", + "build-css": "rework-npm index.css | cleancss -o build/build.css", + "build-js": "browserify --extension=.jsx --extension=.js src/app.jsx | uglifyjs > build/build.js", + "watch-js": "watchify --extension=.jsx --extension=.js src/app.jsx -o build/build.js --debug --verbose", + "watch-css": "nodemon -e css --ignore build/build.css --exec 'rework-npm index.css -o build/build.css'", + "lint-eslint": "eslint .", + "lint-jscs": "jscs .", + "lint": "npm run lint-eslint && npm run lint-jscs" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/mgonto/react-browserify-spa-seed/issues" + }, + "homepage": "https://github.com/mgonto/react-browserify-spa-seed", + "dependencies": { + "bootstrap": "^3.3.0", + "flux": "^2.0.1", + "jwt-decode": "^1.1.0", + "react": "^0.13", + "react-mixin": "^1.1.0", + "react-router": "^0.13.2", + "reqwest": "^1.1.5", + "when": "^3.7.2" + }, + "devDependencies": { + "babelify": "^6.1.0", + "browser-sync": "^2.1.6", + "browserify": "^8.0.3", + "clean-css": "^3.1.9", + "eslint": "^0.14.1", + "nodemon": "^1.5.0", + "rework": "^1.0.1", + "rework-npm": "^1.0.0", + "rework-npm-cli": "^0.1.1", + "serve": "^1.4.0", + "uglify-js": "^2.4.15", + "watchify": "^2.1.1" + } +} diff --git a/src/actions/LoginActions.js b/src/actions/LoginActions.js new file mode 100644 index 0000000..80c6fac --- /dev/null +++ b/src/actions/LoginActions.js @@ -0,0 +1,28 @@ +import AppDispatcher from '../dispatchers/AppDispatcher.js'; +import {LOGIN_USER, LOGOUT_USER} from '../constants/LoginConstants.js'; +import RouterContainer from '../services/RouterContainer' + +export default { + loginUser: (jwt) => { + var savedJwt = localStorage.getItem('jwt'); + + AppDispatcher.dispatch({ + actionType: LOGIN_USER, + jwt: jwt + }); + + if (savedJwt !== jwt) { + var nextPath = RouterContainer.get().getCurrentQuery().nextPath || '/'; + + RouterContainer.get().transitionTo(nextPath); + localStorage.setItem('jwt', jwt); + } + }, + logoutUser: () => { + RouterContainer.get().transitionTo('/login'); + localStorage.removeItem('jwt'); + AppDispatcher.dispatch({ + actionType: LOGOUT_USER + }); + } +} diff --git a/src/actions/QuoteActions.js b/src/actions/QuoteActions.js new file mode 100644 index 0000000..c22cb8d --- /dev/null +++ b/src/actions/QuoteActions.js @@ -0,0 +1,11 @@ +import AppDispatcher from '../dispatchers/AppDispatcher.js'; +import {QUOTE_GET} from '../constants/QuoteConstants.js'; + +export default { + gotQuote: (quote) => { + AppDispatcher.dispatch({ + actionType: QUOTE_GET, + quote: quote + }) + } +} diff --git a/src/app.jsx b/src/app.jsx new file mode 100644 index 0000000..b9df510 --- /dev/null +++ b/src/app.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import Router, {Route} from 'react-router'; +import AuthenticatedApp from './components/AuthenticatedApp' +import Login from './components/Login'; +import Signup from './components/Signup'; +import Home from './components/Home'; +import Quote from './components/Quote'; +import RouterContainer from './services/RouterContainer'; +import LoginActions from './actions/LoginActions'; + +var routes = ( + + + + + + +); + +var router = Router.create({routes}); +RouterContainer.set(router); + +let jwt = localStorage.getItem('jwt'); +if (jwt) { + LoginActions.loginUser(jwt); +} + +router.run(function (Handler) { + React.render(, document.getElementById('content')); +}); + diff --git a/src/components/AuthenticatedApp.jsx b/src/components/AuthenticatedApp.jsx new file mode 100644 index 0000000..74bb2b4 --- /dev/null +++ b/src/components/AuthenticatedApp.jsx @@ -0,0 +1,78 @@ +'use strict'; + +import React from 'react'; +import LoginStore from '../stores/LoginStore' +import { Route, RouteHandler, Link } from 'react-router'; +import AuthService from '../services/AuthService' + +export default class AuthenticatedApp extends React.Component { + constructor() { + super() + this.state = this._getLoginState(); + } + + _getLoginState() { + return { + userLoggedIn: LoginStore.isLoggedIn() + }; + } + + componentDidMount() { + this.changeListener = this._onChange.bind(this); + LoginStore.addChangeListener(this.changeListener); + } + + _onChange() { + this.setState(this._getLoginState()); + } + + componentWillUnmount() { + LoginStore.removeChangeListener(this.changeListener); + } + + render() { + return ( +
+ + +
+ ); + } + + logout(e) { + e.preventDefault(); + AuthService.logout(); + } + + get headerItems() { + if (!this.state.userLoggedIn) { + return ( +
    +
  • + Login +
  • +
  • + Signup +
  • +
) + } else { + return ( +
    +
  • + Home +
  • +
  • + Quote +
  • +
  • + Logout +
  • +
) + } + } +} diff --git a/src/components/AuthenticatedComponent.jsx b/src/components/AuthenticatedComponent.jsx new file mode 100644 index 0000000..d669b62 --- /dev/null +++ b/src/components/AuthenticatedComponent.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import LoginStore from '../stores/LoginStore'; + +export default (ComposedComponent) => { + return class AuthenticatedComponent extends React.Component { + + static willTransitionTo(transition) { + if (!LoginStore.isLoggedIn()) { + transition.redirect('/login', {}, {'nextPath' : transition.path}); + } + } + + constructor() { + super() + this.state = this._getLoginState(); + } + + _getLoginState() { + return { + userLoggedIn: LoginStore.isLoggedIn(), + user: LoginStore.user, + jwt: LoginStore.jwt + }; + } + + componentDidMount() { + this.changeListener = this._onChange.bind(this); + LoginStore.addChangeListener(this.changeListener); + } + + _onChange() { + this.setState(this._getLoginState()); + } + + componentWillUnmount() { + LoginStore.removeChangeListener(this.changeListener); + } + + render() { + return ( + + ); + } + } +}; diff --git a/src/components/Home.jsx b/src/components/Home.jsx new file mode 100644 index 0000000..be8daae --- /dev/null +++ b/src/components/Home.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import AuthenticatedComponent from './AuthenticatedComponent' + +export default AuthenticatedComponent(class Home extends React.Component { + render() { + return (

Hello {this.props.user ? this.props.user.username : ''}

); + } +}); diff --git a/src/components/Login.jsx b/src/components/Login.jsx new file mode 100644 index 0000000..4845409 --- /dev/null +++ b/src/components/Login.jsx @@ -0,0 +1,44 @@ +import React from 'react/addons'; +import ReactMixin from 'react-mixin'; +import Auth from '../services/AuthService' + +export default class Login extends React.Component { + + constructor() { + super() + this.state = { + user: '', + password: '' + }; + } + + login(e) { + e.preventDefault(); + Auth.login(this.state.user, this.state.password) + .catch(function(err) { + alert("There's an error logging in"); + console.log("Error logging in", err); + }); + } + + render() { + return ( +
+

Login

+
+
+ + +
+
+ + +
+ +
+
+ ); + } +} + +ReactMixin(Login.prototype, React.addons.LinkedStateMixin); diff --git a/src/components/Quote.jsx b/src/components/Quote.jsx new file mode 100644 index 0000000..fecf0a5 --- /dev/null +++ b/src/components/Quote.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import AuthenticatedComponent from './AuthenticatedComponent'; +import QuoteStore from '../stores/QuoteStore.js'; +import QuoteService from '../services/QuoteService.js'; + +export default AuthenticatedComponent(class Quote extends React.Component { + constructor(props) { + super(props); + this.state = this.getQuoteState(); + this._onChange = this._onChange.bind(this); + } + + componentDidMount() { + if (!this.state.quote) { + this.requestNextQuote(); + } + + QuoteStore.addChangeListener(this._onChange); + } + + componentWillUnmount() { + QuoteStore.removeChangeListener(this._onChange); + } + + _onChange() { + this.setState(this.getQuoteState()); + } + + requestNextQuote() { + QuoteService.nextQuote(); + } + + getQuoteState() { + return { + quote: QuoteStore.quote + }; + } + + render() { + return ( +
+

{this.state.quote}

+ +
+ ); + } +}); diff --git a/src/components/Signup.jsx b/src/components/Signup.jsx new file mode 100644 index 0000000..1f95f9f --- /dev/null +++ b/src/components/Signup.jsx @@ -0,0 +1,49 @@ +import React from 'react/addons'; +import ReactMixin from 'react-mixin'; +import Auth from '../services/AuthService' + +export default class Signup extends React.Component { + + constructor() { + super() + this.state = { + user: '', + password: '', + extra: '' + }; + } + + signup(e) { + e.preventDefault(); + Auth.signup(this.state.user, this.state.password, this.state.extra) + .catch(function(err) { + alert("There's an error logging in"); + console.log("Error logging in", err); + }); + } + + render() { + return ( +
+

Signup

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ ); + } +} + +ReactMixin(Signup.prototype, React.addons.LinkedStateMixin); diff --git a/src/constants/LoginConstants.js b/src/constants/LoginConstants.js new file mode 100644 index 0000000..b650204 --- /dev/null +++ b/src/constants/LoginConstants.js @@ -0,0 +1,8 @@ +var BASE_URL = 'http://rocket.dev/'; +export default { + BASE_URL: BASE_URL, + LOGIN_URL: BASE_URL + 'api-token-auth/', + SIGNUP_URL: BASE_URL + 'users', + LOGIN_USER: 'LOGIN_USER', + LOGOUT_USER: 'LOGOUT_USER' +} diff --git a/src/constants/QuoteConstants.js b/src/constants/QuoteConstants.js new file mode 100644 index 0000000..85a5ad6 --- /dev/null +++ b/src/constants/QuoteConstants.js @@ -0,0 +1,6 @@ +var BASE_URL = 'http://rocket.dev/'; +export default { + BASE_URL: BASE_URL, + QUOTE_URL: BASE_URL + 'account/', + QUOTE_GET: 'QUOTE_GET' +} diff --git a/src/dispatchers/AppDispatcher.js b/src/dispatchers/AppDispatcher.js new file mode 100644 index 0000000..2299a78 --- /dev/null +++ b/src/dispatchers/AppDispatcher.js @@ -0,0 +1,3 @@ +import { Dispatcher } from 'flux'; + +export default new Dispatcher(); diff --git a/src/services/AuthService.js b/src/services/AuthService.js new file mode 100644 index 0000000..8d05393 --- /dev/null +++ b/src/services/AuthService.js @@ -0,0 +1,46 @@ +import request from 'reqwest'; +import when from 'when'; +import {LOGIN_URL, SIGNUP_URL} from '../constants/LoginConstants'; +import LoginActions from '../actions/LoginActions'; + +class AuthService { + + login(username, password) { + return this.handleAuth(when(request({ + url: LOGIN_URL, + method: 'POST', + crossOrigin: true, + type: 'json', + data: { + username, password + } + }))); + } + + logout() { + LoginActions.logoutUser(); + } + + signup(username, password, extra) { + return this.handleAuth(when(request({ + url: SIGNUP_URL, + method: 'POST', + crossOrigin: true, + type: 'json', + data: { + username, password, extra + } + }))); + } + + handleAuth(loginPromise) { + return loginPromise + .then(function(response) { + var jwt = response.token; + LoginActions.loginUser(jwt); + return true; + }); + } +} + +export default new AuthService() diff --git a/src/services/QuoteService.js b/src/services/QuoteService.js new file mode 100644 index 0000000..7885f53 --- /dev/null +++ b/src/services/QuoteService.js @@ -0,0 +1,27 @@ +import request from 'reqwest'; +import when from 'when'; +import {QUOTE_URL} from '../constants/QuoteConstants'; +import QuoteActions from '../actions/QuoteActions'; +import LoginStore from '../stores/LoginStore.js'; + +class QuoteService { + + nextQuote() { + + request({ + url: QUOTE_URL, + method: 'GET', + crossOrigin: true, + headers: { + 'Authorization': 'Bearer ' + LoginStore.jwt + } + }) + .then(function(response) { + console.log(response); + QuoteActions.gotQuote(response); + }); + } + +} + +export default new QuoteService() diff --git a/src/services/RouterContainer.js b/src/services/RouterContainer.js new file mode 100644 index 0000000..9e29ce6 --- /dev/null +++ b/src/services/RouterContainer.js @@ -0,0 +1,5 @@ +var _router = null; +export default { + set: (router) => _router = router, + get: () => _router +} diff --git a/src/stores/BaseStore.js b/src/stores/BaseStore.js new file mode 100644 index 0000000..dd8fa02 --- /dev/null +++ b/src/stores/BaseStore.js @@ -0,0 +1,29 @@ +import { EventEmitter } from 'events'; +import AppDispatcher from '../dispatchers/AppDispatcher'; + +export default class BaseStore extends EventEmitter { + + constructor() { + super(); + } + + subscribe(actionSubscribe) { + this._dispatchToken = AppDispatcher.register(actionSubscribe()); + } + + get dispatchToken() { + return this._dispatchToken; + } + + emitChange() { + this.emit('CHANGE'); + } + + addChangeListener(cb) { + this.on('CHANGE', cb) + } + + removeChangeListener(cb) { + this.removeListener('CHANGE', cb); + } +} diff --git a/src/stores/LoginStore.js b/src/stores/LoginStore.js new file mode 100644 index 0000000..c7253fa --- /dev/null +++ b/src/stores/LoginStore.js @@ -0,0 +1,44 @@ +import {LOGIN_USER, LOGOUT_USER} from '../constants/LoginConstants'; +import BaseStore from './BaseStore'; +import jwt_decode from 'jwt-decode'; + + +class LoginStore extends BaseStore { + + constructor() { + super(); + this.subscribe(() => this._registerToActions.bind(this)) + this._user = null; + this._jwt = null; + } + + _registerToActions(action) { + switch(action.actionType) { + case LOGIN_USER: + this._jwt = action.jwt; + this._user = jwt_decode(this._jwt); + this.emitChange(); + break; + case LOGOUT_USER: + this._user = null; + this.emitChange(); + break; + default: + break; + }; + } + + get user() { + return this._user; + } + + get jwt() { + return this._jwt; + } + + isLoggedIn() { + return !!this._user; + } +} + +export default new LoginStore(); diff --git a/src/stores/QuoteStore.js b/src/stores/QuoteStore.js new file mode 100644 index 0000000..f916e9c --- /dev/null +++ b/src/stores/QuoteStore.js @@ -0,0 +1,33 @@ +import {QUOTE_GET} from '../constants/QuoteConstants'; +import {LOGOUT_USER} from '../constants/LoginConstants'; +import BaseStore from './BaseStore'; + +class QuoteStore extends BaseStore { + + constructor() { + super(); + this.subscribe(() => this._registerToActions.bind(this)) + this._quote = ''; + } + + _registerToActions(action) { + switch(action.actionType) { + case QUOTE_GET: + this._quote = action.quote; + this.emitChange(); + break; + case LOGOUT_USER: + this._quote = null; + this.emitChange(); + break; + default: + break; + }; + } + + get quote() { + return this._quote; + } +} + +export default new QuoteStore();