Introduce extension point for logout redirection

This commit is contained in:
René Pfeuffer
2019-04-12 11:42:19 +02:00
parent 84c8f7159f
commit 1b60857959
6 changed files with 65 additions and 14 deletions

View File

@@ -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<URI> afterLogoutRedirectTo();
}

View File

@@ -3,6 +3,7 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
import type { History } from "history";
import { import {
logout, logout,
@@ -20,15 +21,16 @@ type Props = {
logoutLink: string, logoutLink: string,
// dispatcher functions // dispatcher functions
logout: (link: string) => void, logout: (link: string, history: History) => void,
// context props // context props
history: History,
t: string => string t: string => string
}; };
class Logout extends React.Component<Props> { class Logout extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.logout(this.props.logoutLink); this.props.logout(this.props.logoutLink, this.props.history);
} }
render() { render() {
@@ -64,7 +66,7 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
logout: (link: string) => dispatch(logout(link)) logout: (link: string, history: History) => dispatch(logout(link, history))
}; };
}; };

View File

@@ -11,6 +11,7 @@ import {
fetchIndexResourcesPending, fetchIndexResourcesPending,
fetchIndexResourcesSuccess fetchIndexResourcesSuccess
} from "./indexResource"; } from "./indexResource";
import type { History } from "history";
// Action // Action
@@ -30,6 +31,10 @@ export const LOGOUT_PENDING = `${LOGOUT}_${types.PENDING_SUFFIX}`;
export const LOGOUT_SUCCESS = `${LOGOUT}_${types.SUCCESS_SUFFIX}`; export const LOGOUT_SUCCESS = `${LOGOUT}_${types.SUCCESS_SUFFIX}`;
export const LOGOUT_FAILURE = `${LOGOUT}_${types.FAILURE_SUFFIX}`; export const LOGOUT_FAILURE = `${LOGOUT}_${types.FAILURE_SUFFIX}`;
type LogoutRedirection = {
logoutRedirect: string
};
// Reducer // Reducer
const initialState = {}; const initialState = {};
@@ -130,9 +135,7 @@ export const fetchMeFailure = (error: Error) => {
// side effects // side effects
const callFetchMe = (link: string): Promise<Me> => { const callFetchMe = (link: string): Promise<Me> => {
return apiClient return apiClient.get(link).then(response => {
.get(link)
.then(response => {
return response.json(); return response.json();
}); });
}; };
@@ -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) { return function(dispatch: any) {
dispatch(logoutPending()); dispatch(logoutPending());
return apiClient return apiClient
.delete(link) .delete(link)
.then(() => { .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()); dispatch(logoutSuccess());
}
}) })
.then(() => { .then(() => {
dispatch(fetchIndexResources()); dispatch(fetchIndexResources());

View File

@@ -16,6 +16,8 @@ import javax.ws.rs.*;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.Optional;
@Path(AuthenticationResource.PATH) @Path(AuthenticationResource.PATH)
@AllowAnonymousAccess @AllowAnonymousAccess
@@ -27,12 +29,14 @@ public class AuthenticationResource {
private final AccessTokenBuilderFactory tokenBuilderFactory; private final AccessTokenBuilderFactory tokenBuilderFactory;
private final AccessTokenCookieIssuer cookieIssuer; private final AccessTokenCookieIssuer cookieIssuer;
private final LogoutRedirection logoutRedirection;
@Inject @Inject
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer) public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer, LogoutRedirection logoutRedirection)
{ {
this.tokenBuilderFactory = tokenBuilderFactory; this.tokenBuilderFactory = tokenBuilderFactory;
this.cookieIssuer = cookieIssuer; this.cookieIssuer = cookieIssuer;
this.logoutRedirection = logoutRedirection;
} }
@@ -121,6 +125,7 @@ public class AuthenticationResource {
@DELETE @DELETE
@Path("access_token") @Path("access_token")
@Produces(MediaType.APPLICATION_JSON)
@StatusCodes({ @StatusCodes({
@ResponseCode(code = 204, condition = "success"), @ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
@@ -135,7 +140,16 @@ public class AuthenticationResource {
cookieIssuer.invalidate(request, response); cookieIssuer.invalidate(request, response);
// TODO anonymous access ?? // TODO anonymous access ??
if (logoutRedirection == null) {
return Response.noContent().build(); return Response.noContent().build();
} else {
Optional<URI> uri = logoutRedirection.afterLogoutRedirectTo();
if (uri.isPresent()) {
return Response.ok(new RedirectAfterLogoutDto(uri.get().toASCIIString())).build();
} else {
return Response.noContent().build();
}
}
} }
} }

View File

@@ -0,0 +1,10 @@
package sonia.scm.api.v2.resources;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class RedirectAfterLogoutDto {
private String logoutRedirect;
}

View File

@@ -103,7 +103,7 @@ public class AuthenticationResourceTest {
@Before @Before
public void prepareEnvironment() { public void prepareEnvironment() {
AuthenticationResource authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer); AuthenticationResource authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer, null);
dispatcher.getRegistry().addSingletonResource(authenticationResource); dispatcher.getRegistry().addSingletonResource(authenticationResource);
AccessToken accessToken = mock(AccessToken.class); AccessToken accessToken = mock(AccessToken.class);