diff --git a/src/App.js b/src/App.js
index f62c227..d6c6430 100644
--- a/src/App.js
+++ b/src/App.js
@@ -6,8 +6,10 @@ import "@patternfly/patternfly/patternfly.css";
import "@patternfly/patternfly/patternfly-addons.css";
import store from "./store";
import { getConfig } from "./config/configActions";
+import { checkAuthentification } from "./auth/authActions";
import * as Containers from "./containers";
-import Header from "./layout/navigation/Header";
+import Header from "./layout/Header";
+import PrivateRoute from "./auth/PrivateRoute";
import Page from "./layout/Page";
class App extends Component {
@@ -16,7 +18,11 @@ class App extends Component {
};
componentDidMount() {
- store.dispatch(getConfig()).then(() => this.setState({ isLoading: false }));
+ store
+ .dispatch(getConfig())
+ .then(() => store.dispatch(checkAuthentification()))
+ .catch(error => console.error(error))
+ .then(() => this.setState({ isLoading: false }));
}
render() {
@@ -28,15 +34,16 @@ class App extends Component {
}>
-
-
+
diff --git a/src/auth/PrivateRoute.js b/src/auth/PrivateRoute.js
new file mode 100644
index 0000000..24ef17a
--- /dev/null
+++ b/src/auth/PrivateRoute.js
@@ -0,0 +1,34 @@
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import { Redirect, Route } from "react-router-dom";
+
+class PrivateRoute extends Component {
+ render() {
+ const { isAuthenticated, component: Component, ...props } = this.props;
+ return (
+
+ isAuthenticated ? (
+
+ ) : (
+
+ )
+ }
+ />
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ isAuthenticated: state.auth.isAuthenticated
+ };
+}
+
+export default connect(mapStateToProps)(PrivateRoute);
diff --git a/src/auth/authActions.js b/src/auth/authActions.js
new file mode 100644
index 0000000..4c2e5d3
--- /dev/null
+++ b/src/auth/authActions.js
@@ -0,0 +1,39 @@
+import http from "../http";
+import * as types from "./authActionsTypes";
+import { setCredentials, removeCredentials } from "./localStorage";
+
+export function logout() {
+ removeCredentials();
+ return {
+ type: types.LOGOUT
+ };
+}
+
+export function checkAuthentification() {
+ return (dispatch, getState) => {
+ const state = getState();
+ return http({
+ method: "get",
+ url: `${state.config.apiURL}/api/v1/`
+ })
+ .then(response => {
+ dispatch({
+ type: types.LOGIN
+ });
+ return response;
+ })
+ .catch(error => {
+ if (error.response && error.response.status === 401) {
+ dispatch(logout());
+ }
+ throw error;
+ });
+ };
+}
+
+export function login(username, password) {
+ return dispatch => {
+ setCredentials({ username, password });
+ return dispatch(checkAuthentification());
+ };
+}
diff --git a/src/auth/authActions.test.js b/src/auth/authActions.test.js
new file mode 100644
index 0000000..14a7619
--- /dev/null
+++ b/src/auth/authActions.test.js
@@ -0,0 +1,38 @@
+import axios from "axios";
+import configureMockStore from "redux-mock-store";
+import thunk from "redux-thunk";
+import axiosMockAdapter from "axios-mock-adapter";
+
+import { checkAuthentification } from "./authActions";
+import * as types from "./authActionsTypes";
+
+const middlewares = [thunk];
+const mockStore = configureMockStore(middlewares);
+
+const axiosMock = new axiosMockAdapter(axios);
+
+it("checkAuthentification", () => {
+ axiosMock.onGet("https://api.example.org/api/v1/").reply(200, {});
+ const expectedActions = [
+ {
+ type: types.LOGIN
+ }
+ ];
+ const store = mockStore({ config: { apiURL: "https://api.example.org" } });
+ return store.dispatch(checkAuthentification()).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+});
+
+it("checkAuthentification unauthorized", () => {
+ axiosMock.onGet("https://api.example.org/api/v1/").reply(401, {});
+ const expectedActions = [
+ {
+ type: types.LOGOUT
+ }
+ ];
+ const store = mockStore({ config: { apiURL: "https://api.example.org" } });
+ return store.dispatch(checkAuthentification()).catch(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+});
diff --git a/src/auth/authActionsTypes.js b/src/auth/authActionsTypes.js
new file mode 100644
index 0000000..f413b89
--- /dev/null
+++ b/src/auth/authActionsTypes.js
@@ -0,0 +1,2 @@
+export const LOGIN = "LOGIN";
+export const LOGOUT = "LOGOUT";
diff --git a/src/auth/authReducer.js b/src/auth/authReducer.js
new file mode 100644
index 0000000..299421d
--- /dev/null
+++ b/src/auth/authReducer.js
@@ -0,0 +1,22 @@
+import * as types from "./authActionsTypes";
+
+const initialState = {
+ isAuthenticated: true
+};
+
+export default function(state = initialState, action) {
+ switch (action.type) {
+ case types.LOGIN:
+ return {
+ ...state,
+ isAuthenticated: true
+ }
+ case types.LOGOUT:
+ return {
+ ...state,
+ isAuthenticated: false
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/auth/authReducer.test.js b/src/auth/authReducer.test.js
new file mode 100644
index 0000000..0734a5a
--- /dev/null
+++ b/src/auth/authReducer.test.js
@@ -0,0 +1,24 @@
+import reducer from "./authReducer";
+import * as types from "./authActionsTypes";
+
+it("returns the initial state", () => {
+ expect(reducer(undefined, {})).toEqual({ isAuthenticated: true });
+});
+
+it("LOGIN", () => {
+ const newState = reducer(undefined, {
+ type: types.LOGIN
+ });
+ expect(newState).toEqual({
+ isAuthenticated: true
+ });
+});
+
+it("LOGOUT", () => {
+ const newState = reducer(undefined, {
+ type: types.LOGOUT
+ });
+ expect(newState).toEqual({
+ isAuthenticated: false
+ });
+});
diff --git a/src/auth/localStorage.js b/src/auth/localStorage.js
new file mode 100644
index 0000000..20831b4
--- /dev/null
+++ b/src/auth/localStorage.js
@@ -0,0 +1,15 @@
+const TOKEN = "ARA";
+
+export function getCredentials() {
+ const credentials = localStorage.getItem(TOKEN);
+ if (!credentials) return null;
+ return JSON.parse(credentials);
+}
+
+export function setCredentials(credentials) {
+ localStorage.setItem(TOKEN, JSON.stringify(credentials));
+}
+
+export function removeCredentials() {
+ localStorage.removeItem(TOKEN);
+}
diff --git a/src/auth/localStorage.test.js b/src/auth/localStorage.test.js
new file mode 100644
index 0000000..aa62762
--- /dev/null
+++ b/src/auth/localStorage.test.js
@@ -0,0 +1,20 @@
+import {
+ setCredentials,
+ getCredentials,
+ removeCredentials,
+} from "./localStorage";
+
+it("localStorage getCredentials", () => {
+ expect(getCredentials()).toBe(null);
+});
+
+it("localStorage setCredentials getCredentials removeCredentials", () => {
+ const credentials = {
+ username: "foo",
+ password: "bar"
+ };
+ setCredentials(credentials);
+ expect(getCredentials()).toEqual(credentials);
+ removeCredentials();
+ expect(getCredentials()).toBe(null);
+});
diff --git a/src/config/configActions.js b/src/config/configActions.js
index b4707e9..6683d9b 100644
--- a/src/config/configActions.js
+++ b/src/config/configActions.js
@@ -1,4 +1,4 @@
-import axios from "axios";
+import http from "../http";
import * as types from "./configActionsTypes";
export function setConfig(config) {
@@ -10,7 +10,7 @@ export function setConfig(config) {
export function getConfig() {
return dispatch => {
- return axios.get(`${process.env.PUBLIC_URL}/config.json`).then(response => {
+ return http.get(`${process.env.PUBLIC_URL}/config.json`).then(response => {
const config = response.data;
dispatch(setConfig(config));
return response;
diff --git a/src/containers.js b/src/containers.js
index 230e86b..9e5288e 100644
--- a/src/containers.js
+++ b/src/containers.js
@@ -1,4 +1,5 @@
export { default as Container404 } from "./layout/Container404";
export { default as LoadingContainer } from "./layout/LoadingContainer";
+export { default as LoginContainer } from "./login/LoginContainer";
export { default as PlaybooksContainer } from "./playbooks/PlaybooksContainer";
export { default as PlaybookContainer } from "./playbooks/PlaybookContainer";
diff --git a/src/http.js b/src/http.js
new file mode 100644
index 0000000..5531a77
--- /dev/null
+++ b/src/http.js
@@ -0,0 +1,12 @@
+import axios from "axios";
+import { getCredentials } from "./auth/localStorage";
+
+axios.interceptors.request.use(config => {
+ const credentials = getCredentials();
+ if (credentials) {
+ config.auth = credentials
+ }
+ return config;
+});
+
+export default axios;
diff --git a/src/images/logo.svg b/src/images/logo.svg
new file mode 100644
index 0000000..7d11757
--- /dev/null
+++ b/src/images/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/layout/navigation/Header.js b/src/layout/Header.js
similarity index 76%
rename from src/layout/navigation/Header.js
rename to src/layout/Header.js
index 6789bfd..059f292 100644
--- a/src/layout/navigation/Header.js
+++ b/src/layout/Header.js
@@ -1,4 +1,5 @@
import React, { Component } from "react";
+import { connect } from "react-redux";
import styled from "styled-components";
import { withRouter } from "react-router";
import {
@@ -9,7 +10,7 @@ import {
NavVariants,
PageHeader
} from "@patternfly/react-core";
-import logo from "./logo.svg";
+import logo from "../images/logo.svg";
const Logo = styled(Brand)`
height: 45px;
@@ -17,7 +18,8 @@ const Logo = styled(Brand)`
class Header extends Component {
render() {
- const { location, history } = this.props;
+ const { location, history, isAuthenticated } = this.props;
+ if (!isAuthenticated) return null;
const TopNav = (