Skip to content

Commit

Permalink
Implement authorization using DRF-JWT.
Browse files Browse the repository at this point in the history
  • Loading branch information
damienzeng73 committed Dec 8, 2017
1 parent 52117c7 commit 0f9946c
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 9 deletions.
6 changes: 6 additions & 0 deletions backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,9 @@
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATIC_URL = '/static/'

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
)
}
6 changes: 5 additions & 1 deletion backend/backend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from django.contrib import admin

from rest_framework.routers import DefaultRouter
from rest_framework_jwt.views import obtain_jwt_token, verify_jwt_token, refresh_jwt_token
from cart.views import ProductsViewSet, UserViewSet

router = DefaultRouter()
Expand All @@ -26,5 +27,8 @@
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^api/', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^api-token-auth/', obtain_jwt_token),
url(r'^api-token-verify/', verify_jwt_token),
url(r'^api-token-refresh/', refresh_jwt_token)
]
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"axios": "^0.17.1",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.4",
"prop-types": "^15.6.0",
"react": "^16.1.1",
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/actions/Auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import axios from 'axios'
import jwtDecode from 'jwt-decode'

import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE } from '../constants/ActionTypes'

export const loginRequest = (username) => {
return {
type: LOGIN_REQUEST,
payload: username
}
}

export const loginSuccess = (user) => {
return {
type: LOGIN_SUCCESS,
payload: user
}
}

export const loginFailure = (error) => {
return {
type: LOGIN_FAILURE,
payload: error
}
}

export const setAuthorizationToken = (token) => {
if (token) {
axios.defaults.headers.common['Authorization'] = `JWT ${token}`
} else {
delete axios.defaults.headers.common['Authorization']
}
}

export const login = (username, password) => {
return dispatch => {
dispatch(loginRequest(username))
axios.post('/api-token-auth/', { username, password })
.then((res) => {
let token = res.data.token
localStorage.setItem('jwtToken', token)
setAuthorizationToken(token)
dispatch(loginSuccess(jwtDecode(token)))
})
.catch((err) => {
dispatch(loginFailure(err))
})
}
}

export const logout = () => {
return dispatch => {
localStorage.removeItem('jwtToken')
setAuthorizationToken(false)
dispatch(loginSuccess({}))
}
}

export const renewAuthorizationToken = (token) => {
return dispatch => {
axios.post('/api-token-refresh/', { token })
}
}

export const checkAuthorizationToken = (token) => {
return dispatch => {
axios.post('/api-token-verify/', { token })
.then((res) => {
dispatch(renewAuthorizationToken(res.data.token))
})
.catch((err) => {
if (err.response.status === 400 && err.response.data.non_field_errors[0] === 'Signature has expired.') {
dispatch(logout())
}
})
}
}
21 changes: 18 additions & 3 deletions frontend/src/components/Account.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Account extends React.Component {
this.handleSignupOnChange = this.handleSignupOnChange.bind(this)
this.handleModalOpen = this.handleModalOpen.bind(this)
this.handleModalOnClose = this.handleModalOnClose.bind(this)
this.handleUserLogin = this.handleUserLogin.bind(this)
this.handleUserSignup = this.handleUserSignup.bind(this)
}

Expand Down Expand Up @@ -56,6 +57,11 @@ class Account extends React.Component {
this.setState({ signup, modalOpen: false })
}

handleUserLogin() {
this.props.login(this.state.login.usernameOrEmail, this.state.login.password)
this.props.history.push('/')
}

handleUserSignup() {
this.props.userSignupRequest(this.state.signup)
this.handleModalOnClose()
Expand All @@ -74,7 +80,7 @@ class Account extends React.Component {
<Form>
<Form.Input
name='usernameOrEmail'
value={this.state.login.usernameOrEmail}
value={this.state.login.username}
label='Username or E-mail'
placeholder='Username or E-mail'
onChange={this.handleLoginOnChange}
Expand All @@ -90,8 +96,15 @@ class Account extends React.Component {
/>

<Segment padded>
<Button primary fluid>Login</Button>
<Button
primary={true}
fluid={true}
onClick={this.handleUserLogin}
>Login
</Button>

<Divider horizontal>Or</Divider>

<Modal trigger={modalTrigger} open={this.state.modalOpen} onClose={this.handleModalOnClose} closeIcon>
<Modal.Header>Sign Up Now</Modal.Header>
<Modal.Content>
Expand Down Expand Up @@ -150,7 +163,9 @@ class Account extends React.Component {
}

Account.propTypes = {
userSignupRequest: PropTypes.func.isRequired
login: PropTypes.func.isRequired,
userSignupRequest: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
}


Expand Down
4 changes: 4 additions & 0 deletions frontend/src/constants/ActionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ export const CLEAR_SHIPPING_OPTIONS = 'CLEAR_SHIPPING_OPTIONS'

export const SET_BILLING_OPTIONS = 'SET_BILLING_OPTIONS'
export const CLEAR_BILLING_OPTIONS = 'CLEAR_BILLING_OPTIONS'

export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_FAILURE = 'LOGIN_FAILURE'
9 changes: 7 additions & 2 deletions frontend/src/containers/AccountPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@ import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'

import Account from '../components/Account'
import { login } from '../actions/Auth'
import { userSignupRequest } from '../actions/Account'

class AccountPage extends React.Component {
render() {
return (
<Account
login={this.props.login}
userSignupRequest={this.props.userSignupRequest}
history={this.props.history}
/>
)
}
}

AccountPage.propTypes = {
userSignupRequest: PropTypes.func.isRequired
login: PropTypes.func.isRequired,
userSignupRequest: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
}


export default withRouter(connect(null, { userSignupRequest })(AccountPage))
export default withRouter(connect(null, { login, userSignupRequest })(AccountPage))
12 changes: 10 additions & 2 deletions frontend/src/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ShoppingPage from './ShoppingPage'
import CartPage from './CartPage'
import AccountPage from './AccountPage'
import { filterProducts } from '../actions/Products'
import { checkAuthorizationToken } from '../actions/Auth'

const Main = () => {
return (
Expand All @@ -31,6 +32,12 @@ class App extends React.Component {
}
}

componentDidMount() {
if (localStorage.jwtToken) {
this.props.checkAuthorizationToken(localStorage.jwtToken)
}
}

componentWillReceiveProps(newProps) {
if (this.props.history.location.pathname !== '/') {
this.setState({ showSearch: false })
Expand Down Expand Up @@ -62,7 +69,8 @@ class App extends React.Component {
App.propTypes = {
itemsInCartCount: PropTypes.number.isRequired,
products: PropTypes.array,
filterProducts: PropTypes.func.isRequired
filterProducts: PropTypes.func.isRequired,
checkAuthorizationToken: PropTypes.func.isRequired
}

const mapStateToProps = (state) => {
Expand All @@ -73,4 +81,4 @@ const mapStateToProps = (state) => {
}


export default withRouter(connect(mapStateToProps, { filterProducts })(App))
export default withRouter(connect(mapStateToProps, { filterProducts, checkAuthorizationToken })(App))
7 changes: 7 additions & 0 deletions frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import { BrowserRouter } from 'react-router-dom'
import jwtDecode from 'jwt-decode'

import './index.css'
import App from './containers/App'
import rootReducer from './reducers/index'
import registerServiceWorker from './registerServiceWorker'
import { loginSuccess, setAuthorizationToken } from './actions/Auth'

const store = createStore(
rootReducer,
Expand All @@ -18,6 +20,11 @@ const store = createStore(
)
)

if (localStorage.jwtToken) {
setAuthorizationToken(localStorage.jwtToken)
store.dispatch(loginSuccess(jwtDecode(localStorage.jwtToken)))
}

ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/reducers/Auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import _ from 'lodash'

import { LOGIN_SUCCESS } from '../constants/ActionTypes'

const initialState = {
isAuthenticated: false,
user: {}
}

const auth = (state=initialState, action={}) => {
switch (action.type) {
case LOGIN_SUCCESS:
return {
isAuthenticated: !_.isEmpty(action.payload),
user: action.payload
}

default:
return state
}
}


export default auth
5 changes: 4 additions & 1 deletion frontend/src/reducers/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { combineReducers } from 'redux'

import products from './Products'
import cart from './Cart'
import shipping from './Shipping'
import billing from './Billing'
import auth from './Auth'

const rootReducers = combineReducers({
products,
cart,
shipping,
billing
billing,
auth
})


Expand Down
4 changes: 4 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3878,6 +3878,10 @@ jsx-ast-utils@^2.0.0:
dependencies:
array-includes "^3.0.3"

jwt-decode@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"

killable@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
Expand Down

0 comments on commit 0f9946c

Please sign in to comment.