From 643e6693b6fa626e4c14416bccfaa4e19113f85e Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Mon, 9 Jul 2018 11:38:13 +0200 Subject: [PATCH] Implemented login & added tests --- .../main/java/sonia/scm/web/VndMediaType.java | 1 + scm-ui/package.json | 14 +++- scm-ui/src/containers/App.js | 29 ++++++- scm-ui/src/containers/Login.js | 4 - scm-ui/src/modules/login.js | 79 +++++++++++++++++-- scm-ui/src/modules/login.test.js | 49 ++++++++++++ scm-ui/src/users/modules/users.js | 17 ++-- .../scm/api/v2/resources/MeResource.java | 34 ++++++++ .../java/sonia/scm/filter/SecurityFilter.java | 35 +------- 9 files changed, 204 insertions(+), 58 deletions(-) create mode 100644 scm-ui/src/modules/login.test.js create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 3ec121f9a4..c30a03c004 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -16,6 +16,7 @@ public class VndMediaType { public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; + public static final String ME = PREFIX + "me" + SUFFIX; private VndMediaType() { } diff --git a/scm-ui/package.json b/scm-ui/package.json index 8513981bf5..145839607c 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -1,4 +1,5 @@ { + "homepage": "/scm", "name": "scm-ui", "version": "0.1.0", "private": true, @@ -21,12 +22,21 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test --env=jsdom", + "test": "yarn flow && jest", "eject": "react-scripts eject", "flow": "flow" }, - "proxy": "http://localhost:8081/scm", + "proxy": { + "/scm/api": { + "target": "http://localhost:8081" + } + }, "devDependencies": { "prettier": "^1.13.7" + }, + "babel": { + "presets": [ + "react-app" + ] } } diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index b30ca75d0d..1ee8df1c98 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -2,15 +2,22 @@ import React, { Component } from "react"; import Navigation from "./Navigation"; import Main from "./Main"; import Login from "./Login"; +import { getIsAuthenticated } from "../modules/login"; +import { connect } from "react-redux"; import { withRouter } from "react-router-dom"; type Props = { - login: boolean + login: boolean, + username: string, + getAuthState: any }; class App extends Component { + componentWillMount() { + this.props.getAuthState(); + } render() { - const { login } = this.props; + const { login, username } = this.props.login; if (!login) { return ( @@ -21,6 +28,7 @@ class App extends Component { } else { return (
+

Welcome, {username}!

@@ -29,4 +37,19 @@ class App extends Component { } } -export default withRouter(App); +const mapDispatchToProps = dispatch => { + return { + getAuthState: () => dispatch(getIsAuthenticated()) + }; +}; + +const mapStateToProps = state => { + return { login: state.login }; +}; + +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(App) +); diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index fb20d48366..e811927831 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -74,7 +74,3 @@ const StyledLogin = injectSheet(styles)( )(Login) ); export default StyledLogin; -// export default connect( -// mapStateToProps, -// mapDispatchToProps -// )(StyledLogin); diff --git a/scm-ui/src/modules/login.js b/scm-ui/src/modules/login.js index e46abc44f4..53e2049ec8 100644 --- a/scm-ui/src/modules/login.js +++ b/scm-ui/src/modules/login.js @@ -1,9 +1,60 @@ //@flow -const LOGIN = "scm/auth/login"; -const LOGIN_REQUEST = "scm/auth/login_request"; -const LOGIN_SUCCESSFUL = "scm/auth/login_successful"; -const LOGIN_FAILED = "scm/auth/login_failed"; +const LOGIN_URL = "/scm/api/rest/v2/auth/access_token"; +const AUTHENTICATION_INFO_URL = "/scm/api/rest/v2/me"; + +export const LOGIN = "scm/auth/login"; +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 GET_IS_AUTHENTICATED_REQUEST = "scm/auth/is_authenticated_request"; +export const GET_IS_AUTHENTICATED = "scm/auth/get_is_authenticated"; +export const IS_AUTHENTICATED = "scm/auth/is_authenticated"; +export const IS_NOT_AUTHENTICATED = "scm/auth/is_not_authenticated"; + +export function getIsAuthenticatedRequest() { + return { + type: GET_IS_AUTHENTICATED_REQUEST + }; +} + +export function getIsAuthenticated() { + return function(dispatch) { + dispatch(getIsAuthenticatedRequest()); + + return fetch(AUTHENTICATION_INFO_URL, { + credentials: "same-origin", + headers: { + Cache: "no-cache" + } + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + dispatch(isNotAuthenticated()); + } + }) + .then(data => { + if (data) { + dispatch(isAuthenticated(data.username)); + } + }); + }; +} + +export function isAuthenticated(username: string) { + return { + type: IS_AUTHENTICATED, + username + }; +} + +export function isNotAuthenticated() { + return { + type: IS_NOT_AUTHENTICATED + }; +} export function loginRequest() { return { @@ -18,18 +69,19 @@ export function login(username: string, password: string) { password: username, username: password }; - console.log(login_data); return function(dispatch) { dispatch(loginRequest()); - return fetch("/api/rest/v2/auth/access_token", { + return fetch(LOGIN_URL, { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, + credentials: "same-origin", body: JSON.stringify(login_data) }).then( response => { if (response.ok) { + dispatch(getIsAuthenticated()); dispatch(loginSuccessful()); } }, @@ -44,7 +96,7 @@ export function loginSuccessful() { }; } -export default function reducer(state = {}, action = {}) { +export default function reducer(state: any = {}, action: any = {}) { switch (action.type) { case LOGIN: return { @@ -64,6 +116,19 @@ export default function reducer(state = {}, action = {}) { login: false, error: action.payload }; + case IS_AUTHENTICATED: + return { + ...state, + login: true, + username: action.username + }; + case IS_NOT_AUTHENTICATED: + return { + ...state, + login: false, + username: null, + error: null + }; default: return state; diff --git a/scm-ui/src/modules/login.test.js b/scm-ui/src/modules/login.test.js new file mode 100644 index 0000000000..dd009a9ee8 --- /dev/null +++ b/scm-ui/src/modules/login.test.js @@ -0,0 +1,49 @@ +// @flow +import reducer, { + LOGIN_REQUEST, + LOGIN_FAILED, + IS_AUTHENTICATED, + IS_NOT_AUTHENTICATED +} from "./login"; +import { LOGIN, LOGIN_SUCCESSFUL } from "./login"; + +test("login", () => { + var newState = reducer({}, { type: LOGIN }); + expect(newState.login).toBe(false); + expect(newState.error).toBe(null); +}); + +test("login request", () => { + var newState = reducer({}, { type: LOGIN_REQUEST }); + expect(newState.login).toBe(undefined); +}); + +test("login successful", () => { + var newState = reducer({ login: false }, { type: LOGIN_SUCCESSFUL }); + expect(newState.login).toBe(true); + expect(newState.error).toBe(null); +}); + +test("login failed", () => { + var newState = reducer({}, { type: LOGIN_FAILED, payload: "error!" }); + expect(newState.login).toBe(false); + expect(newState.error).toBe("error!"); +}); + +test("is authenticated", () => { + var newState = reducer( + { login: false }, + { type: IS_AUTHENTICATED, username: "test" } + ); + expect(newState.login).toBeTruthy(); + expect(newState.username).toBe("test"); +}); + +test("is not authenticated", () => { + var newState = reducer( + { login: true, username: "foo" }, + { type: IS_NOT_AUTHENTICATED } + ); + expect(newState.login).toBe(false); + expect(newState.username).toBeNull(); +}); diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index ed92f7ba10..c1103d60f3 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -1,6 +1,8 @@ -const FETCH_USERS = 'scm/users/FETCH'; -const FETCH_USERS_SUCCESS= 'scm/users/FETCH_SUCCESS'; -const FETCH_USERS_FAILURE = 'scm/users/FETCH_FAILURE'; +// @flow + +const FETCH_USERS = "scm/users/FETCH"; +const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS"; +const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE"; function requestUsers() { return { @@ -8,12 +10,11 @@ function requestUsers() { }; } - function fetchUsers() { return function(dispatch) { dispatch(requestUsers()); return null; - } + }; } export function shouldFetchUsers(state: any): boolean { @@ -26,10 +27,10 @@ export function fetchUsersIfNeeded() { if (shouldFetchUsers(getState())) { dispatch(fetchUsers()); } - } + }; } -export default function reducer(state = {}, action = {}) { +export default function reducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_USERS: return { @@ -53,6 +54,6 @@ export default function reducer(state = {}, action = {}) { }; default: - return state + return state; } } 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 new file mode 100644 index 0000000000..5646ca60cf --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -0,0 +1,34 @@ +package sonia.scm.api.v2.resources; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.shiro.SecurityUtils; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +@Path(MeResource.ME_PATH_V2) +public class MeResource { + static final String ME_PATH_V2 = "v2/me/"; + + @GET + @Produces(VndMediaType.ME) + public Response get() { + MeDto meDto = new MeDto((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal()); + return Response.ok(meDto).build(); + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Setter + class MeDto { + String username; + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java index 07475e5853..965e097f64 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java @@ -75,33 +75,14 @@ public class SecurityFilter extends HttpFilter public static final String URLV2_AUTHENTICATION = "/api/rest/v2/auth"; - //~--- constructors --------------------------------------------------------- + private final ScmConfiguration configuration; - /** - * Constructs ... - * - * - * @param configuration - */ @Inject public SecurityFilter(ScmConfiguration configuration) { this.configuration = configuration; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param response - * @param chain - * - * @throws IOException - * @throws ServletException - */ @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) @@ -139,16 +120,6 @@ public class SecurityFilter extends HttpFilter } } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param subject - * - * @return - */ protected boolean hasPermission(Subject subject) { return ((configuration != null) @@ -173,8 +144,4 @@ public class SecurityFilter extends HttpFilter return username; } - //~--- fields --------------------------------------------------------------- - - /** scm configuration */ - private final ScmConfiguration configuration; }