sipp11
9 years ago
commit
62ddf4ff64
25 changed files with 673 additions and 0 deletions
@ -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. |
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<title>React Browserify SPA seed</title> |
||||
<link rel="stylesheet" type="text/css" href="build/build.css"> |
||||
</head> |
||||
<body> |
||||
<!-- content section --> |
||||
<section id="content"></section> |
||||
|
||||
<!-- Initialize SPA --> |
||||
<script type="text/javascript" src="build/build.js"></script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,9 @@
|
||||
{ |
||||
"name": "A Single Page App without a Framework", |
||||
"type": "sample", |
||||
"tags": [ |
||||
"web", |
||||
"javascript", |
||||
"single-page-app" |
||||
] |
||||
} |
@ -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 <martin@gon.to> (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" |
||||
} |
||||
} |
@ -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 |
||||
}); |
||||
} |
||||
} |
@ -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 |
||||
}) |
||||
} |
||||
} |
@ -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 = ( |
||||
<Route handler={AuthenticatedApp}> |
||||
<Route name="login" handler={Login}/> |
||||
<Route name="signup" handler={Signup}/> |
||||
<Route name="home" path="/" handler={Home}/> |
||||
<Route name="quote" handler={Quote}/> |
||||
</Route> |
||||
); |
||||
|
||||
var router = Router.create({routes}); |
||||
RouterContainer.set(router); |
||||
|
||||
let jwt = localStorage.getItem('jwt'); |
||||
if (jwt) { |
||||
LoginActions.loginUser(jwt); |
||||
} |
||||
|
||||
router.run(function (Handler) { |
||||
React.render(<Handler />, document.getElementById('content')); |
||||
}); |
||||
|
@ -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 ( |
||||
<div className="container"> |
||||
<nav className="navbar navbar-default"> |
||||
<div className="navbar-header"> |
||||
<a className="navbar-brand" href="/">React Flux app with JWT authentication</a> |
||||
</div> |
||||
{this.headerItems} |
||||
</nav> |
||||
<RouteHandler/> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
logout(e) { |
||||
e.preventDefault(); |
||||
AuthService.logout(); |
||||
} |
||||
|
||||
get headerItems() { |
||||
if (!this.state.userLoggedIn) { |
||||
return ( |
||||
<ul className="nav navbar-nav navbar-right"> |
||||
<li> |
||||
<Link to="login">Login</Link> |
||||
</li> |
||||
<li> |
||||
<Link to="signup">Signup</Link> |
||||
</li> |
||||
</ul>) |
||||
} else { |
||||
return ( |
||||
<ul className="nav navbar-nav navbar-right"> |
||||
<li> |
||||
<Link to="home">Home</Link> |
||||
</li> |
||||
<li> |
||||
<Link to="quote">Quote</Link> |
||||
</li> |
||||
<li> |
||||
<a href="" onClick={this.logout}>Logout</a> |
||||
</li> |
||||
</ul>) |
||||
} |
||||
} |
||||
} |
@ -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 ( |
||||
<ComposedComponent |
||||
{...this.props} |
||||
user={this.state.user} |
||||
jwt={this.state.jwt} |
||||
userLoggedIn={this.state.userLoggedIn} /> |
||||
); |
||||
} |
||||
} |
||||
}; |
@ -0,0 +1,8 @@
|
||||
import React from 'react'; |
||||
import AuthenticatedComponent from './AuthenticatedComponent' |
||||
|
||||
export default AuthenticatedComponent(class Home extends React.Component { |
||||
render() { |
||||
return (<h1>Hello {this.props.user ? this.props.user.username : ''}</h1>); |
||||
} |
||||
}); |
@ -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 ( |
||||
<div className="login jumbotron center-block"> |
||||
<h1>Login</h1> |
||||
<form role="form"> |
||||
<div className="form-group"> |
||||
<label htmlFor="username">Username</label> |
||||
<input type="text" valueLink={this.linkState('user')} className="form-control" id="username" placeholder="Username" /> |
||||
</div> |
||||
<div className="form-group"> |
||||
<label htmlFor="password">Password</label> |
||||
<input type="password" valueLink={this.linkState('password')} className="form-control" id="password" ref="password" placeholder="Password" /> |
||||
</div> |
||||
<button type="submit" className="btn btn-default" onClick={this.login.bind(this)}>Submit</button> |
||||
</form> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
ReactMixin(Login.prototype, React.addons.LinkedStateMixin); |
@ -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 ( |
||||
<div> |
||||
<h1>{this.state.quote}</h1> |
||||
<button className="btn btn-primary" type="button" onClick={this.requestNextQuote}>Next Quote</button> |
||||
</div> |
||||
); |
||||
} |
||||
}); |
@ -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 ( |
||||
<div className="login jumbotron center-block"> |
||||
<h1>Signup</h1> |
||||
<form role="form"> |
||||
<div className="form-group"> |
||||
<label htmlFor="username">Username</label> |
||||
<input type="text" valueLink={this.linkState('user')} className="form-control" id="username" placeholder="Username" /> |
||||
</div> |
||||
<div className="form-group"> |
||||
<label htmlFor="password">Password</label> |
||||
<input type="password" valueLink={this.linkState('password')} className="form-control" id="password" ref="password" placeholder="Password" /> |
||||
</div> |
||||
<div className="form-group"> |
||||
<label htmlFor="extra">Extra</label> |
||||
<input type="text" valueLink={this.linkState('extra')} className="form-control" id="password" ref="password" placeholder="Some extra information" /> |
||||
</div> |
||||
<button type="submit" className="btn btn-default" onClick={this.signup.bind(this)}>Submit</button> |
||||
</form> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
ReactMixin(Signup.prototype, React.addons.LinkedStateMixin); |
@ -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' |
||||
} |
@ -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' |
||||
} |
@ -0,0 +1,3 @@
|
||||
import { Dispatcher } from 'flux'; |
||||
|
||||
export default new Dispatcher(); |
@ -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() |
@ -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() |
@ -0,0 +1,5 @@
|
||||
var _router = null; |
||||
export default { |
||||
set: (router) => _router = router, |
||||
get: () => _router |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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(); |
@ -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(); |
Loading…
Reference in new issue