Introduce authentication through API.
--- 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"