Implemented login & added tests

This commit is contained in:
Philipp Czora
2018-07-09 11:38:13 +02:00
parent fbfebe1df7
commit 643e6693b6
9 changed files with 204 additions and 58 deletions

View File

@@ -16,6 +16,7 @@ public class VndMediaType {
public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;
public static final String ME = PREFIX + "me" + SUFFIX;
private VndMediaType() { private VndMediaType() {
} }

View File

@@ -1,4 +1,5 @@
{ {
"homepage": "/scm",
"name": "scm-ui", "name": "scm-ui",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
@@ -21,12 +22,21 @@
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test --env=jsdom", "test": "yarn flow && jest",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"flow": "flow" "flow": "flow"
}, },
"proxy": "http://localhost:8081/scm", "proxy": {
"/scm/api": {
"target": "http://localhost:8081"
}
},
"devDependencies": { "devDependencies": {
"prettier": "^1.13.7" "prettier": "^1.13.7"
},
"babel": {
"presets": [
"react-app"
]
} }
} }

View File

@@ -2,15 +2,22 @@ import React, { Component } from "react";
import Navigation from "./Navigation"; import Navigation from "./Navigation";
import Main from "./Main"; import Main from "./Main";
import Login from "./Login"; import Login from "./Login";
import { getIsAuthenticated } from "../modules/login";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom"; import { withRouter } from "react-router-dom";
type Props = { type Props = {
login: boolean login: boolean,
username: string,
getAuthState: any
}; };
class App extends Component { class App extends Component {
componentWillMount() {
this.props.getAuthState();
}
render() { render() {
const { login } = this.props; const { login, username } = this.props.login;
if (!login) { if (!login) {
return ( return (
@@ -21,6 +28,7 @@ class App extends Component {
} else { } else {
return ( return (
<div className="App"> <div className="App">
<h2>Welcome, {username}!</h2>
<Navigation /> <Navigation />
<Main /> <Main />
</div> </div>
@@ -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)
);

View File

@@ -74,7 +74,3 @@ const StyledLogin = injectSheet(styles)(
)(Login) )(Login)
); );
export default StyledLogin; export default StyledLogin;
// export default connect(
// mapStateToProps,
// mapDispatchToProps
// )(StyledLogin);

View File

@@ -1,9 +1,60 @@
//@flow //@flow
const LOGIN = "scm/auth/login"; const LOGIN_URL = "/scm/api/rest/v2/auth/access_token";
const LOGIN_REQUEST = "scm/auth/login_request"; const AUTHENTICATION_INFO_URL = "/scm/api/rest/v2/me";
const LOGIN_SUCCESSFUL = "scm/auth/login_successful";
const LOGIN_FAILED = "scm/auth/login_failed"; 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() { export function loginRequest() {
return { return {
@@ -18,18 +69,19 @@ export function login(username: string, password: string) {
password: username, password: username,
username: password username: password
}; };
console.log(login_data);
return function(dispatch) { return function(dispatch) {
dispatch(loginRequest()); dispatch(loginRequest());
return fetch("/api/rest/v2/auth/access_token", { return fetch(LOGIN_URL, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json; charset=utf-8" "Content-Type": "application/json; charset=utf-8"
}, },
credentials: "same-origin",
body: JSON.stringify(login_data) body: JSON.stringify(login_data)
}).then( }).then(
response => { response => {
if (response.ok) { if (response.ok) {
dispatch(getIsAuthenticated());
dispatch(loginSuccessful()); 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) { switch (action.type) {
case LOGIN: case LOGIN:
return { return {
@@ -64,6 +116,19 @@ export default function reducer(state = {}, action = {}) {
login: false, login: false,
error: action.payload 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: default:
return state; return state;

View File

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

View File

@@ -1,6 +1,8 @@
const FETCH_USERS = 'scm/users/FETCH'; // @flow
const FETCH_USERS_SUCCESS= 'scm/users/FETCH_SUCCESS';
const FETCH_USERS_FAILURE = 'scm/users/FETCH_FAILURE'; const FETCH_USERS = "scm/users/FETCH";
const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS";
const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE";
function requestUsers() { function requestUsers() {
return { return {
@@ -8,12 +10,11 @@ function requestUsers() {
}; };
} }
function fetchUsers() { function fetchUsers() {
return function(dispatch) { return function(dispatch) {
dispatch(requestUsers()); dispatch(requestUsers());
return null; return null;
} };
} }
export function shouldFetchUsers(state: any): boolean { export function shouldFetchUsers(state: any): boolean {
@@ -26,10 +27,10 @@ export function fetchUsersIfNeeded() {
if (shouldFetchUsers(getState())) { if (shouldFetchUsers(getState())) {
dispatch(fetchUsers()); dispatch(fetchUsers());
} }
} };
} }
export default function reducer(state = {}, action = {}) { export default function reducer(state: any = {}, action: any = {}) {
switch (action.type) { switch (action.type) {
case FETCH_USERS: case FETCH_USERS:
return { return {
@@ -53,6 +54,6 @@ export default function reducer(state = {}, action = {}) {
}; };
default: default:
return state return state;
} }
} }

View File

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

View File

@@ -75,33 +75,14 @@ public class SecurityFilter extends HttpFilter
public static final String URLV2_AUTHENTICATION = "/api/rest/v2/auth"; public static final String URLV2_AUTHENTICATION = "/api/rest/v2/auth";
//~--- constructors --------------------------------------------------------- private final ScmConfiguration configuration;
/**
* Constructs ...
*
*
* @param configuration
*/
@Inject @Inject
public SecurityFilter(ScmConfiguration configuration) public SecurityFilter(ScmConfiguration configuration)
{ {
this.configuration = configuration; this.configuration = configuration;
} }
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param response
* @param chain
*
* @throws IOException
* @throws ServletException
*/
@Override @Override
protected void doFilter(HttpServletRequest request, protected void doFilter(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) 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) protected boolean hasPermission(Subject subject)
{ {
return ((configuration != null) return ((configuration != null)
@@ -173,8 +144,4 @@ public class SecurityFilter extends HttpFilter
return username; return username;
} }
//~--- fields ---------------------------------------------------------------
/** scm configuration */
private final ScmConfiguration configuration;
} }