From 1b608579599259341d1ae4f6928ac24b9dd794da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 12 Apr 2019 11:42:19 +0200 Subject: [PATCH 1/5] Introduce extension point for logout redirection --- .../api/v2/resources/LogoutRedirection.java | 11 +++++++ scm-ui/src/containers/Logout.js | 8 +++-- scm-ui/src/modules/auth.js | 30 ++++++++++++++----- .../v2/resources/AuthenticationResource.java | 18 +++++++++-- .../v2/resources/RedirectAfterLogoutDto.java | 10 +++++++ .../resources/AuthenticationResourceTest.java | 2 +- 6 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RedirectAfterLogoutDto.java diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java new file mode 100644 index 0000000000..c0b59d4dac --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.plugin.ExtensionPoint; + +import java.net.URI; +import java.util.Optional; + +@ExtensionPoint(multi = false) +public interface LogoutRedirection { + Optional afterLogoutRedirectTo(); +} diff --git a/scm-ui/src/containers/Logout.js b/scm-ui/src/containers/Logout.js index fe6662da42..465e1b156a 100644 --- a/scm-ui/src/containers/Logout.js +++ b/scm-ui/src/containers/Logout.js @@ -3,6 +3,7 @@ import React from "react"; import { connect } from "react-redux"; import { translate } from "react-i18next"; import { Redirect } from "react-router-dom"; +import type { History } from "history"; import { logout, @@ -20,15 +21,16 @@ type Props = { logoutLink: string, // dispatcher functions - logout: (link: string) => void, + logout: (link: string, history: History) => void, // context props + history: History, t: string => string }; class Logout extends React.Component { componentDidMount() { - this.props.logout(this.props.logoutLink); + this.props.logout(this.props.logoutLink, this.props.history); } render() { @@ -64,7 +66,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - logout: (link: string) => dispatch(logout(link)) + logout: (link: string, history: History) => dispatch(logout(link, history)) }; }; diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index 4c0d22b23d..ecd529f735 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -11,6 +11,7 @@ import { fetchIndexResourcesPending, fetchIndexResourcesSuccess } from "./indexResource"; +import type { History } from "history"; // Action @@ -30,6 +31,10 @@ export const LOGOUT_PENDING = `${LOGOUT}_${types.PENDING_SUFFIX}`; export const LOGOUT_SUCCESS = `${LOGOUT}_${types.SUCCESS_SUFFIX}`; export const LOGOUT_FAILURE = `${LOGOUT}_${types.FAILURE_SUFFIX}`; +type LogoutRedirection = { + logoutRedirect: string +}; + // Reducer const initialState = {}; @@ -130,11 +135,9 @@ export const fetchMeFailure = (error: Error) => { // side effects const callFetchMe = (link: string): Promise => { - return apiClient - .get(link) - .then(response => { - return response.json(); - }); + return apiClient.get(link).then(response => { + return response.json(); + }); }; export const login = ( @@ -187,13 +190,24 @@ export const fetchMe = (link: string) => { }; }; -export const logout = (link: string) => { +export const logout = (link: string, history: History) => { return function(dispatch: any) { dispatch(logoutPending()); return apiClient .delete(link) - .then(() => { - dispatch(logoutSuccess()); + .then(response => { + return response.status === 200 + ? response.json() + : new Promise(function(resolve) { + resolve(undefined); + }); + }) + .then(json => { + if (json && json.logoutRedirect) { + location.href = json.logoutRedirect; + } else { + dispatch(logoutSuccess()); + } }) .then(() => { dispatch(fetchIndexResources()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index 47918080ab..a10375b83a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -16,6 +16,8 @@ import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Optional; @Path(AuthenticationResource.PATH) @AllowAnonymousAccess @@ -27,12 +29,14 @@ public class AuthenticationResource { private final AccessTokenBuilderFactory tokenBuilderFactory; private final AccessTokenCookieIssuer cookieIssuer; + private final LogoutRedirection logoutRedirection; @Inject - public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer) + public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer, LogoutRedirection logoutRedirection) { this.tokenBuilderFactory = tokenBuilderFactory; this.cookieIssuer = cookieIssuer; + this.logoutRedirection = logoutRedirection; } @@ -121,6 +125,7 @@ public class AuthenticationResource { @DELETE @Path("access_token") + @Produces(MediaType.APPLICATION_JSON) @StatusCodes({ @ResponseCode(code = 204, condition = "success"), @ResponseCode(code = 500, condition = "internal server error") @@ -135,7 +140,16 @@ public class AuthenticationResource { cookieIssuer.invalidate(request, response); // TODO anonymous access ?? - return Response.noContent().build(); + if (logoutRedirection == null) { + return Response.noContent().build(); + } else { + Optional uri = logoutRedirection.afterLogoutRedirectTo(); + if (uri.isPresent()) { + return Response.ok(new RedirectAfterLogoutDto(uri.get().toASCIIString())).build(); + } else { + return Response.noContent().build(); + } + } } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RedirectAfterLogoutDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RedirectAfterLogoutDto.java new file mode 100644 index 0000000000..a6402a0190 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RedirectAfterLogoutDto.java @@ -0,0 +1,10 @@ +package sonia.scm.api.v2.resources; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class RedirectAfterLogoutDto { + private String logoutRedirect; +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java index 1123dc94ce..3493283086 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java @@ -103,7 +103,7 @@ public class AuthenticationResourceTest { @Before public void prepareEnvironment() { - AuthenticationResource authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer); + AuthenticationResource authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer, null); dispatcher.getRegistry().addSingletonResource(authenticationResource); AccessToken accessToken = mock(AccessToken.class); From e56d6504c04ab684f9986dec788b013ae46a538a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 12 Apr 2019 14:32:17 +0200 Subject: [PATCH 2/5] Add unit tests --- .../api/v2/resources/LogoutRedirection.java | 1 + scm-ui/src/modules/auth.js | 5 +- scm-ui/src/modules/auth.test.js | 40 +++++++++++++++- .../resources/AuthenticationResourceTest.java | 46 ++++++++++++++++--- 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java index c0b59d4dac..84fe2ddf7b 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/LogoutRedirection.java @@ -6,6 +6,7 @@ import java.net.URI; import java.util.Optional; @ExtensionPoint(multi = false) +@FunctionalInterface public interface LogoutRedirection { Optional afterLogoutRedirectTo(); } diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index ecd529f735..8b22c8a0c5 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -204,10 +204,9 @@ export const logout = (link: string, history: History) => { }) .then(json => { if (json && json.logoutRedirect) { - location.href = json.logoutRedirect; - } else { - dispatch(logoutSuccess()); + window.location.assign(json.logoutRedirect); } + dispatch(logoutSuccess()); }) .then(() => { dispatch(fetchIndexResources()); diff --git a/scm-ui/src/modules/auth.test.js b/scm-ui/src/modules/auth.test.js index 1d7cb5096e..199d369022 100644 --- a/scm-ui/src/modules/auth.test.js +++ b/scm-ui/src/modules/auth.test.js @@ -26,7 +26,7 @@ import reducer, { FETCH_ME, LOGOUT, getLoginFailure, - getLogoutFailure + getLogoutFailure, } from "./auth"; import configureMockStore from "redux-mock-store"; @@ -224,6 +224,44 @@ describe("auth actions", () => { }); }); + it("should dispatch logout success and redirect", () => { + fetchMock.deleteOnce("/api/v2/auth/access_token", { + status: 200, + body: { logoutRedirect: "http://example.com/cas/logout" } + }); + + fetchMock.getOnce("/api/v2/me", { + status: 401 + }); + + fetchMock.getOnce("/api/v2/", { + _links: { + login: { + login: "/login" + } + } + }); + + window.location.assign = jest.fn(); + + const expectedActions = [ + { type: LOGOUT_PENDING }, + { type: LOGOUT_SUCCESS }, + { type: FETCH_INDEXRESOURCES_PENDING } + ]; + + const store = mockStore({}); + + return store.dispatch(logout("/auth/access_token")).then(() => { + expect(window.location.assign.mock.calls[0][0]).toBe( + "http://example.com/cas/logout" + ); + expect(store.getActions()).toEqual(expectedActions); + + // expect(window.location.href).toEqual("http://example.com/cas/logout"); + }); + }); + it("should dispatch logout failure", () => { fetchMock.deleteOnce("/api/v2/auth/access_token", { status: 500 diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java index 3493283086..b9612fedb2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java @@ -23,10 +23,18 @@ import sonia.scm.security.DefaultAccessTokenCookieIssuer; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MediaType; +import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URISyntaxException; import java.util.Date; +import java.util.Optional; +import static java.net.URI.create; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -49,6 +57,8 @@ public class AuthenticationResourceTest { private AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class)); + private MockHttpResponse response = new MockHttpResponse(); + private static final String AUTH_JSON_TRILLIAN = "{\n" + "\t\"cookie\": true,\n" + "\t\"grant_type\": \"password\",\n" + @@ -123,7 +133,6 @@ public class AuthenticationResourceTest { public void shouldAuthCorrectly() throws URISyntaxException { MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -134,7 +143,6 @@ public class AuthenticationResourceTest { public void shouldAuthCorrectlyWithFormencodedData() throws URISyntaxException { MockHttpRequest request = getMockHttpRequestUrlEncoded(AUTH_FORMENCODED_TRILLIAN); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -146,7 +154,6 @@ public class AuthenticationResourceTest { public void shouldNotAuthUserWithWrongPassword() throws URISyntaxException { MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN_WRONG_PW); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -156,7 +163,6 @@ public class AuthenticationResourceTest { @Test public void shouldNotAuthNonexistingUser() throws URISyntaxException { MockHttpRequest request = getMockHttpRequest(AUTH_JSON_NOT_EXISTING_USER); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -187,16 +193,43 @@ public class AuthenticationResourceTest { @SubjectAware(username = "trillian", password = "secret") public void shouldSuccessfullyLogoutUser() throws URISyntaxException { MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); } + @Test + public void shouldHandleLogoutRedirection() throws URISyntaxException, UnsupportedEncodingException { + mockResourceWithLogoutRedirection(of(create("http://example.com/cas/logout"))); + + MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token"); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertThat(response.getContentAsString(), containsString("http://example.com/cas/logout")); + } + + @Test + public void shouldHandleDisabledLogoutRedirection() throws URISyntaxException { + mockResourceWithLogoutRedirection(empty()); + + MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token"); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + private void mockResourceWithLogoutRedirection(Optional target) { + dispatcher.getRegistry().removeRegistrations(AuthenticationResource.class); + AuthenticationResource authenticationResource = + new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer, () -> target); + dispatcher.getRegistry().addSingletonResource(authenticationResource); + } private void shouldReturnBadRequest(String requestBody) throws URISyntaxException { MockHttpRequest request = getMockHttpRequest(requestBody); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -218,5 +251,4 @@ public class AuthenticationResourceTest { request.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); return request; } - } From 20cf0507a215b92f42796da4b1c05b25635ad460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 12 Apr 2019 15:26:22 +0200 Subject: [PATCH 3/5] Fix injection --- .../api/v2/resources/AuthenticationResource.java | 11 +++++++---- .../v2/resources/AuthenticationResourceTest.java | 15 +++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index a10375b83a..deab53a708 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -29,17 +29,17 @@ public class AuthenticationResource { private final AccessTokenBuilderFactory tokenBuilderFactory; private final AccessTokenCookieIssuer cookieIssuer; - private final LogoutRedirection logoutRedirection; + + @Inject(optional = true) + private LogoutRedirection logoutRedirection; @Inject - public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer, LogoutRedirection logoutRedirection) + public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer) { this.tokenBuilderFactory = tokenBuilderFactory; this.cookieIssuer = cookieIssuer; - this.logoutRedirection = logoutRedirection; } - @POST @Path("access_token") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @@ -152,4 +152,7 @@ public class AuthenticationResource { } } + void setLogoutRedirection(LogoutRedirection logoutRedirection) { + this.logoutRedirection = logoutRedirection; + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java index b9612fedb2..177f975971 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java @@ -111,9 +111,11 @@ public class AuthenticationResourceTest { "}" ); + private AuthenticationResource authenticationResource; + @Before public void prepareEnvironment() { - AuthenticationResource authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer, null); + authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer); dispatcher.getRegistry().addSingletonResource(authenticationResource); AccessToken accessToken = mock(AccessToken.class); @@ -200,7 +202,7 @@ public class AuthenticationResourceTest { @Test public void shouldHandleLogoutRedirection() throws URISyntaxException, UnsupportedEncodingException { - mockResourceWithLogoutRedirection(of(create("http://example.com/cas/logout"))); + authenticationResource.setLogoutRedirection(() -> of(create("http://example.com/cas/logout"))); MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token"); @@ -212,7 +214,7 @@ public class AuthenticationResourceTest { @Test public void shouldHandleDisabledLogoutRedirection() throws URISyntaxException { - mockResourceWithLogoutRedirection(empty()); + authenticationResource.setLogoutRedirection(Optional::empty); MockHttpRequest request = MockHttpRequest.delete("/" + AuthenticationResource.PATH + "/access_token"); @@ -221,13 +223,6 @@ public class AuthenticationResourceTest { assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); } - private void mockResourceWithLogoutRedirection(Optional target) { - dispatcher.getRegistry().removeRegistrations(AuthenticationResource.class); - AuthenticationResource authenticationResource = - new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer, () -> target); - dispatcher.getRegistry().addSingletonResource(authenticationResource); - } - private void shouldReturnBadRequest(String requestBody) throws URISyntaxException { MockHttpRequest request = getMockHttpRequest(requestBody); From 2b2d7ea0e9b44eb56bbdb06f5821882e2b07f9e1 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 17 Apr 2019 11:45:44 +0200 Subject: [PATCH 4/5] do not show login error, when redirecting --- scm-ui/src/containers/Logout.js | 17 +++++++------ scm-ui/src/modules/auth.js | 43 ++++++++++++++++++++++++--------- scm-ui/src/modules/auth.test.js | 30 +++++++++++++++++++---- 3 files changed, 66 insertions(+), 24 deletions(-) diff --git a/scm-ui/src/containers/Logout.js b/scm-ui/src/containers/Logout.js index 465e1b156a..27c4902000 100644 --- a/scm-ui/src/containers/Logout.js +++ b/scm-ui/src/containers/Logout.js @@ -3,13 +3,12 @@ import React from "react"; import { connect } from "react-redux"; import { translate } from "react-i18next"; import { Redirect } from "react-router-dom"; -import type { History } from "history"; import { logout, isAuthenticated, isLogoutPending, - getLogoutFailure + getLogoutFailure, isRedirecting } from "../modules/auth"; import { Loading, ErrorPage } from "@scm-manager/ui-components"; import { getLogoutLink } from "../modules/indexResource"; @@ -17,24 +16,24 @@ import { getLogoutLink } from "../modules/indexResource"; type Props = { authenticated: boolean, loading: boolean, + redirecting: boolean, error: Error, logoutLink: string, // dispatcher functions - logout: (link: string, history: History) => void, + logout: (link: string) => void, // context props - history: History, t: string => string }; class Logout extends React.Component { componentDidMount() { - this.props.logout(this.props.logoutLink, this.props.history); + this.props.logout(this.props.logoutLink); } render() { - const { authenticated, loading, error, t } = this.props; + const { authenticated, redirecting, loading, error, t } = this.props; if (error) { return ( { error={error} /> ); - } else if (loading || authenticated) { + } else if (loading || authenticated || redirecting) { return ; } else { return ; @@ -54,11 +53,13 @@ class Logout extends React.Component { const mapStateToProps = state => { const authenticated = isAuthenticated(state); const loading = isLogoutPending(state); + const redirecting = isRedirecting(state); const error = getLogoutFailure(state); const logoutLink = getLogoutLink(state); return { authenticated, loading, + redirecting, error, logoutLink }; @@ -66,7 +67,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - logout: (link: string, history: History) => dispatch(logout(link, history)) + logout: (link: string) => dispatch(logout(link)) }; }; diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index 8b22c8a0c5..73ba013fa1 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -11,7 +11,6 @@ import { fetchIndexResourcesPending, fetchIndexResourcesSuccess } from "./indexResource"; -import type { History } from "history"; // Action @@ -30,10 +29,7 @@ export const LOGOUT = "scm/auth/LOGOUT"; export const LOGOUT_PENDING = `${LOGOUT}_${types.PENDING_SUFFIX}`; export const LOGOUT_SUCCESS = `${LOGOUT}_${types.SUCCESS_SUFFIX}`; export const LOGOUT_FAILURE = `${LOGOUT}_${types.FAILURE_SUFFIX}`; - -type LogoutRedirection = { - logoutRedirect: string -}; +export const LOGOUT_REDIRECT = `${LOGOUT}_REDIRECT`; // Reducer @@ -59,6 +55,13 @@ export default function reducer( case LOGOUT_SUCCESS: return initialState; + case LOGOUT_REDIRECT: { + // we keep the current state until we are redirected to the new page + return { + ...state, + redirecting: true + }; + } default: return state; } @@ -94,10 +97,16 @@ export const logoutPending = () => { export const logoutSuccess = () => { return { - type: LOGOUT_SUCCESS + type: LOGOUT_SUCCESS, }; }; +export const redirectAfterLogout = () => { + return { + type: LOGOUT_REDIRECT + } +}; + export const logoutFailure = (error: Error) => { return { type: LOGOUT_FAILURE, @@ -190,7 +199,7 @@ export const fetchMe = (link: string) => { }; }; -export const logout = (link: string, history: History) => { +export const logout = (link: string) => { return function(dispatch: any) { dispatch(logoutPending()); return apiClient @@ -199,17 +208,24 @@ export const logout = (link: string, history: History) => { return response.status === 200 ? response.json() : new Promise(function(resolve) { - resolve(undefined); + resolve(); }); }) .then(json => { + let fetchIndex = true; if (json && json.logoutRedirect) { + dispatch(redirectAfterLogout()); window.location.assign(json.logoutRedirect); + fetchIndex = false; + } else { + dispatch(logoutSuccess()); } - dispatch(logoutSuccess()); + return fetchIndex; }) - .then(() => { - dispatch(fetchIndexResources()); + .then((fetchIndex: boolean) => { + if (fetchIndex) { + dispatch(fetchIndexResources()); + } }) .catch(error => { dispatch(logoutFailure(error)); @@ -257,3 +273,8 @@ export const isLogoutPending = (state: Object) => { export const getLogoutFailure = (state: Object) => { return getFailure(state, LOGOUT); }; + +export const isRedirecting = (state: Object) => { + return !!stateAuth(state).redirecting; +}; + diff --git a/scm-ui/src/modules/auth.test.js b/scm-ui/src/modules/auth.test.js index 199d369022..cdf8e7c661 100644 --- a/scm-ui/src/modules/auth.test.js +++ b/scm-ui/src/modules/auth.test.js @@ -26,7 +26,7 @@ import reducer, { FETCH_ME, LOGOUT, getLoginFailure, - getLogoutFailure, + getLogoutFailure, isRedirecting, LOGOUT_REDIRECT, redirectAfterLogout, } from "./auth"; import configureMockStore from "redux-mock-store"; @@ -70,6 +70,17 @@ describe("auth reducer", () => { expect(state.authenticated).toBeUndefined(); }); + it("should keep state and set redirecting to true", () => { + const initialState = { + authenticated: true, + me + }; + const state = reducer(initialState, redirectAfterLogout()); + expect(state.me).toBe(initialState.me); + expect(state.authenticated).toBe(initialState.authenticated); + expect(state.redirecting).toBe(true); + }); + it("should set state authenticated and me after login", () => { const state = reducer(undefined, loginSuccess(me)); expect(state.me).toBe(me); @@ -246,8 +257,7 @@ describe("auth actions", () => { const expectedActions = [ { type: LOGOUT_PENDING }, - { type: LOGOUT_SUCCESS }, - { type: FETCH_INDEXRESOURCES_PENDING } + { type: LOGOUT_REDIRECT } ]; const store = mockStore({}); @@ -257,8 +267,6 @@ describe("auth actions", () => { "http://example.com/cas/logout" ); expect(store.getActions()).toEqual(expectedActions); - - // expect(window.location.href).toEqual("http://example.com/cas/logout"); }); }); @@ -345,4 +353,16 @@ describe("auth selectors", () => { it("should return unknown, if failure state is not set for LOGOUT", () => { expect(getLogoutFailure({})).toBeUndefined(); }); + + it("should return false, if redirecting is not set", () => { + expect(isRedirecting({})).toBe(false); + }); + + it("should return false, if redirecting is false", () => { + expect(isRedirecting({auth: { redirecting: false }})).toBe(false); + }); + + it("should return true, if redirecting is true", () => { + expect(isRedirecting({auth: { redirecting: true }})).toBe(true); + }); }); From 7b167f9cbb38f439428322a50c82bec09edf7581 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 17 Apr 2019 10:46:33 +0000 Subject: [PATCH 5/5] Close branch feature/logout_redirection