diff --git a/scm-ui/src/components/Footer.js b/scm-ui/src/components/Footer.js index fd17c06d8b..db051ef791 100644 --- a/scm-ui/src/components/Footer.js +++ b/scm-ui/src/components/Footer.js @@ -1,9 +1,8 @@ //@flow import React from "react"; -import type { Me } from "../types/me"; type Props = { - me?: Me + me?: string }; class Footer extends React.Component { @@ -15,7 +14,7 @@ class Footer extends React.Component { return (
-

{me.username}

+

{me}

); diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index f15f0371a1..47b8b27fb3 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -18,6 +18,7 @@ type Props = { error: Error, loading: boolean, authenticated?: boolean, + displayName: string, t: string => string, fetchMe: () => void }; @@ -28,7 +29,7 @@ class App extends Component { } render() { - const { entry, loading, error, t, authenticated } = this.props; + const { loading, error, authenticated, displayName, t } = this.props; let content; const navigation = authenticated ? : ""; @@ -50,7 +51,7 @@ class App extends Component {
{navigation}
{content} -
+
); } @@ -64,10 +65,17 @@ const mapDispatchToProps = (dispatch: any) => { const mapStateToProps = state => { let mapped = state.auth.me || {}; + let displayName; if (state.auth.login) { mapped.authenticated = state.auth.login.authenticated; } - return mapped; + if (state.auth.me && state.auth.me.entry) { + displayName = state.auth.me.entry.entity.displayName; + } + return { + ...mapped, + displayName + }; }; export default withRouter( diff --git a/scm-ui/src/types/me.js b/scm-ui/src/types/me.js deleted file mode 100644 index d17798bf5f..0000000000 --- a/scm-ui/src/types/me.js +++ /dev/null @@ -1,4 +0,0 @@ -// @flow -export type Me = { - username: string -}; diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index 288e909a44..1d6f186c14 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -25,6 +25,7 @@ export const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS"; export const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE"; const USERS_URL = "users"; +const USER_URL = "users/"; const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2"; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java index 5646ca60cf..50025d3cdb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -1,34 +1,52 @@ package sonia.scm.api.v2.resources; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.SecurityUtils; +import sonia.scm.user.User; +import sonia.scm.user.UserException; +import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; +import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + @Path(MeResource.ME_PATH_V2) public class MeResource { static final String ME_PATH_V2 = "v2/me/"; + private final UserToUserDtoMapper userToDtoMapper; + + private final IdResourceManagerAdapter adapter; + @Inject + public MeResource(UserToUserDtoMapper userToDtoMapper, UserManager manager) { + this.userToDtoMapper = userToDtoMapper; + this.adapter = new IdResourceManagerAdapter<>(manager, User.class); + } + + /** + * Returns the currently logged in user or a 401 if user is not logged in + */ @GET - @Produces(VndMediaType.ME) - public Response get() { - MeDto meDto = new MeDto((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal()); - return Response.ok(meDto).build(); - } + @Path("") + @Produces(VndMediaType.USER) + @TypeHint(UserDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get(@Context Request request, @Context UriInfo uriInfo) { - @NoArgsConstructor - @AllArgsConstructor - @Getter - @Setter - class MeDto { - String username; + String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal(); + return adapter.get(id, userToDtoMapper::map); } - } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java new file mode 100644 index 0000000000..8c08a433f5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -0,0 +1,103 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.common.io.Resources; +import org.apache.shiro.authc.credential.PasswordService; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import sonia.scm.PageResult; +import sonia.scm.user.User; +import sonia.scm.user.UserException; +import sonia.scm.user.UserManager; +import sonia.scm.web.VndMediaType; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + +@SubjectAware( +// username = "trillian", +// password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +public class MeResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Mock + private UriInfo uriInfo; + @Mock + private UriInfoStore uriInfoStore; + + @Mock + private UserManager userManager; + + @InjectMocks + private UserToUserDtoMapperImpl userToDtoMapper; + + private ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + + @Before + public void prepareEnvironment() throws IOException, UserException { + initMocks(this); + createDummyUser("trillian"); + doNothing().when(userManager).create(userCaptor.capture()); + doNothing().when(userManager).modify(userCaptor.capture()); + doNothing().when(userManager).delete(userCaptor.capture()); + MeResource meResource = new MeResource(userToDtoMapper, userManager); + dispatcher.getRegistry().addSingletonResource(meResource); + when(uriInfo.getBaseUri()).thenReturn(URI.create("/")); + when(uriInfoStore.get()).thenReturn(uriInfo); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldReturnCurrentlyAuthenticatedUser() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + MeResource.ME_PATH_V2); + request.accept(VndMediaType.USER); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"name\":\"trillian\"")); + assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\"")); + assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/trillian\"}")); + assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}")); + } + + private User createDummyUser(String name) { + User user = new User(); + user.setName(name); + user.setPassword("secret"); + user.setCreationDate(System.currentTimeMillis()); + when(userManager.get(name)).thenReturn(user); + return user; + } +}