Introduce authentication through API.
authorAlexandre Segura <mex.zktk@gmail.com>
Fri, 16 Jun 2017 18:36:39 +0200
changeset 44 3b20e2b584fe
parent 43 3c9d3c8f41d1
child 45 c20f32e92759
Introduce authentication through API.
.editorconfig
client/package.json
client/src/APIClient.js
client/src/actions/authActions.js
client/src/components/Login.js
client/src/components/Navbar.js
client/src/constants/actionTypes.js
client/src/reducers/authReducer.js
client/src/reducers/index.js
client/src/sagas/index.js
client/src/store/configureStore.js
client/yarn.lock
--- a/.editorconfig	Thu Jun 15 17:18:22 2017 +0200
+++ b/.editorconfig	Fri Jun 16 18:36:39 2017 +0200
@@ -8,6 +8,6 @@
 insert_final_newline = true
 trim_trailing_whitespace = true
 
-[*.js,*.css]
+[*.js]
 indent_style = space
 indent_size = 2
--- a/client/package.json	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/package.json	Fri Jun 16 18:36:39 2017 +0200
@@ -17,6 +17,7 @@
     "react-redux": "^5.0.5",
     "react-router-redux": "next",
     "redux": "^3.6.0",
+    "redux-history-transitions": "^2.2.0",
     "redux-immutable": "^4.0.0",
     "redux-saga": "^0.15.3",
     "slate": "^0.20.1",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/APIClient.js	Fri Jun 16 18:36:39 2017 +0200
@@ -0,0 +1,66 @@
+class APIClient {
+  constructor(baseURL) {
+    this.baseURL = baseURL;
+  }
+
+  createRequest = (method, uri, data, headers) => {
+
+    headers = headers || new Headers();
+    headers.append("Content-Type", "application/json");
+
+    var options = {
+      method: method,
+      headers: headers,
+    };
+
+    if (data) {
+      options.body = JSON.stringify(data);
+    }
+
+    // TODO : use URL-module to build URL
+    return new Request(this.baseURL + uri, options);
+  }
+
+  createAuthorizedRequest = (method, uri, data) => {
+
+    var headers = new Headers(),
+        token = this.storage.get('token') || '';
+    headers.append("Authorization", "Bearer " + token);
+    headers.append("Content-Type", "application/json");
+
+    return this.createRequest(method, uri, data, headers);
+  }
+
+  request = (method, uri, data) => {
+    console.log(method + ' ' + uri);
+    var req = this.model ? this.createAuthorizedRequest(method, uri, data) : this.createRequest(method, uri, data);
+    return this.fetch(req, { credentials: 'include' });
+  }
+
+  get = (uri, data) => {
+    return this.request('GET', uri, data);
+  }
+
+  post = (uri, data) => {
+    return this.request('POST', uri, data);
+  }
+
+  put = (uri, data) => {
+    return this.request('PUT', uri, data);
+  }
+
+  fetch = (req) => {
+    return new Promise((resolve, reject) => {
+      fetch(req)
+        .then((response) => {
+          if (response.ok) {
+            return response.json().then((data) => resolve(data));
+          } else {
+            return response.json().then((data) => reject(data));
+          }
+        });
+    });
+  }
+}
+
+export default APIClient
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/actions/authActions.js	Fri Jun 16 18:36:39 2017 +0200
@@ -0,0 +1,9 @@
+import * as types from '../constants/actionTypes';
+
+export const loginSubmit = (username, password) => {
+  return {
+    type: types.AUTH_LOGIN_SUBMIT,
+    username,
+    password
+  };
+}
--- a/client/src/components/Login.js	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/src/components/Login.js	Fri Jun 16 18:36:39 2017 +0200
@@ -1,10 +1,10 @@
 import React, { Component } from 'react';
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
-import { Grid, Row, Col, Panel, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap';
+import { Grid, Row, Col, Panel, FormGroup, ControlLabel, FormControl, Button, Alert } from 'react-bootstrap';
 import '../App.css';
 import Navbar from './Navbar';
-import * as sessionsActions from '../actions/sessionsActions';
+import * as authActions from '../actions/authActions';
 
 class Login extends Component {
 
@@ -12,8 +12,13 @@
     const username = this.username.value;
     const password = this.password.value;
 
-    console.log(username, password);
+    this.props.authActions.loginSubmit(username, password);
+  }
 
+  renderError() {
+    return (
+      <Alert bsStyle="danger">Bad credentials</Alert>
+    )
   }
 
   render() {
@@ -33,6 +38,7 @@
                     <ControlLabel>Password</ControlLabel>
                     <FormControl componentClass="input" type="password" inputRef={ref => { this.password = ref; }} />
                   </FormGroup>
+                  { this.props.login.error && this.renderError() }
                   <Button block bsStyle="primary" onClick={this.login}>Login</Button>
                 </form>
               </Panel>
@@ -46,14 +52,14 @@
 
 function mapStateToProps(state, props) {
   return {
-    currentSession: state.get('currentSession'),
-    sessions: state.get('sessions')
+    currentUser: state.get('currentUser'),
+    login: state.get('login')
   };
 }
 
 function mapDispatchToProps(dispatch) {
   return {
-    actions: bindActionCreators(sessionsActions, dispatch)
+    authActions: bindActionCreators(authActions, dispatch)
   }
 }
 
--- a/client/src/components/Navbar.js	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/src/components/Navbar.js	Fri Jun 16 18:36:39 2017 +0200
@@ -22,6 +22,13 @@
   }
 
   renderLogin() {
+
+    if (this.props.currentUser) {
+      return (
+        <NavItem>{ this.props.currentUser.username }</NavItem>
+      );
+    }
+
     return (
       <NavItem onClick={this.onClickLogin} href="/login">Login</NavItem>
     );
@@ -55,7 +62,8 @@
 
 function mapStateToProps(state, props) {
   return {
-    isAuthenticated: state.get('isAuthenticated')
+    isAuthenticated: state.get('isAuthenticated'),
+    currentUser: state.get('currentUser'),
   };
 }
 
--- a/client/src/constants/actionTypes.js	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/src/constants/actionTypes.js	Fri Jun 16 18:36:39 2017 +0200
@@ -9,3 +9,8 @@
 export const UPDATE_SESSION_ASYNC = 'UPDATE_SESSION_ASYNC';
 export const LOAD_SESSIONS = 'LOAD_SESSIONS';
 export const LOAD_SESSIONS_ASYNC = 'LOAD_SESSIONS_ASYNC';
+
+export const AUTH_LOGIN_SUBMIT = 'AUTH_LOGIN_SUBMIT';
+export const AUTH_LOGIN_REQUEST = 'AUTH_LOGIN_REQUEST';
+export const AUTH_LOGIN_SUCCESS = 'AUTH_LOGIN_SUCCESS';
+export const AUTH_LOGIN_ERROR = 'AUTH_LOGIN_ERROR';
--- a/client/src/reducers/authReducer.js	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/src/reducers/authReducer.js	Fri Jun 16 18:36:39 2017 +0200
@@ -1,6 +1,54 @@
+import * as types from '../constants/actionTypes';
+
 export const isAuthenticated = (state = false, action) => {
   switch (action.type) {
     default:
       return state;
   }
-};
+}
+
+export const currentUser = (state = null, action) => {
+  switch (action.type) {
+    case types.AUTH_LOGIN_SUCCESS:
+      return action.user;
+    default:
+      return state;
+  }
+}
+
+export const token = (state = null, action) => {
+  switch (action.type) {
+    case types.AUTH_LOGIN_SUCCESS:
+      console.log('token', action.token)
+      return action.token;
+    default:
+      return state;
+  }
+}
+
+// TODO Use Immutable.Map
+const loginState = {
+  loading: false,
+  success: false,
+  error: false,
+}
+
+export const login = (state = loginState, action) => {
+  switch (action.type) {
+    case types.AUTH_LOGIN_REQUEST:
+      return {
+        loading: true,
+        success: false,
+        error: false,
+      }
+    case types.AUTH_LOGIN_SUCCESS:
+    case types.AUTH_LOGIN_ERROR:
+      return {
+        loading: false,
+        success: action.type === types.AUTH_LOGIN_SUCCESS,
+        error: action.type === types.AUTH_LOGIN_ERROR,
+      }
+    default:
+      return state
+  }
+}
--- a/client/src/reducers/index.js	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/src/reducers/index.js	Fri Jun 16 18:36:39 2017 +0200
@@ -3,13 +3,16 @@
 
 import notes from './notesReducer';
 import { currentSession, sessions } from './sessionsReducer';
-import { isAuthenticated } from './authReducer';
+import { isAuthenticated, currentUser, login, token } from './authReducer';
 
 const rootReducer = combineReducers({
   currentSession,
   sessions,
   notes,
   isAuthenticated,
+  currentUser,
+  login,
+  token,
   router: routerReducer,
 });
 
--- a/client/src/sagas/index.js	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/src/sagas/index.js	Fri Jun 16 18:36:39 2017 +0200
@@ -1,8 +1,9 @@
 import PouchDB from 'pouchdb'
-import { put, takeLatest, all } from 'redux-saga/effects'
+import { put, take, takeLatest, all } from 'redux-saga/effects'
 import * as types from '../constants/actionTypes';
 import PouchDBFind from 'pouchdb-find';
 import Immutable from 'immutable';
+import APIClient from '../APIClient';
 
 PouchDB.debug.disable();
 PouchDB.plugin(PouchDBFind);
@@ -13,6 +14,8 @@
   index: { fields: ['session'] }
 });
 
+const client = new APIClient('http://localhost:8000')
+
 // ---
 
 export function* loadSessions() {
@@ -88,6 +91,36 @@
 
 // ---
 
+export function* watchLoginSubmit() {
+  while (true) {
+    const { username, password } = yield take(types.AUTH_LOGIN_SUBMIT);
+    yield put({ type: types.AUTH_LOGIN_REQUEST, username, password });
+  }
+}
+
+function* watchLoginRequest() {
+  while (true) {
+    try {
+        const { username, password } = yield take(types.AUTH_LOGIN_REQUEST);
+        const response = yield client.post('/api/auth/login/', { username, password });
+        yield put({
+          type: types.AUTH_LOGIN_SUCCESS,
+          user: response.user,
+          token: response.token,
+          meta: {
+            transition: (prevState, nextState, action) => ({
+              pathname: '/sessions',
+            }),
+          },
+        });
+    } catch(e) {
+        yield put({ type: types.AUTH_LOGIN_ERROR, error: e });
+    }
+  }
+}
+
+// ---
+
 export default function* rootSaga() {
   yield all([
     watchLoadSessions(),
@@ -95,5 +128,7 @@
     watchAddNote(),
     watchCreateSession(),
     watchUpdateSession(),
+    watchLoginSubmit(),
+    watchLoginRequest(),
   ])
 }
--- a/client/src/store/configureStore.js	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/src/store/configureStore.js	Fri Jun 16 18:36:39 2017 +0200
@@ -1,8 +1,9 @@
 import rootReducer from '../reducers';
 import rootSaga from '../sagas';
 import { loadSessions } from '../actions/sessionsActions';
-import { createStore, applyMiddleware } from 'redux';
+import { createStore, applyMiddleware, compose } from 'redux';
 import { routerMiddleware } from 'react-router-redux';
+import handleTransitions from 'redux-history-transitions';
 import createSagaMiddleware from 'redux-saga'
 import Immutable from 'immutable';
 
@@ -11,6 +12,13 @@
   sessions: Immutable.List([]),
   notes: Immutable.List([]),
   isAuthenticated: false,
+  currentUser: null,
+  token: null,
+  login: {
+    loading: false,
+    success: false,
+    error: false,
+  }
 };
 
 const storeInitialState = Immutable.Map(defaultState);
@@ -19,8 +27,12 @@
 
   const router = routerMiddleware(history);
   const saga = createSagaMiddleware();
+  const transitions = handleTransitions(history);
 
-  const store = createStore(rootReducer, initialState, applyMiddleware(router, saga));
+  const store = createStore(rootReducer, initialState, compose(
+    applyMiddleware(router, saga),
+    transitions
+  ));
 
   saga.run(rootSaga)
 
--- a/client/yarn.lock	Thu Jun 15 17:18:22 2017 +0200
+++ b/client/yarn.lock	Fri Jun 16 18:36:39 2017 +0200
@@ -5900,6 +5900,10 @@
   dependencies:
     balanced-match "^0.4.2"
 
+redux-history-transitions@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/redux-history-transitions/-/redux-history-transitions-2.2.0.tgz#1d5197f0e586a693d65d84109d14f79bd695f919"
+
 redux-immutable@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/redux-immutable/-/redux-immutable-4.0.0.tgz#3a1a32df66366462b63691f0e1dc35e472bbc9f3"