diff --git a/scm-ui/src/components/PrimaryNavigation.js b/scm-ui/src/components/PrimaryNavigation.js index 4532fd1738..a66d796a4d 100644 --- a/scm-ui/src/components/PrimaryNavigation.js +++ b/scm-ui/src/components/PrimaryNavigation.js @@ -1,15 +1,22 @@ //@flow import React from "react"; import PrimaryNavigationLink from "./PrimaryNavigationLink"; +import PrimaryNavigationAction from "./PrimaryNavigationAction"; -type Props = {}; +type Props = { + onLogout: () => void +}; class PrimaryNavigation extends React.Component { render() { return ( ); diff --git a/scm-ui/src/components/PrimaryNavigationAction.js b/scm-ui/src/components/PrimaryNavigationAction.js new file mode 100644 index 0000000000..bf5b91ad2b --- /dev/null +++ b/scm-ui/src/components/PrimaryNavigationAction.js @@ -0,0 +1,20 @@ +//@flow +import * as React from "react"; + +type Props = { + label: string, + onClick: () => void +}; + +class PrimaryNavigationAction extends React.Component { + render() { + const { label, onClick } = this.props; + return ( +
  • + {label} +
  • + ); + } +} + +export default PrimaryNavigationAction; diff --git a/scm-ui/src/components/PrimaryNavigationLink.js b/scm-ui/src/components/PrimaryNavigationLink.js index e0a8bf4cad..b7c1b5e59b 100644 --- a/scm-ui/src/components/PrimaryNavigationLink.js +++ b/scm-ui/src/components/PrimaryNavigationLink.js @@ -4,16 +4,16 @@ import { Route, Link } from "react-router-dom"; type Props = { to: string, - activeOnlyWhenExact?: boolean, - children?: React.Node + label: string, + activeOnlyWhenExact?: boolean }; class PrimaryNavigationLink extends React.Component { renderLink = (route: any) => { - const { to, children } = this.props; + const { to, label } = this.props; return (
  • - {children} + {label}
  • ); }; diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index c15697179e..6366b8a10a 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -5,25 +5,31 @@ import Login from "./Login"; import { connect } from "react-redux"; import { withRouter } from "react-router-dom"; import { fetchMe } from "../modules/me"; +import { logout } from "../modules/auth"; import "./App.css"; import Header from "../components/Header"; import PrimaryNavigation from "../components/PrimaryNavigation"; import Loading from "../components/Loading"; -import Footer from "../components/Footer"; import ErrorNotification from "../components/ErrorNotification"; type Props = { me: any, error: Error, loading: boolean, - fetchMe: () => void + fetchMe: () => void, + logout: () => void }; class App extends Component { componentDidMount() { this.props.fetchMe(); } + + logout = () => { + this.props.logout(); + }; + render() { const { me, loading, error } = this.props; @@ -39,7 +45,7 @@ class App extends Component { content = ; } else { content =
    ; - navigation = ; + navigation = ; } return ( @@ -53,7 +59,8 @@ class App extends Component { const mapDispatchToProps = (dispatch: ThunkDispatch) => { return { - fetchMe: () => dispatch(fetchMe()) + fetchMe: () => dispatch(fetchMe()), + logout: () => dispatch(logout()) }; }; diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index c70075e012..fac8332275 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import injectSheet from "react-jss"; -import { login } from "../modules/login"; +import { login } from "../modules/auth"; import { connect } from "react-redux"; import InputField from "../components/InputField"; diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index 2ac54c7e58..fd6a01d00a 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -6,7 +6,7 @@ import { routerReducer, routerMiddleware } from "react-router-redux"; import repositories from "./repositories/modules/repositories"; import users from "./users/modules/users"; -import login from "./modules/login"; +import auth from "./modules/auth"; import me from "./modules/me"; import type { BrowserHistory } from "history/createBrowserHistory"; @@ -19,7 +19,7 @@ function createReduxStore(history: BrowserHistory) { router: routerReducer, repositories, users, - login, + auth, me }); diff --git a/scm-ui/src/modules/login.js b/scm-ui/src/modules/auth.js similarity index 56% rename from scm-ui/src/modules/login.js rename to scm-ui/src/modules/auth.js index 01762a4751..6363881ec7 100644 --- a/scm-ui/src/modules/login.js +++ b/scm-ui/src/modules/auth.js @@ -1,6 +1,6 @@ //@flow -import { apiClient, NOT_AUTHENTICATED_ERROR } from "../apiclient"; +import { apiClient } from "../apiclient"; import { fetchMe } from "./me"; const LOGIN_URL = "/auth/access_token"; @@ -9,6 +9,10 @@ export const LOGIN_REQUEST = "scm/auth/login_request"; export const LOGIN_SUCCESSFUL = "scm/auth/login_successful"; export const LOGIN_FAILED = "scm/auth/login_failed"; +export const LOGOUT_REQUEST = "scm/auth/logout_request"; +export const LOGOUT_SUCCESSFUL = "scm/auth/logout_successful"; +export const LOGOUT_FAILED = "scm/auth/logout_failed"; + export function login(username: string, password: string) { const login_data = { cookie: true, @@ -50,6 +54,41 @@ export function loginFailed(error: Error) { }; } +export function logout() { + return function(dispatch: any) { + dispatch(logoutRequest()); + return apiClient + .delete(LOGIN_URL) + .then(() => { + dispatch(logoutSuccess()); + // not the best way or? + dispatch(fetchMe()); + }) + .catch(error => { + dispatch(logoutFailed(error)); + }); + }; +} + +export function logoutRequest() { + return { + type: LOGOUT_REQUEST + }; +} + +export function logoutSuccess() { + return { + type: LOGOUT_SUCCESSFUL + }; +} + +export function logoutFailed(error: Error) { + return { + type: LOGOUT_FAILED, + payload: error + }; +} + export default function reducer(state: any = {}, action: any = {}) { switch (action.type) { case LOGIN_REQUEST: @@ -74,6 +113,26 @@ export default function reducer(state: any = {}, action: any = {}) { error: action.payload }; + case LOGOUT_REQUEST: + return { + ...state, + loading: true, + error: null + }; + case LOGOUT_SUCCESSFUL: + return { + ...state, + loading: false, + login: false, + error: null + }; + case LOGOUT_FAILED: + return { + ...state, + loading: false, + error: action.payload + }; + default: return state; } diff --git a/scm-ui/src/modules/login.test.js b/scm-ui/src/modules/auth.test.js similarity index 56% rename from scm-ui/src/modules/login.test.js rename to scm-ui/src/modules/auth.test.js index b6968ac267..c9b09f38ba 100644 --- a/scm-ui/src/modules/login.test.js +++ b/scm-ui/src/modules/auth.test.js @@ -1,10 +1,14 @@ // @flow import reducer, { login, + logout, LOGIN_REQUEST, LOGIN_FAILED, - LOGIN_SUCCESSFUL -} from "./login"; + LOGIN_SUCCESSFUL, + LOGOUT_REQUEST, + LOGOUT_SUCCESSFUL, + LOGOUT_FAILED +} from "./auth"; import { ME_AUTHENTICATED_REQUEST, ME_AUTHENTICATED_SUCCESS } from "./me"; @@ -66,6 +70,44 @@ describe("action tests", () => { expect(actions[1].payload).toBeDefined(); }); }); + + test("logout success", () => { + fetchMock.deleteOnce("/scm/api/rest/v2/auth/access_token", { + status: 204 + }); + + fetchMock.getOnce("/scm/api/rest/v2/me", { + status: 401 + }); + + const expectedActions = [ + { type: LOGOUT_REQUEST }, + { type: LOGOUT_SUCCESSFUL }, + { type: ME_AUTHENTICATED_REQUEST } + ]; + + const store = mockStore({}); + + return store.dispatch(logout()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + test("logout failed", () => { + fetchMock.deleteOnce("/scm/api/rest/v2/auth/access_token", { + status: 500 + }); + + const expectedActions = [{ type: LOGOUT_REQUEST }, { type: LOGOUT_FAILED }]; + + const store = mockStore({}); + return store.dispatch(logout()).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(LOGOUT_REQUEST); + expect(actions[1].type).toEqual(LOGOUT_FAILED); + expect(actions[1].payload).toBeDefined(); + }); + }); }); describe("reducer tests", () => { @@ -90,4 +132,29 @@ describe("reducer tests", () => { expect(newState.login).toBeFalsy(); expect(newState.error).toBe(err); }); + + test("logout request", () => { + var newState = reducer({ login: true }, { type: LOGOUT_REQUEST }); + expect(newState.loading).toBeTruthy(); + expect(newState.login).toBeTruthy(); + expect(newState.error).toBeNull(); + }); + + test("logout successful", () => { + var newState = reducer({ login: true }, { type: LOGOUT_SUCCESSFUL }); + expect(newState.loading).toBeFalsy(); + expect(newState.login).toBeFalsy(); + expect(newState.error).toBeNull(); + }); + + test("logout failed", () => { + const err = new Error("error!"); + var newState = reducer( + { login: true }, + { type: LOGOUT_FAILED, payload: err } + ); + expect(newState.loading).toBeFalsy(); + expect(newState.login).toBeTruthy(); + expect(newState.error).toBe(err); + }); });