Browse Source

Init which work with DRF-jwt

gulp
sipp11 9 years ago
commit
62ddf4ff64
  1. 3
      .gitignore
  2. 39
      Readme.md
  3. 1
      build/.gitignore
  4. 1
      index.css
  5. 14
      index.html
  6. 9
      metadata.json
  7. 60
      package.json
  8. 28
      src/actions/LoginActions.js
  9. 11
      src/actions/QuoteActions.js
  10. 31
      src/app.jsx
  11. 78
      src/components/AuthenticatedApp.jsx
  12. 49
      src/components/AuthenticatedComponent.jsx
  13. 8
      src/components/Home.jsx
  14. 44
      src/components/Login.jsx
  15. 47
      src/components/Quote.jsx
  16. 49
      src/components/Signup.jsx
  17. 8
      src/constants/LoginConstants.js
  18. 6
      src/constants/QuoteConstants.js
  19. 3
      src/dispatchers/AppDispatcher.js
  20. 46
      src/services/AuthService.js
  21. 27
      src/services/QuoteService.js
  22. 5
      src/services/RouterContainer.js
  23. 29
      src/stores/BaseStore.js
  24. 44
      src/stores/LoginStore.js
  25. 33
      src/stores/QuoteStore.js

3
.gitignore vendored

@ -0,0 +1,3 @@
node_modules
.DS_Store
npm-debug.log

39
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.

1
build/.gitignore vendored

@ -0,0 +1 @@
*

1
index.css

@ -0,0 +1 @@
@import 'bootstrap';

14
index.html

@ -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>

9
metadata.json

@ -0,0 +1,9 @@
{
"name": "A Single Page App without a Framework",
"type": "sample",
"tags": [
"web",
"javascript",
"single-page-app"
]
}

60
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 <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"
}
}

28
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
});
}
}

11
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
})
}
}

31
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 = (
<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'));
});

78
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 (
<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>)
}
}
}

49
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 (
<ComposedComponent
{...this.props}
user={this.state.user}
jwt={this.state.jwt}
userLoggedIn={this.state.userLoggedIn} />
);
}
}
};

8
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 (<h1>Hello {this.props.user ? this.props.user.username : ''}</h1>);
}
});

44
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 (
<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);

47
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 (
<div>
<h1>{this.state.quote}</h1>
<button className="btn btn-primary" type="button" onClick={this.requestNextQuote}>Next Quote</button>
</div>
);
}
});

49
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 (
<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);

8
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'
}

6
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'
}

3
src/dispatchers/AppDispatcher.js

@ -0,0 +1,3 @@
import { Dispatcher } from 'flux';
export default new Dispatcher();

46
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()

27
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()

5
src/services/RouterContainer.js

@ -0,0 +1,5 @@
var _router = null;
export default {
set: (router) => _router = router,
get: () => _router
}

29
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);
}
}

44
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();

33
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();
Loading…
Cancel
Save