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;
}