mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 22:45:45 +01:00
Implemented login & added tests
This commit is contained in:
@@ -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() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
|
|||||||
@@ -74,7 +74,3 @@ const StyledLogin = injectSheet(styles)(
|
|||||||
)(Login)
|
)(Login)
|
||||||
);
|
);
|
||||||
export default StyledLogin;
|
export default StyledLogin;
|
||||||
// export default connect(
|
|
||||||
// mapStateToProps,
|
|
||||||
// mapDispatchToProps
|
|
||||||
// )(StyledLogin);
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
49
scm-ui/src/modules/login.test.js
Normal file
49
scm-ui/src/modules/login.test.js
Normal 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();
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user