mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 07:25:44 +01:00
add restentpoint for login/logout, restructuring of modules and components, add flow usage
This commit is contained in:
@@ -35,3 +35,4 @@ scm-ui/yarn.lock
|
|||||||
scm-ui/.gitignore
|
scm-ui/.gitignore
|
||||||
scm-ui/package-lock.json
|
scm-ui/package-lock.json
|
||||||
node_modules
|
node_modules
|
||||||
|
scm-ui/.flowconfig
|
||||||
|
|||||||
1
pom.xml
1
pom.xml
@@ -70,6 +70,7 @@
|
|||||||
<module>scm-test</module>
|
<module>scm-test</module>
|
||||||
<module>scm-plugins</module>
|
<module>scm-plugins</module>
|
||||||
<module>scm-dao-xml</module>
|
<module>scm-dao-xml</module>
|
||||||
|
<module>scm-ui</module>
|
||||||
<module>scm-webapp</module>
|
<module>scm-webapp</module>
|
||||||
<module>scm-server</module>
|
<module>scm-server</module>
|
||||||
<module>scm-clients</module>
|
<module>scm-clients</module>
|
||||||
|
|||||||
128
scm-ui/flow-typed/npm/history_v4.x.x.js
vendored
Normal file
128
scm-ui/flow-typed/npm/history_v4.x.x.js
vendored
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// flow-typed signature: eb8bd974b677b08dfca89de9ac05b60b
|
||||||
|
// flow-typed version: 43b30482ac/history_v4.x.x/flow_>=v0.25.x
|
||||||
|
|
||||||
|
declare module "history/createBrowserHistory" {
|
||||||
|
declare function Unblock(): void;
|
||||||
|
|
||||||
|
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||||
|
|
||||||
|
declare export type BrowserLocation = {
|
||||||
|
pathname: string,
|
||||||
|
search: string,
|
||||||
|
hash: string,
|
||||||
|
// Browser and Memory specific
|
||||||
|
state: string,
|
||||||
|
key: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare export type BrowserHistory = {
|
||||||
|
length: number,
|
||||||
|
location: BrowserLocation,
|
||||||
|
action: Action,
|
||||||
|
push: (path: string, Array<mixed>) => void,
|
||||||
|
replace: (path: string, Array<mixed>) => void,
|
||||||
|
go: (n: number) => void,
|
||||||
|
goBack: () => void,
|
||||||
|
goForward: () => void,
|
||||||
|
listen: Function,
|
||||||
|
block: (message: string) => Unblock,
|
||||||
|
block: ((location: BrowserLocation, action: Action) => string) => Unblock,
|
||||||
|
push: (path: string) => void,
|
||||||
|
replace: (path: string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type HistoryOpts = {
|
||||||
|
basename?: string,
|
||||||
|
forceRefresh?: boolean,
|
||||||
|
getUserConfirmation?: (
|
||||||
|
message: string,
|
||||||
|
callback: (willContinue: boolean) => void,
|
||||||
|
) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare export default (opts?: HistoryOpts) => BrowserHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "history/createMemoryHistory" {
|
||||||
|
declare function Unblock(): void;
|
||||||
|
|
||||||
|
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||||
|
|
||||||
|
declare export type MemoryLocation = {
|
||||||
|
pathname: string,
|
||||||
|
search: string,
|
||||||
|
hash: string,
|
||||||
|
// Browser and Memory specific
|
||||||
|
state: string,
|
||||||
|
key: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare export type MemoryHistory = {
|
||||||
|
length: number,
|
||||||
|
location: MemoryLocation,
|
||||||
|
action: Action,
|
||||||
|
index: number,
|
||||||
|
entries: Array<string>,
|
||||||
|
push: (path: string, Array<mixed>) => void,
|
||||||
|
replace: (path: string, Array<mixed>) => void,
|
||||||
|
go: (n: number) => void,
|
||||||
|
goBack: () => void,
|
||||||
|
goForward: () => void,
|
||||||
|
// Memory only
|
||||||
|
canGo: (n: number) => boolean,
|
||||||
|
listen: Function,
|
||||||
|
block: (message: string) => Unblock,
|
||||||
|
block: ((location: MemoryLocation, action: Action) => string) => Unblock,
|
||||||
|
push: (path: string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type HistoryOpts = {
|
||||||
|
initialEntries?: Array<string>,
|
||||||
|
initialIndex?: number,
|
||||||
|
keyLength?: number,
|
||||||
|
getUserConfirmation?: (
|
||||||
|
message: string,
|
||||||
|
callback: (willContinue: boolean) => void,
|
||||||
|
) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare export default (opts?: HistoryOpts) => MemoryHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "history/createHashHistory" {
|
||||||
|
declare function Unblock(): void;
|
||||||
|
|
||||||
|
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||||
|
|
||||||
|
declare export type HashLocation = {
|
||||||
|
pathname: string,
|
||||||
|
search: string,
|
||||||
|
hash: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare export type HashHistory = {
|
||||||
|
length: number,
|
||||||
|
location: HashLocation,
|
||||||
|
action: Action,
|
||||||
|
push: (path: string, Array<mixed>) => void,
|
||||||
|
replace: (path: string, Array<mixed>) => void,
|
||||||
|
go: (n: number) => void,
|
||||||
|
goBack: () => void,
|
||||||
|
goForward: () => void,
|
||||||
|
listen: Function,
|
||||||
|
block: (message: string) => Unblock,
|
||||||
|
block: ((location: HashLocation, action: Action) => string) => Unblock,
|
||||||
|
push: (path: string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type HistoryOpts = {
|
||||||
|
basename?: string,
|
||||||
|
hashType: "slash" | "noslash" | "hashbang",
|
||||||
|
getUserConfirmation?: (
|
||||||
|
message: string,
|
||||||
|
callback: (willContinue: boolean) => void,
|
||||||
|
) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare export default (opts?: HistoryOpts) => HashHistory;
|
||||||
|
}
|
||||||
@@ -3,25 +3,30 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ces-theme": "https://github.com/cloudogu/ces-theme.git",
|
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
|
"flow-bin": "^0.75.0",
|
||||||
|
"history": "^4.7.2",
|
||||||
"react": "^16.4.1",
|
"react": "^16.4.1",
|
||||||
"react-dom": "^16.4.1",
|
"react-dom": "^16.4.1",
|
||||||
"react-jss": "^8.6.0",
|
"react-jss": "^8.6.0",
|
||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-scripts": "1.1.4",
|
|
||||||
"redux": "^4.0.0",
|
|
||||||
"redux-logger": "^3.0.6",
|
|
||||||
"redux-thunk": "^2.3.0",
|
|
||||||
"history": "^4.7.2",
|
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"react-router-redux": "^5.0.0-alpha.9",
|
"react-router-redux": "^5.0.0-alpha.9",
|
||||||
"redux-devtools-extension": "^2.13.5"
|
"react-scripts": "1.1.4",
|
||||||
|
"redux": "^4.0.0",
|
||||||
|
"redux-devtools-extension": "^2.13.5",
|
||||||
|
"redux-logger": "^3.0.6",
|
||||||
|
"redux-thunk": "^2.3.0"
|
||||||
},
|
},
|
||||||
"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": "react-scripts test --env=jsdom",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject",
|
||||||
|
"flow": "flow"
|
||||||
|
},
|
||||||
|
"proxy": "http://localhost:8081/scm",
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^1.13.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
scm-ui/pom.xml
Normal file
18
scm-ui/pom.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>sonia.scm</groupId>
|
||||||
|
<artifactId>scm</artifactId>
|
||||||
|
<version>2.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>sonia.scm.clients</groupId>
|
||||||
|
<artifactId>scm-ui</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<version>2.0.0-SNAPSHOT</version>
|
||||||
|
<name>scm-ui</name>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import Navigation from './Navigation';
|
|
||||||
import Main from './Main';
|
|
||||||
import {withRouter} from 'react-router-dom';
|
|
||||||
import 'ces-theme/dist/css/ces.css';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class App extends Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<Navigation />
|
|
||||||
<Main />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(App);
|
|
||||||
64
scm-ui/src/apiclient.js
Normal file
64
scm-ui/src/apiclient.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
// get api base url from environment
|
||||||
|
const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || "";
|
||||||
|
|
||||||
|
export const PAGE_NOT_FOUND_ERROR = Error("page not found");
|
||||||
|
|
||||||
|
// fetch does not send the X-Requested-With header (https://github.com/github/fetch/issues/17),
|
||||||
|
// but we need the header to detect ajax request (AjaxAwareAuthenticationRedirectStrategy).
|
||||||
|
const fetchOptions: RequestOptions = {
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleStatusCode(response: Response) {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw PAGE_NOT_FOUND_ERROR;
|
||||||
|
}
|
||||||
|
throw new Error("server returned status code " + response.status);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUrl(url: string) {
|
||||||
|
return `${apiUrl}/api/rest/v2/${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
get(url: string) {
|
||||||
|
return fetch(createUrl(url), fetchOptions).then(handleStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
post(url: string, payload: any) {
|
||||||
|
return this.httpRequestWithJSONBody(url, payload, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(url: string, payload: any) {
|
||||||
|
let options: RequestOptions = {
|
||||||
|
method: "DELETE"
|
||||||
|
};
|
||||||
|
options = Object.assign(options, fetchOptions);
|
||||||
|
return fetch(createUrl(url), options).then(handleStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRequestWithJSONBody(url: string, payload: any, method: string) {
|
||||||
|
let options: RequestOptions = {
|
||||||
|
method: method,
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
};
|
||||||
|
options = Object.assign(options, fetchOptions);
|
||||||
|
// $FlowFixMe
|
||||||
|
options.headers["Content-Type"] = "application/json";
|
||||||
|
|
||||||
|
return fetch(createUrl(url), options).then(handleStatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export let apiClient = new ApiClient();
|
||||||
35
scm-ui/src/containers/App.js
Normal file
35
scm-ui/src/containers/App.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { Component } from "react";
|
||||||
|
import Navigation from "./Navigation";
|
||||||
|
import Main from "./Main";
|
||||||
|
import Login from "./Login";
|
||||||
|
import { withRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
login: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const { login} = this.props;
|
||||||
|
|
||||||
|
if(login) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Login/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<Navigation />
|
||||||
|
<Main />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(App);
|
||||||
@@ -5,8 +5,8 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import { Route, withRouter } from 'react-router';
|
import { Route, withRouter } from 'react-router';
|
||||||
|
|
||||||
import Repositories from './containers/Repositories';
|
import Repositories from '../repositories/containers/Repositories';
|
||||||
import Users from './containers/Users';
|
import Users from '../users/containers/Users';
|
||||||
import {Switch} from 'react-router-dom';
|
import {Switch} from 'react-router-dom';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import thunk from 'redux-thunk';
|
// @flow
|
||||||
import logger from 'redux-logger';
|
import thunk from "redux-thunk";
|
||||||
import { createStore, compose, applyMiddleware, combineReducers } from 'redux';
|
import logger from "redux-logger";
|
||||||
import { routerReducer, routerMiddleware } from 'react-router-redux';
|
import { createStore, compose, applyMiddleware, combineReducers } from "redux";
|
||||||
|
import { routerReducer, routerMiddleware } from "react-router-redux";
|
||||||
|
|
||||||
import repositories from './modules/repositories';
|
import repositories from "./repositories/modules/repositories";
|
||||||
import users from './modules/users';
|
import users from "./users/modules/users";
|
||||||
|
|
||||||
function createReduxStore(history) {
|
import type {BrowserHistory} from "history/createBrowserHistory";
|
||||||
|
|
||||||
|
function createReduxStore(history: BrowserHistory) {
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
|
||||||
const reducer = combineReducers({
|
const reducer = combineReducers({
|
||||||
|
|||||||
@@ -1,32 +1,41 @@
|
|||||||
import React from 'react';
|
// @flow
|
||||||
import ReactDOM from 'react-dom';
|
import React from "react";
|
||||||
import App from './App';
|
import ReactDOM from "react-dom";
|
||||||
import registerServiceWorker from './registerServiceWorker';
|
import App from "./containers/App";
|
||||||
|
import registerServiceWorker from "./registerServiceWorker";
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from "react-redux";
|
||||||
import createHistory from 'history/createBrowserHistory';
|
import createHistory from "history/createBrowserHistory";
|
||||||
import createReduxStore from './createReduxStore';
|
|
||||||
import { ConnectedRouter } from 'react-router-redux';
|
|
||||||
|
|
||||||
|
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||||
|
|
||||||
|
import createReduxStore from "./createReduxStore";
|
||||||
|
import { ConnectedRouter } from "react-router-redux";
|
||||||
|
|
||||||
|
const publicUrl: string = process.env.PUBLIC_URL || "";
|
||||||
|
|
||||||
// Create a history of your choosing (we're using a browser history in this case)
|
// Create a history of your choosing (we're using a browser history in this case)
|
||||||
const history = createHistory({
|
const history: BrowserHistory = createHistory({
|
||||||
basename: process.env.PUBLIC_URL
|
basename: publicUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
window.appHistory = history;
|
|
||||||
// Add the reducer to your store on the `router` key
|
// Add the reducer to your store on the `router` key
|
||||||
// Also apply our middleware for navigating
|
// Also apply our middleware for navigating
|
||||||
const store = createReduxStore(history);
|
const store = createReduxStore(history);
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
if (!root) {
|
||||||
|
throw new Error("could not find root element");
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
{ /* ConnectedRouter will use the store from Provider automatically */}
|
{/* ConnectedRouter will use the store from Provider automatically */}
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<App />
|
<App />
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('root')
|
root
|
||||||
);
|
);
|
||||||
|
|
||||||
registerServiceWorker();
|
registerServiceWorker();
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchRepositoriesIfNeeded } from '../modules/repositories';
|
import { fetchRepositoriesIfNeeded } from '../modules/repositories';
|
||||||
import Login from '../Login';
|
import Login from '../../containers/Login';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
login: boolean,
|
login: boolean,
|
||||||
error: any,
|
error: Error,
|
||||||
|
repositories: any,
|
||||||
|
fetchRepositoriesIfNeeded: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
class Repositories extends React.Component<Props> {
|
class Repositories extends React.Component<Props> {
|
||||||
@@ -21,15 +23,6 @@ class Repositories extends React.Component<Props> {
|
|||||||
const { login, error, repositories } = this.props;
|
const { login, error, repositories } = this.props;
|
||||||
|
|
||||||
|
|
||||||
if(login) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>SCM</h1>
|
|
||||||
<Login/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if(!login){
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>SCM</h1>
|
<h1>SCM</h1>
|
||||||
@@ -38,8 +31,7 @@ class Repositories extends React.Component<Props> {
|
|||||||
Users hier!
|
Users hier!
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
//@flow
|
|
||||||
const FETCH_REPOSITORIES = 'scm/repositories/FETCH';
|
const FETCH_REPOSITORIES = 'scm/repositories/FETCH';
|
||||||
const FETCH_REPOSITORIES_SUCCESS = 'scm/repositories/FETCH_SUCCESS';
|
const FETCH_REPOSITORIES_SUCCESS = 'scm/repositories/FETCH_SUCCESS';
|
||||||
const FETCH_REPOSITORIES_FAILURE = 'scm/repositories/FETCH_FAILURE';
|
const FETCH_REPOSITORIES_FAILURE = 'scm/repositories/FETCH_FAILURE';
|
||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchUsersIfNeeded } from '../modules/users';
|
import { fetchUsersIfNeeded } from '../modules/users';
|
||||||
import Login from '../Login';
|
import Login from '../../containers/Login';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
login: boolean,
|
login: boolean,
|
||||||
@@ -22,23 +22,13 @@ class Users extends React.Component<Props> {
|
|||||||
const { login, error, users } = this.props;
|
const { login, error, users } = this.props;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(login) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>SCM</h1>
|
|
||||||
<Login/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if(!login){
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>SCM</h1>
|
<h1>SCM</h1>
|
||||||
<h2>Users</h2>
|
<h2>Users</h2>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
//@flow
|
|
||||||
const FETCH_USERS = 'scm/users/FETCH';
|
const FETCH_USERS = 'scm/users/FETCH';
|
||||||
const FETCH_USERS_SUCCESS= 'scm/users/FETCH_SUCCESS';
|
const FETCH_USERS_SUCCESS= 'scm/users/FETCH_SUCCESS';
|
||||||
const FETCH_USERS_FAILURE = 'scm/users/FETCH_FAILURE';
|
const FETCH_USERS_FAILURE = 'scm/users/FETCH_FAILURE';
|
||||||
|
|
||||||
const THRESHOLD_TIMESTAMP = 10000;
|
|
||||||
|
|
||||||
function requestUsers() {
|
function requestUsers() {
|
||||||
return {
|
return {
|
||||||
type: FETCH_USERS
|
type: FETCH_USERS
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.authc.AuthenticationException;
|
||||||
|
import org.apache.shiro.authc.DisabledAccountException;
|
||||||
|
import org.apache.shiro.authc.ExcessiveAttemptsException;
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.api.rest.RestActionResult;
|
||||||
|
import sonia.scm.security.*;
|
||||||
|
import sonia.scm.util.HttpUtil;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.ws.rs.*;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by masuewer on 04.07.18.
|
||||||
|
*/
|
||||||
|
@Path(AuthenticationResource.PATH)
|
||||||
|
public class AuthenticationResource {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationResource.class);
|
||||||
|
|
||||||
|
public static final String PATH = "v2/auth";
|
||||||
|
|
||||||
|
private final AccessTokenBuilderFactory tokenBuilderFactory;
|
||||||
|
private final AccessTokenCookieIssuer cookieIssuer;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
|
||||||
|
{
|
||||||
|
this.tokenBuilderFactory = tokenBuilderFactory;
|
||||||
|
this.cookieIssuer = cookieIssuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("access_token")
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 200, condition = "success"),
|
||||||
|
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
|
||||||
|
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
public Response authenticateViaForm(
|
||||||
|
@Context HttpServletRequest request,
|
||||||
|
@Context HttpServletResponse response,
|
||||||
|
@BeanParam AuthenticationRequest authentication
|
||||||
|
) {
|
||||||
|
return authenticate(request, response, authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("access_token")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 200, condition = "success"),
|
||||||
|
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
|
||||||
|
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
public Response authenticateViaJSONBody(
|
||||||
|
@Context HttpServletRequest request,
|
||||||
|
@Context HttpServletResponse response,
|
||||||
|
AuthenticationRequest authentication
|
||||||
|
) {
|
||||||
|
return authenticate(request, response, authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response authenticate(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
AuthenticationRequest authentication
|
||||||
|
) {
|
||||||
|
authentication.validate();
|
||||||
|
|
||||||
|
Response res;
|
||||||
|
Subject subject = SecurityUtils.getSubject();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subject.login(Tokens.createAuthenticationToken(request, authentication.getUsername(), authentication.getPassword()));
|
||||||
|
|
||||||
|
AccessTokenBuilder tokenBuilder = tokenBuilderFactory.create();
|
||||||
|
if ( authentication.getScope() != null ) {
|
||||||
|
tokenBuilder.scope(Scope.valueOf(authentication.getScope()));
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken token = tokenBuilder.build();
|
||||||
|
|
||||||
|
if (authentication.isCookie()) {
|
||||||
|
cookieIssuer.authenticate(request, response, token);
|
||||||
|
res = Response.noContent().build();
|
||||||
|
} else {
|
||||||
|
res = Response.ok( token.compact() ).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (DisabledAccountException ex)
|
||||||
|
{
|
||||||
|
if (LOG.isTraceEnabled())
|
||||||
|
{
|
||||||
|
LOG.trace(
|
||||||
|
"authentication failed, account user ".concat(authentication.getUsername()).concat(
|
||||||
|
" is locked"), ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG.warn("authentication failed, account {} is locked", authentication.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
res = handleFailedAuthentication(request, ex, Response.Status.FORBIDDEN,
|
||||||
|
WUIAuthenticationFailure.LOCKED);
|
||||||
|
}
|
||||||
|
catch (ExcessiveAttemptsException ex)
|
||||||
|
{
|
||||||
|
if (LOG.isTraceEnabled())
|
||||||
|
{
|
||||||
|
LOG.trace(
|
||||||
|
"authentication failed, account user ".concat(authentication.getUsername()).concat(
|
||||||
|
" is temporary locked"), ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG.warn("authentication failed, account {} is temporary locked", authentication.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
res = handleFailedAuthentication(request, ex, Response.Status.FORBIDDEN,
|
||||||
|
WUIAuthenticationFailure.TEMPORARY_LOCKED);
|
||||||
|
}
|
||||||
|
catch (AuthenticationException ex)
|
||||||
|
{
|
||||||
|
if (LOG.isTraceEnabled())
|
||||||
|
{
|
||||||
|
LOG.trace("authentication failed for user ".concat(authentication.getUsername()), ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG.warn("authentication failed for user {}", authentication.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
res = handleFailedAuthentication(request, ex, Response.Status.UNAUTHORIZED,
|
||||||
|
WUIAuthenticationFailure.WRONG_CREDENTIALS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("access_token")
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 204, condition = "success"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response)
|
||||||
|
{
|
||||||
|
Subject subject = SecurityUtils.getSubject();
|
||||||
|
|
||||||
|
subject.logout();
|
||||||
|
|
||||||
|
// remove authentication cookie
|
||||||
|
cookieIssuer.invalidate(request, response);
|
||||||
|
|
||||||
|
// TODO anonymous access ??
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AuthenticationRequest {
|
||||||
|
|
||||||
|
@FormParam("grant_type")
|
||||||
|
@JsonProperty("grant_type")
|
||||||
|
private String grantType;
|
||||||
|
|
||||||
|
@FormParam("username")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@FormParam("password")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@FormParam("cookie")
|
||||||
|
private boolean cookie;
|
||||||
|
|
||||||
|
@FormParam("scope")
|
||||||
|
private List<String> scope;
|
||||||
|
|
||||||
|
public String getGrantType() {
|
||||||
|
return grantType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCookie() {
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getScope() {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validate() {
|
||||||
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(grantType), "grant_type parameter is required");
|
||||||
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(username), "username parameter is required");
|
||||||
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(password), "password parameter is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Response handleFailedAuthentication(HttpServletRequest request,
|
||||||
|
AuthenticationException ex, Response.Status status,
|
||||||
|
WUIAuthenticationFailure failure) {
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
if (HttpUtil.isWUIRequest(request)) {
|
||||||
|
response = Response.ok(new WUIAuthenticationFailedResult(failure,
|
||||||
|
ex.getMessage())).build();
|
||||||
|
} else {
|
||||||
|
response = Response.status(status).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum WUIAuthenticationFailure { LOCKED, TEMPORARY_LOCKED, WRONG_CREDENTIALS }
|
||||||
|
|
||||||
|
@XmlRootElement(name = "result")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
private static final class WUIAuthenticationFailedResult extends RestActionResult {
|
||||||
|
|
||||||
|
private final WUIAuthenticationFailure failure;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
public WUIAuthenticationFailedResult(WUIAuthenticationFailure failure, String message) {
|
||||||
|
super(false);
|
||||||
|
this.failure = failure;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WUIAuthenticationFailure getFailure() {
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ import org.apache.shiro.subject.Subject;
|
|||||||
import sonia.scm.Priority;
|
import sonia.scm.Priority;
|
||||||
import sonia.scm.SCMContext;
|
import sonia.scm.SCMContext;
|
||||||
import sonia.scm.config.ScmConfiguration;
|
import sonia.scm.config.ScmConfiguration;
|
||||||
|
import sonia.scm.security.SecurityRequests;
|
||||||
import sonia.scm.web.filter.HttpFilter;
|
import sonia.scm.web.filter.HttpFilter;
|
||||||
import sonia.scm.web.filter.SecurityHttpServletRequestWrapper;
|
import sonia.scm.web.filter.SecurityHttpServletRequestWrapper;
|
||||||
|
|
||||||
@@ -72,6 +73,8 @@ public class SecurityFilter extends HttpFilter
|
|||||||
/** Field description */
|
/** Field description */
|
||||||
public static final String URL_AUTHENTICATION = "/api/rest/auth";
|
public static final String URL_AUTHENTICATION = "/api/rest/auth";
|
||||||
|
|
||||||
|
public static final String URLV2_AUTHENTICATION = "/api/rest/v2/auth";
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
//~--- constructors ---------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,10 +107,7 @@ public class SecurityFilter extends HttpFilter
|
|||||||
HttpServletResponse response, FilterChain chain)
|
HttpServletResponse response, FilterChain chain)
|
||||||
throws IOException, ServletException
|
throws IOException, ServletException
|
||||||
{
|
{
|
||||||
String uri =
|
if (!SecurityRequests.isAuthenticationRequest(request))
|
||||||
request.getRequestURI().substring(request.getContextPath().length());
|
|
||||||
|
|
||||||
if (!uri.startsWith(URL_AUTHENTICATION))
|
|
||||||
{
|
{
|
||||||
Subject subject = SecurityUtils.getSubject();
|
Subject subject = SecurityUtils.getSubject();
|
||||||
if (hasPermission(subject))
|
if (hasPermission(subject))
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by masuewer on 04.07.18.
|
||||||
|
*/
|
||||||
|
public final class SecurityRequests {
|
||||||
|
|
||||||
|
private static final Pattern URI_LOGIN_PATTERN = Pattern.compile("/api/rest(?:/v2)?/auth/access_token");
|
||||||
|
|
||||||
|
private SecurityRequests() {}
|
||||||
|
|
||||||
|
public static boolean isAuthenticationRequest(HttpServletRequest request) {
|
||||||
|
String uri = request.getRequestURI().substring(request.getContextPath().length());
|
||||||
|
return isAuthenticationRequest(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAuthenticationRequest(String uri) {
|
||||||
|
return URI_LOGIN_PATTERN.matcher(uri).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -36,24 +36,22 @@ package sonia.scm.web.security;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
import sonia.scm.Priority;
|
import sonia.scm.Priority;
|
||||||
import sonia.scm.config.ScmConfiguration;
|
import sonia.scm.config.ScmConfiguration;
|
||||||
import sonia.scm.filter.Filters;
|
import sonia.scm.filter.Filters;
|
||||||
import sonia.scm.filter.WebElement;
|
import sonia.scm.filter.WebElement;
|
||||||
import sonia.scm.web.filter.AuthenticationFilter;
|
import sonia.scm.security.SecurityRequests;
|
||||||
import sonia.scm.web.WebTokenGenerator;
|
import sonia.scm.web.WebTokenGenerator;
|
||||||
|
import sonia.scm.web.filter.AuthenticationFilter;
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter to handle authentication for the rest api of SCM-Manager.
|
* Filter to handle authentication for the rest api of SCM-Manager.
|
||||||
@@ -66,9 +64,6 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
public class ApiAuthenticationFilter extends AuthenticationFilter
|
public class ApiAuthenticationFilter extends AuthenticationFilter
|
||||||
{
|
{
|
||||||
|
|
||||||
/** login uri */
|
|
||||||
public static final String URI_LOGIN = "/api/rest/auth/access_token";
|
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
//~--- constructors ---------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter
|
|||||||
throws IOException, ServletException
|
throws IOException, ServletException
|
||||||
{
|
{
|
||||||
// skip filter on login resource
|
// skip filter on login resource
|
||||||
if (request.getRequestURI().contains(URI_LOGIN))
|
if (SecurityRequests.isAuthenticationRequest(request))
|
||||||
{
|
{
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.runners.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by masuewer on 04.07.18.
|
||||||
|
*/
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class SecurityRequestsTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsAuthenticationRequestWithContextPath() {
|
||||||
|
when(request.getRequestURI()).thenReturn("/scm/api/rest/auth/access_token");
|
||||||
|
when(request.getContextPath()).thenReturn("/scm");
|
||||||
|
|
||||||
|
assertTrue(SecurityRequests.isAuthenticationRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsAuthenticationRequest() throws Exception {
|
||||||
|
assertTrue(SecurityRequests.isAuthenticationRequest("/api/rest/auth/access_token"));
|
||||||
|
assertTrue(SecurityRequests.isAuthenticationRequest("/api/rest/v2/auth/access_token"));
|
||||||
|
assertFalse(SecurityRequests.isAuthenticationRequest("/api/rest/repositories"));
|
||||||
|
assertFalse(SecurityRequests.isAuthenticationRequest("/api/rest/v2/repositories"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user