# HG changeset patch # User Alexandre Segura # Date 1497630999 -7200 # Node ID 3b20e2b584fe2fa5ce02a9a0718be050ffe21e0c # Parent 3c9d3c8f41d1dc32144c294e5b9180e0c7526477 Introduce authentication through API. diff -r 3c9d3c8f41d1 -r 3b20e2b584fe .editorconfig --- 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 diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/package.json --- 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", diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/APIClient.js --- /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 diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/actions/authActions.js --- /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 + }; +} diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/components/Login.js --- 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 ( + Bad credentials + ) } render() { @@ -33,6 +38,7 @@ Password { this.password = ref; }} /> + { this.props.login.error && this.renderError() } @@ -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) } } diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/components/Navbar.js --- 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 ( + { this.props.currentUser.username } + ); + } + return ( Login ); @@ -55,7 +62,8 @@ function mapStateToProps(state, props) { return { - isAuthenticated: state.get('isAuthenticated') + isAuthenticated: state.get('isAuthenticated'), + currentUser: state.get('currentUser'), }; } diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/constants/actionTypes.js --- 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'; diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/reducers/authReducer.js --- 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 + } +} diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/reducers/index.js --- 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, }); diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/sagas/index.js --- 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(), ]) } diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/src/store/configureStore.js --- 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) diff -r 3c9d3c8f41d1 -r 3b20e2b584fe client/yarn.lock --- 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"